| #!/usr/bin/env python3 |
| # Copyright 2021 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import argparse |
| from collections import defaultdict |
| import json |
| import os |
| import subprocess |
| import sys |
| import tempfile |
| |
| from pathlib import Path |
| from chromite.lib.cros_build_lib import IsInsideChroot |
| |
| user_conf = '/etc/make.conf.user' |
| old_conf_line = 'source ${ROOT}/etc/make.conf.binhost' |
| conf_line = 'source /etc/binhost/${BOARD_USE}' |
| |
| def run_prechecks(): |
| if not IsInsideChroot(): |
| print('This script must be run inside the CrOS chroot') |
| sys.exit(1) |
| |
| with open(user_conf, 'r') as f: |
| has_new_conf = False |
| has_old_conf = False |
| for line in f.read().split('\n'): |
| if line == conf_line: |
| has_new_conf = True |
| if line == old_conf_line: |
| has_old_conf = True |
| |
| if not has_new_conf: |
| print(f'Add "{conf_line}" to {user_conf} and rerun.') |
| print('') |
| if has_old_conf: |
| print(f'You have config from a previous version of this ' |
| f'script in {user_conf}.') |
| print(f'Remove the line "{old_conf_line}" and rerun.') |
| print('') |
| |
| if not has_new_conf or has_old_conf: |
| sys.exit(1) |
| |
| subprocess.run( |
| ['sudo', 'mkdir', '-p', '/etc/binhost'], |
| check=True) |
| |
| def write_binhost(board, uri): |
| if uri: |
| data = f'PORTAGE_BINHOST="{uri}"' |
| else: |
| # Make sure to clear the file if there's no BINHOST found. |
| data = '' |
| |
| subprocess.run( |
| ['sudo', 'tee', f'/etc/binhost/{board}'], |
| input=data.encode(), stdout=subprocess.DEVNULL, check=True) |
| |
| def is_internal(): |
| url = subprocess.run( |
| ['git', 'config', '--get', 'remote.origin.url'], |
| stdout=subprocess.PIPE, |
| cwd='/mnt/host/source/.repo/manifests/', |
| check=True).stdout.decode().strip() |
| |
| if 'chrome-internal.googlesource.com/chromeos/manifest-internal' in url: |
| return True |
| |
| if 'chromium.googlesource.com/chromiumos/manifest' in url: |
| return False |
| |
| print(f'Unknown manifest source {url}') |
| sys.exit(1) |
| |
| def get_snapshot_hashes(): |
| # 48 snapshots = 24 hours |
| commit_range = 'HEAD~48..HEAD' |
| |
| internal = subprocess.run( |
| ['git', 'log', commit_range, '--format=%H'], |
| stdout=subprocess.PIPE, |
| cwd='/mnt/host/source/.repo/manifests/', |
| check=True).stdout.decode().split() |
| |
| if not is_internal(): |
| return internal |
| |
| external = subprocess.run( |
| ['git', 'log', commit_range, |
| '--format=%(trailers:key=Cr-External-Snapshot' |
| ',separator=,valueonly)'], |
| stdout=subprocess.PIPE, |
| cwd='/mnt/host/source/.repo/manifests/', |
| check=True).stdout.decode().split() |
| |
| # Flatten the list of (internal, external) pairs. This gives us a |
| # priority order where newer snapshots > older, and then internal |
| # snapshots > external snapshots. |
| return [rev for pair in zip(internal, external) for rev in pair] |
| |
| def download_json(tmp, rev): |
| subprocess.run(['gsutil', '-m', 'cp', '-r', |
| f'gs://chromeos-prebuilt/snapshot/{rev}', |
| tmp], |
| stderr=subprocess.DEVNULL) |
| # Don't check the return code because gsutil returns non-zero if |
| # some of the paths don't exist. |
| |
| def parse_json(tmp, rev): |
| result = [] |
| for curr, _, files in os.walk(tmp / rev): |
| for f in files: |
| with open(Path(curr) / f, 'r') as f: |
| result.append(json.loads(f.read())) |
| |
| return result |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| description=f"""Find up-to-date binary package sources |
| |
| Chrome OS tries to speed up builds by using binary packages taken from |
| CI builds. However, the configuration file that controls which build |
| these packages are taken from is itself a part of the git repo, which |
| means it is effectivly always out of date. See crbug.com/1073565 for |
| more details. |
| |
| This script tries to solve this by looking up recent binary prebuilts |
| for all boards. This is most useful if you sync to the "stable" |
| manifest branch, which will always have up to date prebuilts. If you |
| haven't used this script before, you must add the line |
| {conf_line} |
| to the file {user_conf} in your chroot.""") |
| parser.add_argument( |
| 'board', nargs='?', default='', type=str, |
| help='By default, we try to configure prebuilts for all ' |
| 'available boards. This can be slow. If you only want to ' |
| 'configure a single board, set this option.') |
| parser.add_argument( |
| 'profile', nargs='?', default='base', type=str, |
| help='Portage allows each board to have many profiles which ' |
| 'can have different build options configured, and therefore ' |
| 'require different prebuilts. This script can only configure ' |
| 'a board to use prebuilts from one profile at a time. By ' |
| 'default we use "base" for everything, and this is usually ' |
| 'correct, but if you set up your sysroot by passing the ' |
| '"--profile" option to setup_board you will need to set this. ' |
| 'Note that this is not persistent. If you run the default ' |
| 'form of this command any binhosts you set with this ' |
| 'will be overwritten.') |
| |
| args = parser.parse_args() |
| |
| run_prechecks() |
| |
| # Snapshot hashes are listed newest-to-oldest, so in descending |
| # order of priority. |
| revs = get_snapshot_hashes() |
| |
| # Files in /etc/binhost overwrite the normal BINHOST selection, so |
| # we don't want to leave stale data in there ever. Make sure |
| # binhost_map contains an entry for every file we manage. If we |
| # didn't get any results, we will clear the file in write_binhost. |
| # |
| # If we're only querying a single builder, don't worry about |
| # it. Not getting a result for a board doesn't mean that file is |
| # stale. |
| binhost_map = defaultdict(str) |
| if not args.board: |
| for path in os.listdir('/etc/binhost'): |
| binhost_map[path] = '' |
| else: |
| binhost_map[args.board] = '' |
| |
| tmp = tempfile.TemporaryDirectory() |
| tmp_path = Path(tmp.name) |
| binhosts_found = 0 |
| for rev in revs: |
| download_json(tmp_path, rev) |
| builds = parse_json(tmp_path, rev) |
| |
| for build in builds: |
| if build['profile']['name'] != args.profile: |
| continue |
| |
| if args.board and build['buildTarget']['name'] != args.board: |
| continue |
| |
| # Higher priority build already found |
| if binhost_map[build['buildTarget']['name']]: |
| continue |
| |
| binhost_map[build['buildTarget']['name']] = build['location'] |
| binhosts_found += 1 |
| |
| # if args.board is set, we only need to find one build. |
| if args.board and binhosts_found: |
| break |
| |
| # Rough estimate of the total number of boards |
| if binhosts_found > 135: |
| break |
| |
| for board, uri in binhost_map.items(): |
| write_binhost(board, uri) |
| |
| print(f'Found binhosts for {binhosts_found} boards') |
| |
| if __name__ == '__main__': |
| main() |