James Ennis pushed to branch jennis/refactor_artifact_log at BuildStream / buildstream
Commits:
-
072f1cbb
by James Ennis at 2019-01-24T16:20:07Z
-
e61c833c
by James Ennis at 2019-01-24T16:20:07Z
-
ff1b28cf
by James Ennis at 2019-01-24T16:20:07Z
-
2977f7b8
by James Ennis at 2019-01-24T16:32:34Z
-
cac1ffca
by James Ennis at 2019-01-24T16:32:34Z
-
ae7880da
by James Ennis at 2019-01-24T16:32:34Z
-
5b33fa48
by James Ennis at 2019-01-24T16:32:34Z
-
cfefc50b
by James Ennis at 2019-01-24T17:36:29Z
-
fb7d29ef
by James Ennis at 2019-01-24T17:38:04Z
-
d06b460a
by James Ennis at 2019-01-24T17:38:14Z
-
074bee0f
by James Ennis at 2019-01-24T17:38:14Z
-
da59dfd0
by James Ennis at 2019-01-24T17:39:12Z
10 changed files:
- buildstream/_artifactcache.py
- + buildstream/_artifactelement.py
- buildstream/_cas/cascache.py
- buildstream/_exceptions.py
- buildstream/_frontend/cli.py
- buildstream/_project.py
- buildstream/_stream.py
- buildstream/element.py
- tests/artifactcache/pull.py
- tests/artifactcache/push.py
Changes:
... | ... | @@ -19,7 +19,6 @@ |
19 | 19 |
|
20 | 20 |
import multiprocessing
|
21 | 21 |
import os
|
22 |
-import string
|
|
23 | 22 |
from collections.abc import Mapping
|
24 | 23 |
|
25 | 24 |
from .types import _KeyStrength
|
... | ... | @@ -77,37 +76,6 @@ class ArtifactCache(): |
77 | 76 |
|
78 | 77 |
self._calculate_cache_quota()
|
79 | 78 |
|
80 |
- # get_artifact_fullname()
|
|
81 |
- #
|
|
82 |
- # Generate a full name for an artifact, including the
|
|
83 |
- # project namespace, element name and cache key.
|
|
84 |
- #
|
|
85 |
- # This can also be used as a relative path safely, and
|
|
86 |
- # will normalize parts of the element name such that only
|
|
87 |
- # digits, letters and some select characters are allowed.
|
|
88 |
- #
|
|
89 |
- # Args:
|
|
90 |
- # element (Element): The Element object
|
|
91 |
- # key (str): The element's cache key
|
|
92 |
- #
|
|
93 |
- # Returns:
|
|
94 |
- # (str): The relative path for the artifact
|
|
95 |
- #
|
|
96 |
- def get_artifact_fullname(self, element, key):
|
|
97 |
- project = element._get_project()
|
|
98 |
- |
|
99 |
- # Normalize ostree ref unsupported chars
|
|
100 |
- valid_chars = string.digits + string.ascii_letters + '-._'
|
|
101 |
- element_name = ''.join([
|
|
102 |
- x if x in valid_chars else '_'
|
|
103 |
- for x in element.normal_name
|
|
104 |
- ])
|
|
105 |
- |
|
106 |
- assert key is not None
|
|
107 |
- |
|
108 |
- # assume project and element names are not allowed to contain slashes
|
|
109 |
- return '{0}/{1}/{2}'.format(project.name, element_name, key)
|
|
110 |
- |
|
111 | 79 |
# setup_remotes():
|
112 | 80 |
#
|
113 | 81 |
# Sets up which remotes to use
|
... | ... | @@ -206,7 +174,7 @@ class ArtifactCache(): |
206 | 174 |
for key in (strong_key, weak_key):
|
207 | 175 |
if key:
|
208 | 176 |
try:
|
209 |
- ref = self.get_artifact_fullname(element, key)
|
|
177 |
+ ref = element.get_artifact_name(key)
|
|
210 | 178 |
|
211 | 179 |
self.cas.update_mtime(ref)
|
212 | 180 |
except CASError:
|
... | ... | @@ -407,18 +375,30 @@ class ArtifactCache(): |
407 | 375 |
|
408 | 376 |
# contains():
|
409 | 377 |
#
|
410 |
- # Check whether the artifact for the specified Element is already available
|
|
411 |
- # in the local artifact cache.
|
|
378 |
+ # Check whether the (project state) artifact of the specified Element is
|
|
379 |
+ # already available in the local artifact cache.
|
|
412 | 380 |
#
|
413 | 381 |
# Args:
|
414 | 382 |
# element (Element): The Element to check
|
415 | 383 |
# key (str): The cache key to use
|
416 | 384 |
#
|
417 |
- # Returns: True if the artifact is in the cache, False otherwise
|
|
385 |
+ # Returns: True if the Element's (project state) artifact is in the cache,
|
|
386 |
+ # False otherwise
|
|
418 | 387 |
#
|
419 | 388 |
def contains(self, element, key):
|
420 |
- ref = self.get_artifact_fullname(element, key)
|
|
389 |
+ ref = element.get_artifact_name(key)
|
|
390 |
+ return self.contains_ref(ref)
|
|
421 | 391 |
|
392 |
+ # contains_ref():
|
|
393 |
+ #
|
|
394 |
+ # Check whether an artifact is already available in the local artifact cache.
|
|
395 |
+ #
|
|
396 |
+ # Args:
|
|
397 |
+ # ref (str): The ref to check
|
|
398 |
+ #
|
|
399 |
+ # Returns: True if the artifact is in the cache, False otherwise
|
|
400 |
+ #
|
|
401 |
+ def contains_ref(self, ref):
|
|
422 | 402 |
return self.cas.contains(ref)
|
423 | 403 |
|
424 | 404 |
# contains_subdir_artifact():
|
... | ... | @@ -434,19 +414,21 @@ class ArtifactCache(): |
434 | 414 |
# Returns: True if the subdir exists & is populated in the cache, False otherwise
|
435 | 415 |
#
|
436 | 416 |
def contains_subdir_artifact(self, element, key, subdir):
|
437 |
- ref = self.get_artifact_fullname(element, key)
|
|
417 |
+ ref = element.get_artifact_name(key)
|
|
438 | 418 |
return self.cas.contains_subdir_artifact(ref, subdir)
|
439 | 419 |
|
440 | 420 |
# list_artifacts():
|
441 | 421 |
#
|
442 | 422 |
# List artifacts in this cache in LRU order.
|
443 | 423 |
#
|
424 |
+ # Args:
|
|
425 |
+ # glob (str): An option glob _expression_ to be used to list artifacts satisfying the glob
|
|
426 |
+ #
|
|
444 | 427 |
# Returns:
|
445 |
- # ([str]) - A list of artifact names as generated by
|
|
446 |
- # `ArtifactCache.get_artifact_fullname` in LRU order
|
|
428 |
+ # ([str]) - A list of artifact names as generated in LRU order
|
|
447 | 429 |
#
|
448 |
- def list_artifacts(self):
|
|
449 |
- return self.cas.list_refs()
|
|
430 |
+ def list_artifacts(self, *, glob=None):
|
|
431 |
+ return self.cas.list_refs(glob=glob)
|
|
450 | 432 |
|
451 | 433 |
# remove():
|
452 | 434 |
#
|
... | ... | @@ -455,8 +437,7 @@ class ArtifactCache(): |
455 | 437 |
#
|
456 | 438 |
# Args:
|
457 | 439 |
# ref (artifact_name): The name of the artifact to remove (as
|
458 |
- # generated by
|
|
459 |
- # `ArtifactCache.get_artifact_fullname`)
|
|
440 |
+ # generated by `Element.get_artifact_name`)
|
|
460 | 441 |
#
|
461 | 442 |
# Returns:
|
462 | 443 |
# (int|None) The amount of space pruned from the repository in
|
... | ... | @@ -503,7 +484,7 @@ class ArtifactCache(): |
503 | 484 |
# Returns: path to extracted artifact
|
504 | 485 |
#
|
505 | 486 |
def extract(self, element, key, subdir=None):
|
506 |
- ref = self.get_artifact_fullname(element, key)
|
|
487 |
+ ref = element.get_artifact_name(key)
|
|
507 | 488 |
|
508 | 489 |
path = os.path.join(self.extractdir, element._get_project().name, element.normal_name)
|
509 | 490 |
|
... | ... | @@ -519,7 +500,7 @@ class ArtifactCache(): |
519 | 500 |
# keys (list): The cache keys to use
|
520 | 501 |
#
|
521 | 502 |
def commit(self, element, content, keys):
|
522 |
- refs = [self.get_artifact_fullname(element, key) for key in keys]
|
|
503 |
+ refs = [element.get_artifact_name(key) for key in keys]
|
|
523 | 504 |
|
524 | 505 |
self.cas.commit(refs, content)
|
525 | 506 |
|
... | ... | @@ -535,8 +516,8 @@ class ArtifactCache(): |
535 | 516 |
# subdir (str): A subdirectory to limit the comparison to
|
536 | 517 |
#
|
537 | 518 |
def diff(self, element, key_a, key_b, *, subdir=None):
|
538 |
- ref_a = self.get_artifact_fullname(element, key_a)
|
|
539 |
- ref_b = self.get_artifact_fullname(element, key_b)
|
|
519 |
+ ref_a = element.get_artifact_name(key_a)
|
|
520 |
+ ref_b = element.get_artifact_name(key_b)
|
|
540 | 521 |
|
541 | 522 |
return self.cas.diff(ref_a, ref_b, subdir=subdir)
|
542 | 523 |
|
... | ... | @@ -597,7 +578,7 @@ class ArtifactCache(): |
597 | 578 |
# (ArtifactError): if there was an error
|
598 | 579 |
#
|
599 | 580 |
def push(self, element, keys):
|
600 |
- refs = [self.get_artifact_fullname(element, key) for key in list(keys)]
|
|
581 |
+ refs = [element.get_artifact_name(key) for key in list(keys)]
|
|
601 | 582 |
|
602 | 583 |
project = element._get_project()
|
603 | 584 |
|
... | ... | @@ -635,7 +616,7 @@ class ArtifactCache(): |
635 | 616 |
# (bool): True if pull was successful, False if artifact was not available
|
636 | 617 |
#
|
637 | 618 |
def pull(self, element, key, *, progress=None, subdir=None, excluded_subdirs=None):
|
638 |
- ref = self.get_artifact_fullname(element, key)
|
|
619 |
+ ref = element.get_artifact_name(key)
|
|
639 | 620 |
|
640 | 621 |
project = element._get_project()
|
641 | 622 |
|
... | ... | @@ -747,11 +728,25 @@ class ArtifactCache(): |
747 | 728 |
# newkey (str): A new cache key for the artifact
|
748 | 729 |
#
|
749 | 730 |
def link_key(self, element, oldkey, newkey):
|
750 |
- oldref = self.get_artifact_fullname(element, oldkey)
|
|
751 |
- newref = self.get_artifact_fullname(element, newkey)
|
|
731 |
+ oldref = element.get_artifact_name(oldkey)
|
|
732 |
+ newref = element.get_artifact_name(newkey)
|
|
752 | 733 |
|
753 | 734 |
self.cas.link_ref(oldref, newref)
|
754 | 735 |
|
736 |
+ # get_artifact_logs():
|
|
737 |
+ #
|
|
738 |
+ # Get the logs of an existing artifact
|
|
739 |
+ #
|
|
740 |
+ # Args:
|
|
741 |
+ # ref (str): The ref of the artifact
|
|
742 |
+ #
|
|
743 |
+ # Returns:
|
|
744 |
+ # logsdir (CasBasedDirectory): A CasBasedDirectory containing the artifact's logs
|
|
745 |
+ #
|
|
746 |
+ def get_artifact_logs(self, ref):
|
|
747 |
+ descend = ["logs"]
|
|
748 |
+ return self.cas.get_toplevel_dir(ref, descend)
|
|
749 |
+ |
|
755 | 750 |
################################################
|
756 | 751 |
# Local Private Methods #
|
757 | 752 |
################################################
|
1 |
+#
|
|
2 |
+# Copyright (C) 2019 Bloomberg Finance LP
|
|
3 |
+#
|
|
4 |
+# This program is free software; you can redistribute it and/or
|
|
5 |
+# modify it under the terms of the GNU Lesser General Public
|
|
6 |
+# License as published by the Free Software Foundation; either
|
|
7 |
+# version 2 of the License, or (at your option) any later version.
|
|
8 |
+#
|
|
9 |
+# This library is distributed in the hope that it will be useful,
|
|
10 |
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11 |
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
12 |
+# Lesser General Public License for more details.
|
|
13 |
+#
|
|
14 |
+# You should have received a copy of the GNU Lesser General Public
|
|
15 |
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
|
16 |
+#
|
|
17 |
+# Authors:
|
|
18 |
+# James Ennis <james ennis codethink co uk>
|
|
19 |
+from ._exceptions import ArtifactElementError
|
|
20 |
+ |
|
21 |
+ |
|
22 |
+# ArtifactElement()
|
|
23 |
+#
|
|
24 |
+# Object to be used for directly processing an artifact
|
|
25 |
+#
|
|
26 |
+# Args:
|
|
27 |
+# context (Context): The Context object
|
|
28 |
+# ref (str): The artifact ref
|
|
29 |
+#
|
|
30 |
+class ArtifactElement():
|
|
31 |
+ def __init__(self, context, ref):
|
|
32 |
+ try:
|
|
33 |
+ project_name, element, key = ref.split('/', 2)
|
|
34 |
+ except ValueError:
|
|
35 |
+ raise ArtifactElementError("Artifact: {} is not of the expected format".format(ref))
|
|
36 |
+ |
|
37 |
+ self._project_name = project_name
|
|
38 |
+ self._element = element
|
|
39 |
+ self._key = key
|
|
40 |
+ self._context = context
|
|
41 |
+ |
|
42 |
+ def get_artifact_name(self):
|
|
43 |
+ return '{0}/{1}/{2}'.format(self._project_name, self._element, self._key)
|
|
44 |
+ |
|
45 |
+ def _cached(self):
|
|
46 |
+ return self._context.artifactcache.contains_ref(self.get_artifact_name())
|
... | ... | @@ -24,6 +24,7 @@ import stat |
24 | 24 |
import errno
|
25 | 25 |
import uuid
|
26 | 26 |
import contextlib
|
27 |
+from fnmatch import fnmatch
|
|
27 | 28 |
|
28 | 29 |
import grpc
|
29 | 30 |
|
... | ... | @@ -32,6 +33,7 @@ from .._protos.buildstream.v2 import buildstream_pb2 |
32 | 33 |
|
33 | 34 |
from .. import utils
|
34 | 35 |
from .._exceptions import CASCacheError
|
36 |
+from ..storage._casbaseddirectory import CasBasedDirectory
|
|
35 | 37 |
|
36 | 38 |
from .casremote import BlobNotFound, _CASBatchRead, _CASBatchUpdate
|
37 | 39 |
|
... | ... | @@ -472,22 +474,35 @@ class CASCache(): |
472 | 474 |
#
|
473 | 475 |
# List refs in Least Recently Modified (LRM) order.
|
474 | 476 |
#
|
477 |
+ # Args:
|
|
478 |
+ # glob (str) - An optional glob _expression_ to be used to list refs satisfying the glob
|
|
479 |
+ #
|
|
475 | 480 |
# Returns:
|
476 | 481 |
# (list) - A list of refs in LRM order
|
477 | 482 |
#
|
478 |
- def list_refs(self):
|
|
483 |
+ def list_refs(self, *, glob=None):
|
|
479 | 484 |
# string of: /path/to/repo/refs/heads
|
480 | 485 |
ref_heads = os.path.join(self.casdir, 'refs', 'heads')
|
486 |
+ path = ref_heads
|
|
487 |
+ |
|
488 |
+ if glob is not None:
|
|
489 |
+ globdir = os.path.dirname(glob)
|
|
490 |
+ if not any(c in "*?[" for c in globdir):
|
|
491 |
+ # path prefix contains no globbing characters so
|
|
492 |
+ # append the glob to optimise the os.walk()
|
|
493 |
+ path = os.path.join(ref_heads, globdir)
|
|
481 | 494 |
|
482 | 495 |
refs = []
|
483 | 496 |
mtimes = []
|
484 | 497 |
|
485 |
- for root, _, files in os.walk(ref_heads):
|
|
498 |
+ for root, _, files in os.walk(path):
|
|
486 | 499 |
for filename in files:
|
487 | 500 |
ref_path = os.path.join(root, filename)
|
488 |
- refs.append(os.path.relpath(ref_path, ref_heads))
|
|
489 |
- # Obtain the mtime (the time a file was last modified)
|
|
490 |
- mtimes.append(os.path.getmtime(ref_path))
|
|
501 |
+ relative_path = os.path.relpath(ref_path, ref_heads) # Relative to refs head
|
|
502 |
+ if not glob or fnmatch(relative_path, glob):
|
|
503 |
+ refs.append(relative_path)
|
|
504 |
+ # Obtain the mtime (the time a file was last modified)
|
|
505 |
+ mtimes.append(os.path.getmtime(ref_path))
|
|
491 | 506 |
|
492 | 507 |
# NOTE: Sorted will sort from earliest to latest, thus the
|
493 | 508 |
# first ref of this list will be the file modified earliest.
|
... | ... | @@ -587,6 +602,22 @@ class CASCache(): |
587 | 602 |
reachable = set()
|
588 | 603 |
self._reachable_refs_dir(reachable, tree, update_mtime=True)
|
589 | 604 |
|
605 |
+ # get_toplevel_dir()
|
|
606 |
+ #
|
|
607 |
+ # Return a CasBasedDirectory object of the specified sub_directories
|
|
608 |
+ #
|
|
609 |
+ # Args:
|
|
610 |
+ # ref (str): The artifact ref
|
|
611 |
+ # descend (list): A list of strings of artifact subdirectories
|
|
612 |
+ #
|
|
613 |
+ # Returns:
|
|
614 |
+ # (CasBasedDirectory): The CasBasedDirectory object
|
|
615 |
+ #
|
|
616 |
+ def get_toplevel_dir(self, ref, descend):
|
|
617 |
+ cache_id = self.resolve_ref(ref, update_mtime=True)
|
|
618 |
+ vdir = CasBasedDirectory(self, cache_id).descend(descend)
|
|
619 |
+ return vdir
|
|
620 |
+ |
|
590 | 621 |
################################################
|
591 | 622 |
# Local Private Methods #
|
592 | 623 |
################################################
|
... | ... | @@ -344,3 +344,12 @@ class AppError(BstError): |
344 | 344 |
#
|
345 | 345 |
class SkipJob(Exception):
|
346 | 346 |
pass
|
347 |
+ |
|
348 |
+ |
|
349 |
+# ArtifactElementError
|
|
350 |
+#
|
|
351 |
+# Raised when errors are encountered by artifact elements
|
|
352 |
+#
|
|
353 |
+class ArtifactElementError(BstError):
|
|
354 |
+ def __init__(self, message, *, detail=None, reason=None, temporary=False):
|
|
355 |
+ super().__init__(message, detail=detail, domain=ErrorDomain.ELEMENT, reason=reason, temporary=True)
|
1 | 1 |
import os
|
2 | 2 |
import sys
|
3 | 3 |
from contextlib import ExitStack
|
4 |
-from fnmatch import fnmatch
|
|
5 | 4 |
from functools import partial
|
6 | 5 |
from tempfile import TemporaryDirectory
|
7 | 6 |
|
... | ... | @@ -859,38 +858,6 @@ def workspace_list(app): |
859 | 858 |
#############################################################
|
860 | 859 |
# Artifact Commands #
|
861 | 860 |
#############################################################
|
862 |
-def _classify_artifacts(names, cas, project_directory):
|
|
863 |
- element_targets = []
|
|
864 |
- artifact_refs = []
|
|
865 |
- element_globs = []
|
|
866 |
- artifact_globs = []
|
|
867 |
- |
|
868 |
- for name in names:
|
|
869 |
- if name.endswith('.bst'):
|
|
870 |
- if any(c in "*?[" for c in name):
|
|
871 |
- element_globs.append(name)
|
|
872 |
- else:
|
|
873 |
- element_targets.append(name)
|
|
874 |
- else:
|
|
875 |
- if any(c in "*?[" for c in name):
|
|
876 |
- artifact_globs.append(name)
|
|
877 |
- else:
|
|
878 |
- artifact_refs.append(name)
|
|
879 |
- |
|
880 |
- if element_globs:
|
|
881 |
- for dirpath, _, filenames in os.walk(project_directory):
|
|
882 |
- for filename in filenames:
|
|
883 |
- element_path = os.path.join(dirpath, filename).lstrip(project_directory).lstrip('/')
|
|
884 |
- if any(fnmatch(element_path, glob) for glob in element_globs):
|
|
885 |
- element_targets.append(element_path)
|
|
886 |
- |
|
887 |
- if artifact_globs:
|
|
888 |
- artifact_refs.extend(ref for ref in cas.list_refs()
|
|
889 |
- if any(fnmatch(ref, glob) for glob in artifact_globs))
|
|
890 |
- |
|
891 |
- return element_targets, artifact_refs
|
|
892 |
- |
|
893 |
- |
|
894 | 861 |
@cli.group(short_help="Manipulate cached artifacts")
|
895 | 862 |
def artifact():
|
896 | 863 |
"""Manipulate cached artifacts"""
|
... | ... | @@ -1045,53 +1012,30 @@ def artifact_push(app, elements, deps, remote): |
1045 | 1012 |
@click.pass_obj
|
1046 | 1013 |
def artifact_log(app, artifacts):
|
1047 | 1014 |
"""Show logs of all artifacts"""
|
1048 |
- from .._exceptions import CASError
|
|
1049 |
- from .._message import MessageType
|
|
1050 |
- from .._pipeline import PipelineSelection
|
|
1051 |
- from ..storage._casbaseddirectory import CasBasedDirectory
|
|
1052 |
- |
|
1053 |
- with ExitStack() as stack:
|
|
1054 |
- stack.enter_context(app.initialized())
|
|
1055 |
- cache = app.context.artifactcache
|
|
1015 |
+ # Guess the element if we're in a workspace
|
|
1016 |
+ if not artifacts:
|
|
1017 |
+ guessed_target = app.context.guess_element()
|
|
1018 |
+ if guessed_target:
|
|
1019 |
+ artifacts = [guessed_target]
|
|
1056 | 1020 |
|
1057 |
- elements, artifacts = _classify_artifacts(artifacts, cache.cas,
|
|
1058 |
- app.project.directory)
|
|
1059 |
- |
|
1060 |
- vdirs = []
|
|
1061 |
- extractdirs = []
|
|
1062 |
- if artifacts:
|
|
1063 |
- for ref in artifacts:
|
|
1064 |
- try:
|
|
1065 |
- cache_id = cache.cas.resolve_ref(ref, update_mtime=True)
|
|
1066 |
- vdir = CasBasedDirectory(cache.cas, cache_id)
|
|
1067 |
- vdirs.append(vdir)
|
|
1068 |
- except CASError as e:
|
|
1069 |
- app._message(MessageType.WARN, "Artifact {} is not cached".format(ref), detail=str(e))
|
|
1070 |
- continue
|
|
1071 |
- if elements:
|
|
1072 |
- elements = app.stream.load_selection(elements, selection=PipelineSelection.NONE)
|
|
1073 |
- for element in elements:
|
|
1074 |
- if not element._cached():
|
|
1075 |
- app._message(MessageType.WARN, "Element {} is not cached".format(element))
|
|
1076 |
- continue
|
|
1077 |
- ref = cache.get_artifact_fullname(element, element._get_cache_key())
|
|
1078 |
- cache_id = cache.cas.resolve_ref(ref, update_mtime=True)
|
|
1079 |
- vdir = CasBasedDirectory(cache.cas, cache_id)
|
|
1080 |
- vdirs.append(vdir)
|
|
1081 |
- |
|
1082 |
- for vdir in vdirs:
|
|
1083 |
- # NOTE: If reading the logs feels unresponsive, here would be a good place to provide progress information.
|
|
1084 |
- logsdir = vdir.descend(["logs"])
|
|
1085 |
- td = stack.enter_context(TemporaryDirectory())
|
|
1086 |
- logsdir.export_files(td, can_link=True)
|
|
1087 |
- extractdirs.append(td)
|
|
1088 |
- |
|
1089 |
- for extractdir in extractdirs:
|
|
1090 |
- for log in (os.path.join(extractdir, log) for log in os.listdir(extractdir)):
|
|
1091 |
- # NOTE: Should click gain the ability to pass files to the pager this can be optimised.
|
|
1092 |
- with open(log) as f:
|
|
1093 |
- data = f.read()
|
|
1094 |
- click.echo_via_pager(data)
|
|
1021 |
+ with app.initialized():
|
|
1022 |
+ logsdirs = app.stream.artifact_log(artifacts)
|
|
1023 |
+ |
|
1024 |
+ with ExitStack() as stack:
|
|
1025 |
+ extractdirs = []
|
|
1026 |
+ for logsdir in logsdirs:
|
|
1027 |
+ # NOTE: If reading the logs feels unresponsive, here would be a good place
|
|
1028 |
+ # to provide progress information.
|
|
1029 |
+ td = stack.enter_context(TemporaryDirectory())
|
|
1030 |
+ logsdir.export_files(td, can_link=True)
|
|
1031 |
+ extractdirs.append(td)
|
|
1032 |
+ |
|
1033 |
+ for extractdir in extractdirs:
|
|
1034 |
+ for log in (os.path.join(extractdir, log) for log in os.listdir(extractdir)):
|
|
1035 |
+ # NOTE: Should click gain the ability to pass files to the pager this can be optimised.
|
|
1036 |
+ with open(log) as f:
|
|
1037 |
+ data = f.read()
|
|
1038 |
+ click.echo_via_pager(data)
|
|
1095 | 1039 |
|
1096 | 1040 |
|
1097 | 1041 |
##################################################################
|
... | ... | @@ -26,6 +26,7 @@ from . import utils |
26 | 26 |
from . import _cachekey
|
27 | 27 |
from . import _site
|
28 | 28 |
from . import _yaml
|
29 |
+from ._artifactelement import ArtifactElement
|
|
29 | 30 |
from ._profile import Topics, profile_start, profile_end
|
30 | 31 |
from ._exceptions import LoadError, LoadErrorReason
|
31 | 32 |
from ._options import OptionPool
|
... | ... | @@ -252,6 +253,19 @@ class Project(): |
252 | 253 |
else:
|
253 | 254 |
return self.config.element_factory.create(self._context, self, meta)
|
254 | 255 |
|
256 |
+ # create_artifact_element()
|
|
257 |
+ #
|
|
258 |
+ # Instantiate and return an ArtifactElement
|
|
259 |
+ #
|
|
260 |
+ # Args:
|
|
261 |
+ # ref (str): A string of the artifact ref
|
|
262 |
+ #
|
|
263 |
+ # Returns:
|
|
264 |
+ # (ArtifactElement): A newly created ArtifactElement object of the appropriate kind
|
|
265 |
+ #
|
|
266 |
+ def create_artifact_element(self, ref):
|
|
267 |
+ return ArtifactElement(self._context, ref)
|
|
268 |
+ |
|
255 | 269 |
# create_source()
|
256 | 270 |
#
|
257 | 271 |
# Instantiate and return a Source
|
... | ... | @@ -27,6 +27,7 @@ import shutil |
27 | 27 |
import tarfile
|
28 | 28 |
import tempfile
|
29 | 29 |
from contextlib import contextmanager, suppress
|
30 |
+from fnmatch import fnmatch
|
|
30 | 31 |
|
31 | 32 |
from ._exceptions import StreamError, ImplError, BstError, set_last_task_error
|
32 | 33 |
from ._message import Message, MessageType
|
... | ... | @@ -439,6 +440,45 @@ class Stream(): |
439 | 440 |
raise StreamError("Error while staging dependencies into a sandbox"
|
440 | 441 |
": '{}'".format(e), detail=e.detail, reason=e.reason) from e
|
441 | 442 |
|
443 |
+ # artifact_log()
|
|
444 |
+ #
|
|
445 |
+ # Show the full log of an artifact
|
|
446 |
+ #
|
|
447 |
+ # Args:
|
|
448 |
+ # targets (str): Targets to view the logs of
|
|
449 |
+ #
|
|
450 |
+ # Returns:
|
|
451 |
+ # logsdir (list): A list of CasBasedDirectory objects containing artifact logs
|
|
452 |
+ #
|
|
453 |
+ def artifact_log(self, targets):
|
|
454 |
+ # Distinguish the artifacts from the elements
|
|
455 |
+ elements, artifacts = self._classify_artifacts(targets)
|
|
456 |
+ |
|
457 |
+ # Obtain Element objects
|
|
458 |
+ if elements:
|
|
459 |
+ elements = self.load_selection(elements, selection=PipelineSelection.NONE)
|
|
460 |
+ |
|
461 |
+ # Obtain ArtifactElement objects
|
|
462 |
+ artifact_elements = []
|
|
463 |
+ if artifacts:
|
|
464 |
+ for ref in artifacts:
|
|
465 |
+ artifact_element = self._project.create_artifact_element(ref)
|
|
466 |
+ artifact_elements.append(artifact_element)
|
|
467 |
+ |
|
468 |
+ # Concatenate the lists
|
|
469 |
+ objects = elements + artifact_elements
|
|
470 |
+ |
|
471 |
+ logsdirs = []
|
|
472 |
+ for obj in objects:
|
|
473 |
+ ref = obj.get_artifact_name()
|
|
474 |
+ if not obj._cached():
|
|
475 |
+ self._message(MessageType.WARN, "{} is not cached".format(ref))
|
|
476 |
+ continue
|
|
477 |
+ |
|
478 |
+ logsdirs.append(self._artifacts.get_artifact_logs(ref))
|
|
479 |
+ |
|
480 |
+ return logsdirs
|
|
481 |
+ |
|
442 | 482 |
# source_checkout()
|
443 | 483 |
#
|
444 | 484 |
# Checkout sources of the target element to the specified location
|
... | ... | @@ -1273,3 +1313,50 @@ class Stream(): |
1273 | 1313 |
required_list.append(element)
|
1274 | 1314 |
|
1275 | 1315 |
return required_list
|
1316 |
+ |
|
1317 |
+ # _classify_artifacts()
|
|
1318 |
+ #
|
|
1319 |
+ # Split up a list of targets into element names and artifact refs
|
|
1320 |
+ #
|
|
1321 |
+ # Args:
|
|
1322 |
+ # targets (list): A list of targets
|
|
1323 |
+ #
|
|
1324 |
+ # Returns:
|
|
1325 |
+ # (list): element names present in the targets
|
|
1326 |
+ # (list): artifact refs present in the targets
|
|
1327 |
+ #
|
|
1328 |
+ def _classify_artifacts(self, targets):
|
|
1329 |
+ element_targets = []
|
|
1330 |
+ artifact_refs = []
|
|
1331 |
+ element_globs = []
|
|
1332 |
+ artifact_globs = []
|
|
1333 |
+ |
|
1334 |
+ for target in targets:
|
|
1335 |
+ if target.endswith('.bst'):
|
|
1336 |
+ if any(c in "*?[" for c in target):
|
|
1337 |
+ element_globs.append(target)
|
|
1338 |
+ else:
|
|
1339 |
+ element_targets.append(target)
|
|
1340 |
+ else:
|
|
1341 |
+ if any(c in "*?[" for c in target):
|
|
1342 |
+ artifact_globs.append(target)
|
|
1343 |
+ else:
|
|
1344 |
+ artifact_refs.append(target)
|
|
1345 |
+ |
|
1346 |
+ if element_globs:
|
|
1347 |
+ for dirpath, _, filenames in os.walk(self._project.element_path):
|
|
1348 |
+ for filename in filenames:
|
|
1349 |
+ element_path = os.path.join(dirpath, filename)
|
|
1350 |
+ length = len(self._project.element_path) + 1
|
|
1351 |
+ element_path = element_path[length:] # Strip out the element_path
|
|
1352 |
+ |
|
1353 |
+ if any(fnmatch(element_path, glob) for glob in element_globs):
|
|
1354 |
+ element_targets.append(element_path)
|
|
1355 |
+ |
|
1356 |
+ if artifact_globs:
|
|
1357 |
+ for glob in artifact_globs:
|
|
1358 |
+ artifact_refs = self._artifacts.list_artifacts(glob=glob)
|
|
1359 |
+ if not artifact_refs:
|
|
1360 |
+ self._message(MessageType.WARN, "No artifacts found for globs: {}".format(', '.join(artifact_globs)))
|
|
1361 |
+ |
|
1362 |
+ return element_targets, artifact_refs
|
... | ... | @@ -82,6 +82,7 @@ import contextlib |
82 | 82 |
from contextlib import contextmanager
|
83 | 83 |
import tempfile
|
84 | 84 |
import shutil
|
85 |
+import string
|
|
85 | 86 |
|
86 | 87 |
from . import _yaml
|
87 | 88 |
from ._variables import Variables
|
... | ... | @@ -577,6 +578,37 @@ class Element(Plugin): |
577 | 578 |
self.__assert_cached()
|
578 | 579 |
return self.__compute_splits(include, exclude, orphans)
|
579 | 580 |
|
581 |
+ def get_artifact_name(self, key=None):
|
|
582 |
+ """Compute and return this element's full artifact name
|
|
583 |
+ |
|
584 |
+ Generate a full name for an artifact, including the project
|
|
585 |
+ namespace, element name and cache key.
|
|
586 |
+ |
|
587 |
+ This can also be used as a relative path safely, and
|
|
588 |
+ will normalize parts of the element name such that only
|
|
589 |
+ digits, letters and some select characters are allowed.
|
|
590 |
+ |
|
591 |
+ Args:
|
|
592 |
+ key (str): The element's cache key. Defaults to None
|
|
593 |
+ |
|
594 |
+ Returns:
|
|
595 |
+ (str): The relative path for the artifact
|
|
596 |
+ """
|
|
597 |
+ project = self._get_project()
|
|
598 |
+ if key is None:
|
|
599 |
+ key = self._get_cache_key()
|
|
600 |
+ |
|
601 |
+ assert key is not None
|
|
602 |
+ |
|
603 |
+ valid_chars = string.digits + string.ascii_letters + '-._'
|
|
604 |
+ element_name = ''.join([
|
|
605 |
+ x if x in valid_chars else '_'
|
|
606 |
+ for x in self.normal_name
|
|
607 |
+ ])
|
|
608 |
+ |
|
609 |
+ # assume project and element names are not allowed to contain slashes
|
|
610 |
+ return '{0}/{1}/{2}'.format(project.name, element_name, key)
|
|
611 |
+ |
|
580 | 612 |
def stage_artifact(self, sandbox, *, path=None, include=None, exclude=None, orphans=True, update_mtimes=None):
|
581 | 613 |
"""Stage this element's output artifact in the sandbox
|
582 | 614 |
|
... | ... | @@ -210,7 +210,7 @@ def test_pull_tree(cli, tmpdir, datafiles): |
210 | 210 |
assert artifactcache.contains(element, element_key)
|
211 | 211 |
|
212 | 212 |
# Retrieve the Directory object from the cached artifact
|
213 |
- artifact_ref = artifactcache.get_artifact_fullname(element, element_key)
|
|
213 |
+ artifact_ref = element.get_artifact_name(element_key)
|
|
214 | 214 |
artifact_digest = cas.resolve_ref(artifact_ref)
|
215 | 215 |
|
216 | 216 |
queue = multiprocessing.Queue()
|
... | ... | @@ -190,7 +190,7 @@ def test_push_directory(cli, tmpdir, datafiles): |
190 | 190 |
assert artifactcache.has_push_remotes(element=element)
|
191 | 191 |
|
192 | 192 |
# Recreate the CasBasedDirectory object from the cached artifact
|
193 |
- artifact_ref = artifactcache.get_artifact_fullname(element, element_key)
|
|
193 |
+ artifact_ref = element.get_artifact_name(element_key)
|
|
194 | 194 |
artifact_digest = cas.resolve_ref(artifact_ref)
|
195 | 195 |
|
196 | 196 |
queue = multiprocessing.Queue()
|