diff options
author | Tristan Maat <tristan.maat@codethink.co.uk> | 2018-03-14 13:11:01 +0000 |
---|---|---|
committer | Tristan Maat <tristan.maat@codethink.co.uk> | 2018-03-27 14:32:52 +0100 |
commit | f761140f18a7d54caf6e6dba8a722b9ff1f4430e (patch) | |
tree | 4175da85352754b9f4f62d281454ab0c331cc6dc | |
parent | fb332267bfccc54ea7bc57af7a676a4eee635bcd (diff) | |
download | buildstream-f761140f18a7d54caf6e6dba8a722b9ff1f4430e.tar.gz |
Make workspaces use objects instead of pipeline helper methods
-rw-r--r-- | buildstream/_frontend/cli.py | 11 | ||||
-rw-r--r-- | buildstream/_pipeline.py | 38 | ||||
-rw-r--r-- | buildstream/_project.py | 157 | ||||
-rw-r--r-- | buildstream/_workspaces.py | 322 | ||||
-rw-r--r-- | buildstream/element.py | 11 | ||||
-rw-r--r-- | buildstream/source.py | 90 |
6 files changed, 376 insertions, 253 deletions
diff --git a/buildstream/_frontend/cli.py b/buildstream/_frontend/cli.py index c0bf74117..c6c9d8156 100644 --- a/buildstream/_frontend/cli.py +++ b/buildstream/_frontend/cli.py @@ -641,7 +641,7 @@ def workspace_close(app, remove_dir, element): app.initialize((element,)) - if app.pipeline.project._get_workspace(app.pipeline.targets[0].name) is None: + if app.pipeline.project._workspaces.get_workspace(app.pipeline.targets[0]) is None: click.echo("ERROR: Workspace '{}' does not exist".format(element), err=True) sys.exit(-1) @@ -716,13 +716,12 @@ def workspace_list(app): sys.exit(-1) workspaces = [] - for element_name, directory in project._list_workspaces(): - workspace_ = { + for element_name, workspace_ in project._workspaces.list(): + workspace_detail = { 'element': element_name, - 'directory': directory, + 'directory': workspace_.path, } - - workspaces.append(workspace_) + workspaces.append(workspace_detail) _yaml.dump({ 'workspaces': workspaces diff --git a/buildstream/_pipeline.py b/buildstream/_pipeline.py index 268e7111c..c1f2afdef 100644 --- a/buildstream/_pipeline.py +++ b/buildstream/_pipeline.py @@ -188,15 +188,14 @@ class Pipeline(): raise PipelineError("{}: {}".format(plugin, e), reason=e.reason) from e def initialize_workspaces(self): - for element_name, workspace in self.project._list_workspaces(): + for element_name, workspace in self.project._workspaces.list(): for target in self.targets: element = target.search(Scope.ALL, element_name) if element is None: self.unused_workspaces.append((element_name, workspace)) - continue - - self.project._set_workspace(element, workspace) + else: + workspace.init(element) def initialize_remote_caches(self): def remote_failed(url, error): @@ -627,7 +626,7 @@ class Pipeline(): raise PipelineError("The given element has no sources", detail=detail) # Check for workspace config - if self.project._get_workspace(target.name): + if self.project._workspaces.get_workspace(target): raise PipelineError("Workspace '{}' is already defined." .format(target.name)) @@ -674,18 +673,17 @@ class Pipeline(): except OSError as e: raise PipelineError("Failed to create workspace directory: {}".format(e)) from e + workspace = self.project._workspaces.create_workspace(target, workdir) + if not no_checkout: if not force and os.listdir(directory): raise PipelineError("Checkout directory is not empty: {}".format(directory)) with target.timed_activity("Staging sources to {}".format(directory)): - for source in target.sources(): - source._init_workspace(directory) - - self.project._set_workspace(target, workdir) + workspace.open() with target.timed_activity("Saving workspace configuration"): - self.project._save_workspace_config() + self.project._workspaces.save_config() # close_workspace # @@ -700,26 +698,26 @@ class Pipeline(): # Remove workspace directory if prompted if remove_dir: - path = self.project._get_workspace(target.name) - if path is not None: + workspace = self.project._workspaces.get_workspace(target) + if workspace is not None: with target.timed_activity("Removing workspace directory {}" - .format(path)): + .format(workspace.path)): try: - shutil.rmtree(path) + shutil.rmtree(workspace.path) except OSError as e: raise PipelineError("Could not remove '{}': {}" - .format(path, e)) from e + .format(workspace.path, e)) from e # Delete the workspace config entry with target.timed_activity("Removing workspace"): try: - self.project._delete_workspace(target.name) + self.project._workspaces.delete_workspace(target) except KeyError: raise PipelineError("Workspace '{}' is currently not defined" .format(target.name)) # Update workspace config - self.project._save_workspace_config() + self.project._workspaces.save_config() # Reset source to avoid checking out the (now empty) workspace for source in target.sources(): @@ -738,15 +736,15 @@ class Pipeline(): def reset_workspace(self, scheduler, track, no_checkout): # When working on workspaces we only have one target target = self.targets[0] - workspace_dir = self.project._get_workspace(target.name) + workspace = self.project._workspaces.get_workspace(target) - if workspace_dir is None: + if workspace is None: raise PipelineError("Workspace '{}' is currently not defined" .format(target.name)) self.close_workspace(True) - self.open_workspace(scheduler, workspace_dir, no_checkout, track, False) + self.open_workspace(scheduler, workspace.path, no_checkout, track, False) # pull() # diff --git a/buildstream/_project.py b/buildstream/_project.py index 33d10e1f4..003418682 100644 --- a/buildstream/_project.py +++ b/buildstream/_project.py @@ -33,6 +33,7 @@ from ._artifactcache import artifact_cache_specs_from_config_node from ._elementfactory import ElementFactory from ._sourcefactory import SourceFactory from ._projectrefs import ProjectRefs, ProjectRefStorage +from ._workspaces import Workspaces # The base BuildStream format version @@ -92,7 +93,7 @@ class Project(): self._elements = {} # Element specific configurations self._sources = {} # Source specific configurations self._aliases = {} # Aliases dictionary - self._workspaces = {} # Workspaces + self._workspaces = None # Workspaces self._plugin_source_origins = [] # Origins of custom sources self._plugin_element_origins = [] # Origins of custom elements self._options = None # Project options, the OptionPool @@ -218,8 +219,7 @@ class Project(): self.artifact_cache_specs = artifact_cache_specs_from_config_node(config) # Workspace configurations - workspace_config = self._load_workspace_config() - self._workspaces = self._parse_workspace_config(workspace_config) + self._workspaces = Workspaces(self) # Assert project version format_version = _yaml.node_get(config, int, 'format-version') @@ -378,157 +378,6 @@ class Project(): origin_dict['path'] = os.path.join(self.directory, origin_dict['path']) destination.append(origin_dict) - # _list_workspaces() - # - # Generator function to enumerate workspaces. - # - # Yields: - # A tuple in the following format: (element, path). - def _list_workspaces(self): - for element, _ in _yaml.node_items(self._workspaces): - yield (element, self._workspaces[element]["path"]) - - # _get_workspace() - # - # Get the path of the workspace source associated with the given - # element's source at the given index - # - # Args: - # element (str) - The element name - # - # Returns: - # None if no workspace is open, the path to the workspace - # otherwise - # - def _get_workspace(self, element): - if element not in self._workspaces: - return None - return self._workspaces[element]["path"] - - # _set_workspace() - # - # Set the path of the workspace associated with the given - # element's source at the given index - # - # Args: - # element (str) - The element name - # path (str) - The path to set the workspace to - # - def _set_workspace(self, element, path): - if element.name not in self._workspaces: - self._workspaces[element.name] = {} - - self._workspaces[element.name]["path"] = path - element._set_source_workspaces(path) - - # _delete_workspace() - # - # Remove the workspace from the workspace element. Note that this - # does *not* remove the workspace from the stored yaml - # configuration, call _save_workspace_config() afterwards. - # - # Args: - # element (str) - The element name - # - def _delete_workspace(self, element): - del self._workspaces[element] - - # _load_workspace_config() - # - # Load the workspace configuration and return a node containing - # all open workspaces for the project - # - # Returns: - # - # A node containing a dict that assigns elements to their - # workspaces. For example: - # - # alpha.bst: /home/me/alpha - # bravo.bst: /home/me/bravo - # - def _load_workspace_config(self): - os.makedirs(os.path.join(self.directory, ".bst"), exist_ok=True) - workspace_file = os.path.join(self.directory, ".bst", "workspaces.yml") - try: - open(workspace_file, "a").close() - except IOError as e: - raise LoadError(LoadErrorReason.MISSING_FILE, - "Could not load workspace config: {}".format(e)) from e - - return _yaml.load(workspace_file) - - # _parse_workspace_config() - # - # If workspace config is in old-style format, i.e. it is using - # source-specific workspaces, try to convert it to element-specific - # workspaces. - # - # This method will rewrite workspace config, if it is in old format. - # - # Args: - # workspaces (dict): current workspace config, usually output of _load_workspace_config() - # - # Returns: - # (bool, dict) Whether the workspace config needs to be - # rewritten and extracted workspaces - # - # Raises: LoadError if there was a problem with the workspace config - # - def _parse_workspace_config(self, workspaces): - version = _yaml.node_get(workspaces, int, "format-version", default_value=0) - - if version == 0: - # Pre-versioning format can be of two forms - for element, config in _yaml.node_items(workspaces): - if isinstance(config, str): - pass - - elif isinstance(config, dict): - sources = list(_yaml.node_items(config)) - if len(sources) > 1: - detail = "There are multiple workspaces open for '{}'.\n" + \ - "This is not supported anymore.\n" + \ - "Please remove this element from '{}'." - raise LoadError(LoadErrorReason.INVALID_DATA, - detail.format(element, - os.path.join(self.directory, ".bst", "workspaces.yml"))) - - workspaces[element] = sources[0][1] - - else: - raise LoadError(LoadErrorReason.INVALID_DATA, - "Workspace config is in unexpected format.") - - res = { - element: {"path": config} - for element, config in _yaml.node_items(workspaces) - } - - elif version == BST_WORKSPACE_FORMAT_VERSION: - res = _yaml.node_get(workspaces, dict, "workspaces", default_value={}) - - else: - raise LoadError(LoadErrorReason.INVALID_DATA, - "Workspace configuration format version {} not supported." - "Your version of buildstream may be too old. Max supported version: {}" - .format(version, BST_WORKSPACE_FORMAT_VERSION)) - - return res - - # _save_workspace_config() - # - # Dump the current workspace element to the project configuration - # file. This makes any changes performed with _delete_workspace or - # _set_workspace permanent - # - def _save_workspace_config(self): - _yaml.dump( - { - "format-version": BST_WORKSPACE_FORMAT_VERSION, - "workspaces": _yaml.node_sanitize(self._workspaces) - }, - os.path.join(self.directory, ".bst", "workspaces.yml")) - def _extract_plugin_paths(self, node, name): if not node: return diff --git a/buildstream/_workspaces.py b/buildstream/_workspaces.py new file mode 100644 index 000000000..9c3933bbd --- /dev/null +++ b/buildstream/_workspaces.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 Codethink Limited +# +# 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 <http://www.gnu.org/licenses/>. +# +# Authors: +# Tristan Maat <tristan.maat@codethink.co.uk> + +import os +from . import utils +from . import _yaml + +from ._exceptions import LoadError, LoadErrorReason + + +BST_WORKSPACE_FORMAT_VERSION = 1 + + +# Workspace() +# +# An object to contain various helper functions and data required for +# workspaces. +# +# last_successful, path and running_files are intended to be public +# properties, but may be best accessed using this classes' helper +# methods. +# +# Args: +# path (str): The path that should host this workspace +# project (Project): The project this workspace is part of +# +class Workspace(): + def __init__(self, path, project): + self.path = path + + self._element = None + self._project = project + self.__key = None + + @classmethod + def from_yaml_node(cls, node, project): + path = _yaml.node_get(node, str, 'path') + + return cls(path, project) + + # _to_dict() + # + # Convert this object to a dict for storage purposes + # + # Returns: + # (dict) A dict representation of the workspace + # + def _to_dict(self): + to_return = ['path'] + + return {key: val for key, val in self.__dict__.items() + if key in to_return and val is not None} + + # open() + # + # "Open" this workspace, calling the init_workspace method of all + # its sources. + # + def open(self): + for source in self._element.sources(): + source._init_workspace(self.path) + + # init() + # + # Initialize the elements and sources associated to this + # workspace. Must be called before this object is used. + # + def init(self, element): + self._element = element + for source in element.sources(): + source._set_workspace(self) + + # invalidate_key() + # + # Invalidate the workspace key, forcing a recalculation next time + # it is accessed. + # + def invalidate_key(self): + self.__key = None + + # stage() + # + # Stage the workspace to the given directory. + # + # Args: + # directory (str) - The directory into which to stage this workspace + # + def stage(self, directory): + fullpath = os.path.join(self._project.directory, self.path) + if os.path.isdir(fullpath): + utils.copy_files(fullpath, directory) + else: + destfile = os.path.join(directory, os.path.basename(self.path)) + utils.safe_copy(fullpath, destfile) + + # get_key() + # + # Get a unique key for this workspace. + # + # Args: + # recalculate (bool) - Whether to recalculate the key + # + # Returns: + # (str) A unique key for this workspace + # + def get_key(self, recalculate=False): + def unique_key(filename): + if os.path.isdir(filename): + return "0" + elif os.path.islink(filename): + return "1" + + try: + return utils.sha256sum(filename) + except FileNotFoundError as e: + raise LoadError(LoadErrorReason.MISSING_FILE, + "Failed loading workspace. Did you remove the " + "workspace directory? {}".format(e)) + + if recalculate or self.__key is None: + fullpath = os.path.join(self._project.directory, self.path) + + # Get a list of tuples of the the project relative paths and fullpaths + if os.path.isdir(fullpath): + filelist = utils.list_relative_paths(fullpath) + filelist = [(relpath, os.path.join(fullpath, relpath)) for relpath in filelist] + else: + filelist = [(self.path, fullpath)] + + self.__key = [(relpath, unique_key(self.path)) for relpath, fullpath in filelist] + + return self.__key + + # get_absolute_path(): + # + # Returns: The absolute path of the element's workspace. + # + def get_absolute_path(self): + return os.path.join(self._project.directory, self.path) + + +# Workspaces() +# +# A class to manage Workspaces for multiple elements. +# +# Args: +# project (Project): The project the workspaces should be associated to +# +class Workspaces(): + def __init__(self, project): + self._project = project + workspace_config = self.__load_config() + self._workspaces = self.__parse_workspace_config(workspace_config) + + # _list_workspaces() + # + # Generator function to enumerate workspaces. + # + # Yields: + # A tuple in the following format: (str, Workspace), where the + # first element is the name of the workspaced element. + def list(self): + for element, _ in _yaml.node_items(self._workspaces): + yield (element, self._workspaces[element]) + + # create_workspace() + # + # Create a workspace in the given path for the given element. + # + # Args: + # element (Element) - The element for which to create a workspace + # path (str) - The path in which the workspace should be kept + # + def create_workspace(self, element, path): + self._workspaces[element.name] = Workspace(path, self._project) + self._workspaces[element.name].init(element) + + return self._workspaces[element.name] + + # _get_workspace() + # + # Get the path of the workspace source associated with the given + # element's source at the given index + # + # Args: + # element (Element) - The element whose workspace to return + # + # Returns: + # (None|Workspace) + # + def get_workspace(self, element): + if element.name not in self._workspaces: + return None + return self._workspaces[element.name] + + # delete_workspace() + # + # Remove the workspace from the workspace element. Note that this + # does *not* remove the workspace from the stored yaml + # configuration, call save_config() afterwards. + # + # Args: + # element (Element) - The element whose workspace to delete + # + def delete_workspace(self, element): + del self._workspaces[element.name] + + # save_config() + # + # Dump the current workspace element to the project configuration + # file. This makes any changes performed with delete_workspace or + # create_workspace permanent + # + def save_config(self): + config = { + 'format-version': BST_WORKSPACE_FORMAT_VERSION, + 'workspaces': { + element: workspace._to_dict() + for element, workspace in _yaml.node_items(self._workspaces) + } + } + + _yaml.dump(_yaml.node_sanitize(config), + os.path.join(self._project.directory, ".bst", "workspaces.yml")) + + # _load_config() + # + # Load the workspace configuration and return a node containing + # all open workspaces for the project + # + # Returns: + # + # A node containing a dict that assigns elements to their + # workspaces. For example: + # + # alpha.bst: /home/me/alpha + # bravo.bst: /home/me/bravo + # + def __load_config(self): + os.makedirs(os.path.join(self._project.directory, ".bst"), exist_ok=True) + workspace_file = os.path.join(self._project.directory, ".bst", "workspaces.yml") + try: + open(workspace_file, "a").close() + except IOError as e: + raise LoadError(LoadErrorReason.MISSING_FILE, + "Could not load workspace config: {}".format(e)) from e + + return _yaml.load(workspace_file) + + # __parse_workspace_config_format() + # + # If workspace config is in old-style format, i.e. it is using + # source-specific workspaces, try to convert it to element-specific + # workspaces. + # + # Args: + # workspaces (dict): current workspace config, usually output of _load_workspace_config() + # + # Returns: + # (dict) The extracted workspaces + # + # Raises: LoadError if there was a problem with the workspace config + # + def __parse_workspace_config(self, workspaces): + version = _yaml.node_get(workspaces, int, "format-version", default_value=0) + + if version == 0: + # Pre-versioning format can be of two forms + for element, config in _yaml.node_items(workspaces): + if isinstance(config, str): + pass + + elif isinstance(config, dict): + sources = list(_yaml.node_items(config)) + if len(sources) > 1: + detail = "There are multiple workspaces open for '{}'.\n" + \ + "This is not supported anymore.\n" + \ + "Please remove this element from '{}'." + raise LoadError(LoadErrorReason.INVALID_DATA, + detail.format(element, + os.path.join(self._project.directory, ".bst", "workspaces.yml"))) + + workspaces[element] = sources[0][1] + + else: + raise LoadError(LoadErrorReason.INVALID_DATA, + "Workspace config is in unexpected format.") + + res = { + element: Workspace(config, self._project) + for element, config in _yaml.node_items(workspaces) + } + + elif version == BST_WORKSPACE_FORMAT_VERSION: + workspaces = _yaml.node_get(workspaces, dict, "workspaces", default_value={}) + res = {element: Workspace.from_yaml_node(node, self._project) + for element, node in _yaml.node_items(workspaces)} + + else: + raise LoadError(LoadErrorReason.INVALID_DATA, + "Workspace configuration format version {} not supported." + "Your version of buildstream may be too old. Max supported version: {}" + .format(version, BST_WORKSPACE_FORMAT_VERSION)) + + return res diff --git a/buildstream/element.py b/buildstream/element.py index 131a834e1..b36ae965c 100644 --- a/buildstream/element.py +++ b/buildstream/element.py @@ -718,6 +718,14 @@ class Element(Plugin): # Private Methods used in BuildStream # ############################################################# + # _get_workspace(): + # + # Returns: + # (Workspace|None): A workspace associated with this element + # + def _get_workspace(self): + return self._get_project()._workspaces.get_workspace(self) + # _write_script(): # # Writes a script to the given directory. @@ -1472,11 +1480,12 @@ class Element(Plugin): def _stage_sources_in_sandbox(self, sandbox, directory, mount_workspaces=True): if mount_workspaces: + workspace = self._get_workspace() # First, mount sources that have an open workspace sources_to_mount = [source for source in self.sources() if source._has_workspace()] for source in sources_to_mount: mount_point = source._get_staging_path(directory) - mount_source = source._get_workspace_path() + mount_source = workspace.get_absolute_path() sandbox.mark_directory(mount_point) sandbox._set_mount_source(mount_point, mount_source) diff --git a/buildstream/source.py b/buildstream/source.py index 0a5c26cd1..d7fac7280 100644 --- a/buildstream/source.py +++ b/buildstream/source.py @@ -28,7 +28,7 @@ from contextlib import contextmanager from . import Plugin from . import _yaml, utils -from ._exceptions import BstError, ImplError, LoadError, LoadErrorReason, ErrorDomain +from ._exceptions import BstError, ImplError, ErrorDomain from ._projectrefs import ProjectRefStorage @@ -91,7 +91,6 @@ class Source(Plugin): self.__tracking = False # Source is scheduled to be tracked self.__assemble_scheduled = False # Source is scheduled to be assembled self.__workspace = None # Directory of the currently active workspace - self.__workspace_key = None # Cached directory content hashes for workspaced source # Collect the composited element configuration and # ask the element to configure itself. @@ -328,7 +327,7 @@ class Source(Plugin): # A workspace is considered inconsistent in the case # that it's directory went missing # - fullpath = self._get_workspace_path() + fullpath = self.__workspace.get_absolute_path() if not os.path.exists(fullpath): self.__consistency = Consistency.INCONSISTENT @@ -337,11 +336,6 @@ class Source(Plugin): def _get_consistency(self): return self.__consistency - # Return the absolute path of the element's workspace - # - def _get_workspace_path(self): - return os.path.join(self.get_project_directory(), self.__workspace) - # Mark a source as scheduled to be tracked # # This is used across the pipeline in sessions where the @@ -363,7 +357,8 @@ class Source(Plugin): self.__assemble_scheduled = True # Invalidate workspace key as the build modifies the workspace directory - self.__workspace_key = None + if self._has_workspace(): + self.__workspace.invalidate_key() # _assemble_done(): # @@ -413,7 +408,8 @@ class Source(Plugin): staging_directory = self._ensure_directory(directory) if self._has_workspace(): - self._stage_workspace(staging_directory) + with self.timed_activity("Staging local files at {}".format(self.__workspace.path)): + self.__workspace.stage(staging_directory) else: self.stage(staging_directory) @@ -434,7 +430,8 @@ class Source(Plugin): key['directory'] = self.__directory if self._has_workspace(): - key['workspace'] = self._get_workspace_key() + assert not self.__assemble_scheduled + key['workspace'] = self.__workspace.get_key() else: key['unique'] = self.get_unique_key() @@ -615,80 +612,29 @@ class Source(Plugin): return new_ref - # Set the current workspace directory + # Set the current workspace # - # Note that this invalidate the workspace key. + # Note that this invalidates the workspace key. # - def _set_workspace(self, directory): - self.__workspace = directory - self.__workspace_key = None + def _set_workspace(self, workspace): + if self._has_workspace(): + self.__workspace.invalidate_key() + self.__workspace = workspace # Return the current workspace directory def _get_workspace(self): - return self.__workspace + return self.__workspace.path # Delete the workspace # - # Note that this invalidate the workspace key. + # Note that this invalidates the workspace key. # def _del_workspace(self): + if self._has_workspace(): + self.__workspace.invalidate_key() self.__workspace = None - self.__workspace_key = None # Whether the source has a set workspace # def _has_workspace(self): return self.__workspace is not None - - # Stage the workspace - # - def _stage_workspace(self, directory): - fullpath = self._get_workspace_path() - - with self.timed_activity("Staging local files at {}".format(self.__workspace)): - if os.path.isdir(fullpath): - utils.copy_files(fullpath, directory) - else: - destfile = os.path.join(directory, os.path.basename(self.__workspace)) - utils.safe_copy(fullpath, destfile) - - # Get a unique key for the workspace - # - # Note that to avoid re-traversing the file system if this function is - # called multiple times, the workspace key is cached. You can still force a - # new calculation to happen by setting the 'recalculate' flag. - # - def _get_workspace_key(self, recalculate=False): - assert not self.__assemble_scheduled - - if recalculate or self.__workspace_key is None: - fullpath = self._get_workspace_path() - - # Get a list of tuples of the the project relative paths and fullpaths - if os.path.isdir(fullpath): - filelist = utils.list_relative_paths(fullpath) - filelist = [(relpath, os.path.join(fullpath, relpath)) for relpath in filelist] - else: - filelist = [(self.__workspace, fullpath)] - - # Return a list of (relative filename, sha256 digest) tuples, a sorted list - # has already been returned by list_relative_paths() - self.__workspace_key = [(relpath, _unique_key(fullpath)) for relpath, fullpath in filelist] - - return self.__workspace_key - - -# Get the sha256 sum for the content of a file -def _unique_key(filename): - - # If it's a directory, just return 0 string - if os.path.isdir(filename): - return "0" - elif os.path.islink(filename): - return "1" - - try: - return utils.sha256sum(filename) - except FileNotFoundError as e: - raise LoadError(LoadErrorReason.MISSING_FILE, - "Failed loading workspace. Did you remove the workspace directory? {}".format(e)) |