| #!/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 = 3 |
| |
| # Hold on to a list of members which get serialized |
| _WORKSPACE_MEMBERS = [ |
| 'prepared', |
| 'path', |
| 'last_successful', |
| 'running_files' |
| ] |
| |
| |
| # 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: |
| # project (Project): The project this workspace is part of |
| # path (str): The path that should host this workspace |
| # last_successful (str): The key of the last successful build of this workspace |
| # running_files (dict): A dict mapping dependency elements to files |
| # changed between failed builds. Should be |
| # made obsolete with failed build artifacts. |
| # |
| class Workspace(): |
| def __init__(self, project, *, last_successful=None, path=None, prepared=False, running_files=None): |
| self.prepared = prepared |
| self.last_successful = last_successful |
| self.path = path |
| self.running_files = running_files if running_files is not None else {} |
| |
| self._project = project |
| self._key = None |
| |
| # to_dict() |
| # |
| # Convert this object to a dict for serialization purposes |
| # |
| # Returns: |
| # (dict) A dict representation of the workspace |
| # |
| def to_dict(self): |
| return {key: val for key, val in self.__dict__.items() |
| if key in _WORKSPACE_MEMBERS and val is not None} |
| |
| # from_dict(): |
| # |
| # Loads a new workspace from a simple dictionary, the dictionary |
| # is expected to be generated from Workspace.to_dict(), or manually |
| # when loading from a YAML file. |
| # |
| # Args: |
| # project (Project): The Project to load this for |
| # dictionary: A simple dictionary object |
| # |
| # Returns: |
| # (Workspace): A newly instantiated Workspace |
| # |
| @classmethod |
| def from_dict(cls, project, dictionary): |
| |
| # Just pass the dictionary as kwargs |
| return cls(project, **dictionary) |
| |
| # differs() |
| # |
| # Checks if two workspaces are different in any way. |
| # |
| # Args: |
| # other (Workspace): Another workspace instance |
| # |
| # Returns: |
| # True if the workspace differs from 'other', otherwise False |
| # |
| def differs(self, other): |
| |
| for member in _WORKSPACE_MEMBERS: |
| member_a = getattr(self, member) |
| member_b = getattr(other, member) |
| |
| if member_a != member_b: |
| return True |
| |
| return False |
| |
| # 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 = self.get_absolute_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) |
| |
| # add_running_files() |
| # |
| # Append a list of files to the running_files for the given |
| # dependency. Duplicate files will be ignored. |
| # |
| # Args: |
| # dep_name (str) - The dependency name whose files to append to |
| # files (str) - A list of files to append |
| # |
| def add_running_files(self, dep_name, files): |
| if dep_name in self.running_files: |
| # ruamel.py cannot serialize sets in python3.4 |
| to_add = set(files) - set(self.running_files[dep_name]) |
| self.running_files[dep_name].extend(to_add) |
| else: |
| self.running_files[dep_name] = list(files) |
| |
| # clear_running_files() |
| # |
| # Clear all running files associated with this workspace. |
| # |
| def clear_running_files(self): |
| self.running_files = {} |
| |
| # 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): |
| try: |
| stat = os.lstat(filename) |
| except OSError as e: |
| raise LoadError(LoadErrorReason.MISSING_FILE, |
| "Failed to stat file in workspace: {}".format(e)) |
| |
| # Use the mtime of any file with sub second precision |
| return stat.st_mtime_ns |
| |
| if recalculate or self._key is None: |
| fullpath = self.get_absolute_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(fullpath)) 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 |
| self._workspaces = self._load_config() |
| |
| # list() |
| # |
| # 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_name (str) - The element name to create a workspace for |
| # path (str) - The path in which the workspace should be kept |
| # |
| def create_workspace(self, element_name, path): |
| self._workspaces[element_name] = Workspace(self._project, path=path) |
| |
| 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_name (str) - The element name whose workspace to return |
| # |
| # Returns: |
| # (None|Workspace) |
| # |
| def get_workspace(self, element_name): |
| if element_name not in self._workspaces: |
| return None |
| return self._workspaces[element_name] |
| |
| # update_workspace() |
| # |
| # Update the datamodel with a new Workspace instance |
| # |
| # Args: |
| # element_name (str): The name of the element to update a workspace for |
| # workspace_dict (Workspace): A serialized workspace dictionary |
| # |
| # Returns: |
| # (bool): Whether the workspace has changed as a result |
| # |
| def update_workspace(self, element_name, workspace_dict): |
| assert element_name in self._workspaces |
| |
| workspace = Workspace.from_dict(self._project, workspace_dict) |
| if self._workspaces[element_name].differs(workspace): |
| self._workspaces[element_name] = workspace |
| return True |
| |
| return False |
| |
| # 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_name (str) - The element name whose workspace to delete |
| # |
| def delete_workspace(self, element_name): |
| 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): |
| assert utils._is_main_process() |
| |
| config = { |
| 'format-version': BST_WORKSPACE_FORMAT_VERSION, |
| 'workspaces': { |
| element: workspace.to_dict() |
| for element, workspace in _yaml.node_items(self._workspaces) |
| } |
| } |
| os.makedirs(os.path.join(self._project.directory, ".bst"), exist_ok=True) |
| _yaml.dump(_yaml.node_sanitize(config), |
| os.path.join(self._project.directory, ".bst", "workspaces.yml")) |
| |
| # _load_config() |
| # |
| # Loads and parses the workspace configuration |
| # |
| # Returns: |
| # (dict) The extracted workspaces |
| # |
| # Raises: LoadError if there was a problem with the workspace config |
| # |
| def _load_config(self): |
| workspace_file = os.path.join(self._project.directory, ".bst", "workspaces.yml") |
| try: |
| node = _yaml.load(workspace_file) |
| except LoadError as e: |
| if e.reason == LoadErrorReason.MISSING_FILE: |
| # Return an empty dict if there was no workspace file |
| return {} |
| |
| raise |
| |
| return self._parse_workspace_config(node) |
| |
| # _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(self._project, path=config) |
| for element, config in _yaml.node_items(workspaces) |
| } |
| |
| elif version >= 1 and version <= BST_WORKSPACE_FORMAT_VERSION: |
| workspaces = _yaml.node_get(workspaces, dict, "workspaces", default_value={}) |
| res = {element: self._load_workspace(self._project, node) |
| 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 |
| |
| # _load_workspace(): |
| # |
| # Loads a new workspace from a YAML node |
| # |
| # Args: |
| # node: A YAML Node |
| # project (Project): The Project to load this for |
| # |
| # Returns: |
| # (Workspace): A newly instantiated Workspace |
| # |
| def _load_workspace(self, project, node): |
| dictionary = { |
| 'prepared': _yaml.node_get(node, bool, 'prepared', default_value=False), |
| 'path': _yaml.node_get(node, str, 'path'), |
| 'last_successful': _yaml.node_get(node, str, 'last_successful', default_value=None), |
| 'running_files': _yaml.node_get(node, dict, 'running_files', default_value=None), |
| } |
| return Workspace.from_dict(self._project, dictionary) |