fix(python): resolve the bundle from executable_path, not the managed install

get_env_vars() and _generate_fontconfig() read the bundled fontconfig and fonts
through get_path(), i.e. the managed install, even when the caller supplied
their own binary. _load_properties() already honours executable_path for
properties.json; these two did not.

Before the floor could reject anything this silently mixed one build's fonts
into another build's launch. Once the floor is live it becomes fatal: every
launch raises UnsupportedVersion while the caller is holding a perfectly good
binary, because resolving the bundle drags in the managed install and that is
what gets version-checked.

Thread executable_path through both, matching _load_properties.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jake Writer
2026-08-31 15:19:46 -06:00
co-authored by Claude Opus 5
parent b1fe7227fa
commit fc3392e427
3 changed files with 78 additions and 8 deletions
+21 -7
View File
@@ -49,7 +49,7 @@ CACHE_PREFS = {
}
def _generate_fontconfig(fontconfig_path: str) -> str:
def _generate_fontconfig(fontconfig_path: str, path: Optional[Path] = None) -> str:
"""
Generates a runtime fontconfig that resolves bundled font paths absolutely.
The bundled fonts.conf uses prefix="cwd" relative paths which break when
@@ -61,7 +61,8 @@ def _generate_fontconfig(fontconfig_path: str) -> str:
"""
import hashlib
fonts_dir = get_path("fonts")
# Beside the caller's own binary when they supplied one; see get_env_vars.
fonts_dir = str(path.parent / "fonts") if path else get_path("fonts")
fonts_conf_src = os.path.join(fontconfig_path, "fonts.conf")
with open(fonts_conf_src, 'r') as f:
@@ -86,10 +87,18 @@ def _generate_fontconfig(fontconfig_path: str) -> str:
def get_env_vars(
config_map: Dict[str, str], user_agent_os: str
config_map: Dict[str, str],
user_agent_os: str,
path: Optional[Path] = None,
) -> Dict[str, Union[str, float, bool]]:
"""
Gets a dictionary of environment variables for Camoufox.
`path` is the caller's own executable, when they supplied one. The bundled
fontconfig is read from beside that binary rather than from the managed
install, the same way _load_properties() already treats properties.json:
a caller running their own build should not be resolved against, or made
to download, a different one.
"""
env_vars: Dict[str, Union[str, float, bool]] = {}
try:
@@ -123,9 +132,14 @@ def get_env_vars(
os_dir = directory_map.get(user_agent_os, user_agent_os)
# v150+ uses "fontconfig/" (matching the Go launcher); older bundles shipped "fontconfigs/".
fontconfig_path = get_path(os.path.join("fontconfig", os_dir))
def _bundle_path(*parts: str) -> str:
if path:
return str(path.parent.joinpath(*parts))
return get_path(os.path.join(*parts))
fontconfig_path = _bundle_path("fontconfig", os_dir)
if not os.path.exists(os.path.join(fontconfig_path, "fonts.conf")):
fontconfig_path = get_path(os.path.join("fontconfigs", os_dir))
fontconfig_path = _bundle_path("fontconfigs", os_dir)
# assert that fonts.conf exists in the directory
if not os.path.exists(os.path.join(fontconfig_path, "fonts.conf")):
@@ -134,7 +148,7 @@ def get_env_vars(
f"fonts.conf not found in {fontconfig_path}! Something ain't right with your camoufox bundle."
)
env_vars['FONTCONFIG_FILE'] = _generate_fontconfig(fontconfig_path)
env_vars['FONTCONFIG_FILE'] = _generate_fontconfig(fontconfig_path, path=path)
return env_vars
@@ -941,7 +955,7 @@ def launch_options(
# Prepare environment variables to pass to Camoufox
env_vars = {
**get_env_vars(config, target_os),
**get_env_vars(config, target_os, path=executable_path),
**env,
}
# Prepare the executable path
@@ -0,0 +1,56 @@
"""A caller's own binary must be resolved against itself, not the managed install.
`executable_path` was honoured for launching and for properties.json, but the
bundled fontconfig and fonts were still read from the managed install via
get_path(). That silently mixed one build's fonts into another's launch, and
once the version floor could actually reject something it became fatal: every
launch raised UnsupportedVersion even though the caller had supplied a perfectly
good binary.
"""
from pathlib import Path
import pytest
from camoufox import pkgman, utils
@pytest.fixture
def bundle(tmp_path, monkeypatch):
"""A self-contained browser bundle, plus a managed install that must not be touched."""
bin_dir = tmp_path / "dist" / "bin"
(bin_dir / "fontconfig" / "linux").mkdir(parents=True)
(bin_dir / "fontconfig" / "linux" / "fonts.conf").write_text(
'<?xml version="1.0"?><fontconfig><dir prefix="cwd">fonts</dir></fontconfig>'
)
(bin_dir / "fonts").mkdir()
def explode(*_args, **_kwargs):
raise AssertionError("resolved against the managed install despite executable_path")
monkeypatch.setattr(pkgman, "get_path", explode)
monkeypatch.setattr(utils, "get_path", explode)
monkeypatch.setattr(utils, "INSTALL_DIR", tmp_path / "cache")
monkeypatch.setattr(utils, "OS_NAME", "lin")
return bin_dir
def test_fontconfig_comes_from_the_supplied_bundle(bundle, monkeypatch):
env = utils.get_env_vars({}, "lin", path=bundle / "camoufox-bin")
generated = Path(env["FONTCONFIG_FILE"])
assert generated.is_file()
# The bundled conf's cwd-relative <dir> is rewritten to this bundle's fonts.
assert str(bundle / "fonts") in generated.read_text()
def test_managed_install_is_used_when_no_path_is_given(tmp_path, monkeypatch):
"""Without executable_path the managed install is still the source."""
calls = []
monkeypatch.setattr(utils, "OS_NAME", "lin")
monkeypatch.setattr(utils, "get_path", lambda *a: calls.append(a) or "/nonexistent")
with pytest.raises(Exception):
utils.get_env_vars({}, "lin")
assert calls, "should have consulted the managed install"
+1 -1
View File
@@ -31,7 +31,7 @@ def captured_launch_config(monkeypatch):
monkeypatch.setattr(utils, "launch_path", lambda *_args, **_kwargs: "/camoufox")
monkeypatch.setattr(utils.LeakWarning, "warn", lambda *_args, **_kwargs: None)
def capture_env(config, _target_os):
def capture_env(config, _target_os, **_kwargs):
captured.clear()
captured.update(config)
return {}