blob: 1d761b93266b44f6deec1be261ea29d17489a9a8 [file] [log] [blame]
# Copyright 2021 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.
import datetime
from recipe_engine import recipe_api
from RECIPE_MODULES.recipe_engine.file.api import SymlinkTree
class ArchiveTree:
"""A representation of a tree to archive in RBE-CAS.
This tree uses the same interface as api.file.SymlinkTree.
"""
def __init__(self, root, api):
"""See ArchiveApi.tree for the public constructor."""
self._tree = api.cas_util.hardlink_tree(root)
self._api = api
@property
def root(self):
"""The root (Path) of the tree."""
return self._tree.root
def register_link(self, target, linkname):
"""Registers a pair of paths to link."""
self._tree.register_link(
self._api.path.abs_to_path(self._api.path.realpath(target)),
linkname,
)
def create_links(self, name, preserve_symlinks=False):
"""Creates all registered links on disk."""
# Use an env var instead of a flag to pass `preserve_symlinks` so we can
# call SymlinkTree.create_links as usual.
# TODO(crbug.com/1216363): this should no longer be necessary once the
# `cas` CLI respects symlinks.
env = {}
if preserve_symlinks:
env["PRESERVE_SYMLINKS"] = "1"
with self._api.context(env=env):
self._tree.create_links(name)
class HardlinkTree(SymlinkTree):
"""A representation of a tree of hardlinks.
This tree differs from the SymlinkTree in the resource passed to it.
See ArchiveApi.hardlink_tree for the public constructor.
"""
pass
class CasUtilApi(recipe_api.RecipeApi):
"""API for archiving and accessing archived files in RBE-CAS."""
def upload(
self,
staging_dir,
upload_paths=(),
step_name=None,
output_property=None,
):
"""Returns the digest of the archived tree created from the provided
staging_dir.
Args:
staging_dir (Path): The root of the directory to upload.
upload_paths (list(Path)): A list of files under the staging_dir
to upload if not uploading the entire directory.
output_property (str): The name of the output property to set with
the digest.
"""
def do_upload(step_name):
# TODO(crbug.com/1279920): Remove retries after "digest mismatch"
# upload failures have been root-caused. The CAS CLI should be able
# to recover from transient errors internally.
return self.m.utils.retry(
lambda: self.m.cas.archive(
step_name,
staging_dir,
*upload_paths,
# TODO(olivernewman): Stop setting log_level after slow CAS
# uploads have been diagnosed.
log_level="debug",
# TODO(fxbug.dev/122153): Stop setting a timeout once CAS
# slowness is resolved and/or the CAS CLI sets shorter
# internal timeouts.
timeout=datetime.timedelta(minutes=15),
),
max_attempts=3,
)
# TODO(fxbug.dev/62939): The two codepaths below mirror what was in
# upload_isolated before the migration happened so that step names stay
# the same. Combine the two paths once we confirm there are no
# dependencies on the step names.
if output_property:
with self.m.step.nest(step_name or "cas") as step:
cas_digest = do_upload("archive")
step.presentation.properties[output_property] = cas_digest
else:
cas_digest = do_upload(step_name or "archive")
return cas_digest
def download(self, digest, output_dir, step_name=None):
"""Downloads the archive with the CAS digest into output_dir."""
self.m.cas.download(
step_name or "download", digest=digest, output_dir=output_dir
)
def tree(self, root):
"""Creates an ArchiveTree, given a root directory.
Args:
root (Path): root of a tree to archive.
"""
return ArchiveTree(root, self.m)
def hardlink_tree(self, root):
"""Creates a HardlinkTree, given a root directory.
Args:
root (Path): root of a tree of hardlinks.
"""
# Pass in the file module to include all necessary deps for SymlinkTree
# which HardlinkTree is a child of.
return HardlinkTree(root, self.m.file.m, self.resource("hardlink.py"))