Skip to content

Commit 37e1532

Browse files
authored
Merge pull request #105 from ateleris/main
Chrome Binary Pinning
2 parents 7f5e777 + dd4f940 commit 37e1532

2 files changed

Lines changed: 127 additions & 18 deletions

File tree

‎html2pdf4doc/main.py‎

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,20 @@ def __str__(self) -> str:
118118

119119
class ChromeDriverManager:
120120
def get_chrome_driver(
121-
self, path_to_cache_dir: str, verify_ssl: bool = True
121+
self,
122+
path_to_cache_dir: str,
123+
verify_ssl: bool = True,
124+
chrome_binary: Optional[str] = None,
122125
) -> str:
123-
chrome_version: Optional[str] = self.get_chrome_version()
126+
chrome_version: Optional[str] = self.get_chrome_version(chrome_binary)
124127

125128
# If Web Driver Manager cannot detect Chrome, it returns None.
126129
if chrome_version is None:
130+
if chrome_binary is not None:
131+
raise HPDError(
132+
f"Could not determine the Chrome version from --chrome-binary: {chrome_binary!r}.",
133+
exit_code=HPDExitCode.COULD_NOT_FIND_CHROME,
134+
)
127135
raise HPDError(
128136
"Web Driver Manager could not detect an existing Chrome installation.",
129137
exit_code=HPDExitCode.COULD_NOT_FIND_CHROME,
@@ -296,7 +304,30 @@ def send_http_get_request(url: str, verify_ssl: bool = True) -> Response:
296304
) from last_error
297305

298306
@staticmethod
299-
def get_chrome_version() -> Optional[str]:
307+
def _probe_chrome_version(chrome_binary: str) -> str:
308+
# Shared by the macOS special case below and --chrome-binary.
309+
version_output = subprocess.run(
310+
[chrome_binary, "--version"],
311+
capture_output=True,
312+
text=True,
313+
check=True,
314+
)
315+
chrome_version = version_output.stdout.strip()
316+
match = re.search(r"\d+(\.\d+)+", chrome_version)
317+
if not match:
318+
raise RuntimeError("Cannot extract the version part using regex.")
319+
return match.group(0)
320+
321+
@staticmethod
322+
def get_chrome_version(
323+
chrome_binary: Optional[str] = None,
324+
) -> Optional[str]:
325+
if chrome_binary is not None:
326+
try:
327+
return ChromeDriverManager._probe_chrome_version(chrome_binary)
328+
except (OSError, subprocess.CalledProcessError, RuntimeError):
329+
return None
330+
300331
# Special case: GitHub Actions macOS CI machines have both
301332
# Google Chrome for Testing and normal Google Chrome installed, and
302333
# sometimes their versions are of different major version families.
@@ -311,20 +342,9 @@ def get_chrome_version() -> Optional[str]:
311342
"a normal Chrome available."
312343
)
313344

314-
version_output = subprocess.run(
315-
[chrome_path, "--version"],
316-
capture_output=True,
317-
text=True,
318-
check=True,
345+
chrome_version = ChromeDriverManager._probe_chrome_version(
346+
chrome_path
319347
)
320-
chrome_version = version_output.stdout.strip()
321-
match = re.search(r"\d+(\.\d+)+", chrome_version)
322-
if not match:
323-
raise RuntimeError(
324-
"Cannot extract the version part using regex."
325-
)
326-
327-
chrome_version = match.group(0)
328348

329349
print( # noqa: T201
330350
f"html2pdf4doc: Google Chrome for Testing Version: {chrome_version}"
@@ -462,13 +482,16 @@ def create_webdriver(
462482
page_load_timeout: int,
463483
verify_ssl: bool = True,
464484
debug: bool = False,
485+
chrome_binary: Optional[str] = None,
465486
) -> webdriver.Chrome:
466487
print("html2pdf4doc: Creating ChromeDriver service.", flush=True) # noqa: T201
467488

468489
path_to_chrome_driver: str
469490
if chromedriver_argument is None:
470491
path_to_chrome_driver = chrome_driver_manager.get_chrome_driver(
471-
path_to_cache_dir, verify_ssl=verify_ssl
492+
path_to_cache_dir,
493+
verify_ssl=verify_ssl,
494+
chrome_binary=chrome_binary,
472495
)
473496
else:
474497
path_to_chrome_driver = chromedriver_argument
@@ -484,6 +507,8 @@ def create_webdriver(
484507
service = Service(path_to_chrome_driver)
485508

486509
webdriver_options = Options()
510+
if chrome_binary is not None:
511+
webdriver_options.binary_location = chrome_binary
487512
webdriver_options.add_argument("start-maximized")
488513
webdriver_options.add_argument("disable-infobars")
489514
# Doesn't seem to be needed.
@@ -591,6 +616,11 @@ def _main() -> None:
591616
"By default SSL certificate verification is enabled."
592617
),
593618
)
619+
command_parser_get_driver.add_argument(
620+
"--chrome-binary",
621+
type=str,
622+
help="Optional path to a Chrome/Chromium binary. Falls back to $HTML2PDF4DOC_CHROME_BINARY, then auto-detection.",
623+
)
594624

595625
#
596626
# Print command.
@@ -649,6 +679,11 @@ def _main() -> None:
649679
"message is printed and the execution continues."
650680
),
651681
)
682+
command_parser_print.add_argument(
683+
"--chrome-binary",
684+
type=str,
685+
help="Optional path to a Chrome/Chromium binary. Falls back to $HTML2PDF4DOC_CHROME_BINARY, then auto-detection.",
686+
)
652687
command_parser_print.add_argument(
653688
"--strict2",
654689
action="store_true",
@@ -665,6 +700,10 @@ def _main() -> None:
665700

666701
args = parser.parse_args()
667702

703+
chrome_binary: Optional[str] = args.chrome_binary or os.environ.get(
704+
"HTML2PDF4DOC_CHROME_BINARY"
705+
)
706+
668707
chrome_driver_manager = ChromeDriverManager()
669708

670709
path_to_cache_dir: str
@@ -676,6 +715,7 @@ def _main() -> None:
676715
path_to_chrome = chrome_driver_manager.get_chrome_driver(
677716
path_to_cache_dir,
678717
verify_ssl=not args.disable_ssl_check,
718+
chrome_binary=chrome_binary,
679719
)
680720
print(f"html2pdf4doc: ChromeDriver available at path: {path_to_chrome}") # noqa: T201
681721
sys.exit(0)
@@ -695,6 +735,7 @@ def _main() -> None:
695735
page_load_timeout,
696736
verify_ssl=not args.disable_ssl_check,
697737
debug=args.debug,
738+
chrome_binary=chrome_binary,
698739
)
699740

