| #!/usr/bin/env python3 |
| # Copyright 2026 The Emscripten Authors. All rights reserved. |
| # Emscripten is available under two separate licenses, the MIT license and the |
| # University of Illinois/NCSA Open Source License. Both these licenses can be |
| # found in the LICENSE file. |
| |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| r"""Download the Cross-Origin Storage Chrome extension for COS browser tests. |
| |
| The COS extension polyfills navigator.crossOriginStorage in Chrome so that |
| automated browser tests can exercise the cache-miss and cache-hit paths without |
| requiring a native browser implementation of the API. |
| |
| Usage |
| ----- |
| |
| Run once to download and unpack the extension:: |
| |
| python3 test/setup_cos_extension.py |
| |
| Then pass the printed path as EMTEST_COS_EXTENSION_PATH when running the |
| browser tests:: |
| |
| EMTEST_COS_EXTENSION_PATH=$(python3 test/setup_cos_extension.py) \\ |
| python3 test/runner.py browser.test_cross_origin_storage_miss_then_hit |
| |
| Or, with --print-path suppressed so only the path is printed (suitable for |
| shell variable assignment), use --quiet:: |
| |
| ext=$(python3 test/setup_cos_extension.py --quiet) |
| EMTEST_COS_EXTENSION_PATH=$ext python3 test/runner.py \\ |
| browser.test_cross_origin_storage_fallback \\ |
| browser.test_cross_origin_storage_miss_then_hit |
| |
| The extension is downloaded from its GitHub source repository and unpacked into |
| out/cos_extension/ (relative to the Emscripten root). Re-run with --force to |
| refresh an existing download. |
| |
| Source: https://github.com/web-ai-community/cross-origin-storage-extension |
| """ |
| |
| import argparse |
| import io |
| import os |
| import sys |
| import urllib.request |
| import zipfile |
| |
| # Archive of the main branch of the COS extension source repository. |
| EXTENSION_ARCHIVE_URL = ( |
| 'https://github.com/web-ai-community/cross-origin-storage-extension' |
| '/archive/refs/heads/main.zip' |
| ) |
| |
| # Default destination relative to the Emscripten root (i.e. two levels up from |
| # this script, which lives in test/). |
| _SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) |
| DEFAULT_DEST = os.path.join(_SCRIPT_DIR, '..', 'out', 'cos_extension') |
| |
| |
| def _find_manifest(root): |
| """Return the directory containing manifest.json, searching up to 2 levels. |
| |
| Returns None if root does not exist or no manifest.json is found. |
| """ |
| if not os.path.isdir(root): |
| return None |
| if os.path.exists(os.path.join(root, 'manifest.json')): |
| return root |
| for entry in os.scandir(root): |
| if entry.is_dir(): |
| candidate = os.path.join(entry.path, 'manifest.json') |
| if os.path.exists(candidate): |
| return entry.path |
| return None |
| |
| |
| def download_and_unpack(dest_dir, quiet=False): |
| """Download the extension archive from GitHub and unpack it into dest_dir. |
| |
| Returns the path of the directory that contains manifest.json. |
| """ |
| if not quiet: |
| print('Downloading COS extension from GitHub...', file=sys.stderr) |
| req = urllib.request.Request( |
| EXTENSION_ARCHIVE_URL, |
| headers={'User-Agent': 'emscripten-test-setup'}, |
| ) |
| with urllib.request.urlopen(req) as response: |
| data = response.read() |
| |
| if not quiet: |
| print(f'Unpacking to {dest_dir} ...', file=sys.stderr) |
| os.makedirs(dest_dir, exist_ok=True) |
| |
| with zipfile.ZipFile(io.BytesIO(data)) as zf: |
| # GitHub archives wrap everything in a top-level directory, e.g. |
| # "cross-origin-storage-extension-main/". Strip that prefix. |
| names = zf.namelist() |
| prefix = names[0].split('/')[0] + '/' if names else '' |
| for member in names: |
| if member == prefix: |
| continue |
| rel = member[len(prefix):] |
| if not rel: |
| continue |
| target = os.path.join(dest_dir, rel) |
| if member.endswith('/'): |
| os.makedirs(target, exist_ok=True) |
| else: |
| os.makedirs(os.path.dirname(target), exist_ok=True) |
| with zf.open(member) as src, open(target, 'wb') as dst: |
| dst.write(src.read()) |
| |
| extension_dir = _find_manifest(dest_dir) |
| if extension_dir is None: |
| print( |
| f'ERROR: manifest.json not found anywhere under {dest_dir}.\n' |
| 'The extension repository structure may have changed.\n' |
| 'Please report this at https://github.com/emscripten-core/emscripten/issues', |
| file=sys.stderr, |
| ) |
| sys.exit(1) |
| |
| return extension_dir |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description=__doc__, |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| ) |
| parser.add_argument( |
| '--dest', |
| default=DEFAULT_DEST, |
| help='Directory to unpack the extension into (default: out/cos_extension)', |
| ) |
| parser.add_argument( |
| '--force', |
| action='store_true', |
| help='Re-download even if the extension is already present', |
| ) |
| parser.add_argument( |
| '--quiet', |
| action='store_true', |
| help='Suppress informational messages; print only the extension path', |
| ) |
| args = parser.parse_args() |
| |
| dest = os.path.realpath(args.dest) |
| |
| extension_dir = _find_manifest(dest) |
| if extension_dir and not args.force: |
| if not args.quiet: |
| print( |
| f'Extension already present at {extension_dir} ' |
| f'(use --force to re-download)', |
| file=sys.stderr, |
| ) |
| else: |
| extension_dir = download_and_unpack(dest, quiet=args.quiet) |
| if not args.quiet: |
| print(f'Extension ready at {extension_dir}', file=sys.stderr) |
| |
| # Always print the path as the last line so callers can capture it with |
| # $(...) or --quiet. |
| print(extension_dir) |
| |
| |
| if __name__ == '__main__': |
| main() |