| #!/usr/bin/env python3 | |
| # Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file | |
| # for details. All rights reserved. Use of this source code is governed by a | |
| # BSD-style license that can be found in the LICENSE file. | |
| import argparse | |
| import json | |
| import os | |
| import shutil | |
| import stat | |
| import sys | |
| import tempfile | |
| import urllib.request | |
| import zipfile | |
| import utils | |
| VERSION = "148.0.7778.96" # Default version, but can be overridden | |
| API_URL = "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json" | |
| def get_latest_stable_version(): | |
| print("Fetching latest stable Chrome version from googlechromelabs...") | |
| try: | |
| req = urllib.request.Request(API_URL, | |
| headers={'User-Agent': 'Mozilla/5.0'}) | |
| with urllib.request.urlopen(req) as response: | |
| data = json.loads(response.read().decode('utf-8')) | |
| channel_data = data.get("channels", {}).get("Stable") | |
| if channel_data: | |
| return channel_data.get("version") | |
| except Exception as e: | |
| print(f"Failed to fetch latest version: {e}") | |
| return None | |
| def parse_options(): | |
| parser = argparse.ArgumentParser( | |
| description= | |
| 'Update chrome-headless-shell binaries for Playwright in third_party.') | |
| parser.add_argument( | |
| '--version', | |
| help= | |
| f'Chrome version to download. If not specified and --latest is not used, defaults to {VERSION}.' | |
| ) | |
| parser.add_argument( | |
| '--latest', | |
| action='store_true', | |
| help='Download the latest stable version of chrome-headless-shell.') | |
| return parser.parse_args() | |
| def download_and_extract(name, url, extract_to): | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| zip_path = os.path.join(tmpdir, f"{name}.zip") | |
| print(f"Downloading {url} to {zip_path}...") | |
| try: | |
| urllib.request.urlretrieve(url, zip_path) | |
| except Exception as e: | |
| print(f"Failed to download {url}: {e}") | |
| return False | |
| print(f"Extracting {zip_path} to {tmpdir}...") | |
| with zipfile.ZipFile(zip_path, 'r') as zip_ref: | |
| zip_ref.extractall(tmpdir) | |
| # Chrome zip usually contains a single top-level directory | |
| zip_dirs = [ | |
| d for d in os.listdir(tmpdir) | |
| if os.path.isdir(os.path.join(tmpdir, d)) | |
| ] | |
| if not zip_dirs: | |
| print(f"No directory found in zip for {name}") | |
| return False | |
| src_dir = os.path.join(tmpdir, zip_dirs[0]) | |
| print(f"Moving contents of {src_dir} to {extract_to}...") | |
| os.makedirs(extract_to, exist_ok=True) | |
| for item in os.listdir(src_dir): | |
| s = os.path.join(src_dir, item) | |
| d = os.path.join(extract_to, item) | |
| if os.path.exists(d): | |
| if os.path.isdir(d): | |
| shutil.rmtree(d) | |
| else: | |
| os.remove(d) | |
| shutil.move(s, d) | |
| return True | |
| def set_executable(file_path): | |
| if os.path.exists(file_path): | |
| print(f"Making {file_path} executable...") | |
| st = os.stat(file_path) | |
| os.chmod(file_path, | |
| st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) | |
| else: | |
| print(f"Warning: File {file_path} not found to set executable.") | |
| def main(): | |
| args = parse_options() | |
| version = args.version | |
| if args.latest: | |
| version = get_latest_stable_version() | |
| if not version: | |
| print("Error: Could not retrieve latest stable version.") | |
| sys.exit(1) | |
| elif not version: | |
| version = VERSION | |
| print(f"Using Chrome version: {version}") | |
| base_url = f"{utils.CLOUD_STORAGE_URL}/chrome-for-testing-public/{version}" | |
| urls = { | |
| "linux": | |
| f"{base_url}/linux64/chrome-headless-shell-linux64.zip", | |
| "mac-arm64": | |
| f"{base_url}/mac-arm64/chrome-headless-shell-mac-arm64.zip", | |
| "mac-x64": | |
| f"{base_url}/mac-x64/chrome-headless-shell-mac-x64.zip", | |
| "windows": | |
| f"{base_url}/win64/chrome-headless-shell-win64.zip" | |
| } | |
| chrome_headless_dir = os.path.join(utils.THIRD_PARTY, 'chrome_headless') | |
| linux_dir = os.path.join(chrome_headless_dir, 'linux') | |
| mac_dir = os.path.join(chrome_headless_dir, 'mac') | |
| mac_arm_dir = os.path.join(mac_dir, 'mac-arm64') | |
| mac_x64_dir = os.path.join(mac_dir, 'mac-x64') | |
| windows_dir = os.path.join(chrome_headless_dir, 'windows') | |
| # Clean up existing directories if they exist | |
| for d in [linux_dir, mac_dir, windows_dir]: | |
| if os.path.exists(d): | |
| print(f"Removing existing directory {d}...") | |
| shutil.rmtree(d) | |
| # 1. Linux | |
| if download_and_extract("linux", urls["linux"], linux_dir): | |
| set_executable(os.path.join(linux_dir, "chrome-headless-shell")) | |
| # 2. Mac | |
| download_arm = download_and_extract("mac-arm64", urls["mac-arm64"], | |
| mac_arm_dir) | |
| download_x64 = download_and_extract("mac-x64", urls["mac-x64"], mac_x64_dir) | |
| if download_arm and download_x64: | |
| set_executable(os.path.join(mac_arm_dir, "chrome-headless-shell")) | |
| set_executable(os.path.join(mac_x64_dir, "chrome-headless-shell")) | |
| # 3. Windows | |
| download_and_extract("windows", urls["windows"], windows_dir) | |
| print("\nLocal setup complete.") | |
| print("If you want to upload these to Google Storage, run:") | |
| print( | |
| f" (cd {chrome_headless_dir}; upload_to_google_storage.py -a --bucket r8-deps linux)" | |
| ) | |
| print( | |
| f" (cd {chrome_headless_dir}; upload_to_google_storage.py -a --bucket r8-deps mac)" | |
| ) | |
| print( | |
| f" (cd {chrome_headless_dir}; upload_to_google_storage.py -a --bucket r8-deps windows)" | |
| ) | |
| if __name__ == '__main__': | |
| sys.exit(main()) |