90 lines
4.1 KiB
Python
90 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Fetch pinned native cross-build dependencies without cloning Git history."""
|
|
import argparse
|
|
import ast
|
|
import json
|
|
import hashlib
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
# These dependencies correspond to features explicitly disabled by this recipe.
|
|
# Unknown/new dependencies stay enabled; do not infer a skip from their names.
|
|
DISABLED = {'dng_sdk', 'icu', 'dawn', 'angle2', 'swiftshader', 'emsdk'}
|
|
|
|
|
|
def evaluate(node, variables):
|
|
if isinstance(node, ast.Constant):
|
|
return node.value
|
|
if isinstance(node, ast.Dict):
|
|
return {evaluate(k, variables): evaluate(v, variables) for k, v in zip(node.keys, node.values)}
|
|
if isinstance(node, (ast.List, ast.Tuple)):
|
|
return [evaluate(n, variables) for n in node.elts]
|
|
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
|
|
return evaluate(node.left, variables) + evaluate(node.right, variables)
|
|
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == 'Var' and len(node.args) == 1:
|
|
return variables[evaluate(node.args[0], variables)]
|
|
raise ValueError('Unsupported DEPS expression: ' + ast.dump(node))
|
|
|
|
|
|
def dependencies(source):
|
|
values = {}
|
|
for node in ast.parse((source / 'DEPS').read_text()).body:
|
|
if isinstance(node, ast.Assign):
|
|
for name in node.targets:
|
|
if isinstance(name, ast.Name) and name.id in ('vars', 'deps'):
|
|
values[name.id] = evaluate(node.value, values.get('vars', {}))
|
|
if not values.get('deps'):
|
|
raise ValueError('No dependency map in DEPS')
|
|
return values['deps']
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser(description=__doc__)
|
|
p.add_argument('--source', type=Path, required=True)
|
|
p.add_argument('--cache', type=Path, help='Immutable dependency checkouts shared across version profiles')
|
|
p.add_argument('--no-fetch-gn', action='store_true', help='Use an existing verified GN for a local test')
|
|
a = p.parse_args()
|
|
source = a.source.resolve()
|
|
records = {}
|
|
for relative, value in dependencies(source).items():
|
|
if not isinstance(value, str):
|
|
continue # GN/Ninja CIPD packages are handled by upstream fetch-gn.
|
|
if Path(relative).name in DISABLED:
|
|
continue
|
|
if '..' in Path(relative).parts:
|
|
raise ValueError('Dependency path escapes source: ' + relative)
|
|
link = source / relative
|
|
path = link.absolute()
|
|
if not path.is_relative_to(source):
|
|
raise ValueError('Dependency path escapes source: ' + relative)
|
|
url, revision = value.rsplit('@', 1)
|
|
if not re.fullmatch(r'[0-9a-f]{40}', revision):
|
|
raise ValueError('Dependency is not commit-pinned: ' + value)
|
|
if a.cache:
|
|
key = hashlib.sha256((url + '@' + revision).encode()).hexdigest()
|
|
cached = a.cache.resolve() / key
|
|
if link.is_symlink():
|
|
link.unlink()
|
|
if not link.exists():
|
|
link.parent.mkdir(parents=True, exist_ok=True)
|
|
link.symlink_to(cached, target_is_directory=True)
|
|
path = cached
|
|
if not (path / '.git').exists():
|
|
subprocess.run(['git', 'init', '-q', str(path)], check=True)
|
|
subprocess.run(['git', '-C', str(path), 'remote', 'add', 'origin', url], check=True)
|
|
current = subprocess.run(['git', '-C', str(path), 'rev-parse', 'HEAD'], capture_output=True, text=True)
|
|
if current.stdout.strip() != revision:
|
|
subprocess.run(['git', '-C', str(path), 'remote', 'set-url', 'origin', url], check=True)
|
|
subprocess.run(['git', '-C', str(path), 'fetch', '--depth=1', 'origin', revision], check=True)
|
|
subprocess.run(['git', '-C', str(path), 'checkout', '--detach', revision], check=True)
|
|
records[relative] = revision
|
|
if not a.no_fetch_gn:
|
|
subprocess.run([sys.executable, str(source / 'bin/fetch-gn')], cwd=source, check=True)
|
|
(source / 'native-cross-deps.json').write_text(json.dumps(records, indent=2) + '\n')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|