richardmaw-codethink pushed to branch master at BuildStream / buildstream
Commits:
-
3697a611
by Richard Maw at 2018-12-12T16:31:38Z
-
b3dceb16
by Richard Maw at 2018-12-12T16:32:41Z
-
ba08a0cd
by Richard Maw at 2018-12-12T16:32:41Z
-
70fb9554
by Richard Maw at 2018-12-12T16:32:41Z
-
f773e746
by Richard Maw at 2018-12-12T16:33:02Z
-
b6528441
by richardmaw-codethink at 2018-12-12T18:00:59Z
4 changed files:
Changes:
... | ... | @@ -2,6 +2,8 @@ |
2 | 2 |
buildstream 1.3.1
|
3 | 3 |
=================
|
4 | 4 |
|
5 |
+ o Added `bst artifact log` subcommand for viewing build logs.
|
|
6 |
+ |
|
5 | 7 |
o BREAKING CHANGE: The bst source-bundle command has been removed. The
|
6 | 8 |
functionality it provided has been replaced by the `--include-build-scripts`
|
7 | 9 |
option of the `bst source-checkout` command. To produce a tarball containing
|
1 | 1 |
import os
|
2 | 2 |
import sys
|
3 |
+from contextlib import ExitStack
|
|
4 |
+from fnmatch import fnmatch
|
|
5 |
+from tempfile import TemporaryDirectory
|
|
3 | 6 |
|
4 | 7 |
import click
|
5 | 8 |
from .. import _yaml
|
... | ... | @@ -107,6 +110,23 @@ def complete_target(args, incomplete): |
107 | 110 |
return complete_list
|
108 | 111 |
|
109 | 112 |
|
113 |
+def complete_artifact(args, incomplete):
|
|
114 |
+ from .._context import Context
|
|
115 |
+ ctx = Context()
|
|
116 |
+ |
|
117 |
+ config = None
|
|
118 |
+ for i, arg in enumerate(args):
|
|
119 |
+ if arg in ('-c', '--config'):
|
|
120 |
+ config = args[i + 1]
|
|
121 |
+ ctx.load(config)
|
|
122 |
+ |
|
123 |
+ # element targets are valid artifact names
|
|
124 |
+ complete_list = complete_target(args, incomplete)
|
|
125 |
+ complete_list.extend(ref for ref in ctx.artifactcache.cas.list_refs() if ref.startswith(incomplete))
|
|
126 |
+ |
|
127 |
+ return complete_list
|
|
128 |
+ |
|
129 |
+ |
|
110 | 130 |
def override_completions(cmd, cmd_param, args, incomplete):
|
111 | 131 |
"""
|
112 | 132 |
:param cmd_param: command definition
|
... | ... | @@ -121,13 +141,15 @@ def override_completions(cmd, cmd_param, args, incomplete): |
121 | 141 |
# We can't easily extend click's data structures without
|
122 | 142 |
# modifying click itself, so just do some weak special casing
|
123 | 143 |
# right here and select which parameters we want to handle specially.
|
124 |
- if isinstance(cmd_param.type, click.Path) and \
|
|
125 |
- (cmd_param.name == 'elements' or
|
|
126 |
- cmd_param.name == 'element' or
|
|
127 |
- cmd_param.name == 'except_' or
|
|
128 |
- cmd_param.opts == ['--track'] or
|
|
129 |
- cmd_param.opts == ['--track-except']):
|
|
130 |
- return complete_target(args, incomplete)
|
|
144 |
+ if isinstance(cmd_param.type, click.Path):
|
|
145 |
+ if (cmd_param.name == 'elements' or
|
|
146 |
+ cmd_param.name == 'element' or
|
|
147 |
+ cmd_param.name == 'except_' or
|
|
148 |
+ cmd_param.opts == ['--track'] or
|
|
149 |
+ cmd_param.opts == ['--track-except']):
|
|
150 |
+ return complete_target(args, incomplete)
|
|
151 |
+ if cmd_param.name == 'artifacts':
|
|
152 |
+ return complete_artifact(args, incomplete)
|
|
131 | 153 |
|
132 | 154 |
raise CompleteUnhandled()
|
133 | 155 |
|
... | ... | @@ -915,3 +937,101 @@ def workspace_list(app): |
915 | 937 |
|
916 | 938 |
with app.initialized():
|
917 | 939 |
app.stream.workspace_list()
|
940 |
+ |
|
941 |
+ |
|
942 |
+#############################################################
|
|
943 |
+# Artifact Commands #
|
|
944 |
+#############################################################
|
|
945 |
+def _classify_artifacts(names, cas, project_directory):
|
|
946 |
+ element_targets = []
|
|
947 |
+ artifact_refs = []
|
|
948 |
+ element_globs = []
|
|
949 |
+ artifact_globs = []
|
|
950 |
+ |
|
951 |
+ for name in names:
|
|
952 |
+ if name.endswith('.bst'):
|
|
953 |
+ if any(c in "*?[" for c in name):
|
|
954 |
+ element_globs.append(name)
|
|
955 |
+ else:
|
|
956 |
+ element_targets.append(name)
|
|
957 |
+ else:
|
|
958 |
+ if any(c in "*?[" for c in name):
|
|
959 |
+ artifact_globs.append(name)
|
|
960 |
+ else:
|
|
961 |
+ artifact_refs.append(name)
|
|
962 |
+ |
|
963 |
+ if element_globs:
|
|
964 |
+ for dirpath, _, filenames in os.walk(project_directory):
|
|
965 |
+ for filename in filenames:
|
|
966 |
+ element_path = os.path.join(dirpath, filename).lstrip(project_directory).lstrip('/')
|
|
967 |
+ if any(fnmatch(element_path, glob) for glob in element_globs):
|
|
968 |
+ element_targets.append(element_path)
|
|
969 |
+ |
|
970 |
+ if artifact_globs:
|
|
971 |
+ artifact_refs.extend(ref for ref in cas.list_refs()
|
|
972 |
+ if any(fnmatch(ref, glob) for glob in artifact_globs))
|
|
973 |
+ |
|
974 |
+ return element_targets, artifact_refs
|
|
975 |
+ |
|
976 |
+ |
|
977 |
+@cli.group(short_help="Manipulate cached artifacts")
|
|
978 |
+def artifact():
|
|
979 |
+ """Manipulate cached artifacts"""
|
|
980 |
+ pass
|
|
981 |
+ |
|
982 |
+ |
|
983 |
+################################################################
|
|
984 |
+# Artifact Log Command #
|
|
985 |
+################################################################
|
|
986 |
+@artifact.command(name='log', short_help="Show logs of an artifact")
|
|
987 |
+@click.argument('artifacts', type=click.Path(), nargs=-1)
|
|
988 |
+@click.pass_obj
|
|
989 |
+def artifact_log(app, artifacts):
|
|
990 |
+ """Show logs of all artifacts"""
|
|
991 |
+ from .._exceptions import CASError
|
|
992 |
+ from .._message import MessageType
|
|
993 |
+ from .._pipeline import PipelineSelection
|
|
994 |
+ from ..storage._casbaseddirectory import CasBasedDirectory
|
|
995 |
+ |
|
996 |
+ with ExitStack() as stack:
|
|
997 |
+ stack.enter_context(app.initialized())
|
|
998 |
+ cache = app.context.artifactcache
|
|
999 |
+ |
|
1000 |
+ elements, artifacts = _classify_artifacts(artifacts, cache.cas,
|
|
1001 |
+ app.project.directory)
|
|
1002 |
+ |
|
1003 |
+ vdirs = []
|
|
1004 |
+ extractdirs = []
|
|
1005 |
+ if artifacts:
|
|
1006 |
+ for ref in artifacts:
|
|
1007 |
+ try:
|
|
1008 |
+ cache_id = cache.cas.resolve_ref(ref, update_mtime=True)
|
|
1009 |
+ vdir = CasBasedDirectory(cache.cas, cache_id)
|
|
1010 |
+ vdirs.append(vdir)
|
|
1011 |
+ except CASError as e:
|
|
1012 |
+ app._message(MessageType.WARN, "Artifact {} is not cached".format(ref), detail=str(e))
|
|
1013 |
+ continue
|
|
1014 |
+ if elements:
|
|
1015 |
+ elements = app.stream.load_selection(elements, selection=PipelineSelection.NONE)
|
|
1016 |
+ for element in elements:
|
|
1017 |
+ if not element._cached():
|
|
1018 |
+ app._message(MessageType.WARN, "Element {} is not cached".format(element))
|
|
1019 |
+ continue
|
|
1020 |
+ ref = cache.get_artifact_fullname(element, element._get_cache_key())
|
|
1021 |
+ cache_id = cache.cas.resolve_ref(ref, update_mtime=True)
|
|
1022 |
+ vdir = CasBasedDirectory(cache.cas, cache_id)
|
|
1023 |
+ vdirs.append(vdir)
|
|
1024 |
+ |
|
1025 |
+ for vdir in vdirs:
|
|
1026 |
+ # NOTE: If reading the logs feels unresponsive, here would be a good place to provide progress information.
|
|
1027 |
+ logsdir = vdir.descend(["logs"])
|
|
1028 |
+ td = stack.enter_context(TemporaryDirectory())
|
|
1029 |
+ logsdir.export_files(td, can_link=True)
|
|
1030 |
+ extractdirs.append(td)
|
|
1031 |
+ |
|
1032 |
+ for extractdir in extractdirs:
|
|
1033 |
+ for log in (os.path.join(extractdir, log) for log in os.listdir(extractdir)):
|
|
1034 |
+ # NOTE: Should click gain the ability to pass files to the pager this can be optimised.
|
|
1035 |
+ with open(log) as f:
|
|
1036 |
+ data = f.read()
|
|
1037 |
+ 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', '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
|
|
63 |
+ |
|
64 |
+ # Read the log via glob
|
|
65 |
+ result = cli.run(project=project, args=['artifact', 'log', 'test/base/*'])
|
|
66 |
+ assert result.exit_code == 0
|
|
67 |
+ # The artifact is cached under both a strong key and a weak key
|
|
68 |
+ assert (log + log) == result.output
|