diff options
Diffstat (limited to 'src/buildstream/_project.py')
-rw-r--r-- | src/buildstream/_project.py | 975 |
1 files changed, 975 insertions, 0 deletions
diff --git a/src/buildstream/_project.py b/src/buildstream/_project.py new file mode 100644 index 000000000..c40321c66 --- /dev/null +++ b/src/buildstream/_project.py @@ -0,0 +1,975 @@ +# +# Copyright (C) 2016-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 Van Berkom <tristan.vanberkom@codethink.co.uk> +# Tiago Gomes <tiago.gomes@codethink.co.uk> + +import os +import sys +from collections import OrderedDict +from collections.abc import Mapping +from pathlib import Path +from pluginbase import PluginBase +from . import utils +from . import _cachekey +from . import _site +from . import _yaml +from ._artifactelement import ArtifactElement +from ._profile import Topics, PROFILER +from ._exceptions import LoadError, LoadErrorReason +from ._options import OptionPool +from ._artifactcache import ArtifactCache +from ._sourcecache import SourceCache +from .sandbox import SandboxRemote +from ._elementfactory import ElementFactory +from ._sourcefactory import SourceFactory +from .types import CoreWarnings +from ._projectrefs import ProjectRefs, ProjectRefStorage +from ._versions import BST_FORMAT_VERSION +from ._loader import Loader +from .element import Element +from ._message import Message, MessageType +from ._includes import Includes +from ._platform import Platform +from ._workspaces import WORKSPACE_PROJECT_FILE + + +# Project Configuration file +_PROJECT_CONF_FILE = 'project.conf' + + +# HostMount() +# +# A simple object describing the behavior of +# a host mount. +# +class HostMount(): + + def __init__(self, path, host_path=None, optional=False): + + # Support environment variable expansion in host mounts + path = os.path.expandvars(path) + if host_path is not None: + host_path = os.path.expandvars(host_path) + + self.path = path # Path inside the sandbox + self.host_path = host_path # Path on the host + self.optional = optional # Optional mounts do not incur warnings or errors + + if self.host_path is None: + self.host_path = self.path + + +# Represents project configuration that can have different values for junctions. +class ProjectConfig: + def __init__(self): + self.element_factory = None + self.source_factory = None + self.options = None # OptionPool + self.base_variables = {} # The base set of variables + self.element_overrides = {} # Element specific configurations + self.source_overrides = {} # Source specific configurations + self.mirrors = OrderedDict() # contains dicts of alias-mappings to URIs. + self.default_mirror = None # The name of the preferred mirror. + self._aliases = {} # Aliases dictionary + + +# Project() +# +# The Project Configuration +# +class Project(): + + def __init__(self, directory, context, *, junction=None, cli_options=None, + default_mirror=None, parent_loader=None, + search_for_project=True): + + # The project name + self.name = None + + self._context = context # The invocation Context, a private member + + if search_for_project: + self.directory, self._invoked_from_workspace_element = self._find_project_dir(directory) + else: + self.directory = directory + self._invoked_from_workspace_element = None + + self._absolute_directory_path = Path(self.directory).resolve() + + # Absolute path to where elements are loaded from within the project + self.element_path = None + + # Default target elements + self._default_targets = None + + # ProjectRefs for the main refs and also for junctions + self.refs = ProjectRefs(self.directory, 'project.refs') + self.junction_refs = ProjectRefs(self.directory, 'junction.refs') + + self.config = ProjectConfig() + self.first_pass_config = ProjectConfig() + + self.junction = junction # The junction Element object, if this is a subproject + + self.ref_storage = None # ProjectRefStorage setting + self.base_environment = {} # The base set of environment variables + self.base_env_nocache = None # The base nocache mask (list) for the environment + + # + # Private Members + # + + self._default_mirror = default_mirror # The name of the preferred mirror. + + self._cli_options = cli_options + self._cache_key = None + + self._fatal_warnings = [] # A list of warnings which should trigger an error + + self._shell_command = [] # The default interactive shell command + self._shell_environment = {} # Statically set environment vars + self._shell_host_files = [] # A list of HostMount objects + + self.artifact_cache_specs = None + self.source_cache_specs = None + self.remote_execution_specs = None + self._sandbox = None + self._splits = None + + self._context.add_project(self) + + self._partially_loaded = False + self._fully_loaded = False + self._project_includes = None + + with PROFILER.profile(Topics.LOAD_PROJECT, self.directory.replace(os.sep, '-')): + self._load(parent_loader=parent_loader) + + self._partially_loaded = True + + @property + def options(self): + return self.config.options + + @property + def base_variables(self): + return self.config.base_variables + + @property + def element_overrides(self): + return self.config.element_overrides + + @property + def source_overrides(self): + return self.config.source_overrides + + # translate_url(): + # + # Translates the given url which may be specified with an alias + # into a fully qualified url. + # + # Args: + # url (str): A url, which may be using an alias + # first_pass (bool): Whether to use first pass configuration (for junctions) + # + # Returns: + # str: The fully qualified url, with aliases resolved + # + # This method is provided for :class:`.Source` objects to resolve + # fully qualified urls based on the shorthand which is allowed + # to be specified in the YAML + def translate_url(self, url, *, first_pass=False): + if first_pass: + config = self.first_pass_config + else: + config = self.config + + if url and utils._ALIAS_SEPARATOR in url: + url_alias, url_body = url.split(utils._ALIAS_SEPARATOR, 1) + alias_url = _yaml.node_get(config._aliases, str, url_alias, default_value=None) + if alias_url: + url = alias_url + url_body + + return url + + # get_shell_config() + # + # Gets the project specified shell configuration + # + # Returns: + # (list): The shell command + # (dict): The shell environment + # (list): The list of HostMount objects + # + def get_shell_config(self): + return (self._shell_command, self._shell_environment, self._shell_host_files) + + # get_cache_key(): + # + # Returns the cache key, calculating it if necessary + # + # Returns: + # (str): A hex digest cache key for the Context + # + def get_cache_key(self): + if self._cache_key is None: + + # Anything that alters the build goes into the unique key + # (currently nothing here) + self._cache_key = _cachekey.generate_key(_yaml.new_empty_node()) + + return self._cache_key + + # get_path_from_node() + # + # Fetches the project path from a dictionary node and validates it + # + # Paths are asserted to never lead to a directory outside of the project + # directory. In addition, paths can not point to symbolic links, fifos, + # sockets and block/character devices. + # + # The `check_is_file` and `check_is_dir` parameters can be used to + # perform additional validations on the path. Note that an exception + # will always be raised if both parameters are set to ``True``. + # + # Args: + # node (dict): A dictionary loaded from YAML + # key (str): The key whose value contains a path to validate + # check_is_file (bool): If ``True`` an error will also be raised + # if path does not point to a regular file. + # Defaults to ``False`` + # check_is_dir (bool): If ``True`` an error will be also raised + # if path does not point to a directory. + # Defaults to ``False`` + # Returns: + # (str): The project path + # + # Raises: + # (LoadError): In case that the project path is not valid or does not + # exist + # + def get_path_from_node(self, node, key, *, + check_is_file=False, check_is_dir=False): + path_str = _yaml.node_get(node, str, key) + path = Path(path_str) + full_path = self._absolute_directory_path / path + + provenance = _yaml.node_get_provenance(node, key=key) + + if full_path.is_symlink(): + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID_KIND, + "{}: Specified path '{}' must not point to " + "symbolic links " + .format(provenance, path_str)) + + if path.parts and path.parts[0] == '..': + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID, + "{}: Specified path '{}' first component must " + "not be '..'" + .format(provenance, path_str)) + + try: + if sys.version_info[0] == 3 and sys.version_info[1] < 6: + full_resolved_path = full_path.resolve() + else: + full_resolved_path = full_path.resolve(strict=True) # pylint: disable=unexpected-keyword-arg + except FileNotFoundError: + raise LoadError(LoadErrorReason.MISSING_FILE, + "{}: Specified path '{}' does not exist" + .format(provenance, path_str)) + + is_inside = self._absolute_directory_path in full_resolved_path.parents or ( + full_resolved_path == self._absolute_directory_path) + + if not is_inside: + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID, + "{}: Specified path '{}' must not lead outside of the " + "project directory" + .format(provenance, path_str)) + + if path.is_absolute(): + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID, + "{}: Absolute path: '{}' invalid.\n" + "Please specify a path relative to the project's root." + .format(provenance, path)) + + if full_resolved_path.is_socket() or ( + full_resolved_path.is_fifo() or + full_resolved_path.is_block_device()): + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID_KIND, + "{}: Specified path '{}' points to an unsupported " + "file kind" + .format(provenance, path_str)) + + if check_is_file and not full_resolved_path.is_file(): + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID_KIND, + "{}: Specified path '{}' is not a regular file" + .format(provenance, path_str)) + + if check_is_dir and not full_resolved_path.is_dir(): + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID_KIND, + "{}: Specified path '{}' is not a directory" + .format(provenance, path_str)) + + return path_str + + def _validate_node(self, node): + _yaml.node_validate(node, [ + 'format-version', + 'element-path', 'variables', + 'environment', 'environment-nocache', + 'split-rules', 'elements', 'plugins', + 'aliases', 'name', 'defaults', + 'artifacts', 'options', + 'fail-on-overlap', 'shell', 'fatal-warnings', + 'ref-storage', 'sandbox', 'mirrors', 'remote-execution', + 'sources', 'source-caches', '(@)' + ]) + + # create_element() + # + # Instantiate and return an element + # + # Args: + # meta (MetaElement): The loaded MetaElement + # first_pass (bool): Whether to use first pass configuration (for junctions) + # + # Returns: + # (Element): A newly created Element object of the appropriate kind + # + def create_element(self, meta, *, first_pass=False): + if first_pass: + return self.first_pass_config.element_factory.create(self._context, self, meta) + else: + return self.config.element_factory.create(self._context, self, meta) + + # create_artifact_element() + # + # Instantiate and return an ArtifactElement + # + # Args: + # ref (str): A string of the artifact ref + # + # Returns: + # (ArtifactElement): A newly created ArtifactElement object of the appropriate kind + # + def create_artifact_element(self, ref): + return ArtifactElement(self._context, ref) + + # create_source() + # + # Instantiate and return a Source + # + # Args: + # meta (MetaSource): The loaded MetaSource + # first_pass (bool): Whether to use first pass configuration (for junctions) + # + # Returns: + # (Source): A newly created Source object of the appropriate kind + # + def create_source(self, meta, *, first_pass=False): + if first_pass: + return self.first_pass_config.source_factory.create(self._context, self, meta) + else: + return self.config.source_factory.create(self._context, self, meta) + + # get_alias_uri() + # + # Returns the URI for a given alias, if it exists + # + # Args: + # alias (str): The alias. + # first_pass (bool): Whether to use first pass configuration (for junctions) + # + # Returns: + # str: The URI for the given alias; or None: if there is no URI for + # that alias. + def get_alias_uri(self, alias, *, first_pass=False): + if first_pass: + config = self.first_pass_config + else: + config = self.config + + return _yaml.node_get(config._aliases, str, alias, default_value=None) + + # get_alias_uris() + # + # Args: + # alias (str): The alias. + # first_pass (bool): Whether to use first pass configuration (for junctions) + # + # Returns a list of every URI to replace an alias with + def get_alias_uris(self, alias, *, first_pass=False): + if first_pass: + config = self.first_pass_config + else: + config = self.config + + if not alias or alias not in config._aliases: + return [None] + + mirror_list = [] + for key, alias_mapping in config.mirrors.items(): + if alias in alias_mapping: + if key == config.default_mirror: + mirror_list = alias_mapping[alias] + mirror_list + else: + mirror_list += alias_mapping[alias] + mirror_list.append(_yaml.node_get(config._aliases, str, alias)) + return mirror_list + + # load_elements() + # + # Loads elements from target names. + # + # Args: + # targets (list): Target names + # rewritable (bool): Whether the loaded files should be rewritable + # this is a bit more expensive due to deep copies + # fetch_subprojects (bool): Whether we should fetch subprojects as a part of the + # loading process, if they are not yet locally cached + # + # Returns: + # (list): A list of loaded Element + # + def load_elements(self, targets, *, + rewritable=False, fetch_subprojects=False): + with self._context.timed_activity("Loading elements", silent_nested=True): + meta_elements = self.loader.load(targets, rewritable=rewritable, + ticker=None, + fetch_subprojects=fetch_subprojects) + + with self._context.timed_activity("Resolving elements"): + elements = [ + Element._new_from_meta(meta) + for meta in meta_elements + ] + + Element._clear_meta_elements_cache() + + # Now warn about any redundant source references which may have + # been discovered in the resolve() phase. + redundant_refs = Element._get_redundant_source_refs() + if redundant_refs: + detail = "The following inline specified source references will be ignored:\n\n" + lines = [ + "{}:{}".format(source._get_provenance(), ref) + for source, ref in redundant_refs + ] + detail += "\n".join(lines) + self._context.message( + Message(None, MessageType.WARN, "Ignoring redundant source references", detail=detail)) + + return elements + + # ensure_fully_loaded() + # + # Ensure project has finished loading. At first initialization, a + # project can only load junction elements. Other elements require + # project to be fully loaded. + # + def ensure_fully_loaded(self): + if self._fully_loaded: + return + assert self._partially_loaded + self._fully_loaded = True + + if self.junction: + self.junction._get_project().ensure_fully_loaded() + + self._load_second_pass() + + # invoked_from_workspace_element() + # + # Returns the element whose workspace was used to invoke buildstream + # if buildstream was invoked from an external workspace + # + def invoked_from_workspace_element(self): + return self._invoked_from_workspace_element + + # cleanup() + # + # Cleans up resources used loading elements + # + def cleanup(self): + # Reset the element loader state + Element._reset_load_state() + + # get_default_target() + # + # Attempts to interpret which element the user intended to run a command on. + # This is for commands that only accept a single target element and thus, + # this only uses the workspace element (if invoked from workspace directory) + # and does not use the project default targets. + # + def get_default_target(self): + return self._invoked_from_workspace_element + + # get_default_targets() + # + # Attempts to interpret which elements the user intended to run a command on. + # This is for commands that accept multiple target elements. + # + def get_default_targets(self): + + # If _invoked_from_workspace_element has a value, + # a workspace element was found before a project config + # Therefore the workspace does not contain a project + if self._invoked_from_workspace_element: + return (self._invoked_from_workspace_element,) + + # Default targets from project configuration + if self._default_targets: + return tuple(self._default_targets) + + # If default targets are not configured, default to all project elements + default_targets = [] + for root, dirs, files in os.walk(self.element_path): + # Do not recurse down the ".bst" directory which is where we stage + # junctions and other BuildStream internals. + if ".bst" in dirs: + dirs.remove(".bst") + for file in files: + if file.endswith(".bst"): + rel_dir = os.path.relpath(root, self.element_path) + rel_file = os.path.join(rel_dir, file).lstrip("./") + default_targets.append(rel_file) + + return tuple(default_targets) + + # _load(): + # + # Loads the project configuration file in the project + # directory process the first pass. + # + # Raises: LoadError if there was a problem with the project.conf + # + def _load(self, parent_loader=None): + + # Load builtin default + projectfile = os.path.join(self.directory, _PROJECT_CONF_FILE) + self._default_config_node = _yaml.load(_site.default_project_config) + + # Load project local config and override the builtin + try: + self._project_conf = _yaml.load(projectfile) + except LoadError as e: + # Raise a more specific error here + if e.reason == LoadErrorReason.MISSING_FILE: + raise LoadError(LoadErrorReason.MISSING_PROJECT_CONF, str(e)) from e + else: + raise + + pre_config_node = _yaml.node_copy(self._default_config_node) + _yaml.composite(pre_config_node, self._project_conf) + + # Assert project's format version early, before validating toplevel keys + format_version = _yaml.node_get(pre_config_node, int, 'format-version') + if BST_FORMAT_VERSION < format_version: + major, minor = utils.get_bst_version() + raise LoadError( + LoadErrorReason.UNSUPPORTED_PROJECT, + "Project requested format version {}, but BuildStream {}.{} only supports up until format version {}" + .format(format_version, major, minor, BST_FORMAT_VERSION)) + + self._validate_node(pre_config_node) + + # The project name, element path and option declarations + # are constant and cannot be overridden by option conditional statements + self.name = _yaml.node_get(self._project_conf, str, 'name') + + # Validate that project name is a valid symbol name + _yaml.assert_symbol_name(_yaml.node_get_provenance(pre_config_node, 'name'), + self.name, "project name") + + self.element_path = os.path.join( + self.directory, + self.get_path_from_node(pre_config_node, 'element-path', + check_is_dir=True) + ) + + self.config.options = OptionPool(self.element_path) + self.first_pass_config.options = OptionPool(self.element_path) + + defaults = _yaml.node_get(pre_config_node, Mapping, 'defaults') + _yaml.node_validate(defaults, ['targets']) + self._default_targets = _yaml.node_get(defaults, list, "targets") + + # Fatal warnings + self._fatal_warnings = _yaml.node_get(pre_config_node, list, 'fatal-warnings', default_value=[]) + + self.loader = Loader(self._context, self, + parent=parent_loader) + + self._project_includes = Includes(self.loader, copy_tree=False) + + project_conf_first_pass = _yaml.node_copy(self._project_conf) + self._project_includes.process(project_conf_first_pass, only_local=True) + config_no_include = _yaml.node_copy(self._default_config_node) + _yaml.composite(config_no_include, project_conf_first_pass) + + self._load_pass(config_no_include, self.first_pass_config, + ignore_unknown=True) + + # Use separate file for storing source references + self.ref_storage = _yaml.node_get(pre_config_node, str, 'ref-storage') + if self.ref_storage not in [ProjectRefStorage.INLINE, ProjectRefStorage.PROJECT_REFS]: + p = _yaml.node_get_provenance(pre_config_node, 'ref-storage') + raise LoadError(LoadErrorReason.INVALID_DATA, + "{}: Invalid value '{}' specified for ref-storage" + .format(p, self.ref_storage)) + + if self.ref_storage == ProjectRefStorage.PROJECT_REFS: + self.junction_refs.load(self.first_pass_config.options) + + # _load_second_pass() + # + # Process the second pass of loading the project configuration. + # + def _load_second_pass(self): + project_conf_second_pass = _yaml.node_copy(self._project_conf) + self._project_includes.process(project_conf_second_pass) + config = _yaml.node_copy(self._default_config_node) + _yaml.composite(config, project_conf_second_pass) + + self._load_pass(config, self.config) + + self._validate_node(config) + + # + # Now all YAML composition is done, from here on we just load + # the values from our loaded configuration dictionary. + # + + # Load artifacts pull/push configuration for this project + project_specs = ArtifactCache.specs_from_config_node(config, self.directory) + override_specs = ArtifactCache.specs_from_config_node( + self._context.get_overrides(self.name), self.directory) + + self.artifact_cache_specs = override_specs + project_specs + + if self.junction: + parent = self.junction._get_project() + self.artifact_cache_specs = parent.artifact_cache_specs + self.artifact_cache_specs + + # Load source caches with pull/push config + self.source_cache_specs = SourceCache.specs_from_config_node(config, self.directory) + + # Load remote-execution configuration for this project + project_specs = SandboxRemote.specs_from_config_node(config, self.directory) + override_specs = SandboxRemote.specs_from_config_node( + self._context.get_overrides(self.name), self.directory) + + if override_specs is not None: + self.remote_execution_specs = override_specs + elif project_specs is not None: + self.remote_execution_specs = project_specs + else: + self.remote_execution_specs = self._context.remote_execution_specs + + # Load sandbox environment variables + self.base_environment = _yaml.node_get(config, Mapping, 'environment') + self.base_env_nocache = _yaml.node_get(config, list, 'environment-nocache') + + # Load sandbox configuration + self._sandbox = _yaml.node_get(config, Mapping, 'sandbox') + + # Load project split rules + self._splits = _yaml.node_get(config, Mapping, 'split-rules') + + # Support backwards compatibility for fail-on-overlap + fail_on_overlap = _yaml.node_get(config, bool, 'fail-on-overlap', default_value=None) + + if (CoreWarnings.OVERLAPS not in self._fatal_warnings) and fail_on_overlap: + self._fatal_warnings.append(CoreWarnings.OVERLAPS) + + # Deprecation check + if fail_on_overlap is not None: + self._context.message( + Message( + None, + MessageType.WARN, + "Use of fail-on-overlap within project.conf " + + "is deprecated. Consider using fatal-warnings instead." + ) + ) + + # Load project.refs if it exists, this may be ignored. + if self.ref_storage == ProjectRefStorage.PROJECT_REFS: + self.refs.load(self.options) + + # Parse shell options + shell_options = _yaml.node_get(config, Mapping, 'shell') + _yaml.node_validate(shell_options, ['command', 'environment', 'host-files']) + self._shell_command = _yaml.node_get(shell_options, list, 'command') + + # Perform environment expansion right away + shell_environment = _yaml.node_get(shell_options, Mapping, 'environment', default_value={}) + for key in _yaml.node_keys(shell_environment): + value = _yaml.node_get(shell_environment, str, key) + self._shell_environment[key] = os.path.expandvars(value) + + # Host files is parsed as a list for convenience + host_files = _yaml.node_get(shell_options, list, 'host-files', default_value=[]) + for host_file in host_files: + if isinstance(host_file, str): + mount = HostMount(host_file) + else: + # Some validation + index = host_files.index(host_file) + host_file_desc = _yaml.node_get(shell_options, Mapping, 'host-files', indices=[index]) + _yaml.node_validate(host_file_desc, ['path', 'host_path', 'optional']) + + # Parse the host mount + path = _yaml.node_get(host_file_desc, str, 'path') + host_path = _yaml.node_get(host_file_desc, str, 'host_path', default_value=None) + optional = _yaml.node_get(host_file_desc, bool, 'optional', default_value=False) + mount = HostMount(path, host_path, optional) + + self._shell_host_files.append(mount) + + # _load_pass(): + # + # Loads parts of the project configuration that are different + # for first and second pass configurations. + # + # Args: + # config (dict) - YaML node of the configuration file. + # output (ProjectConfig) - ProjectConfig to load configuration onto. + # ignore_unknown (bool) - Whether option loader shoud ignore unknown options. + # + def _load_pass(self, config, output, *, + ignore_unknown=False): + + # Element and Source type configurations will be composited later onto + # element/source types, so we delete it from here and run our final + # assertion after. + output.element_overrides = _yaml.node_get(config, Mapping, 'elements', default_value={}) + output.source_overrides = _yaml.node_get(config, Mapping, 'sources', default_value={}) + _yaml.node_del(config, 'elements', safe=True) + _yaml.node_del(config, 'sources', safe=True) + _yaml.node_final_assertions(config) + + self._load_plugin_factories(config, output) + + # Load project options + options_node = _yaml.node_get(config, Mapping, 'options', default_value={}) + output.options.load(options_node) + if self.junction: + # load before user configuration + output.options.load_yaml_values(self.junction.options, transform=self.junction._subst_string) + + # Collect option values specified in the user configuration + overrides = self._context.get_overrides(self.name) + override_options = _yaml.node_get(overrides, Mapping, 'options', default_value={}) + output.options.load_yaml_values(override_options) + if self._cli_options: + output.options.load_cli_values(self._cli_options, ignore_unknown=ignore_unknown) + + # We're done modifying options, now we can use them for substitutions + output.options.resolve() + + # + # Now resolve any conditionals in the remaining configuration, + # any conditionals specified for project option declarations, + # or conditionally specifying the project name; will be ignored. + # + # Don't forget to also resolve options in the element and source overrides. + output.options.process_node(config) + output.options.process_node(output.element_overrides) + output.options.process_node(output.source_overrides) + + # Load base variables + output.base_variables = _yaml.node_get(config, Mapping, 'variables') + + # Add the project name as a default variable + _yaml.node_set(output.base_variables, 'project-name', self.name) + + # Extend variables with automatic variables and option exports + # Initialize it as a string as all variables are processed as strings. + # Based on some testing (mainly on AWS), maximum effective + # max-jobs value seems to be around 8-10 if we have enough cores + # users should set values based on workload and build infrastructure + platform = Platform.get_platform() + _yaml.node_set(output.base_variables, 'max-jobs', str(platform.get_cpu_count(8))) + + # Export options into variables, if that was requested + output.options.export_variables(output.base_variables) + + # Override default_mirror if not set by command-line + output.default_mirror = self._default_mirror or _yaml.node_get(overrides, str, + 'default-mirror', default_value=None) + + mirrors = _yaml.node_get(config, list, 'mirrors', default_value=[]) + for mirror in mirrors: + allowed_mirror_fields = [ + 'name', 'aliases' + ] + _yaml.node_validate(mirror, allowed_mirror_fields) + mirror_name = _yaml.node_get(mirror, str, 'name') + alias_mappings = {} + for alias_mapping, uris in _yaml.node_items(_yaml.node_get(mirror, Mapping, 'aliases')): + assert isinstance(uris, list) + alias_mappings[alias_mapping] = list(uris) + output.mirrors[mirror_name] = alias_mappings + if not output.default_mirror: + output.default_mirror = mirror_name + + # Source url aliases + output._aliases = _yaml.node_get(config, Mapping, 'aliases', default_value={}) + + # _find_project_dir() + # + # Returns path of the project directory, if a configuration file is found + # in given directory or any of its parent directories. + # + # Args: + # directory (str) - directory from where the command was invoked + # + # Raises: + # LoadError if project.conf is not found + # + # Returns: + # (str) - the directory that contains the project, and + # (str) - the name of the element required to find the project, or None + # + def _find_project_dir(self, directory): + workspace_element = None + config_filenames = [_PROJECT_CONF_FILE, WORKSPACE_PROJECT_FILE] + found_directory, filename = utils._search_upward_for_files( + directory, config_filenames + ) + if filename == _PROJECT_CONF_FILE: + project_directory = found_directory + elif filename == WORKSPACE_PROJECT_FILE: + workspace_project_cache = self._context.get_workspace_project_cache() + workspace_project = workspace_project_cache.get(found_directory) + if workspace_project: + project_directory = workspace_project.get_default_project_path() + workspace_element = workspace_project.get_default_element() + else: + raise LoadError( + LoadErrorReason.MISSING_PROJECT_CONF, + "None of {names} found in '{path}' or any of its parent directories" + .format(names=config_filenames, path=directory)) + + return project_directory, workspace_element + + def _load_plugin_factories(self, config, output): + plugin_source_origins = [] # Origins of custom sources + plugin_element_origins = [] # Origins of custom elements + + # Plugin origins and versions + origins = _yaml.node_get(config, list, 'plugins', default_value=[]) + source_format_versions = {} + element_format_versions = {} + for origin in origins: + allowed_origin_fields = [ + 'origin', 'sources', 'elements', + 'package-name', 'path', + ] + allowed_origins = ['core', 'local', 'pip'] + _yaml.node_validate(origin, allowed_origin_fields) + + origin_value = _yaml.node_get(origin, str, 'origin') + if origin_value not in allowed_origins: + raise LoadError( + LoadErrorReason.INVALID_YAML, + "Origin '{}' is not one of the allowed types" + .format(origin_value)) + + # Store source versions for checking later + source_versions = _yaml.node_get(origin, Mapping, 'sources', default_value={}) + for key in _yaml.node_keys(source_versions): + if key in source_format_versions: + raise LoadError( + LoadErrorReason.INVALID_YAML, + "Duplicate listing of source '{}'".format(key)) + source_format_versions[key] = _yaml.node_get(source_versions, int, key) + + # Store element versions for checking later + element_versions = _yaml.node_get(origin, Mapping, 'elements', default_value={}) + for key in _yaml.node_keys(element_versions): + if key in element_format_versions: + raise LoadError( + LoadErrorReason.INVALID_YAML, + "Duplicate listing of element '{}'".format(key)) + element_format_versions[key] = _yaml.node_get(element_versions, int, key) + + # Store the origins if they're not 'core'. + # core elements are loaded by default, so storing is unnecessary. + if _yaml.node_get(origin, str, 'origin') != 'core': + self._store_origin(origin, 'sources', plugin_source_origins) + self._store_origin(origin, 'elements', plugin_element_origins) + + pluginbase = PluginBase(package='buildstream.plugins') + output.element_factory = ElementFactory(pluginbase, + plugin_origins=plugin_element_origins, + format_versions=element_format_versions) + output.source_factory = SourceFactory(pluginbase, + plugin_origins=plugin_source_origins, + format_versions=source_format_versions) + + # _store_origin() + # + # Helper function to store plugin origins + # + # Args: + # origin (node) - a node indicating the origin of a group of + # plugins. + # plugin_group (str) - The name of the type of plugin that is being + # loaded + # destination (list) - A list of nodes to store the origins in + # + # Raises: + # LoadError if 'origin' is an unexpected value + def _store_origin(self, origin, plugin_group, destination): + expected_groups = ['sources', 'elements'] + if plugin_group not in expected_groups: + raise LoadError(LoadErrorReason.INVALID_DATA, + "Unexpected plugin group: {}, expecting {}" + .format(plugin_group, expected_groups)) + node_keys = [key for key in _yaml.node_keys(origin)] + if plugin_group in node_keys: + origin_node = _yaml.node_copy(origin) + plugins = _yaml.node_get(origin, Mapping, plugin_group, default_value={}) + _yaml.node_set(origin_node, 'plugins', [k for k in _yaml.node_keys(plugins)]) + for group in expected_groups: + if group in origin_node: + _yaml.node_del(origin_node, group) + + if _yaml.node_get(origin_node, str, 'origin') == 'local': + path = self.get_path_from_node(origin, 'path', + check_is_dir=True) + # paths are passed in relative to the project, but must be absolute + _yaml.node_set(origin_node, 'path', os.path.join(self.directory, path)) + destination.append(origin_node) + + # _warning_is_fatal(): + # + # Returns true if the warning in question should be considered fatal based on + # the project configuration. + # + # Args: + # warning_str (str): The warning configuration string to check against + # + # Returns: + # (bool): True if the warning should be considered fatal and cause an error. + # + def _warning_is_fatal(self, warning_str): + return warning_str in self._fatal_warnings |