blob: bbce5ef3aac9b940426b4610ea658abe96534d58 [file] [log] [blame]
# Copyright 2024 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.
from __future__ import annotations
import configparser
import dataclasses
import io
import re
from typing import TYPE_CHECKING
from PB.recipe_modules.pigweed.checkout.options import (
Options as CheckoutOptions,
)
from recipe_engine import recipe_api
if TYPE_CHECKING: # pragma: no cover
from typing import Generator
from PB.recipe_modules.pigweed.bazel_roll.git_repository import (
GitRepository,
)
from recipe_engine import config_types
from RECIPE_MODULES.pigweed.checkout import api as checkout_api
from RECIPE_MODULES.pigweed.roll_util import api as roll_util_api
@dataclasses.dataclass
class RevisionChange:
old: str
new: str
class BazelRollApi(recipe_api.RecipeApi):
RevisionChange = RevisionChange
def workspace_path(
self,
root: config_types.Path,
value: str,
) -> config_types.Path:
"""Figure out the location of the WORKSPACE or MODULE.bazel file.
If value is '', look for root / 'WORKSPACE' and root / 'MODULE.bazel'.
If exactly one exists, return it. If not, error out.
If root / value is a file, return it.
If root / value is a directory, set root = root / value and apply the
above logic. This enables applying this logic to subdirectories.
Args:
api: Recipe API object.
root: Checkout root.
value: Relative path specified in properties.
Returns:
Path to the WORKSPACE or MODULE.bazel file.
"""
if value:
value_path = root / value
self.m.path.mock_add_file(value_path)
if self.m.path.isfile(value_path):
return value_path
elif self.m.path.isdir(value_path): # pragma: no cover
root = value_path
else:
self.m.step.empty( # pragma: no cover
f'{value_path} does not exist',
status='FAILURE',
)
workspace = root / 'WORKSPACE'
module_bazel = root / 'MODULE.bazel'
self.m.path.mock_add_file(workspace)
if self.m.path.isfile(module_bazel) and self.m.path.isfile(workspace):
self.m.step.empty( # pragma: no cover
f'{module_bazel} and {workspace} both exist',
status='FAILURE',
)
if self.m.path.isfile(module_bazel):
return module_bazel # pragma: no cover
if self.m.path.isfile(workspace):
return workspace
self.m.step.empty( # pragma: no cover
'no WORKSPACE or MODULE.bazel file found',
status='FAILURE',
)
def update_git_repository(
self,
checkout: checkout_self.m.CheckoutContext,
git_repository: GitRepository,
) -> dict[str, self.m.roll_util.Roll]:
workspace_path = self.workspace_path(
checkout.root,
git_repository.workspace_path,
)
git_repository.branch = git_repository.branch or 'main'
new_revision: Optional[str] = None
# First, try to get new_revision from the trigger.
bb_remote: Optional[str] = None
commit: common_pb2.GitilesCommit = (
self.m.buildbucket.build.input.gitiles_commit
)
if commit and commit.project:
new_revision = commit.id
host: str = commit.host
bb_remote: str = f'https://{host}/{commit.project}'
# If we still don't have a revision then it wasn't in the trigger.
# (Perhaps this was manually triggered.) In this case we update to the
# property-specified branch HEAD.
if new_revision is None:
new_revision = git_repository.branch
# If this was triggered by a gitiles poller, check that the triggering
# repository matches project_remote. Exception: allow a trigger to be
# for the top-level project instead.
use_trigger_for_project = True
if bb_remote:
if checkout.remotes_equivalent(checkout.options.remote, bb_remote):
use_trigger_for_project = False
elif not checkout.remotes_equivalent(
git_repository.remote,
bb_remote,
):
self.m.step.empty(
'triggering repository ({}) does not match project remote '
'({})'.format(bb_remote, git_repository.remote),
status='FAILURE',
)
project_dir = self.m.path.start_dir / 'project'
project_checkout = self.m.checkout(
CheckoutOptions(
remote=git_repository.remote,
branch=git_repository.branch,
use_trigger=use_trigger_for_project,
),
root=project_dir,
)
# In case new_revision is a branch name we need to retrieve the hash it
# resolves to.
if not re.search(r'^[0-9a-f]{40}$', new_revision):
new_revision = self.m.checkout.get_revision(
project_dir, 'get new revision', test_data='2' * 40
)
update_result = self.m.bazel.update_commit_hash(
checkout=checkout,
project_remote=git_repository.remote,
new_revision=new_revision,
path=workspace_path,
)
if not update_result:
self.m.step.empty('failed to update commit hash', status='FAILURE')
direction = self.m.roll_util.get_roll_direction(
project_dir, update_result.old_revision, new_revision
)
# If the primary roll is not necessary or is backwards we can exit
# immediately.
if not self.m.roll_util.can_roll(direction):
self.m.roll_util.skip_roll_step(
git_repository.remote, update_result.old_revision, new_revision
)
return
project_name = git_repository.name or update_result.project_name
if not project_name:
self.m.step.empty(
f'could not find name line in {workspace_path}',
status='FAILURE',
)
rolls: dict[str, self.m.roll_util.Roll] = {
workspace_path: self.m.roll_util.create_roll(
project_name=project_name,
old_revision=update_result.old_revision,
new_revision=new_revision,
proj_dir=project_dir,
direction=direction,
),
}
return rolls