# # Copyright (C) 2019 Codethink Limited # Copyright (C) 2019 Bloomberg Finance LP # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . # # Authors: # Tom Pollard # Tristan Van Berkom """ Artifact ========= Implementation of the Artifact class which aims to 'abstract' direct artifact composite interaction away from Element class """ import os import shutil from . import _yaml from ._exceptions import ArtifactError from .types import Scope, _KeyStrength from .storage._casbaseddirectory import CasBasedDirectory # An Artifact class to abtract artifact operations # from the Element class # # Args: # element (Element): The Element object # context (Context): The BuildStream context # class Artifact(): def __init__(self, element, context): self._element = element self._context = context self._artifacts = context.artifactcache # get_files(): # # Get a virtual directory for the artifact files content # # Args: # key (str): The key for the artifact to extract, # or None for the default key # # Returns: # (Directory): The virtual directory object # (str): The chosen key # def get_files(self, key=None): subdir = "files" return self._get_subdirectory(subdir, key) # get_buildtree(): # # Get a virtual directory for the artifact buildtree content # # Args: # key (str): The key for the artifact to extract, # or None for the default key # # Returns: # (Directory): The virtual directory object # (str): The chosen key # def get_buildtree(self, key=None): subdir = "buildtree" return self._get_subdirectory(subdir, key) # get_extract_key(): # # Get the key used to extract the artifact # # Returns: # (str): The key # def get_extract_key(self): element = self._element context = self._context # Use weak cache key, if context allows use of weak cache keys key_strength = _KeyStrength.STRONG key = element._get_cache_key(strength=key_strength) if not context.get_strict() and not key: key = element._get_cache_key(strength=_KeyStrength.WEAK) return key # cache(): # # Create the artifact and commit to cache # # Args: # rootdir (str): An absolute path to the temp rootdir for artifact construct # sandbox_build_dir (Directory): Virtual Directory object for the sandbox build-root # collectvdir (Directory): Virtual Directoy object from within the sandbox for collection # buildresult (tuple): bool, short desc and detailed desc of result # keys (list): list of keys for the artifact commit metadata # publicdata (dict): dict of public data to commit to artifact metadata # # Returns: # (int): The size of the newly cached artifact # def cache(self, rootdir, sandbox_build_dir, collectvdir, buildresult, keys, publicdata): context = self._context element = self._element assemblevdir = CasBasedDirectory(cas_cache=self._artifacts.cas) logsvdir = assemblevdir.descend("logs", create=True) metavdir = assemblevdir.descend("meta", create=True) # Create artifact directory structure assembledir = os.path.join(rootdir, 'artifact') logsdir = os.path.join(assembledir, 'logs') metadir = os.path.join(assembledir, 'meta') os.mkdir(assembledir) os.mkdir(logsdir) os.mkdir(metadir) if collectvdir is not None: filesvdir = assemblevdir.descend("files", create=True) filesvdir.import_files(collectvdir) if sandbox_build_dir: buildtreevdir = assemblevdir.descend("buildtree", create=True) buildtreevdir.import_files(sandbox_build_dir) # Write some logs out to normal directories: logsdir and metadir # Copy build log log_filename = context.get_log_filename() element._build_log_path = os.path.join(logsdir, 'build.log') if log_filename: shutil.copyfile(log_filename, element._build_log_path) # Store public data _yaml.dump(_yaml.node_sanitize(publicdata), os.path.join(metadir, 'public.yaml')) # Store result build_result_dict = {"success": buildresult[0], "description": buildresult[1]} if buildresult[2] is not None: build_result_dict["detail"] = buildresult[2] _yaml.dump(build_result_dict, os.path.join(metadir, 'build-result.yaml')) # Store keys.yaml _yaml.dump(_yaml.node_sanitize({ 'strong': element._get_cache_key(), 'weak': element._get_cache_key(_KeyStrength.WEAK), }), os.path.join(metadir, 'keys.yaml')) # Store dependencies.yaml _yaml.dump(_yaml.node_sanitize({ e.name: e._get_cache_key() for e in element.dependencies(Scope.BUILD) }), os.path.join(metadir, 'dependencies.yaml')) # Store workspaced.yaml _yaml.dump(_yaml.node_sanitize({ 'workspaced': bool(element._get_workspace()) }), os.path.join(metadir, 'workspaced.yaml')) # Store workspaced-dependencies.yaml _yaml.dump(_yaml.node_sanitize({ 'workspaced-dependencies': [ e.name for e in element.dependencies(Scope.BUILD) if e._get_workspace() ] }), os.path.join(metadir, 'workspaced-dependencies.yaml')) metavdir.import_files(metadir) logsvdir.import_files(logsdir) artifact_size = assemblevdir.get_size() self._artifacts.commit(element, assemblevdir, keys) return artifact_size # cached_buildtree() # # Check if artifact is cached with expected buildtree. A # buildtree will not be present if the res tof the partial artifact # is not cached. # # Returns: # (bool): True if artifact cached with buildtree, False if # element not cached or missing expected buildtree. # Note this only confirms if a buildtree is present, # not its contents. # def cached_buildtree(self): context = self._context element = self._element if not element._cached(): return False key_strength = _KeyStrength.STRONG if context.get_strict() else _KeyStrength.WEAK if not self._artifacts.contains_subdir_artifact(element, element._get_cache_key(strength=key_strength), 'buildtree'): return False return True # buildtree_exists() # # Check if artifact was created with a buildtree. This does not check # whether the buildtree is present in the local cache. # # Returns: # (bool): True if artifact was created with buildtree # def buildtree_exists(self): if not self._element._cached(): return False artifact_vdir, _ = self._get_directory() return artifact_vdir._exists('buildtree') # load_public_data(): # # Loads the public data from the cached artifact # # Returns: # (dict): The artifacts cached public data # def load_public_data(self): element = self._element assert element._cached() # Load the public data from the artifact artifact_vdir, _ = self._get_directory() meta_file = artifact_vdir._objpath('meta', 'public.yaml') data = _yaml.load(meta_file, shortname='meta/public.yaml') return data # load_build_result(): # # Load the build result from the cached artifact # # Args: # key (str): The key for the artifact to extract # # Returns: # (bool): Whether the artifact of this element present in the artifact cache is of a success # (str): Short description of the result # (str): Detailed description of the result # def load_build_result(self, key): assert key is not None artifact_vdir, _ = self._get_directory(key) meta_file = artifact_vdir._objpath('meta', 'build-result.yaml') if not os.path.exists(meta_file): build_result = (True, "succeeded", None) return build_result data = _yaml.load(meta_file, shortname='meta/build-result.yaml') success = _yaml.node_get(data, bool, 'success') description = _yaml.node_get(data, str, 'description', default_value=None) detail = _yaml.node_get(data, str, 'detail', default_value=None) build_result = (success, description, detail) return build_result # get_metadata_keys(): # # Retrieve the strong and weak keys from the given artifact. # # Args: # key (str): The artifact key, or None for the default key # metadata_keys (dict): The elements cached strong/weak # metadata keys, empty if not yet cached # # Returns: # (str): The strong key # (str): The weak key # (dict): The key dict, None if not updated # def get_metadata_keys(self, key, metadata_keys): # Now extract it and possibly derive the key artifact_vdir, key = self._get_directory(key) # Now try the cache, once we're sure about the key if key in metadata_keys: return (metadata_keys[key]['strong'], metadata_keys[key]['weak'], None) # Parse the expensive yaml now and cache the result meta_file = artifact_vdir._objpath('meta', 'keys.yaml') meta = _yaml.load(meta_file, shortname='meta/keys.yaml') strong_key = _yaml.node_get(meta, str, 'strong') weak_key = _yaml.node_get(meta, str, 'weak') assert key in (strong_key, weak_key) metadata_keys[strong_key] = _yaml.node_sanitize(meta) metadata_keys[weak_key] = _yaml.node_sanitize(meta) return (strong_key, weak_key, metadata_keys) # get_metadata_dependencies(): # # Retrieve the hash of dependency keys from the given artifact. # # Args: # key (str): The artifact key, or None for the default key # metadata_dependencies (dict): The elements cached dependency metadata keys, # empty if not yet cached # metadata_keys (dict): The elements cached strong/weak # metadata keys, empty if not yet cached # # Returns: # (dict): A dictionary of element names and their keys # (dict): The depedencies key dict, None if not updated # (dict): The elements key dict, None if not updated # def get_metadata_dependencies(self, key, metadata_dependencies, metadata_keys): # Extract it and possibly derive the key artifact_vdir, key = self._get_directory(key) # Now try the cache, once we're sure about the key if key in metadata_dependencies: return (metadata_dependencies[key], None, None) # Parse the expensive yaml now and cache the result meta_file = artifact_vdir._objpath('meta', 'dependencies.yaml') meta = _yaml.load(meta_file, shortname='meta/dependencies.yaml') # Cache it under both strong and weak keys strong_key, weak_key, metadata_keys = self.get_metadata_keys(key, metadata_keys) metadata_dependencies[strong_key] = _yaml.node_sanitize(meta) metadata_dependencies[weak_key] = _yaml.node_sanitize(meta) return (meta, metadata_dependencies, metadata_keys) # get_metadata_workspaced(): # # Retrieve the hash of dependency from the given artifact. # # Args: # key (str): The artifact key, or None for the default key # meta_data_workspaced (dict): The elements cached boolean metadata # of whether it's workspaced, empty if # not yet cached # metadata_keys (dict): The elements cached strong/weak # metadata keys, empty if not yet cached # # Returns: # (bool): Whether the given artifact was workspaced # (dict): The workspaced key dict, None if not updated # (dict): The elements key dict, None if not updated # def get_metadata_workspaced(self, key, metadata_workspaced, metadata_keys): # Extract it and possibly derive the key artifact_vdir, key = self._get_directory(key) # Now try the cache, once we're sure about the key if key in metadata_workspaced: return (metadata_workspaced[key], None, None) # Parse the expensive yaml now and cache the result meta_file = artifact_vdir._objpath('meta', 'workspaced.yaml') meta = _yaml.load(meta_file, shortname='meta/workspaced.yaml') workspaced = _yaml.node_get(meta, bool, 'workspaced') # Cache it under both strong and weak keys strong_key, weak_key, metadata_keys = self.get_metadata_keys(key, metadata_keys) metadata_workspaced[strong_key] = workspaced metadata_workspaced[weak_key] = workspaced return (workspaced, metadata_workspaced, metadata_keys) # get_metadata_workspaced_dependencies(): # # Retrieve the hash of workspaced dependencies keys from the given artifact. # # Args: # key (str): The artifact key, or None for the default key # metadata_workspaced_dependencies (dict): The elements cached metadata of # which dependencies are workspaced, # empty if not yet cached # metadata_keys (dict): The elements cached strong/weak # metadata keys, empty if not yet cached # # Returns: # (list): List of which dependencies are workspaced # (dict): The workspaced depedencies key dict, None if not updated # (dict): The elements key dict, None if not updated # def get_metadata_workspaced_dependencies(self, key, metadata_workspaced_dependencies, metadata_keys): # Extract it and possibly derive the key artifact_vdir, key = self._get_directory(key) # Now try the cache, once we're sure about the key if key in metadata_workspaced_dependencies: return (metadata_workspaced_dependencies[key], None, None) # Parse the expensive yaml now and cache the result meta_file = artifact_vdir._objpath('meta', 'workspaced-dependencies.yaml') meta = _yaml.load(meta_file, shortname='meta/workspaced-dependencies.yaml') workspaced = _yaml.node_sanitize(_yaml.node_get(meta, list, 'workspaced-dependencies')) # Cache it under both strong and weak keys strong_key, weak_key, metadata_keys = self.get_metadata_keys(key, metadata_keys) metadata_workspaced_dependencies[strong_key] = workspaced metadata_workspaced_dependencies[weak_key] = workspaced return (workspaced, metadata_workspaced_dependencies, metadata_keys) # cached(): # # Check whether the artifact corresponding to the specified cache key is # available. This also checks whether all required parts of the artifact # are available, which may depend on command and configuration. # # This is used by _update_state() to set __strong_cached and __weak_cached. # # Args: # key (str): The artifact key # # Returns: # (bool): Whether artifact is in local cache # def cached(self, key): context = self._context try: vdir, _ = self._get_directory(key) except ArtifactError: # Either ref or top-level artifact directory missing return False # Check whether all metadata is available metadigest = vdir._get_child_digest('meta') if not self._artifacts.cas.contains_directory(metadigest, with_files=True): return False # Additional checks only relevant if artifact was created with 'files' subdirectory if vdir._exists('files'): # Determine whether directories are required require_directories = context.require_artifact_directories # Determine whether file contents are required as well require_files = context.require_artifact_files filesdigest = vdir._get_child_digest('files') # Check whether 'files' subdirectory is available, with or without file contents if (require_directories and not self._artifacts.cas.contains_directory(filesdigest, with_files=require_files)): return False return True # cached_logs() # # Check if the artifact is cached with log files. # # Args: # key (str): The artifact key # # Returns: # (bool): True if artifact is cached with logs, False if # element not cached or missing logs. # def cached_logs(self, key=None): if not self._element._cached(): return False vdir, _ = self._get_directory(key) logsdigest = vdir._get_child_digest('logs') return self._artifacts.cas.contains_directory(logsdigest, with_files=True) # _get_directory(): # # Get a virtual directory for the artifact contents # # Args: # key (str): The key for the artifact to extract, # or None for the default key # # Returns: # (Directory): The virtual directory object # (str): The chosen key # def _get_directory(self, key=None): element = self._element if key is None: key = self.get_extract_key() return (self._artifacts.get_artifact_directory(element, key), key) # _get_subdirectory(): # # Get a virtual directory for the artifact subdir contents # # Args: # subdir (str): The specific artifact subdir # key (str): The key for the artifact to extract, # or None for the default key # # Returns: # (Directory): The virtual subdirectory object # (str): The chosen key # def _get_subdirectory(self, subdir, key=None): artifact_vdir, key = self._get_directory(key) sub_vdir = artifact_vdir.descend(subdir) return (sub_vdir, key)