chromix 152.0.7977.82__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, xiaozhou26
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: chromix
3
+ Version: 152.0.7977.82
4
+ Summary: Drive the Chromix stealth Chromium engine with a CloakBrowser-compatible API.
5
+ Author: xiaozhou26
6
+ License: BSD 3-Clause License
7
+
8
+ Copyright (c) 2026, xiaozhou26
9
+ All rights reserved.
10
+
11
+ Redistribution and use in source and binary forms, with or without
12
+ modification, are permitted provided that the following conditions are met:
13
+
14
+ 1. Redistributions of source code must retain the above copyright notice, this
15
+ list of conditions and the following disclaimer.
16
+
17
+ 2. Redistributions in binary form must reproduce the above copyright notice,
18
+ this list of conditions and the following disclaimer in the documentation
19
+ and/or other materials provided with the distribution.
20
+
21
+ 3. Neither the name of the copyright holder nor the names of its
22
+ contributors may be used to endorse or promote products derived from
23
+ this software without specific prior written permission.
24
+
25
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
26
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
27
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
28
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
29
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
30
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
31
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
33
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35
+
36
+ Project-URL: Homepage, https://github.com/xiaozhou26/Chromix
37
+ Project-URL: Source, https://github.com/xiaozhou26/Chromix
38
+ Project-URL: Issues, https://github.com/xiaozhou26/Chromix/issues
39
+ Keywords: chromium,stealth,browser,automation,anti-bot,cloakbrowser
40
+ Classifier: License :: OSI Approved :: BSD License
41
+ Classifier: Programming Language :: Python :: 3
42
+ Classifier: Operating System :: Microsoft :: Windows
43
+ Requires-Python: >=3.8
44
+ Description-Content-Type: text/markdown
45
+ License-File: LICENSE
46
+ Provides-Extra: playwright
47
+ Requires-Dist: playwright; extra == "playwright"
48
+ Dynamic: license-file
49
+
50
+ # chromix (Python)
51
+
52
+ Drive the Chromix stealth Chromium engine with a **CloakBrowser-compatible API** —
53
+ function names, keyword arguments, return types (Playwright `Browser` / `BrowserContext`)
54
+ and `CLOAKBROWSER_*` env-var names all match the [`cloakbrowser`](https://github.com/CloakHQ/CloakBrowser)
55
+ wrapper, so existing CloakBrowser scripts run on Chromix by changing only the import:
56
+
57
+ ```diff
58
+ - from cloakbrowser import launch
59
+ + from chromix import launch
60
+ ```
61
+
62
+ ```python
63
+ from chromix import launch
64
+
65
+ browser = launch(proxy="http://user:pass@proxy:8080", geoip=True, humanize=True)
66
+ page = browser.new_page()
67
+ page.goto("https://example.com")
68
+ browser.close()
69
+ ```
70
+
71
+ ## Install
72
+
73
+ ```bash
74
+ pip install chromix playwright
75
+ ```
76
+
77
+ The distribution and import package are both named `chromix`. To install the
78
+ SDK directly from a repository checkout instead, run:
79
+
80
+ ```bash
81
+ pip install ./sdk/python playwright
82
+ ```
83
+
84
+ On first launch the stealth Chromium binary is downloaded from this repo's GitHub
85
+ Release, SHA256-verified, and cached under `~/.cache/chromix`. Point
86
+ `CLOAKBROWSER_BINARY_PATH` at a local build (e.g. your own `chrome.exe`) to skip
87
+ the download.
88
+
89
+ ## API
90
+
91
+ | Function | Description |
92
+ |---|---|
93
+ | `launch(**opts)` | Returns a Playwright `Browser` |
94
+ | `launch_async(**opts)` | Async variant |
95
+ | `launch_context(**opts)` | Returns a `BrowserContext` (viewport/locale/color_scheme pre-set) |
96
+ | `launch_context_async(**opts)` | Async variant |
97
+ | `launch_persistent_context(user_data_dir, **opts)` | Persistent profile |
98
+ | `launch_persistent_context_async(user_data_dir, **opts)` | Async variant |
99
+ | `build_args` / `get_default_stealth_args` | Arg assembly (32-bit random seed + platform claim) |
100
+ | `maybe_resolve_geoip(geoip, proxy, tz, locale, args)` | Egress IP → (tz, locale, exit_ip) |
101
+ | `ensure_binary` / `clear_cache` / `binary_info` / `check_for_update` | Binary management |
102
+ | `HumanConfig` / `resolve_human_config` | Behavioral-layer config (`default` / `careful` presets) |
103
+ | `ProxySettings` | Playwright-shaped proxy TypedDict |
104
+
105
+ Options (`headless, proxy, args, stealth_args, timezone, locale, geoip, humanize,
106
+ human_preset, human_config, extension_paths, license_key, browser_version,
107
+ release_channel, user_agent, viewport, color_scheme`) match CloakBrowser
108
+ name-for-name; `**kwargs` passes through to `playwright.chromium.launch()` /
109
+ `browser.new_context()`.
110
+
111
+ ## Env vars
112
+
113
+ - `CLOAKBROWSER_BINARY_PATH` — use a local chrome binary instead of downloading
114
+ - `CLOAKBROWSER_VERSION` / `CLOAKBROWSER_RELEASE_CHANNEL` — pin a version/channel
115
+ - `CLOAKBROWSER_GEOIP_TIMEOUT_SECONDS` — geoip lookup timeout
116
+ - `CLOAKBROWSER_WIDEVINE_CDM` — explicit Widevine CDM dir (DRM); `CLOAKBROWSER_WIDEVINE=0` disables DRM
117
+ - `CHROMIX_CACHE_DIR` / `CHROMIX_DOWNLOAD_HOST` — cache location / release host override
118
+
119
+ High-risk engine ports are available only through explicit browser `args`:
120
+
121
+ ```python
122
+ browser = launch(args=[
123
+ "--fingerprint-devtools-runtime-suppression",
124
+ "--fingerprint-canvas-bridge=127.0.0.1:9228",
125
+ "--fingerprint-canvas-bridge-unsafe",
126
+ "--fingerprint-webrtc-fake-srflx=203.0.113.20",
127
+ "--fingerprint-webrtc-fake-srflx-allow-udp",
128
+ ])
129
+ ```
130
+
131
+ Runtime suppression can break console/binding-based automation. Canvas Bridge
132
+ removes the sandbox from bridge renderer processes and forwards canvas/WebGL
133
+ operations to the configured endpoint. Fake srflx does not enable non-proxied
134
+ UDP unless the separate `allow-udp` flag is supplied.
135
+
136
+ ## CLI
137
+
138
+ ```bash
139
+ python -m chromix install # pre-download the binary
140
+ python -m chromix info # binary / cache info
141
+ python -m chromix widevine # fetch the Widevine CDM (Linux x64)
142
+ python -m chromix clear-cache
143
+ ```
144
+
145
+ ## Intentional differences from CloakBrowser
146
+
147
+ 1. `license_key` is accepted and ignored (one open tier).
148
+ 2. `geoip` queries ip-api.com over HTTP instead of a local GeoLite2 database;
149
+ explicit `timezone=` / `locale=` always win.
150
+ 3. No `cloakbrowser/puppeteer` subpath — use the Playwright surface.
151
+ 4. Widevine is enabled automatically when a CDM is present (installed Chrome,
152
+ `CLOAKBROWSER_WIDEVINE_CDM`, or `python -m chromix widevine`).
153
+
154
+ ## License
155
+
156
+ The Python SDK is available under the BSD 3-Clause License. See
157
+ [`LICENSE`](LICENSE).
@@ -0,0 +1,108 @@
1
+ # chromix (Python)
2
+
3
+ Drive the Chromix stealth Chromium engine with a **CloakBrowser-compatible API** —
4
+ function names, keyword arguments, return types (Playwright `Browser` / `BrowserContext`)
5
+ and `CLOAKBROWSER_*` env-var names all match the [`cloakbrowser`](https://github.com/CloakHQ/CloakBrowser)
6
+ wrapper, so existing CloakBrowser scripts run on Chromix by changing only the import:
7
+
8
+ ```diff
9
+ - from cloakbrowser import launch
10
+ + from chromix import launch
11
+ ```
12
+
13
+ ```python
14
+ from chromix import launch
15
+
16
+ browser = launch(proxy="http://user:pass@proxy:8080", geoip=True, humanize=True)
17
+ page = browser.new_page()
18
+ page.goto("https://example.com")
19
+ browser.close()
20
+ ```
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install chromix playwright
26
+ ```
27
+
28
+ The distribution and import package are both named `chromix`. To install the
29
+ SDK directly from a repository checkout instead, run:
30
+
31
+ ```bash
32
+ pip install ./sdk/python playwright
33
+ ```
34
+
35
+ On first launch the stealth Chromium binary is downloaded from this repo's GitHub
36
+ Release, SHA256-verified, and cached under `~/.cache/chromix`. Point
37
+ `CLOAKBROWSER_BINARY_PATH` at a local build (e.g. your own `chrome.exe`) to skip
38
+ the download.
39
+
40
+ ## API
41
+
42
+ | Function | Description |
43
+ |---|---|
44
+ | `launch(**opts)` | Returns a Playwright `Browser` |
45
+ | `launch_async(**opts)` | Async variant |
46
+ | `launch_context(**opts)` | Returns a `BrowserContext` (viewport/locale/color_scheme pre-set) |
47
+ | `launch_context_async(**opts)` | Async variant |
48
+ | `launch_persistent_context(user_data_dir, **opts)` | Persistent profile |
49
+ | `launch_persistent_context_async(user_data_dir, **opts)` | Async variant |
50
+ | `build_args` / `get_default_stealth_args` | Arg assembly (32-bit random seed + platform claim) |
51
+ | `maybe_resolve_geoip(geoip, proxy, tz, locale, args)` | Egress IP → (tz, locale, exit_ip) |
52
+ | `ensure_binary` / `clear_cache` / `binary_info` / `check_for_update` | Binary management |
53
+ | `HumanConfig` / `resolve_human_config` | Behavioral-layer config (`default` / `careful` presets) |
54
+ | `ProxySettings` | Playwright-shaped proxy TypedDict |
55
+
56
+ Options (`headless, proxy, args, stealth_args, timezone, locale, geoip, humanize,
57
+ human_preset, human_config, extension_paths, license_key, browser_version,
58
+ release_channel, user_agent, viewport, color_scheme`) match CloakBrowser
59
+ name-for-name; `**kwargs` passes through to `playwright.chromium.launch()` /
60
+ `browser.new_context()`.
61
+
62
+ ## Env vars
63
+
64
+ - `CLOAKBROWSER_BINARY_PATH` — use a local chrome binary instead of downloading
65
+ - `CLOAKBROWSER_VERSION` / `CLOAKBROWSER_RELEASE_CHANNEL` — pin a version/channel
66
+ - `CLOAKBROWSER_GEOIP_TIMEOUT_SECONDS` — geoip lookup timeout
67
+ - `CLOAKBROWSER_WIDEVINE_CDM` — explicit Widevine CDM dir (DRM); `CLOAKBROWSER_WIDEVINE=0` disables DRM
68
+ - `CHROMIX_CACHE_DIR` / `CHROMIX_DOWNLOAD_HOST` — cache location / release host override
69
+
70
+ High-risk engine ports are available only through explicit browser `args`:
71
+
72
+ ```python
73
+ browser = launch(args=[
74
+ "--fingerprint-devtools-runtime-suppression",
75
+ "--fingerprint-canvas-bridge=127.0.0.1:9228",
76
+ "--fingerprint-canvas-bridge-unsafe",
77
+ "--fingerprint-webrtc-fake-srflx=203.0.113.20",
78
+ "--fingerprint-webrtc-fake-srflx-allow-udp",
79
+ ])
80
+ ```
81
+
82
+ Runtime suppression can break console/binding-based automation. Canvas Bridge
83
+ removes the sandbox from bridge renderer processes and forwards canvas/WebGL
84
+ operations to the configured endpoint. Fake srflx does not enable non-proxied
85
+ UDP unless the separate `allow-udp` flag is supplied.
86
+
87
+ ## CLI
88
+
89
+ ```bash
90
+ python -m chromix install # pre-download the binary
91
+ python -m chromix info # binary / cache info
92
+ python -m chromix widevine # fetch the Widevine CDM (Linux x64)
93
+ python -m chromix clear-cache
94
+ ```
95
+
96
+ ## Intentional differences from CloakBrowser
97
+
98
+ 1. `license_key` is accepted and ignored (one open tier).
99
+ 2. `geoip` queries ip-api.com over HTTP instead of a local GeoLite2 database;
100
+ explicit `timezone=` / `locale=` always win.
101
+ 3. No `cloakbrowser/puppeteer` subpath — use the Playwright surface.
102
+ 4. Widevine is enabled automatically when a CDM is present (installed Chrome,
103
+ `CLOAKBROWSER_WIDEVINE_CDM`, or `python -m chromix widevine`).
104
+
105
+ ## License
106
+
107
+ The Python SDK is available under the BSD 3-Clause License. See
108
+ [`LICENSE`](LICENSE).
@@ -0,0 +1,46 @@
1
+ """chromix — drive the Chromix stealth Chromium engine with a CloakBrowser-compatible API.
2
+
3
+ Function names, keyword arguments, return types (Playwright ``Browser`` /
4
+ ``BrowserContext``) and ``CLOAKBROWSER_*`` env-var names all match the
5
+ ``cloakbrowser`` wrapper (github.com/CloakHQ/CloakBrowser), so existing
6
+ CloakBrowser scripts run on Chromix by changing only the import:
7
+
8
+ - from cloakbrowser import launch
9
+ + from chromix import launch
10
+
11
+ from chromix import launch
12
+
13
+ browser = launch(proxy="http://user:pass@proxy:8080", geoip=True, humanize=True)
14
+ page = browser.new_page()
15
+ page.goto("https://example.com")
16
+ browser.close()
17
+ """
18
+ from .api import (
19
+ HumanConfig,
20
+ ProxySettings,
21
+ binary_info,
22
+ build_args,
23
+ check_for_update,
24
+ clear_cache,
25
+ ensure_binary,
26
+ get_default_stealth_args,
27
+ launch,
28
+ launch_async,
29
+ launch_context,
30
+ launch_context_async,
31
+ launch_persistent_context,
32
+ launch_persistent_context_async,
33
+ maybe_resolve_geoip,
34
+ resolve_human_config,
35
+ )
36
+
37
+ __version__ = "152.0.7977.82"
38
+
39
+ __all__ = [
40
+ "launch", "launch_async", "launch_context", "launch_context_async",
41
+ "launch_persistent_context", "launch_persistent_context_async",
42
+ "ProxySettings", "build_args", "maybe_resolve_geoip",
43
+ "get_default_stealth_args", "ensure_binary", "clear_cache",
44
+ "binary_info", "check_for_update", "HumanConfig", "resolve_human_config",
45
+ "__version__",
46
+ ]
@@ -0,0 +1,40 @@
1
+ """CLI: ``python -m chromix`` — pre-download / inspect / manage the binary."""
2
+ import argparse
3
+ import json
4
+ import sys
5
+
6
+ from . import __version__, binary_info, clear_cache, ensure_binary
7
+
8
+
9
+ def main():
10
+ ap = argparse.ArgumentParser(
11
+ prog="chromix",
12
+ description="Manage the Chromix stealth Chromium binary.")
13
+ sub = ap.add_subparsers(dest="cmd")
14
+ sub.add_parser("install", help="download the stealth binary now")
15
+ sub.add_parser("info", help="show binary / cache info as JSON")
16
+ sub.add_parser("clear-cache", help="delete all cached binaries")
17
+ sub.add_parser("widevine", help="fetch the Widevine CDM (Linux x64) for DRM playback")
18
+ ap.add_argument("--version", action="version", version=f"chromix {__version__}")
19
+ args = ap.parse_args()
20
+
21
+ if args.cmd == "install":
22
+ print(ensure_binary())
23
+ elif args.cmd == "info":
24
+ print(json.dumps(binary_info(), indent=2))
25
+ elif args.cmd == "clear-cache":
26
+ clear_cache()
27
+ print("cache cleared")
28
+ elif args.cmd == "widevine":
29
+ from .widevine import ensure_widevine
30
+ cdm = ensure_widevine()
31
+ if cdm:
32
+ print(f"CDM ready: {cdm}")
33
+ else:
34
+ sys.exit("no Widevine CDM source available on this platform")
35
+ else:
36
+ ap.print_help()
37
+
38
+
39
+ if __name__ == "__main__":
40
+ main()
@@ -0,0 +1,107 @@
1
+ """Binary management for chromix: download, verify, cache the stealth Chromium bundle.
2
+
3
+ Detects the platform, downloads the matching bundle from the GitHub Release,
4
+ verifies it against SHA256SUMS, and caches it under ``~/.cache/chromix``.
5
+ """
6
+ from __future__ import annotations
7
+ import hashlib
8
+ import os
9
+ import platform
10
+ import sys
11
+ import tarfile
12
+ import urllib.request
13
+ import zipfile
14
+ from pathlib import Path
15
+
16
+ _REPO = "xiaozhou26/Chromix"
17
+ # Release channels track the latest verified binary for each supported major.
18
+ # Source/package versions may move ahead while the staged Chromium build runs.
19
+ _CHANNELS = {
20
+ "stable": {"tag": "v151.0.7922.173"},
21
+ "latest": {"tag": "v152.0.7977.75"},
22
+ }
23
+ _CACHE = Path(os.environ.get("CHROMIX_CACHE_DIR",
24
+ Path.home() / ".cache" / "chromix"))
25
+
26
+
27
+ def _host(tag: str) -> str:
28
+ return os.environ.get("CHROMIX_DOWNLOAD_HOST",
29
+ f"https://github.com/{_REPO}/releases/download/{tag}")
30
+
31
+
32
+ # platform key -> (release asset, archive kind, launcher relative path)
33
+ _ASSETS = {
34
+ "linux-x64": ("chromix-linux-x64.tar.gz", "tar", "chromix/chromix"),
35
+ "win-x64": ("chromix-win-x64.zip", "zip", "chromix/chromix.cmd"),
36
+ "mac-arm64": ("chromix-mac-arm64.tar.gz", "tar", "chromix/chromix"),
37
+ "mac-x64": ("chromix-mac-x64.tar.gz", "tar", "chromix/chromix"),
38
+ }
39
+
40
+
41
+ def resolve_platform() -> str | None:
42
+ sysname, mach = platform.system(), platform.machine().lower()
43
+ if sysname == "Linux" and mach in ("x86_64", "amd64"):
44
+ return "linux-x64"
45
+ if sysname == "Windows" and mach in ("amd64", "x86_64"):
46
+ return "win-x64"
47
+ if sysname == "Darwin":
48
+ return "mac-arm64" if mach in ("arm64", "aarch64") else "mac-x64"
49
+ return None
50
+
51
+
52
+ def _sha256(path: Path) -> str:
53
+ h = hashlib.sha256()
54
+ with open(path, "rb") as fh:
55
+ for chunk in iter(lambda: fh.read(1 << 20), b""):
56
+ h.update(chunk)
57
+ return h.hexdigest()
58
+
59
+
60
+ def _expected_sha(asset: str, host: str) -> str | None:
61
+ """Fetch SHA256SUMS from the release and return the hash for `asset`."""
62
+ try:
63
+ with urllib.request.urlopen(f"{host}/SHA256SUMS", timeout=30) as r:
64
+ for line in r.read().decode().splitlines():
65
+ parts = line.split()
66
+ if len(parts) == 2 and parts[1].lstrip("*") == asset:
67
+ return parts[0].lower()
68
+ except Exception:
69
+ return None
70
+ return None
71
+
72
+
73
+ def _download(plat: str, host: str, tag: str) -> Path:
74
+ """Ensure the bundle for `plat` is present + verified; return the launcher path."""
75
+ asset, kind, launcher_rel = _ASSETS[plat]
76
+ root = _CACHE / tag / plat # cache per release tag so channels don't collide
77
+ launcher = root / launcher_rel
78
+ if launcher.exists():
79
+ return launcher
80
+ root.mkdir(parents=True, exist_ok=True)
81
+ archive = root / asset
82
+ url = f"{host}/{asset}"
83
+ sys.stderr.write(f"[chromix] downloading {url} ...\n")
84
+ urllib.request.urlretrieve(url, archive)
85
+
86
+ expected = _expected_sha(asset, host)
87
+ if expected:
88
+ actual = _sha256(archive)
89
+ if actual != expected:
90
+ archive.unlink(missing_ok=True)
91
+ raise RuntimeError(f"SHA256 mismatch for {asset}: expected {expected}, got {actual}")
92
+ sys.stderr.write("[chromix] SHA256 verified\n")
93
+ else:
94
+ sys.stderr.write("[chromix] WARNING: no SHA256SUMS published; skipping verification\n")
95
+
96
+ if kind == "tar":
97
+ with tarfile.open(archive) as t:
98
+ t.extractall(root)
99
+ else:
100
+ with zipfile.ZipFile(archive) as z:
101
+ z.extractall(root)
102
+ archive.unlink(missing_ok=True)
103
+ if launcher.exists() and not launcher.name.endswith(".cmd"):
104
+ launcher.chmod(0o755)
105
+ if not launcher.exists():
106
+ raise RuntimeError(f"bundle extracted but launcher missing: {launcher}")
107
+ return launcher
@@ -0,0 +1,46 @@
1
+ """Linux Fontconfig wiring for the bundled Windows font assets."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import sys
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ def linux_font_env(executable: str | os.PathLike) -> dict[str, str]:
12
+ """Return ``FONTCONFIG_FILE`` when a Linux bundle contains ``fonts/``."""
13
+ if sys.platform != "linux":
14
+ return {}
15
+
16
+ fonts_dir = Path(executable).resolve().parent / "fonts"
17
+ template = fonts_dir / "fonts.conf.template"
18
+ if not template.is_file():
19
+ return {}
20
+
21
+ try:
22
+ cache_dir = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) \
23
+ / "chromix" / "fontconfig"
24
+ cache_dir.mkdir(parents=True, exist_ok=True)
25
+ config = template.read_text(encoding="utf-8")
26
+ config = config.replace("@FONTS_DIR@", str(fonts_dir))
27
+ config = config.replace("@CACHE_DIR@", str(cache_dir))
28
+ config_path = Path(tempfile.gettempdir()) / f"chromix-fontconfig-{os.getuid()}.conf"
29
+ config_path.write_text(config, encoding="utf-8")
30
+ return {"FONTCONFIG_FILE": str(config_path)}
31
+ except OSError:
32
+ return {}
33
+
34
+
35
+ def apply_font_env(executable: str | os.PathLike,
36
+ launch_kwargs: dict[str, Any]) -> None:
37
+ """Merge the bundled-font environment into Playwright launch options."""
38
+ font_env = linux_font_env(executable)
39
+ user_env = launch_kwargs.get("env")
40
+ if not font_env and user_env is None:
41
+ return
42
+ merged = dict(os.environ)
43
+ merged.update(font_env)
44
+ if user_env:
45
+ merged.update(user_env)
46
+ launch_kwargs["env"] = merged