700741
@atexit.register

‎tests/unit/test_chrome_driver_manager.py‎

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import subprocess
12
import tempfile
23
from typing import Any, Dict, Optional
34

@@ -10,7 +11,10 @@
1011

1112
class FailingChromeDriverManager(ChromeDriverManager):
1213
@staticmethod
13-
def get_chrome_version() -> Optional[str]:
14+
def get_chrome_version(
15+
chrome_binary: Optional[str] = None,
16+
) -> Optional[str]:
17+
del chrome_binary
1418
return None
1519

1620

@@ -121,3 +125,67 @@ def fake_get(*args: Any, **kwargs: Any) -> requests.Response:
121125
ChromeDriverManager.send_http_get_request("https://example.com")
122126

123127
assert "--disable-ssl-check" in str(exc_info.value)
128+
129+
130+
def test_get_chrome_version_probes_explicit_binary_directly(
131+
monkeypatch: pytest.MonkeyPatch,
132+
) -> None:
133+
def fake_run(*args: Any, **kwargs: Any) -> Any:
134+
del kwargs
135+
assert args[0] == ["/opt/my-chrome/chrome", "--version"]
136+
return subprocess.CompletedProcess(
137+
args=args[0],
138+
returncode=0,
139+
stdout="Google Chrome for Testing 152.0.7977.82\n",
140+
)
141+
142+
def fail_if_called(*args: Any, **kwargs: Any) -> Any:
143+
del args, kwargs
144+
raise AssertionError(
145+
"OS auto-detection must not run when --chrome-binary is given"
146+
)
147+
148+
monkeypatch.setattr("html2pdf4doc.main.subprocess.run", fake_run)
149+
monkeypatch.setattr(
150+
"html2pdf4doc.main.OperationSystemManager.get_browser_version_from_os",
151+
fail_if_called,
152+
)
153+
154+
version = ChromeDriverManager.get_chrome_version("/opt/my-chrome/chrome")
155+
156+
assert version == "152.0.7977.82"
157+
158+
159+
def test_get_chrome_version_returns_none_when_binary_is_unusable(
160+
monkeypatch: pytest.MonkeyPatch,
161+
) -> None:
162+
def fake_run(*args: Any, **kwargs: Any) -> Any:
163+
del args, kwargs
164+
raise FileNotFoundError("no such file")
165+
166+
monkeypatch.setattr("html2pdf4doc.main.subprocess.run", fake_run)
167+
168+
version = ChromeDriverManager.get_chrome_version("/does/not/exist")
169+
170+
assert version is None
171+
172+
173+
def test_get_chrome_driver_reports_the_bad_binary_path_when_given(
174+
monkeypatch: pytest.MonkeyPatch,
175+
) -> None:
176+
def fake_run(*args: Any, **kwargs: Any) -> Any:
177+
del args, kwargs
178+
raise FileNotFoundError("no such file")
179+
180+
monkeypatch.setattr("html2pdf4doc.main.subprocess.run", fake_run)
181+
182+
chrome_driver_manager = ChromeDriverManager()
183+
184+
with tempfile.TemporaryDirectory() as tmpdir:
185+
with pytest.raises(HPDError) as exc_info:
186+
chrome_driver_manager.get_chrome_driver(
187+
tmpdir, chrome_binary="/does/not/exist"
188+
)
189+
190+
assert exc_info.value.exit_code == HPDExitCode.COULD_NOT_FIND_CHROME
191+
assert "/does/not/exist" in str(exc_info.value)

0 commit comments

Comments
 (0)