1# Copyright 2024 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import argparse 6import logging 7import os 8import pathlib 9import shlex 10import shutil 11import subprocess 12import sys 13import tarfile 14import tempfile 15import time 16import urllib.request 17 18import scripthash 19 20_THIS_DIR = pathlib.Path(__file__).resolve().parent 21_SRC_ROOT = _THIS_DIR.parents[1] 22_CHECKOUT_SRC_ROOT_SUBDIR = '.3pp/chromium' 23 24 25def parse_args(): 26 parser = argparse.ArgumentParser() 27 # TODO(agrieve): Add required=True once 3pp builds with > python3.6. 28 subparsers = parser.add_subparsers() 29 30 subparser = subparsers.add_parser( 31 'latest', help='Prints the version as $LATEST.$RUNTIME_DEPS_HASH') 32 subparser.set_defaults(action='latest') 33 34 subparser = subparsers.add_parser( 35 'checkout', help='Copies files into the workdir used by docker') 36 subparser.add_argument('checkout_dir') 37 subparser.add_argument('--version', help='Output from "latest"') 38 subparser.set_defaults(action='checkout') 39 40 subparser = subparsers.add_parser( 41 'install', 42 help=('Run from workdir, inside docker container. ' 43 'Builds & copies outputs into |output_prefix| directory')) 44 subparser.add_argument('output_prefix', 45 help='The path to install the compiled package to.') 46 subparser.add_argument('deps_prefix', 47 help='The path to a directory containing all deps.') 48 subparser.add_argument('--version', help='Output from "latest"') 49 subparser.add_argument('--checkout-dir', help='Directory to use as CWD') 50 subparser.set_defaults(action='install') 51 52 subparser = subparsers.add_parser( 53 'local-test', help='Run latest / checkout / install locally') 54 subparser.add_argument('--checkout-dir', 55 default='3pp_workdir', 56 help='Workdir to use') 57 subparser.add_argument('--output-prefix', 58 default='3pp_out', 59 help='Directory for final artifacts') 60 subparser.set_defaults(action='local-test') 61 62 args = parser.parse_args() 63 if not hasattr(args, 'action'): 64 parser.print_help() 65 sys.exit(1) 66 67 if hasattr(args, 'version'): 68 args.version = args.version or os.environ.get('_3PP_VERSION') 69 if not args.version: 70 parser.error('Must set --version or _3PP_VERSION') 71 if hasattr(args, 'output_prefix') and args.output_prefix: 72 args.output_prefix = os.path.abspath(args.output_prefix) 73 if hasattr(args, 'checkout_dir') and args.checkout_dir: 74 args.checkout_dir = os.path.abspath(args.checkout_dir) 75 76 if args.action == 'checkout': 77 # 3pp bot recipe does this, so needed only when running locally. 78 os.makedirs(args.checkout_dir, exist_ok=True) 79 80 if args.action == 'install': 81 if args.checkout_dir: 82 logging.info('Setting CWD=%s', args.checkout_dir) 83 os.chdir(args.checkout_dir) 84 85 if not os.path.exists(_CHECKOUT_SRC_ROOT_SUBDIR): 86 parser.error(f'Does not exist: {_CHECKOUT_SRC_ROOT_SUBDIR}.' 87 f' Use --checkout-dir?') 88 89 # 3pp bot recipe does this, so needed only when running locally. 90 os.makedirs(args.output_prefix, exist_ok=True) 91 92 return args 93 94 95def path_within_checkout(subpath): 96 return os.path.abspath(os.path.join(_CHECKOUT_SRC_ROOT_SUBDIR, subpath)) 97 98 99def _all_files(path): 100 if os.path.isfile(path): 101 return [path] 102 assert os.path.isdir(path), 'Not a file or dir: ' + path 103 all_paths = pathlib.Path(path).glob('**/*') 104 return [str(f) for f in all_paths if f.is_file()] 105 106 107def _resolve_runtime_deps(runtime_deps): 108 ret = [] 109 for p in runtime_deps: 110 if p.startswith('//'): 111 ret.append(os.path.relpath(str(_SRC_ROOT / p[2:]))) 112 elif os.path.isabs(p): 113 ret.append(os.path.relpath(p)) 114 else: 115 ret.append(p) 116 return ret 117 118 119def copy_runtime_deps(checkout_dir, runtime_deps): 120 # Make 3pp_common scripts available in the docker container install.py 121 # will run in. 122 dest_dir = os.path.join(checkout_dir, _CHECKOUT_SRC_ROOT_SUBDIR) 123 124 for src_path in _resolve_runtime_deps(runtime_deps): 125 relpath = os.path.relpath(src_path, _SRC_ROOT) 126 dest_path = os.path.join(dest_dir, relpath) 127 os.makedirs(os.path.dirname(dest_path), exist_ok=True) 128 if os.path.isfile(src_path): 129 shutil.copy(src_path, dest_path) 130 else: 131 shutil.copytree(src_path, 132 dest_path, 133 ignore=shutil.ignore_patterns('.*', '__pycache__')) 134 logging.info('Runtime deps:') 135 sys.stderr.write('\n'.join(_all_files(checkout_dir)) + '\n') 136 137 138def download_file(url, dest): 139 logging.info('Downloading %s', url) 140 with urllib.request.urlopen(url) as r: 141 with open(dest, 'wb') as f: 142 shutil.copyfileobj(r, f) 143 144 145def extract_tar(path, dest): 146 logging.info('Extracting %s to %s', path, dest) 147 with tarfile.open(path) as f: 148 f.extractall(dest) 149 150 151def run_cmd(cmd, check=True, *args, **kwargs): 152 logging.info('Running: %s', shlex.join(cmd)) 153 return subprocess.run(cmd, check=check, *args, **kwargs) 154 155 156def apply_patches(patches_dir, checkout_dir): 157 for path in sorted(pathlib.Path(patches_dir).glob('*.patch')): 158 cmd = ['git', 'apply', '-v', str(path)] 159 run_cmd(cmd, cwd=checkout_dir) 160 161 162def main(*, do_latest, do_install, runtime_deps): 163 logging.basicConfig( 164 level=logging.DEBUG, 165 format='%(levelname).1s %(relativeCreated)6d %(message)s') 166 args = parse_args() 167 runtime_deps = [str(_THIS_DIR)] + runtime_deps 168 169 if args.action == 'local-test': 170 logging.warning('Will use work dir: %s', args.checkout_dir) 171 logging.warning('Will use output dir: %s', args.output_prefix) 172 if os.path.exists(args.checkout_dir) and os.listdir(args.checkout_dir): 173 logging.warning( 174 '*** Work dir not empty. This often causes failures. ***') 175 time.sleep(4) 176 # Approximates what 3pp recipe does for minimal configs. 177 # https://source.chromium.org/search?q=symbol:Chromium3ppApi.execute&ss=chromium 178 prog = os.path.abspath(sys.argv[0]) 179 cmd = [prog, 'latest'] 180 version = run_cmd(cmd, stdout=subprocess.PIPE, text=True).stdout 181 os.environ['_3PP_VERSION'] = version 182 checkout_dir = args.checkout_dir 183 run_cmd([prog, 'checkout', checkout_dir]) 184 run_cmd([prog, 'install', args.output_prefix, 'UNUSED-DEPS-DIR'], 185 cwd=checkout_dir) 186 logging.warning('Local test complete.') 187 return 188 189 if args.action == 'latest': 190 version = do_latest() 191 assert version, 'do_latest() returned ' + repr(version) 192 extra_paths = [] 193 for p in _resolve_runtime_deps(runtime_deps): 194 extra_paths += _all_files(p) 195 deps_hash = scripthash.compute(extra_paths=extra_paths) 196 print(f'{version}.{deps_hash}') 197 return 198 199 # Remove the hash at the end: 30.4.0-alpha05.HASH => 30.4.0-alpha05 200 args.version = args.version.rsplit('.', 1)[0] 201 if args.action == 'checkout': 202 copy_runtime_deps(args.checkout_dir, runtime_deps) 203 return 204 205 assert args.action == 'install' 206 do_install(args) 207 prefix_len = len(args.output_prefix) + 1 208 logging.info( 209 'Contents of %s: \n%s\n', args.output_prefix, 210 '\n'.join(p[prefix_len:] for p in _all_files(args.output_prefix))) 211