blob: ff84a68d6ff2f0a5a988992bfb90ee661751b6a5 [file] [log] [blame]
# 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
BUILD_INIT_VERSION = "git_revision:d20cd84e02c8105ba719f26b0fa4579f5d8026e8"
DEFAULT_SPEC_DIR = "infra/config/generated"
class SpecApi(recipe_api.RecipeApi):
"""API for working with textproto files."""
class ParseError(Exception):
pass
class NotFoundError(Exception):
pass
@property
def _build_init_path(self):
return self.m.cipd.ensure_tool(
"fuchsia/infra/build_init/${platform}", BUILD_INIT_VERSION
)
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):
# 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")]
spec_path = self.m.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)