| # Copyright 2023 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Bazel-related functions.""" |
| |
| from __future__ import annotations |
| |
| import dataclasses |
| import shlex |
| import re |
| from typing import TYPE_CHECKING |
| |
| from PB.recipe_modules.pigweed.bazel.options import Options |
| from recipe_engine import recipe_api |
| |
| if TYPE_CHECKING: # pragma: no cover |
| from recipe_engine import config_types |
| from RECIPE_MODULES.pigweed.checkout import api as checkout_api |
| |
| |
| @dataclasses.dataclass |
| class BazelRunner: |
| api: recipe_api.RecipeApi |
| checkout: checkout_api.CheckoutContext |
| options: Options |
| _bazel: config_types.Path | None = None |
| continue_after_build_error: bool = False |
| |
| def _ensure_bazelisk(self) -> config_types.Path: |
| ensure_file = self.api.cipd.EnsureFile() |
| ensure_file.add_package( |
| 'fuchsia/third_party/bazelisk/${platform}', |
| self.options.bazelisk_version or 'latest', |
| ) |
| |
| root = self.api.path.mkdtemp() |
| self.api.cipd.ensure(root, ensure_file, name='ensure bazelisk') |
| return root / 'bazelisk' |
| |
| def ensure(self) -> config_types.Path: |
| if self._bazel: |
| return self._bazel |
| |
| self._bazel = self._ensure_bazelisk() |
| |
| self.api.step('bazel version', [self._bazel, 'version']) |
| return self._bazel |
| |
| def _override_args(self) -> list[str]: |
| if self.api.path.exists(self.checkout.root / 'MODULE.bazel'): |
| # We're in a bzlmod-managed workspace. |
| flag = "--override_module" # pragma: no cover |
| else: |
| # We're in a traditional workspace. |
| flag = "--override_repository" |
| |
| return [ |
| f'{flag}={repo}={path}' |
| for repo, path in self.checkout.bazel_overrides.items() |
| ] |
| |
| def run(self, **kwargs) -> None: |
| config_name = self.options.config_path or 'pigweed.json' |
| config_path = self.checkout.root / config_name |
| self.api.path.mock_add_file(config_path) |
| |
| config = {} |
| if self.api.path.isfile(config_path): |
| config = self.api.file.read_json( |
| f'read {config_name}', |
| config_path, |
| test_data={ |
| 'pw': { |
| 'bazel_presubmit': { |
| 'remote_cache': True, |
| 'upload_local_results': True, |
| 'programs': { |
| 'default': [ |
| ['build', '//...'], |
| ['test', '//...'], |
| ], |
| }, |
| }, |
| }, |
| }, |
| ) |
| config = config.get('pw', config).get('bazel_presubmit', config) |
| |
| base_args: list[str] = [] |
| |
| # Don't limit the amount Bazel will write to stdout/stderr. |
| base_args.append('--experimental_ui_max_stdouterr_bytes=-1') |
| |
| if config.get('remote'): |
| # TODO: b/368128573 - Support remote execution on MacOS. |
| if self.api.platform.is_linux: |
| base_args.append('--config=remote') |
| else: |
| self.api.step.empty( |
| 'ignoring remote because not running on Linux' |
| ) |
| |
| elif config.get('remote_cache'): |
| # --config=remote already implies --config=remote_cache. |
| base_args.append('--config=remote_cache') |
| |
| if self.api.buildbucket.build.builder.project == 'pigweed': |
| instance_name = 'pigweed-rbe-open' |
| else: |
| instance_name = 'pigweed-rbe-private' |
| |
| if self.api.buildbucket_util.is_tryjob: |
| instance_name += '-pre' |
| |
| base_args.append(f'--bes_instance_name={instance_name}') |
| |
| if instance_name == 'pigweed-rbe-open': |
| # Ted messed up and gave the pigweed-rbe-open RBE instance a |
| # different name (default-instance instead of default_instance). |
| # Sadly this is annoying to fix because instances cannot be renamed, |
| # and you can't have more than one instance in a GCP region. |
| # |
| # TODO: b/312215590 - Fix this. |
| base_args.append( |
| '--remote_instance_name=projects/pigweed-rbe-open/instances/default-instance' |
| ) |
| else: |
| base_args.append( |
| f'--remote_instance_name=projects/{instance_name}/instances/default_instance' |
| ) |
| |
| if config.get('upload_local_results'): |
| if not config.get('remote_cache'): |
| self.api.step.empty( |
| 'ignoring upload_local_results since remote_cache is False' |
| ) |
| else: |
| base_args.append('--remote_upload_local_results=true') |
| |
| base_args.extend(self._override_args()) |
| |
| if self.continue_after_build_error: |
| base_args.append('--keep_going') |
| |
| with ( |
| self.api.context(cwd=self.checkout.root), |
| self.api.defer.context() as defer, |
| ): |
| for invocation in self.options.invocations: |
| assert invocation.args |
| name: str = ' '.join(['bazel'] + list(invocation.args)) |
| defer( |
| self.api.step, |
| name, |
| [self.ensure(), *invocation.args, *base_args], |
| **kwargs, |
| ) |
| |
| programs = config.get('programs', {}) |
| for program in self.options.program or ('default',): |
| with self.api.step.nest(program): |
| assert program in programs, f'{program} not in {programs}' |
| assert programs[program] |
| for args in programs[program]: |
| json_path = self.api.path.mkdtemp() / 'metadata.json' |
| |
| cmd = [ |
| self.api.bazel.resource('wrapper.py'), |
| '--json', |
| self.api.json.output(leak_to=json_path), |
| '--', |
| self.ensure(), |
| *args, |
| *base_args, |
| ] |
| defer( |
| self.api.step, |
| shlex.join(args), |
| cmd, |
| **kwargs, |
| ) |
| |
| self.api.path.mock_add_file(json_path) |
| if self.api.path.isfile(json_path): |
| with self.api.step.nest('resultstore link') as pres: |
| data = self.api.file.read_json( |
| 'read', |
| json_path, |
| test_data={ |
| 'resultstore': 'https://result.store/', |
| }, |
| ) |
| if 'resultstore' in data: |
| pres.links['resultstore'] = data[ |
| 'resultstore' |
| ] |
| else: # pragma: no cover |
| pres.step_summary_text = ( |
| 'no resultstore link found' |
| ) |
| |
| |
| class BazelApi(recipe_api.RecipeApi): |
| """Bazel utilities.""" |
| |
| BazelRunner = BazelRunner |
| |
| def new_runner( |
| self, |
| checkout: checkout_api.CheckoutContext, |
| options: Options | None, |
| continue_after_build_error: bool = False, |
| ) -> BazelRunner: |
| return BazelRunner( |
| self.m, |
| checkout=checkout, |
| options=options, |
| continue_after_build_error=continue_after_build_error, |
| ) |