richardmaw-codethink pushed to branch richardmaw/artifact-log at BuildStream / buildstream
Commits:
-
132a5ef2
by Richard Maw at 2018-11-01T16:36:14Z
-
fc0136c1
by Richard Maw at 2018-11-01T16:36:14Z
-
d614987b
by Richard Maw at 2018-11-01T16:36:14Z
-
f667dbe5
by Richard Maw at 2018-11-01T16:36:14Z
-
e5d02f62
by Richard Maw at 2018-11-01T16:36:14Z
4 changed files:
Changes:
| 1 |
+===============
|
|
| 2 |
+buildstream 1.4
|
|
| 3 |
+===============
|
|
| 4 |
+ |
|
| 5 |
+ o Added `bst artifact log` subcommand for viewing build logs.
|
|
| 6 |
+ |
|
| 1 | 7 |
=================
|
| 2 | 8 |
buildstream 1.3.1
|
| 3 | 9 |
=================
|
| ... | ... | @@ -112,6 +112,18 @@ def complete_target(args, incomplete): |
| 112 | 112 |
return complete_path("File", incomplete, base_directory=base_directory)
|
| 113 | 113 |
|
| 114 | 114 |
|
| 115 |
+def complete_artifact(args, incomplete):
|
|
| 116 |
+ from .._context import Context
|
|
| 117 |
+ ctx = Context()
|
|
| 118 |
+ |
|
| 119 |
+ config = None
|
|
| 120 |
+ for i, arg in enumerate(args):
|
|
| 121 |
+ if arg in ('-c', '--config'):
|
|
| 122 |
+ config = args[i + 1]
|
|
| 123 |
+ ctx.load(config)
|
|
| 124 |
+ return [ref for ref in ctx.artifactcache.list_artifacts() if ref.startswith(incomplete)]
|
|
| 125 |
+ |
|
| 126 |
+ |
|
| 115 | 127 |
def override_completions(cmd, cmd_param, args, incomplete):
|
| 116 | 128 |
"""
|
| 117 | 129 |
:param cmd_param: command definition
|
| ... | ... | @@ -126,13 +138,15 @@ def override_completions(cmd, cmd_param, args, incomplete): |
| 126 | 138 |
# We can't easily extend click's data structures without
|
| 127 | 139 |
# modifying click itself, so just do some weak special casing
|
| 128 | 140 |
# right here and select which parameters we want to handle specially.
|
| 129 |
- if isinstance(cmd_param.type, click.Path) and \
|
|
| 130 |
- (cmd_param.name == 'elements' or
|
|
| 131 |
- cmd_param.name == 'element' or
|
|
| 132 |
- cmd_param.name == 'except_' or
|
|
| 133 |
- cmd_param.opts == ['--track'] or
|
|
| 134 |
- cmd_param.opts == ['--track-except']):
|
|
| 135 |
- return complete_target(args, incomplete)
|
|
| 141 |
+ if isinstance(cmd_param.type, click.Path):
|
|
| 142 |
+ if (cmd_param.name == 'elements' or
|
|
| 143 |
+ cmd_param.name == 'element' or
|
|
| 144 |
+ cmd_param.name == 'except_' or
|
|
| 145 |
+ cmd_param.opts == ['--track'] or
|
|
| 146 |
+ cmd_param.opts == ['--track-except']):
|
|
| 147 |
+ return complete_target(args, incomplete)
|
|
| 148 |
+ if cmd_param.name == 'artifacts':
|
|
| 149 |
+ return complete_artifact(args, incomplete)
|
|
| 136 | 150 |
|
| 137 | 151 |
raise CompleteUnhandled()
|
| 138 | 152 |
|
| ... | ... | @@ -829,3 +843,65 @@ def source_bundle(app, element, force, directory, |
| 829 | 843 |
force=force,
|
| 830 | 844 |
compression=compression,
|
| 831 | 845 |
except_targets=except_)
|
| 846 |
+ |
|
| 847 |
+ |
|
| 848 |
+#############################################################
|
|
| 849 |
+# Artifact Command #
|
|
| 850 |
+#############################################################
|
|
| 851 |
+@cli.group(short_help="Manipulate cached Artifacts")
|
|
| 852 |
+def artifact():
|
|
| 853 |
+ """Manipulate cached Artifacts"""
|
|
| 854 |
+ pass
|
|
| 855 |
+ |
|
| 856 |
+ |
|
| 857 |
+################################################################
|
|
| 858 |
+# Artifact Log Command #
|
|
| 859 |
+################################################################
|
|
| 860 |
+@artifact.command(name='log', short_help="Show logs of an artifact")
|
|
| 861 |
+@click.option('-e', '--element', 'elements', help="Show logs for artifacts of this element",
|
|
| 862 |
+ type=click.Path(readable=False), multiple=True, required=False)
|
|
| 863 |
+@click.argument('artifacts', type=click.Path(), nargs=-1)
|
|
| 864 |
+@click.pass_obj
|
|
| 865 |
+def artifact_log(app, elements, artifacts):
|
|
| 866 |
+ """Show logs of all artifacts"""
|
|
| 867 |
+ from tempfile import TemporaryDirectory
|
|
| 868 |
+ from .._exceptions import ArtifactError
|
|
| 869 |
+ from .._message import MessageType
|
|
| 870 |
+ from .._pipeline import PipelineSelection
|
|
| 871 |
+ |
|
| 872 |
+ with app.initialized(), TemporaryDirectory(dir=os.path.join(app.context.artifactdir, 'tmp')) as td:
|
|
| 873 |
+ cache = app.context.artifactcache
|
|
| 874 |
+ |
|
| 875 |
+ refs = []
|
|
| 876 |
+ if artifacts:
|
|
| 877 |
+ for artifact in artifacts:
|
|
| 878 |
+ try:
|
|
| 879 |
+ o = cache.resolve_ref(artifact)
|
|
| 880 |
+ except ArtifactError as e:
|
|
| 881 |
+ app._message(MessageType.WARN, "Artifact {} is not cached".format(artifact), detail=str(e))
|
|
| 882 |
+ continue
|
|
| 883 |
+ refs.append((artifact, o))
|
|
| 884 |
+ if elements:
|
|
| 885 |
+ elements = app.stream.load_selection(elements, selection=PipelineSelection.NONE)
|
|
| 886 |
+ for element in elements:
|
|
| 887 |
+ if not element._cached():
|
|
| 888 |
+ app._message(MessageType.WARN, "Element {} is not cached".format(element))
|
|
| 889 |
+ continue
|
|
| 890 |
+ ref = cache.get_artifact_fullname(element, element._get_cache_key())
|
|
| 891 |
+ o = cache.resolve_ref(ref)
|
|
| 892 |
+ refs.append((ref, o))
|
|
| 893 |
+ |
|
| 894 |
+ logs = []
|
|
| 895 |
+ for ref, o in refs:
|
|
| 896 |
+ # TODO: CASCache._get_subdir is a local-private method
|
|
| 897 |
+ logsdir = cache._get_subdir(o, "logs")
|
|
| 898 |
+ destdir = os.path.join(td, ref)
|
|
| 899 |
+ # TODO: CASCache._checkout is a local-private method
|
|
| 900 |
+ cache._checkout(destdir, logsdir)
|
|
| 901 |
+ logs.extend(os.path.join(destdir, log) for log in os.listdir(destdir))
|
|
| 902 |
+ |
|
| 903 |
+ for log in logs:
|
|
| 904 |
+ # NOTE: Should click gain the ability to pass files to the pager this can be optimised.
|
|
| 905 |
+ with open(log) as f:
|
|
| 906 |
+ data = f.read()
|
|
| 907 |
+ click.echo_via_pager(data)
|
| ... | ... | @@ -6,6 +6,7 @@ from tests.testutils import cli |
| 6 | 6 |
DATA_DIR = os.path.dirname(os.path.realpath(__file__))
|
| 7 | 7 |
|
| 8 | 8 |
MAIN_COMMANDS = [
|
| 9 |
+ 'artifact ',
|
|
| 9 | 10 |
'build ',
|
| 10 | 11 |
'checkout ',
|
| 11 | 12 |
'fetch ',
|
| 1 |
+#
|
|
| 2 |
+# Copyright (C) 2018 Codethink Limited
|
|
| 3 |
+# Copyright (C) 2018 Bloomberg Finance LP
|
|
| 4 |
+#
|
|
| 5 |
+# This program is free software; you can redistribute it and/or
|
|
| 6 |
+# modify it under the terms of the GNU Lesser General Public
|
|
| 7 |
+# License as published by the Free Software Foundation; either
|
|
| 8 |
+# version 2 of the License, or (at your option) any later version.
|
|
| 9 |
+#
|
|
| 10 |
+# This library is distributed in the hope that it will be useful,
|
|
| 11 |
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
| 12 |
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
| 13 |
+# Lesser General Public License for more details.
|
|
| 14 |
+#
|
|
| 15 |
+# You should have received a copy of the GNU Lesser General Public
|
|
| 16 |
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
|
| 17 |
+#
|
|
| 18 |
+# Authors: Richard Maw <richard maw codethink co uk>
|
|
| 19 |
+#
|
|
| 20 |
+ |
|
| 21 |
+import os
|
|
| 22 |
+import pytest
|
|
| 23 |
+ |
|
| 24 |
+from tests.testutils import cli_integration as cli
|
|
| 25 |
+ |
|
| 26 |
+ |
|
| 27 |
+pytestmark = pytest.mark.integration
|
|
| 28 |
+ |
|
| 29 |
+ |
|
| 30 |
+# Project directory
|
|
| 31 |
+DATA_DIR = os.path.join(
|
|
| 32 |
+ os.path.dirname(os.path.realpath(__file__)),
|
|
| 33 |
+ "project",
|
|
| 34 |
+)
|
|
| 35 |
+ |
|
| 36 |
+ |
|
| 37 |
+@pytest.mark.integration
|
|
| 38 |
+@pytest.mark.datafiles(DATA_DIR)
|
|
| 39 |
+def test_artifact_log(cli, tmpdir, datafiles):
|
|
| 40 |
+ project = os.path.join(datafiles.dirname, datafiles.basename)
|
|
| 41 |
+ |
|
| 42 |
+ # Get the cache key of our test element
|
|
| 43 |
+ result = cli.run(project=project, silent=True, args=[
|
|
| 44 |
+ '--no-colors',
|
|
| 45 |
+ 'show', '--deps', 'none', '--format', '%{full-key}',
|
|
| 46 |
+ 'base.bst'
|
|
| 47 |
+ ])
|
|
| 48 |
+ key = result.output.strip()
|
|
| 49 |
+ |
|
| 50 |
+ # Ensure we have an artifact to read
|
|
| 51 |
+ result = cli.run(project=project, args=['build', 'base.bst'])
|
|
| 52 |
+ assert result.exit_code == 0
|
|
| 53 |
+ |
|
| 54 |
+ # Read the log via the element name
|
|
| 55 |
+ result = cli.run(project=project, args=['artifact', 'log', '-e', 'base.bst'])
|
|
| 56 |
+ assert result.exit_code == 0
|
|
| 57 |
+ log = result.output
|
|
| 58 |
+ |
|
| 59 |
+ # Read the log via the key
|
|
| 60 |
+ result = cli.run(project=project, args=['artifact', 'log', 'test/base/' + key])
|
|
| 61 |
+ assert result.exit_code == 0
|
|
| 62 |
+ assert log == result.output
|
