| # Copyright 2024 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Utils for interacting with builders & builder props in src.""" |
| |
| import json |
| import logging |
| import pathlib |
| import subprocess |
| |
| _THIS_DIR = pathlib.Path(__file__).resolve().parent |
| _SRC_DIR = _THIS_DIR.parents[1] |
| # TODO(crbug.com/41492688): Support src-internal configs too. When this is done, |
| # ensure tools/utr/recipe.py is not using the public reclient instance |
| _BUILDER_PROP_DIRS = _SRC_DIR.joinpath('infra', 'config', 'generated', |
| 'builders') |
| _INTERNAL_BUILDER_PROP_DIRS = _SRC_DIR.joinpath('internal', 'infra', 'config', |
| 'generated', 'builders') |
| |
| |
| def find_builder_props(builder_name, bucket_name=None, project_name=None): |
| """Finds the checked-in json props file for the builder. |
| |
| Args: |
| builder_name: Builder name of the builder |
| bucket_name: Bucket name of the builder |
| project_name: Project name of the builder |
| |
| Returns: |
| Tuple of (Dict of the builder's input props, LUCI project of the builder). |
| Both elements will be None if the builder wasn't found. |
| """ |
| |
| def _walk_props_dir(props_dir): |
| matches = [] |
| if not props_dir.exists(): |
| return matches |
| for bucket_path in props_dir.iterdir(): |
| if not bucket_path.is_dir() or (bucket_name |
| and bucket_path.name != bucket_name): |
| continue |
| for builder_path in bucket_path.iterdir(): |
| if builder_path.name != builder_name: |
| continue |
| prop_file = builder_path.joinpath('properties.json') |
| if not prop_file.exists(): |
| logging.warning( |
| 'Found generated dir for builder at %s, but no prop file?', |
| builder_path) |
| continue |
| matches.append(prop_file) |
| return matches |
| |
| possible_matches = [] |
| if not project_name or project_name.startswith('chrome'): |
| matches = _walk_props_dir(_INTERNAL_BUILDER_PROP_DIRS) |
| if matches: |
| project_name = project_name or 'chrome' |
| possible_matches += matches |
| |
| if not project_name or project_name.startswith('chromium'): |
| matches = _walk_props_dir(_BUILDER_PROP_DIRS) |
| if matches: |
| project_name = project_name or 'chromium' |
| possible_matches += matches |
| |
| if not possible_matches: |
| if (project_name and project_name.startswith('chrome') |
| and not _INTERNAL_BUILDER_PROP_DIRS.exists()): |
| logging.warning( |
| '[red]%s looks like an internal builder, but src-internal is not ' |
| 'present in this checkout. Make sure checkout_src_internal is set to ' |
| 'True in your .gclient file and run `gclient sync`.[/]', builder_name) |
| # Try also fetching the props from buildbucket. This will give us needed |
| # vals like recipe and builder-group name for builders that aren't |
| # bootstrapped. |
| if bucket_name and project_name: |
| logging.info( |
| 'Prop file not found, attempting to fetch props from buildbucket.') |
| props = fetch_props_from_buildbucket(builder_name, bucket_name, |
| project_name) |
| if props: |
| return props, project_name |
| logging.error( |
| '[red]No props found. Are you sure you have the correct project ' |
| '("%s"), bucket ("%s"), and builder name ("%s")?[/]', project_name, |
| bucket_name, builder_name) |
| if not _INTERNAL_BUILDER_PROP_DIRS.exists(): |
| logging.warning( |
| 'src-internal not detected in this checkout. Perhaps the builder ' |
| 'is a "chrome" one, in which: case make sure to add src-internal to ' |
| "your checkout if a you're a Googler.") |
| return None, None |
| if len(possible_matches) > 1: |
| logging.error( |
| '[red]Found multiple prop files for builder %s. Pass in a project ' |
| '("-p") and bucket name ("-B").[/]', builder_name) |
| for m in possible_matches: |
| logging.error(m) |
| return None, None |
| |
| prop_file = possible_matches[0] |
| logging.debug('Found prop file %s', prop_file) |
| with open(possible_matches[0]) as f: |
| props = json.load(f) |
| |
| return props, project_name |
| |
| |
| def fetch_props_from_buildbucket(builder_name, bucket_name, project_name): |
| """Calls out to buildbucket for the input props for the given builder |
| |
| Args: |
| builder_name: Builder name of the builder |
| bucket_name: Bucket name of the builder |
| project_name: Project name of the builder |
| |
| Returns: |
| Dict of the builder's input props |
| """ |
| input_json = { |
| 'id': { |
| 'project': project_name, |
| 'bucket': bucket_name, |
| 'builder': builder_name, |
| } |
| } |
| cmd = [ |
| 'luci-auth', |
| 'context', |
| '--', |
| 'prpc', |
| 'call', |
| 'cr-buildbucket.appspot.com', |
| 'buildbucket.v2.Builders.GetBuilder', |
| ] |
| logging.debug('Running prpc:') |
| logging.debug(' '.join(cmd)) |
| p = subprocess.run(cmd, |
| input=json.dumps(input_json), |
| text=True, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| check=False) |
| if p.returncode: |
| logging.warning('Error fetching the build template from buildbucket') |
| # Use the "basic_logger" here (and below) to avoid rich from coloring random |
| # bits of the printed error. |
| logging.getLogger('basic_logger').warning(p.stdout.strip()) |
| return None |
| builder_info = json.loads(p.stdout) |
| props_s = builder_info.get('config', {}).get('properties', '{}') |
| return json.loads(props_s) |