Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions src/buildstream/_frontend/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1694,3 +1694,124 @@ def artifact_delete(app, artifacts, deps):
"""Remove artifacts from the local cache"""
with app.initialized():
app.stream.artifact_delete(artifacts, selection=deps)


#############################################################
# Buildtree Commands #
#############################################################
@cli.group(short_help="Manipulate cached buildtree.")
def buildtree():
"""Manipulate cached buildtree"""


#####################################################################
# Buildtree Checkout Command #
#####################################################################
@buildtree.command(name="checkout", short_help="Checkout contents of a buildtree")
@click.option("--buildroot", is_flag=True, help="Export full buildroot instead buildtree.")
@click.option("--force", "-f", is_flag=True, help="Allow files to be overwritten")
@click.option("--hardlinks", is_flag=True, help="Checkout hardlinks instead of copying if possible")
@click.option(
"--tar",
default=None,
metavar="LOCATION",
type=click.Path(),
help="Create a tarball from the artifact contents instead "
"of a file tree. If LOCATION is '-', the tarball "
"will be dumped to the standard output.",
)
@click.option(
"--compression",
default=None,
type=click.Choice(["gz", "xz", "bz2"]),
help="The compression option of the tarball created.",
)
@click.option(
"--directory", default=None, type=click.Path(file_okay=False), help="The directory to checkout the artifact to"
)
@click.option(
"--artifact-remote",
"artifact_remotes",
type=RemoteSpecType(RemoteSpecPurpose.PULL),
multiple=True,
help="A remote for downloading artifacts",
)
@click.option(
"--ignore-project-artifact-remotes",
is_flag=True,
help="Ignore remote artifact cache servers recommended by projects",
)
@click.argument("target", required=False, type=click.Path(readable=False))
@click.pass_obj
def buildtree_checkout(
app,
buildroot,
force,
hardlinks,
tar,
compression,
directory,
artifact_remotes,
ignore_project_artifact_remotes,
target,
):
"""Checkout buildtree

When this command is executed from a workspace directory, the default
is to checkout the artifact of the workspace element.
"""
from .. import utils

if hardlinks and tar:
click.echo("ERROR: options --hardlinks and --tar conflict", err=True)
sys.exit(-1)

if tar and directory:
click.echo("ERROR: options --directory and --tar conflict", err=True)
sys.exit(-1)

if not tar:
if compression:
click.echo("ERROR: --compression can only be provided if --tar is provided", err=True)
sys.exit(-1)
else:
location = tar
try:
inferred_compression = utils._get_compression(tar)
except UtilError as e:
click.echo("ERROR: Invalid file extension given with '--tar': {}".format(e), err=True)
sys.exit(-1)
if compression and inferred_compression != "" and inferred_compression != compression:
click.echo(
"WARNING: File extension and compression differ."
"File extension has been overridden by --compression",
err=True,
)
if not compression:
compression = inferred_compression

with app.initialized():
if not target:
target = app.stream.get_default_target()
if not target:
raise AppError('Missing argument "ELEMENT".')

if not tar:
if directory is None:
location = os.path.abspath(os.path.join(os.getcwd(), target))
if location[-4:] == ".bst":
location = location[:-4]
else:
location = directory

app.stream.buildtree_checkout(
target,
location=location,
buildroot=buildroot,
force=force,
hardlinks=hardlinks,
compression=compression,
tar=bool(tar),
artifact_remotes=artifact_remotes,
ignore_project_artifact_remotes=ignore_project_artifact_remotes,
)
78 changes: 78 additions & 0 deletions src/buildstream/_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,84 @@ def artifact_delete(self, targets, *, selection=_PipelineSelection.NONE):
if not ref_removed:
self._context.messenger.info("No artifacts were removed")

# buildtree_checkout()
#
# Checkout target buildtree artifact to the specified location
#
# Args:
# target: Target to checkout
# location: Location to checkout the artifact to
# force: Whether files can be overwritten if necessary
# hardlinks: Whether checking out files hardlinked to
# their artifacts is acceptable
# tar: If true, a tarball from the artifact contents will
# be created, otherwise the file tree of the artifact
# will be placed at the given location. If true and
# location is '-', the tarball will be dumped on the
# standard output.
# artifact_remotes: Artifact cache remotes specified on the commmand line
# ignore_project_artifact_remotes: Whether to ignore artifact remotes specified by projects
#
def buildtree_checkout(
self,
target: str,
*,
location: Optional[str] = None,
buildroot: bool = False,
force: bool = False,
hardlinks: bool = False,
compression: str = "",
tar: bool = False,
artifact_remotes: Iterable[RemoteSpec] = (),
ignore_project_artifact_remotes: bool = False,
):
elements = self._load(
(target,),
selection=_PipelineSelection.NONE,
load_artifacts=True,
attempt_artifact_metadata=True,
connect_artifact_cache=True,
artifact_remotes=artifact_remotes,
ignore_project_artifact_remotes=ignore_project_artifact_remotes,
)

assert len(elements) == 1
element: Element = elements[0]

self._check_location_writable(location, force=force, tar=tar)

self.query_cache([element])
self._pull_missing_artifacts([element])

if buildroot:
# If the user requested the buildroot but no buildroot is available, fail.
if not element._buildroot_exists():
raise StreamError("No buildroot artifact available", reason="missing-buildroot")
else:
# If the user requested the buildtree but no buildtree is available,
# suggest checking out the buildroot if one is available.
# Otherwise, fail if neither is available.
if not element._buildtree_exists():
if element._buildroot_exists():
raise StreamError(
"No buildtree artifact available, but a buildroot is available. "
"Use the --buildroot option to check out the buildroot.",
reason="missing-buildtree-buildroot-exists",
)
raise StreamError(
"Neither a buildtree nor a buildroot artifact is available.",
reason="missing-buildtree-and-buildroot",
)

try:
artifact = element._get_artifact()
virdir = artifact.get_buildroot() if buildroot else artifact.get_buildtree()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing we need to be aware of (and also give a suggestion about to the user) is that not all elements have a build tree: only build elements do.

So it's not enough to check for the build root, we also need to check for the build tree. Then depending on the availability and the user request, we can suggest an alternative to the user.

buildtree available buildroot available user requested result
Y Y build tree extract build tree
Y Y build root extract build root
N Y build tree error + suggest extract build root
N Y build root extract build root
N N build tree error
N N build root error

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. I've accounted for all of the cases you described and added tests covering them.

Could you please take another look when you have a chance?

self._export_artifact(tar, location, compression, element, hardlinks, virdir)
except BstError as e:
raise StreamError(
"Error while exporting buildtree artifacts" ": '{}'".format(e), detail=e.detail, reason=e.reason
) from e

# source_checkout()
#
# Checkout sources of the target element to the specified location
Expand Down
Loading