blob: 970ffdf40b7c17d1db11b011e23c7a01206672cf [file]
#!/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()