| # Copyright 2016 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 recipe_engine import recipe_api |
| |
| from urlparse import urlparse |
| |
| # Flags added to all jiri commands. |
| COMMON_FLAGS = [ |
| '-vv', |
| '-time', |
| ] |
| |
| |
| class JiriApi(recipe_api.RecipeApi): |
| """JiriApi provides support for Jiri managed checkouts.""" |
| |
| def __init__(self, *args, **kwargs): |
| super(JiriApi, self).__init__(*args, **kwargs) |
| self._jiri_executable = None |
| |
| def __call__(self, *args, **kwargs): |
| """Return a jiri command step.""" |
| subcommand = args[0] # E.g., 'init' or 'update' |
| flags = COMMON_FLAGS + list(args[1:]) |
| |
| assert self._jiri_executable |
| full_cmd = [self._jiri_executable, subcommand] + flags |
| |
| name = kwargs.pop('name', 'jiri ' + subcommand) |
| return self.m.step(name, full_cmd, **kwargs) |
| |
| def ensure_jiri(self, version=None): |
| with self.m.step.nest('ensure_jiri'): |
| with self.m.context(infra_steps=True): |
| pkgs = self.m.cipd.EnsureFile() |
| pkgs.add_package('fuchsia/tools/jiri/${platform}', version or 'stable') |
| cipd_dir = self.m.path['start_dir'].join('cipd', 'jiri') |
| self.m.cipd.ensure(cipd_dir, pkgs) |
| self._jiri_executable = cipd_dir.join('jiri') |
| return self._jiri_executable |
| |
| @property |
| def jiri(self): |
| return self._jiri_executable |
| |
| def init(self, dir=None, **kwargs): |
| cmd = [ |
| 'init', |
| '-analytics-opt=false', |
| '-rewrite-sso-to-https=true', |
| '-cache', |
| self.m.path['cache'].join('git'), |
| '-shared', |
| ] |
| if dir: |
| cmd.append(dir) |
| |
| return self(*cmd, **kwargs) |
| |
| def project(self, projects=[], out=None, test_data=None): |
| """ |
| Args: |
| projects (List): A heterogeneous list of strings representing the name of |
| projects to list info about (defaults to all). |
| out (str): Path to write json results to, with the following schema: |
| [ |
| { |
| "name": "zircon", |
| "path": "local/path/to/zircon", |
| "relativePath": "zircon", |
| "remote": "https://fuchsia.googlesource.com/zircon", |
| "revision": "af8fd6138748bc11d31a5bde3303cdc19c7e04e9", |
| "current_branch": "master", |
| "branches": [ |
| "master" |
| ] |
| } |
| ... |
| ] |
| |
| |
| Returns: |
| A step to provide structured info on existing projects and branches. |
| """ |
| cmd = [ |
| 'project', |
| '-json-output', |
| self.m.json.output(leak_to=out), |
| ] + projects |
| |
| if test_data is None: |
| test_data = [ |
| { |
| 'name': p, |
| # Specify a path under start_dir to satisfy consumers that expect a |
| # "realistic" path, such as LUCI's PathApi.abs_to_path. |
| 'path': str(self.m.path['start_dir'].join('path', 'to', p)), |
| 'remote': 'https://fuchsia.googlesource.com/' + p, |
| 'revision': 'c22471f4e3f842ae18dd9adec82ed9eb78ed1127', |
| 'current_branch': '', |
| 'branches': ['(HEAD detached at c22471f)'] |
| } for p in projects |
| ] |
| |
| return self(*cmd, step_test_data=lambda: self.test_api.project(test_data)) |
| |
| def update(self, |
| gc=False, |
| rebase_tracked=False, |
| local_manifest=False, |
| run_hooks=True, |
| snapshot=None, |
| attempts=3, |
| **kwargs): |
| cmd = [ |
| 'update', |
| '-autoupdate=false', |
| '-attempts=%d' % attempts, |
| ] |
| if gc: |
| cmd.append('-gc=true') |
| if rebase_tracked: |
| cmd.append('-rebase-tracked') |
| if local_manifest: |
| cmd.append('-local-manifest=true') |
| if not run_hooks: |
| cmd.append('-run-hooks=false') |
| if snapshot is not None: |
| cmd.append(snapshot) |
| |
| return self(*cmd, **kwargs) |
| |
| def run_hooks(self, local_manifest=False, attempts=3): |
| cmd = [ |
| 'run-hooks', |
| '-attempts=%d' % attempts, |
| ] |
| if local_manifest: |
| cmd.append('-local-manifest=true') |
| return self(*cmd) |
| |
| def clean(self, all=False, **kwargs): |
| cmd = [ |
| 'project', |
| '-clean-all' if all else '-clean', |
| ] |
| kwargs.setdefault('name', 'jiri project clean') |
| |
| return self(*cmd, **kwargs) |
| |
| def import_manifest(self, |
| manifest, |
| remote, |
| name=None, |
| revision=None, |
| overwrite=False, |
| remote_branch=None, |
| **kwargs): |
| """Imports manifest into Jiri project. |
| |
| Args: |
| manifest (str): A file within the repository to use. |
| remote (str): A remote manifest repository address. |
| name (str): The name of the remote manifest project. |
| revision (str): A revision to checkout for the remote. |
| remote_branch (str): A branch of the remote manifest repository |
| to checkout. If a revision is specified, this value is ignored. |
| |
| Returns: |
| A step result. |
| """ |
| cmd = ['import'] |
| if name: |
| cmd.extend(['-name', name]) |
| if overwrite: |
| cmd.extend(['-overwrite=true']) |
| |
| # revision cannot be passed along with remote-branch, because jiri. |
| if remote_branch: |
| cmd.extend(['-remote-branch', remote_branch]) |
| elif revision: |
| cmd.extend(['-revision', revision]) |
| |
| cmd.extend([manifest, remote]) |
| |
| return self(*cmd, **kwargs) |
| |
| def edit_manifest(self, |
| manifest, |
| projects=None, |
| imports=None, |
| test_data=None, |
| **kwargs): |
| """Creates a step to edit a Jiri manifest. |
| |
| Args: |
| manifest (str): Path to the manifest to edit relative to the project root. |
| e.g. "manifest/zircon" |
| projects (List): A heterogeneous list whose entries are either: |
| * A string representing the name of a project to edit. |
| * A (name, revision) tuple representing a project to edit. |
| imports (List): The same as projects, except each list entry represents an |
| import to edit. |
| |
| Returns: |
| A step to edit the manifest. |
| """ |
| |
| cmd = [ |
| 'edit', |
| '-json-output', |
| self.m.json.output(), |
| ] |
| # Test data consisting of (name, revision) tuples of imports to edit in the |
| # given manifest. |
| test_imports = [] |
| |
| if imports: |
| for i in imports: |
| if type(i) is str: |
| cmd.extend(['-import', i]) |
| test_imports.append((i, "HEAD")) |
| elif type(i) is tuple: |
| cmd.extend(['-import', '%s=%s' % i]) |
| test_imports.append(i) |
| |
| # Test data consisting of (name, revision) tuples of projects to edit in the |
| # given manifest. |
| test_projects = [] |
| |
| if projects: |
| for p in projects: |
| if type(p) is str: |
| cmd.extend(['-project', p]) |
| test_projects.append((p, "HEAD")) |
| elif type(p) is tuple: |
| cmd.extend(['-project', '%s=%s' % p]) |
| test_projects.append(p) |
| cmd.extend([manifest]) |
| |
| # Generate test data |
| if test_data is None: |
| test_data = self.test_api.example_edit( |
| imports=test_imports, |
| projects=test_projects, |
| ) |
| |
| step = self( |
| *cmd, |
| step_test_data=lambda: self.m.json.test_api.output(test_data), |
| **kwargs) |
| return step.json.output |
| |
| def patch(self, |
| ref, |
| host=None, |
| project=None, |
| delete=False, |
| force=False, |
| rebase=False, |
| cherrypick=False): |
| cmd = ['patch'] |
| if host: |
| cmd.extend(['-host', host]) |
| if project: |
| cmd.extend(['-project', project]) |
| if delete: |
| cmd.extend(['-delete=true']) |
| if force: |
| cmd.extend(['-force=true']) |
| if rebase: # pragma: no cover |
| cmd.extend(['-rebase=true']) |
| if cherrypick: # pragma: no cover |
| cmd.extend(['-cherry-pick=true']) |
| cmd.extend([ref]) |
| |
| return self(*cmd) |
| |
| def override(self, project, remote, new_revision='HEAD'): |
| """Overrides a given project entry with a new revision. |
| |
| Args: |
| project (str): name of the project. |
| remote (str): URL to the remote repository. |
| new_revision (str|None): new revision to override the project's current. |
| """ |
| cmd = ['override', '-revision', new_revision, project, remote] |
| return self(*cmd) |
| |
| def snapshot(self, file=None, test_data=None, **kwargs): |
| cmd = [ |
| 'snapshot', |
| self.m.raw_io.output(name='snapshot', leak_to=file), |
| ] |
| if test_data is None: |
| test_data = self.test_api.example_snapshot |
| step = self( |
| *cmd, |
| step_test_data=lambda: self.test_api.snapshot(test_data), |
| **kwargs) |
| return step.raw_io.output |
| |
| def source_manifest(self, file=None, test_data=None, **kwargs): |
| """Generates a source manifest JSON file. |
| |
| Args: |
| file (Path): Optional output path for the source manifest. |
| |
| Returns: |
| The contents of the source manifest as a Python dictionary. |
| """ |
| cmd = [ |
| 'source-manifest', |
| self.m.json.output(name='source manifest', leak_to=file), |
| ] |
| if test_data is None: |
| test_data = self.test_api.example_source_manifest |
| step = self( |
| *cmd, |
| step_test_data=lambda: self.test_api.source_manifest(test_data), |
| **kwargs) |
| return step.json.output |
| |
| def emit_source_manifest(self): |
| """Emits a source manifest for this build for the current jiri checkout.""" |
| manifest = self.source_manifest() |
| self.m.source_manifest.set_json_manifest('checkout', manifest) |
| |
| def read_manifest_element(self, manifest, element_type, element_name): |
| """Reads information about a <project> or <import> from a manifest file. |
| |
| Args: |
| manifest (str|Path): Path to the manifest file. |
| element_type (str): One of 'import' or 'project'. |
| element_name (str): The name of the element. |
| |
| Returns: |
| A dict containing the project fields. Any fields that are missing or have |
| empty values are omitted from the dict. Examples: |
| |
| # Read remote attribute of the returned project |
| print(project['remote']) # https://fuchsia.googlesource.com/my_project |
| |
| # Check whether remote attribute was present and non-empty. |
| if 'remote' in project: |
| ... |
| """ |
| if element_type == 'project': |
| # This is a Go template matching the schema in pkg/text/template. See docs |
| # for `__manifest` for more details. We format the template as JSON to |
| # make it easy to parse into a dict. The template contains the fields in |
| # a manifest <project>. See //jiri/project/manifest.go for the original |
| # definition. Add fields to this template as-needed. |
| template = ''' |
| { |
| "gerrithost": "{{.GerritHost}}", |
| "githooks": "{{.GitHooks}}", |
| "historydepth": "{{.HistoryDepth}}", |
| "name": "{{.Name}}", |
| "path": "{{.Path}}", |
| "remote": "{{.Remote}}", |
| "remotebranch": "{{.RemoteBranch}}", |
| "revision": "{{.Revision}}" |
| } |
| ''' |
| else: |
| assert element_type == 'import' |
| # This template contains the fields in a manifest <import>. See |
| # //jiri/project/manifest.go for the original definition. |
| template = ''' |
| { |
| "manifest": "{{.Manifest}}", |
| "name": "{{.Name}}", |
| "remote": "{{.Remote}}", |
| "revision": "{{.Revision}}", |
| "remotebranch": "{{.RemoteBranch}}", |
| "root": "{{.Root}}" |
| } |
| ''' |
| # Parse the result as JSON |
| with self.m.step.nest('read_manifest_' + element_name): |
| element_json = self.__manifest( |
| manifest=manifest, |
| element_name=element_name, |
| template=template, |
| stdout=self.m.json.output(), |
| ).stdout |
| |
| # Strip whitespace from any attribute values. Discard empty values. |
| return {k: v.strip() for k, v in element_json.iteritems() if v.strip()} |
| |
| def __manifest(self, manifest, element_name, template, **kwargs): |
| """Reads information about a <project> or <import> from a manifest file. |
| |
| The template argument is a Go template string matching the schema defined in |
| pkg/text/template: https://golang.org/pkg/text/template/#hdr-Examples. |
| |
| Any of a project's or import's fields may be specified in the template. See |
| https://fuchsia.googlesource.com/jiri/+/master/project/project.go for a list |
| of all project fields. For a list of all import fields, see: |
| https://fuchsia.googlesource.com/jiri/+/master/project/manifest.go. |
| |
| Example Usage: |
| |
| # Read the remote= attribute of some <project>. |
| # |
| # Example output: https://code.com/my_project.git |
| __manifest(manifest='manifest', element_name='my_project', |
| template='{{.Remote}}') |
| |
| # Read the remote= and path= attributes from some <import>, and |
| # format the output as "$remote is cloned to $path". |
| # |
| # Example output: https://code.com/my_import.git is cloned to /my_import. |
| __manifest(manifest='manifest', element_name='my_import', |
| template='{{.Remote}}) is cloned to {{.Path}}') |
| |
| Args: |
| manifest (str|Path): Path to the manifest file. |
| element_name (str): The name of the <project> or <import> to read from. |
| template (str): A Go template string matching pkg/text/template. |
| |
| Returns: |
| The filled-in template string. If the <project> or <import> did not have |
| a value for some field in the template, the empty string is filled-in for |
| that field. |
| """ |
| return self('manifest', '-element', element_name, '-template', template, |
| manifest, **kwargs) |