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
|
