fix(browser): resolve snap and Flatpak Chromium profiles on Linux

real_profile_data_dir hard-wired Linux to $XDG_CONFIG_HOME/<name>, and the
xdg fragment map only knew the native package names. Ubuntu's default snap
Chromium (xdg reports chromium_chromium.desktop, profile under
~/snap/chromium/common/chromium) and Flatpak builds (~/.var/app/<id>/config/…)
therefore ended in 'profile directory was not found' for a browser the user
runs every day, and Flatpak Chrome (com.google.Chrome.desktop) was reported as
'not a supported Chromium browser'.

Try the native, snap and Flatpak locations and return the first that exists;
fall back to the native path so the error message still names a concrete
directory. Map the Flatpak application ids in the xdg lookup.

Tests cover the xdg names for all four browsers in native and Flatpak form,
and the directory preference order with a temp HOME.
This commit is contained in:
Jan-Stefan Janetzky
2026-08-26 14:55:06 +02:00
committed by Teknium
parent f5e6028417
commit 7e2c2b1b08
2 changed files with 101 additions and 5 deletions

View File

@@ -104,14 +104,30 @@ _WINDOWS_PROGID_MAP = (
)
# Linux xdg default-web-browser .desktop name fragments → canonical key.
# Includes the Flatpak application ids (``com.google.Chrome.desktop`` etc.),
# which share none of the native package name fragments.
_LINUX_DESKTOP_MAP = (
("google-chrome", "chrome"),
("com.google.chrome", "chrome"),
("chromium", "chromium"),
("brave", "brave"),
("microsoft-edge", "edge"),
("com.microsoft.edge", "edge"),
("msedge", "edge"),
)
# Where sandboxed Linux packages keep the profile instead of $XDG_CONFIG_HOME.
_LINUX_FLATPAK_IDS = {
"chrome": "com.google.Chrome",
"chromium": "org.chromium.Chromium",
"brave": "com.brave.Browser",
"edge": "com.microsoft.Edge",
}
_LINUX_SNAP_PROFILE_PARTS = {
"chromium": ("snap", "chromium", "common", "chromium"),
"brave": ("snap", "brave", "current", ".config", "BraveSoftware", "Brave-Browser"),
}
# macOS LaunchServices bundle-id fragments → canonical key.
_DARWIN_BUNDLE_MAP = (
("com.google.chrome", "chrome"),
@@ -150,10 +166,12 @@ def _real_profile_relparts(browser: str) -> tuple:
def real_profile_data_dir(browser: str, system: str | None = None) -> str | None:
"""Return the default user-data-dir for a Chromium ``browser`` on ``system``.
Returns None for unknown browsers. Does not check existence — callers that
need that should stat the result. Paths are built with the TARGET system's
separator (posix for Darwin/Linux, backslash for Windows) so an explicit
``system`` argument resolves correctly regardless of the host OS.
Returns None for unknown browsers. On Linux the native ($XDG_CONFIG_HOME),
snap and Flatpak locations are tried and the first existing one wins; the
native path is returned when none exists so the caller's error names it.
Darwin/Windows paths are not stat'ed. Paths are built with the TARGET
system's separator (posix for Darwin/Linux, backslash for Windows) so an
explicit ``system`` argument resolves correctly regardless of the host OS.
"""
if browser not in _CHROMIUM_BROWSERS:
return None
@@ -167,7 +185,19 @@ def real_profile_data_dir(browser: str, system: str | None = None) -> str | None
return ntpath.join(local, *win_parts)
# Linux / other POSIX
config = os.environ.get("XDG_CONFIG_HOME") or posixpath.join(home, ".config")
return posixpath.join(config, *linux_name.split("/"))
candidates = [posixpath.join(config, *linux_name.split("/"))]
snap_parts = _LINUX_SNAP_PROFILE_PARTS.get(browser)
if snap_parts:
candidates.append(posixpath.join(home, *snap_parts))
flatpak_id = _LINUX_FLATPAK_IDS.get(browser)
if flatpak_id:
candidates.append(
posixpath.join(home, ".var", "app", flatpak_id, "config", *linux_name.split("/"))
)
for candidate in candidates:
if os.path.isdir(candidate):
return candidate
return candidates[0]
def chromium_executable(browser: str, system: str | None = None) -> str | None:

View File

@@ -104,3 +104,69 @@ class TestDetectDefaultDarwin:
def test_bundle_map(self, bundle, expected):
with self._run_with(_ls_dump(_handler("https", bundle))):
assert bc._detect_default_darwin() == expected
class TestDetectDefaultLinux:
def _run_with(self, output: str):
class _Proc:
stdout = output
return patch.object(bc.subprocess, "run", return_value=_Proc())
@pytest.mark.parametrize(
"desktop,expected",
[
("google-chrome.desktop", "chrome"),
("com.google.Chrome.desktop", "chrome"),
("chromium_chromium.desktop", "chromium"),
("org.chromium.Chromium.desktop", "chromium"),
("brave-browser.desktop", "brave"),
("com.brave.Browser.desktop", "brave"),
("microsoft-edge.desktop", "edge"),
("com.microsoft.Edge.desktop", "edge"),
("firefox.desktop", None),
("org.mozilla.firefox.desktop", None),
("", None),
],
)
def test_xdg_desktop_names(self, desktop, expected):
with self._run_with(desktop + "\n"):
assert bc._detect_default_linux() == expected
def test_missing_xdg_settings_fails_closed(self):
with patch.object(bc.subprocess, "run", side_effect=FileNotFoundError("xdg-settings")):
assert bc._detect_default_linux() is None
class TestLinuxProfileDir:
def _env(self, monkeypatch, home):
monkeypatch.setenv("HOME", str(home))
monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
def test_native_path_when_nothing_exists(self, tmp_path, monkeypatch):
self._env(monkeypatch, tmp_path)
assert bc.real_profile_data_dir("chromium", "Linux") == str(tmp_path / ".config" / "chromium")
def test_snap_chromium_profile_is_found(self, tmp_path, monkeypatch):
self._env(monkeypatch, tmp_path)
snap = tmp_path / "snap" / "chromium" / "common" / "chromium"
snap.mkdir(parents=True)
assert bc.real_profile_data_dir("chromium", "Linux") == str(snap)
def test_flatpak_chrome_profile_is_found(self, tmp_path, monkeypatch):
self._env(monkeypatch, tmp_path)
flatpak = tmp_path / ".var" / "app" / "com.google.Chrome" / "config" / "google-chrome"
flatpak.mkdir(parents=True)
assert bc.real_profile_data_dir("chrome", "Linux") == str(flatpak)
def test_native_profile_wins_when_present(self, tmp_path, monkeypatch):
self._env(monkeypatch, tmp_path)
native = tmp_path / ".config" / "BraveSoftware" / "Brave-Browser"
native.mkdir(parents=True)
(tmp_path / ".var" / "app" / "com.brave.Browser" / "config" / "BraveSoftware" / "Brave-Browser").mkdir(parents=True)
assert bc.real_profile_data_dir("brave", "Linux") == str(native)
def test_xdg_config_home_is_honoured(self, tmp_path, monkeypatch):
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("XDG_CONFIG_HOME", "/home/t/.config")
assert bc.real_profile_data_dir("edge", "Linux") == "/home/t/.config/microsoft-edge"