summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Ennis <james.ennis@codethink.co.uk>2019-03-05 16:02:19 +0000
committerJürg Billeter <j@bitron.ch>2019-03-13 17:04:04 +0000
commit01abedd6f1102c342f6c205842f9769893a142b1 (patch)
tree45480a2acb6dea6bb61e15f2d95a58c639509ab4
parent8e630cac4ef5559c7ddfdbf4ad951c9dec5caf1d (diff)
downloadbuildstream-01abedd6f1102c342f6c205842f9769893a142b1.tar.gz
cli.py: Add artifact delete command
This command provides a --no-prune option because or a large cache, pruning can be an expensive operation. If a developer wishes to quicky rebuild an artifact, they may consider using this option.
-rw-r--r--buildstream/_frontend/cli.py14
-rw-r--r--buildstream/_stream.py42
-rw-r--r--tests/frontend/artifact.py104
-rw-r--r--tests/frontend/completions.py1
4 files changed, 160 insertions, 1 deletions
diff --git a/buildstream/_frontend/cli.py b/buildstream/_frontend/cli.py
index 02ca52e85..d8c46ce0c 100644
--- a/buildstream/_frontend/cli.py
+++ b/buildstream/_frontend/cli.py
@@ -1106,6 +1106,20 @@ def artifact_log(app, artifacts):
click.echo_via_pager(data)
+###################################################################
+# Artifact Delete Command #
+###################################################################
+@artifact.command(name='delete', short_help="Remove artifacts from the local cache")
+@click.option('--no-prune', 'no_prune', default=False, is_flag=True,
+ help="Do not prune the local cache of unreachable refs")
+@click.argument('artifacts', type=click.Path(), nargs=-1)
+@click.pass_obj
+def artifact_delete(app, artifacts, no_prune):
+ """Remove artifacts from the local cache"""
+ with app.initialized():
+ app.stream.artifact_delete(artifacts, no_prune)
+
+
##################################################################
# DEPRECATED Commands #
##################################################################
diff --git a/buildstream/_stream.py b/buildstream/_stream.py
index b0fce3817..5c880427c 100644
--- a/buildstream/_stream.py
+++ b/buildstream/_stream.py
@@ -30,11 +30,12 @@ from contextlib import contextmanager, suppress
from fnmatch import fnmatch
from ._artifactelement import verify_artifact_ref
-from ._exceptions import StreamError, ImplError, BstError, ArtifactElementError, set_last_task_error
+from ._exceptions import StreamError, ImplError, BstError, ArtifactElementError, CASCacheError, set_last_task_error
from ._message import Message, MessageType
from ._scheduler import Scheduler, SchedStatus, TrackQueue, FetchQueue, BuildQueue, PullQueue, PushQueue
from ._pipeline import Pipeline, PipelineSelection
from ._profile import Topics, profile_start, profile_end
+from .types import _KeyStrength
from . import utils, _yaml, _site
from . import Scope, Consistency
@@ -520,6 +521,45 @@ class Stream():
return logsdirs
+ # artifact_delete()
+ #
+ # Remove artifacts from the local cache
+ #
+ # Args:
+ # targets (str): Targets to remove
+ # no_prune (bool): Whether to prune the unreachable refs, default False
+ #
+ def artifact_delete(self, targets, no_prune):
+ # Return list of Element and/or ArtifactElement objects
+ target_objects = self.load_selection(targets, selection=PipelineSelection.NONE, load_refs=True)
+
+ # Some of the targets may refer to the same key, so first obtain a
+ # set of the refs to be removed.
+ remove_refs = set()
+ for obj in target_objects:
+ for key_strength in [_KeyStrength.STRONG, _KeyStrength.WEAK]:
+ key = obj._get_cache_key(strength=key_strength)
+ remove_refs.add(obj.get_artifact_name(key=key))
+
+ ref_removed = False
+ for ref in remove_refs:
+ try:
+ self._artifacts.remove(ref, defer_prune=True)
+ except CASCacheError as e:
+ self._message(MessageType.WARN, "{}".format(e))
+ continue
+
+ self._message(MessageType.INFO, "Removed: {}".format(ref))
+ ref_removed = True
+
+ # Prune the artifact cache
+ if ref_removed and not no_prune:
+ with self._context.timed_activity("Pruning artifact cache"):
+ self._artifacts.prune()
+
+ if not ref_removed:
+ self._message(MessageType.INFO, "No artifacts were removed")
+
# source_checkout()
#
# Checkout sources of the target element to the specified location
diff --git a/tests/frontend/artifact.py b/tests/frontend/artifact.py
index c8301c529..3c3203d8a 100644
--- a/tests/frontend/artifact.py
+++ b/tests/frontend/artifact.py
@@ -22,6 +22,7 @@ import os
import pytest
from buildstream.plugintestutils import cli
+from tests.testutils import create_artifact_share
# Project directory
@@ -62,3 +63,106 @@ def test_artifact_log(cli, datafiles):
assert result.exit_code == 0
# The artifact is cached under both a strong key and a weak key
assert (log + log) == result.output
+
+
+# Test that we can delete the artifact of the element which corresponds
+# to the current project state
+@pytest.mark.datafiles(DATA_DIR)
+def test_artifact_delete_element(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ element = 'target.bst'
+
+ # Build the element and ensure it's cached
+ result = cli.run(project=project, args=['build', element])
+ result.assert_success()
+ assert cli.get_element_state(project, element) == 'cached'
+
+ result = cli.run(project=project, args=['artifact', 'delete', element])
+ result.assert_success()
+ assert cli.get_element_state(project, element) != 'cached'
+
+
+# Test that we can delete an artifact by specifying its ref.
+@pytest.mark.datafiles(DATA_DIR)
+def test_artifact_delete_artifact(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ element = 'target.bst'
+
+ # Configure a local cache
+ local_cache = os.path.join(str(tmpdir), 'artifacts')
+ cli.configure({'cachedir': local_cache})
+
+ # First build an element so that we can find its artifact
+ result = cli.run(project=project, args=['build', element])
+ result.assert_success()
+
+ # Obtain the artifact ref
+ cache_key = cli.get_element_key(project, element)
+ artifact = os.path.join('test', os.path.splitext(element)[0], cache_key)
+
+ # Explicitly check that the ARTIFACT exists in the cache
+ assert os.path.exists(os.path.join(local_cache, 'cas', 'refs', 'heads', artifact))
+
+ # Delete the artifact
+ result = cli.run(project=project, args=['artifact', 'delete', artifact])
+ result.assert_success()
+
+ # Check that the ARTIFACT is no longer in the cache
+ assert not os.path.exists(os.path.join(local_cache, 'cas', 'refs', 'heads', artifact))
+
+
+# Test the `bst artifact delete` command with multiple, different arguments.
+@pytest.mark.datafiles(DATA_DIR)
+def test_artifact_delete_element_and_artifact(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ element = 'target.bst'
+ dep = 'compose-all.bst'
+
+ # Configure a local cache
+ local_cache = os.path.join(str(tmpdir), 'artifacts')
+ cli.configure({'cachedir': local_cache})
+
+ # First build an element so that we can find its artifact
+ result = cli.run(project=project, args=['build', element])
+ result.assert_success()
+ assert cli.get_element_state(project, element) == 'cached'
+ assert cli.get_element_state(project, dep) == 'cached'
+
+ # Obtain the artifact ref
+ cache_key = cli.get_element_key(project, element)
+ artifact = os.path.join('test', os.path.splitext(element)[0], cache_key)
+
+ # Explicitly check that the ARTIFACT exists in the cache
+ assert os.path.exists(os.path.join(local_cache, 'cas', 'refs', 'heads', artifact))
+
+ # Delete the artifact
+ result = cli.run(project=project, args=['artifact', 'delete', artifact, dep])
+ result.assert_success()
+
+ # Check that the ARTIFACT is no longer in the cache
+ assert not os.path.exists(os.path.join(local_cache, 'cas', 'refs', 'heads', artifact))
+
+ # Check that the dependency ELEMENT is no longer cached
+ assert cli.get_element_state(project, dep) != 'cached'
+
+
+# Test that we receive the appropriate stderr when we try to delete an artifact
+# that is not present in the cache.
+@pytest.mark.datafiles(DATA_DIR)
+def test_artifact_delete_unbuilt_artifact(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ element = 'target.bst'
+
+ # delete it, just in case it's there
+ _ = cli.run(project=project, args=['artifact', 'delete', element])
+
+ # Ensure the element is not cached
+ assert cli.get_element_state(project, element) != 'cached'
+
+ # Now try and remove it again (now we know its not there)
+ result = cli.run(project=project, args=['artifact', 'delete', element])
+
+ cache_key = cli.get_element_key(project, element)
+ artifact = os.path.join('test', os.path.splitext(element)[0], cache_key)
+ expected_err = "WARNING Could not find ref '{}'".format(artifact)
+ assert expected_err in result.stderr
diff --git a/tests/frontend/completions.py b/tests/frontend/completions.py
index 1f29fdae6..7810a06d5 100644
--- a/tests/frontend/completions.py
+++ b/tests/frontend/completions.py
@@ -57,6 +57,7 @@ SOURCE_COMMANDS = [
ARTIFACT_COMMANDS = [
'checkout ',
+ 'delete ',
'push ',
'pull ',
'log ',