blob: 92dbcd6fb7e88d0f343e35946a8435f847e2c294 [file] [log] [blame]
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import contextlib
import urllib
from recipe_engine import recipe_api
DEFAULT_CIPD_VERSION = 'git_revision:4520b945a32ae72645adf048ff86bfdc87ac9810'
class IsolatedApi(recipe_api.RecipeApi):
"""API for interacting with isolated.
The isolated client implements a tar-like scatter-gather mechanism for
archiving files. The tool's source lives at
http://go.chromium.org/luci/client/cmd/isolated.
This module will deploy the client to [CACHE]/isolated_client/; users should
add this path to the named cache for their builder.
"""
def __init__(self, isolated_properties, *args, **kwargs):
super(IsolatedApi, self).__init__(*args, **kwargs)
self._server = isolated_properties.get('server', None)
self._namespace = isolated_properties.get('namespace', 'default-gzip')
self._version = isolated_properties.get('version', DEFAULT_CIPD_VERSION)
self._client_dir = None
self._client = None
def initialize(self):
if self._test_data.enabled:
self._server = 'https://example.isolateserver.appspot.com'
if self.m.runtime.is_experimental:
self._version = 'latest'
self._client_dir = self.m.path['cache'].join('isolated_client')
def _ensure_isolated(self):
"""Ensures that the isolated Go binary is installed."""
if self._client:
return
with self.m.step.nest('ensure isolated'):
with self.m.context(infra_steps=True):
pkgs = self.m.cipd.EnsureFile()
pkgs.add_package('infra/tools/luci/isolated/${platform}', self._version)
self.m.cipd.ensure(self._client_dir, pkgs)
self._client = self._client_dir.join('isolated')
@property
def isolate_server(self):
"""Returns the associated isolate server."""
assert self._server
return self._server
@property
def namespace(self):
"""Returns the associated namespace."""
assert self._namespace
return self._namespace
def _run(self, name, cmd, step_test_data=None):
"""Return an isolated command step.
Args:
name: (str): name of the step.
cmd (list(str|Path)): isolated client subcommand to run.
"""
self._ensure_isolated()
return self.m.step(name,
[self._client] + list(cmd),
step_test_data=step_test_data,
infra_step=True)
@contextlib.contextmanager
def on_path(self):
"""This context manager ensures the go isolated client is available on
$PATH.
Example:
with api.isolated.on_path():
# do your steps which require the isolated binary on path
"""
self._ensure_isolated()
with self.m.context(env_prefixes={'PATH': [self._client_dir]}):
yield
def isolated(self, root_dir):
"""Returns an Isolated object that can be used to archive a set of files
and directories, relative to a given root directory.
Args:
root_dir (Path): directory relative to which files and directory will be
isolated.
"""
return Isolated(self.m, root_dir)
def download(self, step_name, isolated_hash, output_dir, isolate_server=None):
"""Downloads an isolated tree from an isolate server.
Args:
step_name (str): name of the step.
isolated_hash (str): the hash of an isolated tree.
output_dir (Path): Path to an output directory. If a non-existent
directory, it will be created; else if already existent,
conflicting files will be overwritten and non-conflicting files
already in the directory will be ignored.
isolate_server (str|None): an isolate server to donwload from; if None,
the module's default server will be used instead.
"""
isolate_server = isolate_server or self.isolate_server
cmd = [
'download',
'-verbose',
'-isolate-server', isolate_server,
'-isolated', isolated_hash,
'-output-dir', output_dir,
]
return self._run(step_name, cmd)
class Isolated(object):
"""Used to gather a list of files and directories to an isolated, relative to
a provided root directory."""
def __init__(self, api, root_dir):
assert root_dir
self._api = api
self._root_dir = root_dir
self._files = []
self._dirs = []
def _isolated_path_format(self, path):
"""Returns the path format consumed by the isolated CLI."""
if self._root_dir.is_parent_of(path):
return '%s:%s' % (
self._root_dir,
self._api.path.join(*path.pieces[len(self._root_dir.pieces):])
)
else:
assert path == self._root_dir, \
"isolated path must be equal to or within %s" % self._root_dir
return '%s:.' % self._root_dir
def add_file(self, path):
"""Stages a single file to be added to the isolated.
Args:
path (Path): absolute path to a file.
"""
assert self._root_dir.is_parent_of(path)
self._files.append(path)
def add_files(self, paths):
"""Stages a list of files to be added to the isolated.
Args:
paths list(Path): list of absolute paths to files.
"""
for path in paths:
self.add_file(path)
def add_dir(self, path):
"""Stages a single directory to be added to the isolated.
Args:
path (Path): absolute path to a directory.
"""
assert self._root_dir == path or self._root_dir.is_parent_of(path)
self._dirs.append(path)
def archive(self, step_name, isolate_server=None):
"""Step to archive all staged files and directories.
If no isolate_server is provided, the IsolatedApis's default server will be
used instead.
Args:
step_name (str): name of the step.
isolate_server (str): an isolate server to archive to.
Returns:
The hash of the isolated tree.
"""
isolate_server = isolate_server or self._api.isolated.isolate_server
cmd = [
'archive',
'-verbose',
'-isolate-server', isolate_server,
'-namespace', self._api.isolated.namespace,
'-dump-hash', self._api.raw_io.output_text(),
]
for f in self._files:
cmd.extend(['-files', self._isolated_path_format(f)])
for d in self._dirs:
cmd.extend(['-dirs', self._isolated_path_format(d)])
isolated_hash = self._api.isolated._run(
step_name,
cmd,
step_test_data=self._api.isolated.test_api.archive,
).raw_io.output_text
q = {
'hash': isolated_hash,
'namespace': self._api.isolated.namespace,
}
self._api.step.active_result.presentation.links['isolated UI'] = (
'%s/browse?%s' % (isolate_server, urllib.urlencode(q))
)
return isolated_hash