| # Copyright 2019 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from google.protobuf import text_format as textpb |
| from recipe_engine import recipe_api |
| |
| import base64 |
| import os |
| |
| BUILD_INIT_VERSION = 'git_revision:bd277978791e1e35bf76e0df57f6aef14aef0757' |
| |
| DEFAULT_SPEC_DIR = 'infra/config/generated' |
| |
| |
| class SpecApi(recipe_api.RecipeApi): |
| """API for working with textproto files.""" |
| |
| def __init__(self, *args, **kwargs): |
| super(SpecApi, self).__init__(*args, **kwargs) |
| self._build_init_path = None |
| |
| class ParseError(Exception): |
| pass |
| |
| class NotFoundError(Exception): |
| pass |
| |
| def _ensure_build_init(self): |
| with self.m.step.nest('ensure build_init'): |
| with self.m.context(infra_steps=True): |
| pkgs = self.m.cipd.EnsureFile() |
| pkgs.add_package('fuchsia/infra/build_init/${platform}', |
| BUILD_INIT_VERSION) |
| build_init_dir = self.m.path['start_dir'].join('cipd', 'build_init') |
| self.m.cipd.ensure(build_init_dir, pkgs) |
| self._build_init_path = build_init_dir.join('build_init') |
| return self._build_init_path |
| |
| def load(self, |
| spec_remote, |
| Type, |
| spec_dir=DEFAULT_SPEC_DIR, |
| spec_revision=None): |
| spec, _ = self.get_spec_revision(spec_remote, Type, spec_dir, spec_revision) |
| return spec |
| |
| def get_spec_revision(self, |
| spec_remote, |
| Type, |
| spec_dir=DEFAULT_SPEC_DIR, |
| spec_revision=None): |
| """Loads a spec message from the given Buildbucket build input. |
| |
| If the build input is for a gitiles commit, the Gitiles host, project, |
| and ref are inferred from the build input and the spec is fetched from |
| Gitiles. |
| |
| If the build input is for a gerrit change, the project is rebased on top |
| of the parent gitiles_commit in build. |
| |
| Args: |
| spec_remote (string): URL of the specs git repository. |
| spec_dir (string): The directory within spec_remote containing specs. |
| Specifically we'll look for the spec in |
| "<spec_dir>/<project>/specs/<bucket>/<builder>.textproto". |
| spec_revision (string or None): The git revision to fetch the spec from. |
| |
| Returns: |
| spec (Type), rev (string): The parsed proto API spec and its revision. |
| |
| Raises: |
| NotFoundError if the spec file could not be located. |
| ParseError if the proto fails to parse into the given Type. |
| """ |
| build = self.m.buildbucket.build |
| with self.m.context(infra_steps=True): |
| self._ensure_build_init() |
| assert self._build_init_path |
| # build_init takes a text build.proto on stdin. base64 encode just in case |
| # the input contains non utf-8 text which breaks logdog. |
| build_msg = base64.b64encode(build.SerializeToString()) |
| builder_name = build.builder.builder |
| # Have subbuilders use the same specs as the parent builders. |
| if builder_name.endswith('-subbuild'): |
| builder_name = builder_name[:builder_name.index('-subbuild')] |
| # This path is relative to the git remote, and doesn't contain any local |
| # directory placeholder, so it's safe to use os.path.join() rather than |
| # the standard recipe idiom of <Path object>.join(). |
| spec_path = os.path.join(spec_dir, build.builder.project, 'specs', |
| build.builder.bucket, |
| builder_name + '.textproto') |
| if not spec_revision: |
| assert build.input.gitiles_commit |
| commit_remote = 'https://%s/%s' % (build.input.gitiles_commit.host, |
| build.input.gitiles_commit.project) |
| if commit_remote == spec_remote: |
| # build_init will ignore the spec_revision in this case and use the |
| # revision of the gitiles_commit associated with the build. This is just |
| # for consistency to return the correct spec revision being used. |
| spec_revision = build.input.gitiles_commit.id |
| else: |
| # Get revision at HEAD and send to build_init. |
| ref = 'refs/heads/master' |
| spec_revision = self.m.gitiles.refs( |
| spec_remote, |
| refspath='refs/heads', |
| test_data=lambda: self.m.json.test_api.output( |
| {'refs/heads/master': 'deadbeef'})).get(ref, None) |
| assert spec_revision, 'Failed to retrieve spec revision' |
| cmd = [ |
| self._build_init_path, '-spec_remote', spec_remote, '-spec_path', |
| spec_path, '-spec_ref', spec_revision |
| ] |
| |
| # build_init creates a git checkout. Do this in separate directory to avoid |
| # clashing with jiri checkout. |
| with self.m.context(cwd=self.m.path.mkdtemp('build_init_workspace')): |
| step_result = self.m.step( |
| name='build_init', |
| cmd=cmd, |
| stdin=self.m.raw_io.input_text(data=build_msg), |
| stdout=self.m.raw_io.output(), |
| ok_ret=(0, 2), # 0 = ok, 2 = file not found. |
| timeout=10 * 60, |
| ) |
| |
| if step_result.retcode == 2: |
| raise SpecApi.NotFoundError('could not find spec at "%s"' % spec_path) |
| |
| spec = step_result.stdout.strip() |
| if not spec: |
| # Return parse error if the file is empty. google.protobuf considers |
| # that a valid proto so this results in an empty spec otherwise. |
| raise SpecApi.ParseError('file is empty') |
| |
| try: |
| return textpb.Merge(spec, Type()), spec_revision |
| except Exception as e: |
| raise SpecApi.ParseError(e) |