xref: /aosp_15_r20/external/angle/build/3pp_common/common.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
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