Tristan Van Berkom pushed to branch edbaunton/doc-typo at BuildStream / buildstream
Commits:
- 
fb222ba1
by Josh Smith at 2018-07-27T08:50:45Z
- 
48916b8a
by Tristan Maat at 2018-07-27T10:22:41Z
- 
23e080b9
by James Ennis at 2018-07-27T11:13:50Z
- 
196cfffc
by James Ennis at 2018-07-27T11:13:50Z
- 
2b93574e
by James Ennis at 2018-07-27T11:13:50Z
- 
63e2320e
by Josh Smith at 2018-07-27T11:16:55Z
- 
6ea97b17
by Javier Jardón at 2018-07-27T11:54:20Z
- 
8b46e874
by Jonathan Maw at 2018-07-27T12:24:56Z
- 
a2e9c62a
by Jonathan Maw at 2018-07-27T12:24:56Z
- 
7c993ac0
by Jonathan Maw at 2018-07-27T12:24:56Z
- 
2889003c
by Jonathan Maw at 2018-07-27T12:24:56Z
- 
f81e8e7b
by Jonathan Maw at 2018-07-27T12:24:56Z
- 
755ed898
by Jonathan Maw at 2018-07-27T12:24:56Z
- 
19c01a56
by Jonathan Maw at 2018-07-27T12:24:56Z
- 
84872141
by Jonathan Maw at 2018-07-27T12:24:56Z
- 
bd51a0b2
by Jonathan Maw at 2018-07-27T12:24:56Z
- 
909120ab
by Jonathan Maw at 2018-07-27T12:24:56Z
- 
1cbc2e17
by Jonathan Maw at 2018-07-27T12:24:56Z
- 
202d9d26
by Jonathan Maw at 2018-07-27T12:24:56Z
- 
2b23898d
by Jonathan Maw at 2018-07-27T12:24:56Z
- 
bd4d0355
by Jonathan Maw at 2018-07-27T13:08:00Z
- 
cb4693b2
by Josh Smith at 2018-07-27T14:00:26Z
- 
8a96679a
by Josh Smith at 2018-07-27T14:01:07Z
- 
f5c8ff61
by Josh Smith at 2018-07-27T14:10:45Z
- 
32ddb544
by Qinusty at 2018-07-27T14:57:30Z
- 
2f59328a
by Ed Baunton at 2018-07-27T15:05:31Z
26 changed files:
- .gitignore
- NEWS
- README.rst
- buildstream/__init__.py
- buildstream/_artifactcache/cascache.py
- buildstream/_context.py
- buildstream/_frontend/app.py
- buildstream/_frontend/cli.py
- buildstream/_loader/loader.py
- buildstream/_project.py
- buildstream/_versions.py
- buildstream/element.py
- buildstream/plugins/sources/bzr.py
- buildstream/plugins/sources/git.py
- buildstream/source.py
- buildstream/utils.py
- + doc/source/examples/git-mirror.rst
- + doc/source/examples/tar-mirror.rst
- doc/source/format_project.rst
- doc/source/main_install.rst
- doc/source/using_config.rst
- doc/source/using_examples.rst
- tests/completions/completions.py
- + tests/frontend/mirror.py
- + tests/frontend/project/sources/fetch_source.py
- tests/testutils/repo/repo.py
Changes:
| ... | ... | @@ -15,6 +15,7 @@ tmp | 
| 15 | 15 |  .coverage
 | 
| 16 | 16 |  .coverage.*
 | 
| 17 | 17 |  .cache
 | 
| 18 | +.pytest_cache/
 | |
| 18 | 19 |  *.bst/
 | 
| 19 | 20 |  | 
| 20 | 21 |  # Pycache, in case buildstream is ran directly from within the source
 | 
| ... | ... | @@ -5,6 +5,10 @@ buildstream 1.3.1 | 
| 5 | 5 |    o Add a `--tar` option to `bst checkout` which allows a tarball to be
 | 
| 6 | 6 |      created from the artifact contents.
 | 
| 7 | 7 |  | 
| 8 | +  o Fetching and tracking will consult mirrors defined in project config,
 | |
| 9 | +    and the preferred mirror to fetch from can be defined in the command
 | |
| 10 | +    line or user config.
 | |
| 11 | + | |
| 8 | 12 |  =================
 | 
| 9 | 13 |  buildstream 1.1.4
 | 
| 10 | 14 |  =================
 | 
| ... | ... | @@ -25,7 +25,7 @@ BuildStream offers the following advantages: | 
| 25 | 25 |  | 
| 26 | 26 |  * **Declarative build instructions/definitions**
 | 
| 27 | 27 |  | 
| 28 | -  BuildStream provides a a flexible and extensible framework for the modelling
 | |
| 28 | +  BuildStream provides a flexible and extensible framework for the modelling
 | |
| 29 | 29 |    of software build pipelines in a declarative YAML format, which allows you to
 | 
| 30 | 30 |    manipulate filesystem data in a controlled, reproducible sandboxed environment.
 | 
| 31 | 31 |  | 
| ... | ... | @@ -61,25 +61,29 @@ How does BuildStream work? | 
| 61 | 61 |  ==========================
 | 
| 62 | 62 |  BuildStream operates on a set of YAML files (.bst files), as follows:
 | 
| 63 | 63 |  | 
| 64 | -* loads the YAML files which describe the target(s) and all dependencies
 | |
| 65 | -* evaluates the version information and build instructions to calculate a build
 | |
| 64 | +* Loads the YAML files which describe the target(s) and all dependencies.
 | |
| 65 | +* Evaluates the version information and build instructions to calculate a build
 | |
| 66 | 66 |    graph for the target(s) and all dependencies and unique cache-keys for each
 | 
| 67 | -  element
 | |
| 68 | -* retrieves elements from cache if they are already built, or builds them in a
 | |
| 69 | -  sandboxed environment using the instructions declared in the .bst files
 | |
| 70 | -* transforms/configures and/or deploys the resulting target(s) based on the
 | |
| 67 | +  element.
 | |
| 68 | +* Retrieves previously built elements (artifacts) from a local/remote cache, or
 | |
| 69 | +  builds the elements in a sandboxed environment using the instructions declared
 | |
| 70 | +  in the .bst files.
 | |
| 71 | +* Transforms/configures and/or deploys the resulting target(s) based on the
 | |
| 71 | 72 |    instructions declared in the .bst files.
 | 
| 72 | 73 |  | 
| 73 | 74 |  | 
| 74 | 75 |  How can I get started?
 | 
| 75 | 76 |  ======================
 | 
| 76 | -The easiest way to get started is to explore some existing .bst files, for example:
 | |
| 77 | +To start using BuildStream, first,
 | |
| 78 | +`install <https://buildstream.gitlab.io/buildstream/main_install.html>`_
 | |
| 79 | +BuildStream onto your machine and then follow our
 | |
| 80 | +`tutorial <https://buildstream.gitlab.io/buildstream/using_tutorial.html>`_.
 | |
| 81 | + | |
| 82 | +We also recommend exploring some existing BuildStream projects:
 | |
| 77 | 83 |  | 
| 78 | 84 |  * https://gitlab.gnome.org/GNOME/gnome-build-meta/
 | 
| 79 | 85 |  * https://gitlab.com/freedesktop-sdk/freedesktop-sdk
 | 
| 80 | 86 |  * https://gitlab.com/baserock/definitions
 | 
| 81 | -* https://gitlab.com/BuildStream/buildstream-examples/tree/master/build-x86image
 | |
| 82 | -* https://gitlab.com/BuildStream/buildstream-examples/tree/master/netsurf-flatpak
 | |
| 83 | 87 |  | 
| 84 | 88 |  If you have any questions please ask on our `#buildstream <irc://irc.gnome.org/buildstream>`_ channel in `irc.gnome.org <irc://irc.gnome.org>`_
 | 
| 85 | 89 |  | 
| ... | ... | @@ -29,7 +29,7 @@ if "_BST_COMPLETION" not in os.environ: | 
| 29 | 29 |      from .utils import UtilError, ProgramNotFoundError
 | 
| 30 | 30 |      from .sandbox import Sandbox, SandboxFlags
 | 
| 31 | 31 |      from .plugin import Plugin
 | 
| 32 | -    from .source import Source, SourceError, Consistency
 | |
| 32 | +    from .source import Source, SourceError, Consistency, SourceFetcher
 | |
| 33 | 33 |      from .element import Element, ElementError, Scope
 | 
| 34 | 34 |      from .buildelement import BuildElement
 | 
| 35 | 35 |      from .scriptelement import ScriptElement | 
| ... | ... | @@ -240,7 +240,8 @@ class CASCache(ArtifactCache): | 
| 240 | 240 |  | 
| 241 | 241 |              except grpc.RpcError as e:
 | 
| 242 | 242 |                  if e.code() != grpc.StatusCode.NOT_FOUND:
 | 
| 243 | -                    raise
 | |
| 243 | +                    raise ArtifactError("Failed to pull artifact {}: {}".format(
 | |
| 244 | +                        element._get_brief_display_key(), e)) from e
 | |
| 244 | 245 |  | 
| 245 | 246 |          return False
 | 
| 246 | 247 |  | 
| ... | ... | @@ -285,6 +286,7 @@ class CASCache(ArtifactCache): | 
| 285 | 286 |  | 
| 286 | 287 |                      except grpc.RpcError as e:
 | 
| 287 | 288 |                          if e.code() != grpc.StatusCode.NOT_FOUND:
 | 
| 289 | +                            # Intentionally re-raise RpcError for outer except block.
 | |
| 288 | 290 |                              raise
 | 
| 289 | 291 |  | 
| 290 | 292 |                      missing_blobs = {}
 | 
| ... | ... | @@ -197,29 +197,55 @@ class Context(): | 
| 197 | 197 |                              "\nValid values are, for example: 800M 10G 1T 50%\n"
 | 
| 198 | 198 |                              .format(str(e))) from e
 | 
| 199 | 199 |  | 
| 200 | -        # If we are asked not to set a quota, we set it to the maximum
 | |
| 201 | -        # disk space available minus a headroom of 2GB, such that we
 | |
| 202 | -        # at least try to avoid raising Exceptions.
 | |
| 200 | +        # Headroom intended to give BuildStream a bit of leeway.
 | |
| 201 | +        # This acts as the minimum size of cache_quota and also
 | |
| 202 | +        # is taken from the user requested cache_quota.
 | |
| 203 | 203 |          #
 | 
| 204 | -        # Of course, we might still end up running out during a build
 | |
| 205 | -        # if we end up writing more than 2G, but hey, this stuff is
 | |
| 206 | -        # already really fuzzy.
 | |
| 207 | -        #
 | |
| 208 | -        if cache_quota is None:
 | |
| 209 | -            stat = os.statvfs(artifactdir_volume)
 | |
| 210 | -            # Again, the artifact directory may not yet have been
 | |
| 211 | -            # created
 | |
| 212 | -            if not os.path.exists(self.artifactdir):
 | |
| 213 | -                cache_size = 0
 | |
| 214 | -            else:
 | |
| 215 | -                cache_size = utils._get_dir_size(self.artifactdir)
 | |
| 216 | -            cache_quota = cache_size + stat.f_bsize * stat.f_bavail
 | |
| 217 | - | |
| 218 | 204 |          if 'BST_TEST_SUITE' in os.environ:
 | 
| 219 | 205 |              headroom = 0
 | 
| 220 | 206 |          else:
 | 
| 221 | 207 |              headroom = 2e9
 | 
| 222 | 208 |  | 
| 209 | +        stat = os.statvfs(artifactdir_volume)
 | |
| 210 | +        available_space = (stat.f_bsize * stat.f_bavail)
 | |
| 211 | + | |
| 212 | +        # Again, the artifact directory may not yet have been created yet
 | |
| 213 | +        #
 | |
| 214 | +        if not os.path.exists(self.artifactdir):
 | |
| 215 | +            cache_size = 0
 | |
| 216 | +        else:
 | |
| 217 | +            cache_size = utils._get_dir_size(self.artifactdir)
 | |
| 218 | + | |
| 219 | +        # Ensure system has enough storage for the cache_quota
 | |
| 220 | +        #
 | |
| 221 | +        # If cache_quota is none, set it to the maximum it could possibly be.
 | |
| 222 | +        #
 | |
| 223 | +        # Also check that cache_quota is atleast as large as our headroom.
 | |
| 224 | +        #
 | |
| 225 | +        if cache_quota is None:  # Infinity, set to max system storage
 | |
| 226 | +            cache_quota = cache_size + available_space
 | |
| 227 | +        if cache_quota < headroom:  # Check minimum
 | |
| 228 | +            raise LoadError(LoadErrorReason.INVALID_DATA,
 | |
| 229 | +                            "Invalid cache quota ({}): ".format(utils._pretty_size(cache_quota)) +
 | |
| 230 | +                            "BuildStream requires a minimum cache quota of 2G.")
 | |
| 231 | +        elif cache_quota > cache_size + available_space:  # Check maximum
 | |
| 232 | +            raise LoadError(LoadErrorReason.INVALID_DATA,
 | |
| 233 | +                            ("Your system does not have enough available " +
 | |
| 234 | +                             "space to support the cache quota specified.\n" +
 | |
| 235 | +                             "You currently have:\n" +
 | |
| 236 | +                             "- {used} of cache in use at {local_cache_path}\n" +
 | |
| 237 | +                             "- {available} of available system storage").format(
 | |
| 238 | +                                 used=utils._pretty_size(cache_size),
 | |
| 239 | +                                 local_cache_path=self.artifactdir,
 | |
| 240 | +                                 available=utils._pretty_size(available_space)))
 | |
| 241 | + | |
| 242 | +        # Place a slight headroom (2e9 (2GB) on the cache_quota) into
 | |
| 243 | +        # cache_quota to try and avoid exceptions.
 | |
| 244 | +        #
 | |
| 245 | +        # Of course, we might still end up running out during a build
 | |
| 246 | +        # if we end up writing more than 2G, but hey, this stuff is
 | |
| 247 | +        # already really fuzzy.
 | |
| 248 | +        #
 | |
| 223 | 249 |          self.cache_quota = cache_quota - headroom
 | 
| 224 | 250 |          self.cache_lower_threshold = self.cache_quota / 2
 | 
| 225 | 251 |  | 
| ... | ... | @@ -259,7 +285,7 @@ class Context(): | 
| 259 | 285 |          # Shallow validation of overrides, parts of buildstream which rely
 | 
| 260 | 286 |          # on the overrides are expected to validate elsewhere.
 | 
| 261 | 287 |          for _, overrides in _yaml.node_items(self._project_overrides):
 | 
| 262 | -            _yaml.node_validate(overrides, ['artifacts', 'options', 'strict'])
 | |
| 288 | +            _yaml.node_validate(overrides, ['artifacts', 'options', 'strict', 'default-mirror'])
 | |
| 263 | 289 |  | 
| 264 | 290 |          profile_end(Topics.LOAD_CONTEXT, 'load')
 | 
| 265 | 291 |  | 
| ... | ... | @@ -202,7 +202,8 @@ class App(): | 
| 202 | 202 |          # Load the Project
 | 
| 203 | 203 |          #
 | 
| 204 | 204 |          try:
 | 
| 205 | -            self.project = Project(directory, self.context, cli_options=self._main_options['option'])
 | |
| 205 | +            self.project = Project(directory, self.context, cli_options=self._main_options['option'],
 | |
| 206 | +                                   default_mirror=self._main_options.get('default_mirror'))
 | |
| 206 | 207 |          except LoadError as e:
 | 
| 207 | 208 |  | 
| 208 | 209 |              # Let's automatically start a `bst init` session in this case
 | 
| ... | ... | @@ -217,6 +217,8 @@ def print_version(ctx, param, value): | 
| 217 | 217 |                help="Elements must be rebuilt when their dependencies have changed")
 | 
| 218 | 218 |  @click.option('--option', '-o', type=click.Tuple([str, str]), multiple=True, metavar='OPTION VALUE',
 | 
| 219 | 219 |                help="Specify a project option")
 | 
| 220 | +@click.option('--default-mirror', default=None,
 | |
| 221 | +              help="The mirror to fetch from first, before attempting other mirrors")
 | |
| 220 | 222 |  @click.pass_context
 | 
| 221 | 223 |  def cli(context, **kwargs):
 | 
| 222 | 224 |      """Build and manipulate BuildStream projects
 | 
| ... | ... | @@ -513,7 +513,7 @@ class Loader(): | 
| 513 | 513 |                  if self._fetch_subprojects:
 | 
| 514 | 514 |                      if ticker:
 | 
| 515 | 515 |                          ticker(filename, 'Fetching subproject from {} source'.format(source.get_kind()))
 | 
| 516 | -                    source.fetch()
 | |
| 516 | +                    source._fetch()
 | |
| 517 | 517 |                  else:
 | 
| 518 | 518 |                      detail = "Try fetching the project with `bst fetch {}`".format(filename)
 | 
| 519 | 519 |                      raise LoadError(LoadErrorReason.SUBPROJECT_FETCH_NEEDED,
 | 
| ... | ... | @@ -19,7 +19,7 @@ | 
| 19 | 19 |  | 
| 20 | 20 |  import os
 | 
| 21 | 21 |  import multiprocessing  # for cpu_count()
 | 
| 22 | -from collections import Mapping
 | |
| 22 | +from collections import Mapping, OrderedDict
 | |
| 23 | 23 |  from pluginbase import PluginBase
 | 
| 24 | 24 |  from . import utils
 | 
| 25 | 25 |  from . import _cachekey
 | 
| ... | ... | @@ -35,9 +35,6 @@ from ._projectrefs import ProjectRefs, ProjectRefStorage | 
| 35 | 35 |  from ._versions import BST_FORMAT_VERSION
 | 
| 36 | 36 |  | 
| 37 | 37 |  | 
| 38 | -# The separator we use for user specified aliases
 | |
| 39 | -_ALIAS_SEPARATOR = ':'
 | |
| 40 | - | |
| 41 | 38 |  # Project Configuration file
 | 
| 42 | 39 |  _PROJECT_CONF_FILE = 'project.conf'
 | 
| 43 | 40 |  | 
| ... | ... | @@ -70,7 +67,7 @@ class HostMount(): | 
| 70 | 67 |  #
 | 
| 71 | 68 |  class Project():
 | 
| 72 | 69 |  | 
| 73 | -    def __init__(self, directory, context, *, junction=None, cli_options=None):
 | |
| 70 | +    def __init__(self, directory, context, *, junction=None, cli_options=None, default_mirror=None):
 | |
| 74 | 71 |  | 
| 75 | 72 |          # The project name
 | 
| 76 | 73 |          self.name = None
 | 
| ... | ... | @@ -94,6 +91,8 @@ class Project(): | 
| 94 | 91 |          self.base_env_nocache = None             # The base nocache mask (list) for the environment
 | 
| 95 | 92 |          self.element_overrides = {}              # Element specific configurations
 | 
| 96 | 93 |          self.source_overrides = {}               # Source specific configurations
 | 
| 94 | +        self.mirrors = OrderedDict()             # contains dicts of alias-mappings to URIs.
 | |
| 95 | +        self.default_mirror = default_mirror     # The name of the preferred mirror.
 | |
| 97 | 96 |  | 
| 98 | 97 |          #
 | 
| 99 | 98 |          # Private Members
 | 
| ... | ... | @@ -133,8 +132,8 @@ class Project(): | 
| 133 | 132 |      # fully qualified urls based on the shorthand which is allowed
 | 
| 134 | 133 |      # to be specified in the YAML
 | 
| 135 | 134 |      def translate_url(self, url):
 | 
| 136 | -        if url and _ALIAS_SEPARATOR in url:
 | |
| 137 | -            url_alias, url_body = url.split(_ALIAS_SEPARATOR, 1)
 | |
| 135 | +        if url and utils._ALIAS_SEPARATOR in url:
 | |
| 136 | +            url_alias, url_body = url.split(utils._ALIAS_SEPARATOR, 1)
 | |
| 138 | 137 |              alias_url = self._aliases.get(url_alias)
 | 
| 139 | 138 |              if alias_url:
 | 
| 140 | 139 |                  url = alias_url + url_body
 | 
| ... | ... | @@ -202,6 +201,36 @@ class Project(): | 
| 202 | 201 |          self._assert_plugin_format(source, version)
 | 
| 203 | 202 |          return source
 | 
| 204 | 203 |  | 
| 204 | +    # get_alias_uri()
 | |
| 205 | +    #
 | |
| 206 | +    # Returns the URI for a given alias, if it exists
 | |
| 207 | +    #
 | |
| 208 | +    # Args:
 | |
| 209 | +    #    alias (str): The alias.
 | |
| 210 | +    #
 | |
| 211 | +    # Returns:
 | |
| 212 | +    #    str: The URI for the given alias; or None: if there is no URI for
 | |
| 213 | +    #         that alias.
 | |
| 214 | +    def get_alias_uri(self, alias):
 | |
| 215 | +        return self._aliases.get(alias)
 | |
| 216 | + | |
| 217 | +    # get_alias_uris()
 | |
| 218 | +    #
 | |
| 219 | +    # Returns a list of every URI to replace an alias with
 | |
| 220 | +    def get_alias_uris(self, alias):
 | |
| 221 | +        if not alias or alias not in self._aliases:
 | |
| 222 | +            return [None]
 | |
| 223 | + | |
| 224 | +        mirror_list = []
 | |
| 225 | +        for key, alias_mapping in self.mirrors.items():
 | |
| 226 | +            if alias in alias_mapping:
 | |
| 227 | +                if key == self.default_mirror:
 | |
| 228 | +                    mirror_list = alias_mapping[alias] + mirror_list
 | |
| 229 | +                else:
 | |
| 230 | +                    mirror_list += alias_mapping[alias]
 | |
| 231 | +        mirror_list.append(self._aliases[alias])
 | |
| 232 | +        return mirror_list
 | |
| 233 | + | |
| 205 | 234 |      # _load():
 | 
| 206 | 235 |      #
 | 
| 207 | 236 |      # Loads the project configuration file in the project directory.
 | 
| ... | ... | @@ -249,7 +278,7 @@ class Project(): | 
| 249 | 278 |              'aliases', 'name',
 | 
| 250 | 279 |              'artifacts', 'options',
 | 
| 251 | 280 |              'fail-on-overlap', 'shell',
 | 
| 252 | -            'ref-storage', 'sandbox'
 | |
| 281 | +            'ref-storage', 'sandbox', 'mirrors',
 | |
| 253 | 282 |          ])
 | 
| 254 | 283 |  | 
| 255 | 284 |          # The project name, element path and option declarations
 | 
| ... | ... | @@ -290,6 +319,10 @@ class Project(): | 
| 290 | 319 |          #
 | 
| 291 | 320 |          self.options.process_node(config)
 | 
| 292 | 321 |  | 
| 322 | +        # Override default_mirror if not set by command-line
 | |
| 323 | +        if not self.default_mirror:
 | |
| 324 | +            self.default_mirror = _yaml.node_get(overrides, str, 'default-mirror', default_value=None)
 | |
| 325 | + | |
| 293 | 326 |          #
 | 
| 294 | 327 |          # Now all YAML composition is done, from here on we just load
 | 
| 295 | 328 |          # the values from our loaded configuration dictionary.
 | 
| ... | ... | @@ -414,6 +447,21 @@ class Project(): | 
| 414 | 447 |  | 
| 415 | 448 |              self._shell_host_files.append(mount)
 | 
| 416 | 449 |  | 
| 450 | +        mirrors = _yaml.node_get(config, list, 'mirrors', default_value=[])
 | |
| 451 | +        for mirror in mirrors:
 | |
| 452 | +            allowed_mirror_fields = [
 | |
| 453 | +                'name', 'aliases'
 | |
| 454 | +            ]
 | |
| 455 | +            _yaml.node_validate(mirror, allowed_mirror_fields)
 | |
| 456 | +            mirror_name = _yaml.node_get(mirror, str, 'name')
 | |
| 457 | +            alias_mappings = {}
 | |
| 458 | +            for alias_mapping, uris in _yaml.node_items(mirror['aliases']):
 | |
| 459 | +                assert isinstance(uris, list)
 | |
| 460 | +                alias_mappings[alias_mapping] = list(uris)
 | |
| 461 | +            self.mirrors[mirror_name] = alias_mappings
 | |
| 462 | +            if not self.default_mirror:
 | |
| 463 | +                self.default_mirror = mirror_name
 | |
| 464 | + | |
| 417 | 465 |      # _assert_plugin_format()
 | 
| 418 | 466 |      #
 | 
| 419 | 467 |      # Helper to raise a PluginError if the loaded plugin is of a lesser version then
 | 
| ... | ... | @@ -23,7 +23,7 @@ | 
| 23 | 23 |  # This version is bumped whenever enhancements are made
 | 
| 24 | 24 |  # to the `project.conf` format or the core element format.
 | 
| 25 | 25 |  #
 | 
| 26 | -BST_FORMAT_VERSION = 10
 | |
| 26 | +BST_FORMAT_VERSION = 11
 | |
| 27 | 27 |  | 
| 28 | 28 |  | 
| 29 | 29 |  # The base BuildStream artifact version
 | 
| ... | ... | @@ -349,8 +349,8 @@ class Element(Plugin): | 
| 349 | 349 |          generated script is run:
 | 
| 350 | 350 |  | 
| 351 | 351 |          - All element variables have been exported.
 | 
| 352 | -        - The cwd is `self.get_variable('build_root')/self.normal_name`.
 | |
| 353 | -        - $PREFIX is set to `self.get_variable('install_root')`.
 | |
| 352 | +        - The cwd is `self.get_variable('build-root')/self.normal_name`.
 | |
| 353 | +        - $PREFIX is set to `self.get_variable('install-root')`.
 | |
| 354 | 354 |          - The directory indicated by $PREFIX is an empty directory.
 | 
| 355 | 355 |  | 
| 356 | 356 |          Files are expected to be installed to $PREFIX.
 | 
| ... | ... | @@ -102,7 +102,7 @@ class BzrSource(Source): | 
| 102 | 102 |      def track(self):
 | 
| 103 | 103 |          with self.timed_activity("Tracking {}".format(self.url),
 | 
| 104 | 104 |                                   silent_nested=True):
 | 
| 105 | -            self._ensure_mirror()
 | |
| 105 | +            self._ensure_mirror(skip_ref_check=True)
 | |
| 106 | 106 |              ret, out = self.check_output([self.host_bzr, "version-info",
 | 
| 107 | 107 |                                            "--custom", "--template={revno}",
 | 
| 108 | 108 |                                            self._get_branch_dir()],
 | 
| ... | ... | @@ -214,7 +214,7 @@ class BzrSource(Source): | 
| 214 | 214 |              yield repodir
 | 
| 215 | 215 |              self._atomic_replace_mirrordir(repodir)
 | 
| 216 | 216 |  | 
| 217 | -    def _ensure_mirror(self):
 | |
| 217 | +    def _ensure_mirror(self, skip_ref_check=False):
 | |
| 218 | 218 |          with self._atomic_repodir() as repodir:
 | 
| 219 | 219 |              # Initialize repo if no metadata
 | 
| 220 | 220 |              bzr_metadata_dir = os.path.join(repodir, ".bzr")
 | 
| ... | ... | @@ -223,18 +223,21 @@ class BzrSource(Source): | 
| 223 | 223 |                            fail="Failed to initialize bzr repository")
 | 
| 224 | 224 |  | 
| 225 | 225 |              branch_dir = os.path.join(repodir, self.tracking)
 | 
| 226 | +            branch_url = self.url + "/" + self.tracking
 | |
| 226 | 227 |              if not os.path.exists(branch_dir):
 | 
| 227 | 228 |                  # `bzr branch` the branch if it doesn't exist
 | 
| 228 | 229 |                  # to get the upstream code
 | 
| 229 | -                branch_url = self.url + "/" + self.tracking
 | |
| 230 | 230 |                  self.call([self.host_bzr, "branch", branch_url, branch_dir],
 | 
| 231 | 231 |                            fail="Failed to branch from {} to {}".format(branch_url, branch_dir))
 | 
| 232 | 232 |  | 
| 233 | 233 |              else:
 | 
| 234 | 234 |                  # `bzr pull` the branch if it does exist
 | 
| 235 | 235 |                  # to get any changes to the upstream code
 | 
| 236 | -                self.call([self.host_bzr, "pull", "--directory={}".format(branch_dir)],
 | |
| 236 | +                self.call([self.host_bzr, "pull", "--directory={}".format(branch_dir), branch_url],
 | |
| 237 | 237 |                            fail="Failed to pull new changes for {}".format(branch_dir))
 | 
| 238 | +        if not skip_ref_check and not self._check_ref():
 | |
| 239 | +            raise SourceError("Failed to ensure ref '{}' was mirrored".format(self.ref),
 | |
| 240 | +                              reason="ref-not-mirrored")
 | |
| 238 | 241 |  | 
| 239 | 242 |  | 
| 240 | 243 |  def setup():
 | 
| ... | ... | @@ -78,7 +78,7 @@ from io import StringIO | 
| 78 | 78 |  | 
| 79 | 79 |  from configparser import RawConfigParser
 | 
| 80 | 80 |  | 
| 81 | -from buildstream import Source, SourceError, Consistency
 | |
| 81 | +from buildstream import Source, SourceError, Consistency, SourceFetcher
 | |
| 82 | 82 |  from buildstream import utils
 | 
| 83 | 83 |  | 
| 84 | 84 |  GIT_MODULES = '.gitmodules'
 | 
| ... | ... | @@ -88,18 +88,20 @@ GIT_MODULES = '.gitmodules' | 
| 88 | 88 |  # for the primary git source and also for each submodule it
 | 
| 89 | 89 |  # might have at a given time
 | 
| 90 | 90 |  #
 | 
| 91 | -class GitMirror():
 | |
| 91 | +class GitMirror(SourceFetcher):
 | |
| 92 | 92 |  | 
| 93 | 93 |      def __init__(self, source, path, url, ref):
 | 
| 94 | 94 |  | 
| 95 | +        super().__init__()
 | |
| 95 | 96 |          self.source = source
 | 
| 96 | 97 |          self.path = path
 | 
| 97 | -        self.url = source.translate_url(url)
 | |
| 98 | +        self.url = url
 | |
| 98 | 99 |          self.ref = ref
 | 
| 99 | -        self.mirror = os.path.join(source.get_mirror_directory(), utils.url_directory_name(self.url))
 | |
| 100 | +        self.mirror = os.path.join(source.get_mirror_directory(), utils.url_directory_name(url))
 | |
| 101 | +        self.mark_download_url(url)
 | |
| 100 | 102 |  | 
| 101 | 103 |      # Ensures that the mirror exists
 | 
| 102 | -    def ensure(self):
 | |
| 104 | +    def ensure(self, alias_override=None):
 | |
| 103 | 105 |  | 
| 104 | 106 |          # Unfortunately, git does not know how to only clone just a specific ref,
 | 
| 105 | 107 |          # so we have to download all of those gigs even if we only need a couple
 | 
| ... | ... | @@ -112,22 +114,47 @@ class GitMirror(): | 
| 112 | 114 |              # system configured tmpdir is not on the same partition.
 | 
| 113 | 115 |              #
 | 
| 114 | 116 |              with self.source.tempdir() as tmpdir:
 | 
| 115 | -                self.source.call([self.source.host_git, 'clone', '--mirror', '-n', self.url, tmpdir],
 | |
| 116 | -                                 fail="Failed to clone git repository {}".format(self.url),
 | |
| 117 | +                url = self.source.translate_url(self.url, alias_override=alias_override)
 | |
| 118 | +                self.source.call([self.source.host_git, 'clone', '--mirror', '-n', url, tmpdir],
 | |
| 119 | +                                 fail="Failed to clone git repository {}".format(url),
 | |
| 117 | 120 |                                   fail_temporarily=True)
 | 
| 118 | 121 |  | 
| 119 | 122 |                  try:
 | 
| 120 | 123 |                      shutil.move(tmpdir, self.mirror)
 | 
| 121 | 124 |                  except (shutil.Error, OSError) as e:
 | 
| 122 | 125 |                      raise SourceError("{}: Failed to move cloned git repository {} from '{}' to '{}'"
 | 
| 123 | -                                      .format(self.source, self.url, tmpdir, self.mirror)) from e
 | |
| 126 | +                                      .format(self.source, url, tmpdir, self.mirror)) from e
 | |
| 127 | + | |
| 128 | +    def _fetch(self, alias_override=None):
 | |
| 129 | +        url = self.source.translate_url(self.url, alias_override=alias_override)
 | |
| 130 | + | |
| 131 | +        if alias_override:
 | |
| 132 | +            remote_name = utils.url_directory_name(alias_override)
 | |
| 133 | +            _, remotes = self.source.check_output(
 | |
| 134 | +                [self.source.host_git, 'remote'],
 | |
| 135 | +                fail="Failed to retrieve list of remotes in {}".format(self.mirror),
 | |
| 136 | +                cwd=self.mirror
 | |
| 137 | +            )
 | |
| 138 | +            if remote_name not in remotes:
 | |
| 139 | +                self.source.call(
 | |
| 140 | +                    [self.source.host_git, 'remote', 'add', remote_name, url],
 | |
| 141 | +                    fail="Failed to add remote {} with url {}".format(remote_name, url),
 | |
| 142 | +                    cwd=self.mirror
 | |
| 143 | +                )
 | |
| 144 | +        else:
 | |
| 145 | +            remote_name = "origin"
 | |
| 124 | 146 |  | 
| 125 | -    def fetch(self):
 | |
| 126 | -        self.source.call([self.source.host_git, 'fetch', 'origin', '--prune'],
 | |
| 127 | -                         fail="Failed to fetch from remote git repository: {}".format(self.url),
 | |
| 147 | +        self.source.call([self.source.host_git, 'fetch', remote_name, '--prune'],
 | |
| 148 | +                         fail="Failed to fetch from remote git repository: {}".format(url),
 | |
| 128 | 149 |                           fail_temporarily=True,
 | 
| 129 | 150 |                           cwd=self.mirror)
 | 
| 130 | 151 |  | 
| 152 | +    def fetch(self, alias_override=None):
 | |
| 153 | +        self.ensure(alias_override)
 | |
| 154 | +        if not self.has_ref():
 | |
| 155 | +            self._fetch(alias_override)
 | |
| 156 | +        self.assert_ref()
 | |
| 157 | + | |
| 131 | 158 |      def has_ref(self):
 | 
| 132 | 159 |          if not self.ref:
 | 
| 133 | 160 |              return False
 | 
| ... | ... | @@ -171,13 +198,14 @@ class GitMirror(): | 
| 171 | 198 |  | 
| 172 | 199 |      def init_workspace(self, directory):
 | 
| 173 | 200 |          fullpath = os.path.join(directory, self.path)
 | 
| 201 | +        url = self.source.translate_url(self.url)
 | |
| 174 | 202 |  | 
| 175 | 203 |          self.source.call([self.source.host_git, 'clone', '--no-checkout', self.mirror, fullpath],
 | 
| 176 | 204 |                           fail="Failed to clone git mirror {} in directory: {}".format(self.mirror, fullpath),
 | 
| 177 | 205 |                           fail_temporarily=True)
 | 
| 178 | 206 |  | 
| 179 | -        self.source.call([self.source.host_git, 'remote', 'set-url', 'origin', self.url],
 | |
| 180 | -                         fail='Failed to add remote origin "{}"'.format(self.url),
 | |
| 207 | +        self.source.call([self.source.host_git, 'remote', 'set-url', 'origin', url],
 | |
| 208 | +                         fail='Failed to add remote origin "{}"'.format(url),
 | |
| 181 | 209 |                           cwd=fullpath)
 | 
| 182 | 210 |  | 
| 183 | 211 |          self.source.call([self.source.host_git, 'checkout', '--force', self.ref],
 | 
| ... | ... | @@ -277,6 +305,8 @@ class GitSource(Source): | 
| 277 | 305 |                  checkout = self.node_get_member(submodule, bool, 'checkout')
 | 
| 278 | 306 |                  self.submodule_checkout_overrides[path] = checkout
 | 
| 279 | 307 |  | 
| 308 | +        self.mark_download_url(self.original_url)
 | |
| 309 | + | |
| 280 | 310 |      def preflight(self):
 | 
| 281 | 311 |          # Check if git is installed, get the binary at the same time
 | 
| 282 | 312 |          self.host_git = utils.get_host_tool('git')
 | 
| ... | ... | @@ -328,31 +358,13 @@ class GitSource(Source): | 
| 328 | 358 |                                   .format(self.tracking, self.mirror.url),
 | 
| 329 | 359 |                                   silent_nested=True):
 | 
| 330 | 360 |              self.mirror.ensure()
 | 
| 331 | -            self.mirror.fetch()
 | |
| 361 | +            self.mirror._fetch()
 | |
| 332 | 362 |  | 
| 333 | 363 |              # Update self.mirror.ref and node.ref from the self.tracking branch
 | 
| 334 | 364 |              ret = self.mirror.latest_commit(self.tracking)
 | 
| 335 | 365 |  | 
| 336 | 366 |          return ret
 | 
| 337 | 367 |  | 
| 338 | -    def fetch(self):
 | |
| 339 | - | |
| 340 | -        with self.timed_activity("Fetching {}".format(self.mirror.url), silent_nested=True):
 | |
| 341 | - | |
| 342 | -            # Here we are only interested in ensuring that our mirror contains
 | |
| 343 | -            # the self.mirror.ref commit.
 | |
| 344 | -            self.mirror.ensure()
 | |
| 345 | -            if not self.mirror.has_ref():
 | |
| 346 | -                self.mirror.fetch()
 | |
| 347 | - | |
| 348 | -            self.mirror.assert_ref()
 | |
| 349 | - | |
| 350 | -            # Here after performing any fetches, we need to also ensure that
 | |
| 351 | -            # we've cached the desired refs in our mirrors of submodules.
 | |
| 352 | -            #
 | |
| 353 | -            self.refresh_submodules()
 | |
| 354 | -            self.fetch_submodules()
 | |
| 355 | - | |
| 356 | 368 |      def init_workspace(self, directory):
 | 
| 357 | 369 |          # XXX: may wish to refactor this as some code dupe with stage()
 | 
| 358 | 370 |          self.refresh_submodules()
 | 
| ... | ... | @@ -384,6 +396,10 @@ class GitSource(Source): | 
| 384 | 396 |                  if checkout:
 | 
| 385 | 397 |                      mirror.stage(directory)
 | 
| 386 | 398 |  | 
| 399 | +    def get_source_fetchers(self):
 | |
| 400 | +        self.refresh_submodules()
 | |
| 401 | +        return [self.mirror] + self.submodules
 | |
| 402 | + | |
| 387 | 403 |      ###########################################################
 | 
| 388 | 404 |      #                     Local Functions                     #
 | 
| 389 | 405 |      ###########################################################
 | 
| ... | ... | @@ -405,6 +421,7 @@ class GitSource(Source): | 
| 405 | 421 |      # Assumes that we have our mirror and we have the ref which we point to
 | 
| 406 | 422 |      #
 | 
| 407 | 423 |      def refresh_submodules(self):
 | 
| 424 | +        self.mirror.ensure()
 | |
| 408 | 425 |          submodules = []
 | 
| 409 | 426 |  | 
| 410 | 427 |          # XXX Here we should issue a warning if either:
 | 
| ... | ... | @@ -426,19 +443,6 @@ class GitSource(Source): | 
| 426 | 443 |  | 
| 427 | 444 |          self.submodules = submodules
 | 
| 428 | 445 |  | 
| 429 | -    # Ensures that we have mirrored git repositories for all
 | |
| 430 | -    # the submodules existing at the given commit of the main git source.
 | |
| 431 | -    #
 | |
| 432 | -    # Also ensure that these mirrors have the required commits
 | |
| 433 | -    # referred to at the given commit of the main git source.
 | |
| 434 | -    #
 | |
| 435 | -    def fetch_submodules(self):
 | |
| 436 | -        for mirror in self.submodules:
 | |
| 437 | -            mirror.ensure()
 | |
| 438 | -            if not mirror.has_ref():
 | |
| 439 | -                mirror.fetch()
 | |
| 440 | -                mirror.assert_ref()
 | |
| 441 | - | |
| 442 | 446 |  | 
| 443 | 447 |  # Plugin entry point
 | 
| 444 | 448 |  def setup():
 | 
| ... | ... | @@ -65,6 +65,33 @@ these methods are mandatory to implement. | 
| 65 | 65 |  | 
| 66 | 66 |    **Optional**: If left unimplemented, this will default to calling
 | 
| 67 | 67 |    :func:`Source.stage() <buildstream.source.Source.stage>`
 | 
| 68 | + | |
| 69 | +* :func:`Source.get_source_fetchers() <buildstream.source.Source.get_source_fetchers>`
 | |
| 70 | + | |
| 71 | +  Get the objects that are used for fetching.
 | |
| 72 | + | |
| 73 | +  **Optional**: This only needs to be implemented for sources that need to
 | |
| 74 | +  download from multiple URLs while fetching (e.g. a git repo and its
 | |
| 75 | +  submodules). For details on how to define a SourceFetcher, see
 | |
| 76 | +  :ref:`SourceFetcher <core_source_fetcher>`.
 | |
| 77 | + | |
| 78 | + | |
| 79 | +.. _core_source_fetcher:
 | |
| 80 | + | |
| 81 | +SourceFetcher - Object for fetching individual URLs
 | |
| 82 | +===================================================
 | |
| 83 | + | |
| 84 | + | |
| 85 | +Abstract Methods
 | |
| 86 | +----------------
 | |
| 87 | +SourceFetchers expose the following abstract methods. Unless explicitly
 | |
| 88 | +mentioned, these methods are mandatory to implement.
 | |
| 89 | + | |
| 90 | +* :func:`SourceFetcher.fetch() <buildstream.source.SourceFetcher.fetch>`
 | |
| 91 | + | |
| 92 | +  Fetches the URL associated with this SourceFetcher, optionally taking an
 | |
| 93 | +  alias override.
 | |
| 94 | + | |
| 68 | 95 |  """
 | 
| 69 | 96 |  | 
| 70 | 97 |  import os
 | 
| ... | ... | @@ -114,6 +141,63 @@ class SourceError(BstError): | 
| 114 | 141 |          super().__init__(message, detail=detail, domain=ErrorDomain.SOURCE, reason=reason, temporary=temporary)
 | 
| 115 | 142 |  | 
| 116 | 143 |  | 
| 144 | +class SourceFetcher():
 | |
| 145 | +    """SourceFetcher()
 | |
| 146 | + | |
| 147 | +    This interface exists so that a source that downloads from multiple
 | |
| 148 | +    places (e.g. a git source with submodules) has a consistent interface for
 | |
| 149 | +    fetching and substituting aliases.
 | |
| 150 | + | |
| 151 | +    *Since: 1.4*
 | |
| 152 | +    """
 | |
| 153 | +    def __init__(self):
 | |
| 154 | +        self.__alias = None
 | |
| 155 | + | |
| 156 | +    #############################################################
 | |
| 157 | +    #                      Abstract Methods                     #
 | |
| 158 | +    #############################################################
 | |
| 159 | +    def fetch(self, alias_override=None):
 | |
| 160 | +        """Fetch remote sources and mirror them locally, ensuring at least
 | |
| 161 | +        that the specific reference is cached locally.
 | |
| 162 | + | |
| 163 | +        Args:
 | |
| 164 | +           alias_override (str): The alias to use instead of the default one
 | |
| 165 | +               defined by the :ref:`aliases <project_source_aliases>` field
 | |
| 166 | +               in the project's config.
 | |
| 167 | + | |
| 168 | +        Raises:
 | |
| 169 | +           :class:`.SourceError`
 | |
| 170 | + | |
| 171 | +        Implementors should raise :class:`.SourceError` if the there is some
 | |
| 172 | +        network error or if the source reference could not be matched.
 | |
| 173 | +        """
 | |
| 174 | +        raise ImplError("Source fetcher '{}' does not implement fetch()".format(type(self)))
 | |
| 175 | + | |
| 176 | +    #############################################################
 | |
| 177 | +    #                       Public Methods                      #
 | |
| 178 | +    #############################################################
 | |
| 179 | +    def mark_download_url(self, url):
 | |
| 180 | +        """Identifies the URL that this SourceFetcher uses to download
 | |
| 181 | + | |
| 182 | +        This must be called during the fetcher's initialization
 | |
| 183 | + | |
| 184 | +        Args:
 | |
| 185 | +           url (str): The url used to download.
 | |
| 186 | +        """
 | |
| 187 | +        # Not guaranteed to be a valid alias yet.
 | |
| 188 | +        # Ensuring it's a valid alias currently happens in Project.get_alias_uris
 | |
| 189 | +        alias, _ = url.split(utils._ALIAS_SEPARATOR, 1)
 | |
| 190 | +        self.__alias = alias
 | |
| 191 | + | |
| 192 | +    #############################################################
 | |
| 193 | +    #            Private Methods used in BuildStream            #
 | |
| 194 | +    #############################################################
 | |
| 195 | + | |
| 196 | +    # Returns the alias used by this fetcher
 | |
| 197 | +    def _get_alias(self):
 | |
| 198 | +        return self.__alias
 | |
| 199 | + | |
| 200 | + | |
| 117 | 201 |  class Source(Plugin):
 | 
| 118 | 202 |      """Source()
 | 
| 119 | 203 |  | 
| ... | ... | @@ -125,7 +209,7 @@ class Source(Plugin): | 
| 125 | 209 |      __defaults = {}          # The defaults from the project
 | 
| 126 | 210 |      __defaults_set = False   # Flag, in case there are not defaults at all
 | 
| 127 | 211 |  | 
| 128 | -    def __init__(self, context, project, meta):
 | |
| 212 | +    def __init__(self, context, project, meta, *, alias_override=None):
 | |
| 129 | 213 |          provenance = _yaml.node_get_provenance(meta.config)
 | 
| 130 | 214 |          super().__init__("{}-{}".format(meta.element_name, meta.element_index),
 | 
| 131 | 215 |                           context, project, provenance, "source")
 | 
| ... | ... | @@ -135,6 +219,11 @@ class Source(Plugin): | 
| 135 | 219 |          self.__element_kind = meta.element_kind         # The kind of the element owning this source
 | 
| 136 | 220 |          self.__directory = meta.directory               # Staging relative directory
 | 
| 137 | 221 |          self.__consistency = Consistency.INCONSISTENT   # Cached consistency state
 | 
| 222 | +        self.__alias_override = alias_override          # Tuple of alias and its override to use instead
 | |
| 223 | +        self.__expected_alias = None                    # A hacky way to store the first alias used
 | |
| 224 | + | |
| 225 | +        # FIXME: Reconstruct a MetaSource from a Source instead of storing it.
 | |
| 226 | +        self.__meta = meta                              # MetaSource stored so we can copy this source later.
 | |
| 138 | 227 |  | 
| 139 | 228 |          # Collect the composited element configuration and
 | 
| 140 | 229 |          # ask the element to configure itself.
 | 
| ... | ... | @@ -284,6 +373,36 @@ class Source(Plugin): | 
| 284 | 373 |          """
 | 
| 285 | 374 |          self.stage(directory)
 | 
| 286 | 375 |  | 
| 376 | +    def mark_download_url(self, url):
 | |
| 377 | +        """Identifies the URL that this Source uses to download
 | |
| 378 | + | |
| 379 | +        This must be called during :func:`~buildstream.plugin.Plugin.configure` if
 | |
| 380 | +        :func:`~buildstream.source.Source.translate_url` is not called.
 | |
| 381 | + | |
| 382 | +        Args:
 | |
| 383 | +           url (str): The url used to download
 | |
| 384 | + | |
| 385 | +        *Since: 1.4*
 | |
| 386 | +        """
 | |
| 387 | +        alias, _ = url.split(utils._ALIAS_SEPARATOR, 1)
 | |
| 388 | +        self.__expected_alias = alias
 | |
| 389 | + | |
| 390 | +    def get_source_fetchers(self):
 | |
| 391 | +        """Get the objects that are used for fetching
 | |
| 392 | + | |
| 393 | +        If this source doesn't download from multiple URLs,
 | |
| 394 | +        returning None and falling back on the default behaviour
 | |
| 395 | +        is recommended.
 | |
| 396 | + | |
| 397 | +        Returns:
 | |
| 398 | +           list: A list of SourceFetchers. If SourceFetchers are not supported,
 | |
| 399 | +                 this will be an empty list.
 | |
| 400 | + | |
| 401 | +        *Since: 1.4*
 | |
| 402 | +        """
 | |
| 403 | + | |
| 404 | +        return []
 | |
| 405 | + | |
| 287 | 406 |      #############################################################
 | 
| 288 | 407 |      #                       Public Methods                      #
 | 
| 289 | 408 |      #############################################################
 | 
| ... | ... | @@ -300,18 +419,42 @@ class Source(Plugin): | 
| 300 | 419 |          os.makedirs(directory, exist_ok=True)
 | 
| 301 | 420 |          return directory
 | 
| 302 | 421 |  | 
| 303 | -    def translate_url(self, url):
 | |
| 422 | +    def translate_url(self, url, *, alias_override=None):
 | |
| 304 | 423 |          """Translates the given url which may be specified with an alias
 | 
| 305 | 424 |          into a fully qualified url.
 | 
| 306 | 425 |  | 
| 307 | 426 |          Args:
 | 
| 308 | 427 |             url (str): A url, which may be using an alias
 | 
| 428 | +           alias_override (str): Optionally, an URI to override the alias with. (*Since: 1.4*)
 | |
| 309 | 429 |  | 
| 310 | 430 |          Returns:
 | 
| 311 | 431 |             str: The fully qualified url, with aliases resolved
 | 
| 312 | 432 |          """
 | 
| 313 | -        project = self._get_project()
 | |
| 314 | -        return project.translate_url(url)
 | |
| 433 | +        # Alias overriding can happen explicitly (by command-line) or
 | |
| 434 | +        # implicitly (the Source being constructed with an __alias_override).
 | |
| 435 | +        if alias_override or self.__alias_override:
 | |
| 436 | +            url_alias, url_body = url.split(utils._ALIAS_SEPARATOR, 1)
 | |
| 437 | +            if url_alias:
 | |
| 438 | +                if alias_override:
 | |
| 439 | +                    url = alias_override + url_body
 | |
| 440 | +                else:
 | |
| 441 | +                    # Implicit alias overrides may only be done for one
 | |
| 442 | +                    # specific alias, so that sources that fetch from multiple
 | |
| 443 | +                    # URLs and use different aliases default to only overriding
 | |
| 444 | +                    # one alias, rather than getting confused.
 | |
| 445 | +                    override_alias = self.__alias_override[0]
 | |
| 446 | +                    override_url = self.__alias_override[1]
 | |
| 447 | +                    if url_alias == override_alias:
 | |
| 448 | +                        url = override_url + url_body
 | |
| 449 | +            return url
 | |
| 450 | +        else:
 | |
| 451 | +            # Sneakily store the alias if it hasn't already been stored
 | |
| 452 | +            if not self.__expected_alias and url and utils._ALIAS_SEPARATOR in url:
 | |
| 453 | +                url_alias, _ = url.split(utils._ALIAS_SEPARATOR, 1)
 | |
| 454 | +                self.__expected_alias = url_alias
 | |
| 455 | + | |
| 456 | +            project = self._get_project()
 | |
| 457 | +            return project.translate_url(url)
 | |
| 315 | 458 |  | 
| 316 | 459 |      def get_project_directory(self):
 | 
| 317 | 460 |          """Fetch the project base directory
 | 
| ... | ... | @@ -375,7 +518,45 @@ class Source(Plugin): | 
| 375 | 518 |      # Wrapper function around plugin provided fetch method
 | 
| 376 | 519 |      #
 | 
| 377 | 520 |      def _fetch(self):
 | 
| 378 | -        self.fetch()
 | |
| 521 | +        project = self._get_project()
 | |
| 522 | +        source_fetchers = self.get_source_fetchers()
 | |
| 523 | +        if source_fetchers:
 | |
| 524 | +            for fetcher in source_fetchers:
 | |
| 525 | +                alias = fetcher._get_alias()
 | |
| 526 | +                success = False
 | |
| 527 | +                for uri in project.get_alias_uris(alias):
 | |
| 528 | +                    try:
 | |
| 529 | +                        fetcher.fetch(uri)
 | |
| 530 | +                    # FIXME: Need to consider temporary vs. permanent failures,
 | |
| 531 | +                    #        and how this works with retries.
 | |
| 532 | +                    except BstError as e:
 | |
| 533 | +                        last_error = e
 | |
| 534 | +                        continue
 | |
| 535 | +                    success = True
 | |
| 536 | +                    break
 | |
| 537 | +                if not success:
 | |
| 538 | +                    raise last_error
 | |
| 539 | +        else:
 | |
| 540 | +            alias = self._get_alias()
 | |
| 541 | +            if not project.mirrors or not alias:
 | |
| 542 | +                self.fetch()
 | |
| 543 | +                return
 | |
| 544 | + | |
| 545 | +            context = self._get_context()
 | |
| 546 | +            source_kind = type(self)
 | |
| 547 | +            for uri in project.get_alias_uris(alias):
 | |
| 548 | +                new_source = source_kind(context, project, self.__meta,
 | |
| 549 | +                                         alias_override=(alias, uri))
 | |
| 550 | +                new_source._preflight()
 | |
| 551 | +                try:
 | |
| 552 | +                    new_source.fetch()
 | |
| 553 | +                # FIXME: Need to consider temporary vs. permanent failures,
 | |
| 554 | +                #        and how this works with retries.
 | |
| 555 | +                except BstError as e:
 | |
| 556 | +                    last_error = e
 | |
| 557 | +                    continue
 | |
| 558 | +                return
 | |
| 559 | +            raise last_error
 | |
| 379 | 560 |  | 
| 380 | 561 |      # Wrapper for stage() api which gives the source
 | 
| 381 | 562 |      # plugin a fully constructed path considering the
 | 
| ... | ... | @@ -582,7 +763,7 @@ class Source(Plugin): | 
| 582 | 763 |      # Wrapper for track()
 | 
| 583 | 764 |      #
 | 
| 584 | 765 |      def _track(self):
 | 
| 585 | -        new_ref = self.track()
 | |
| 766 | +        new_ref = self.__do_track()
 | |
| 586 | 767 |          current_ref = self.get_ref()
 | 
| 587 | 768 |  | 
| 588 | 769 |          if new_ref is None:
 | 
| ... | ... | @@ -594,10 +775,48 @@ class Source(Plugin): | 
| 594 | 775 |  | 
| 595 | 776 |          return new_ref
 | 
| 596 | 777 |  | 
| 778 | +    # Returns the alias if it's defined in the project
 | |
| 779 | +    def _get_alias(self):
 | |
| 780 | +        alias = self.__expected_alias
 | |
| 781 | +        project = self._get_project()
 | |
| 782 | +        if project.get_alias_uri(alias):
 | |
| 783 | +            # The alias must already be defined in the project's aliases
 | |
| 784 | +            # otherwise http://foo gets treated like it contains an alias
 | |
| 785 | +            return alias
 | |
| 786 | +        else:
 | |
| 787 | +            return None
 | |
| 788 | + | |
| 597 | 789 |      #############################################################
 | 
| 598 | 790 |      #                   Local Private Methods                   #
 | 
| 599 | 791 |      #############################################################
 | 
| 600 | 792 |  | 
| 793 | +    # Tries to call track for every mirror, stopping once it succeeds
 | |
| 794 | +    def __do_track(self):
 | |
| 795 | +        project = self._get_project()
 | |
| 796 | +        # If there are no mirrors, or no aliases to replace, there's nothing to do here.
 | |
| 797 | +        alias = self._get_alias()
 | |
| 798 | +        if not project.mirrors or not alias:
 | |
| 799 | +            return self.track()
 | |
| 800 | + | |
| 801 | +        context = self._get_context()
 | |
| 802 | +        source_kind = type(self)
 | |
| 803 | + | |
| 804 | +        # NOTE: We are assuming here that tracking only requires substituting the
 | |
| 805 | +        #       first alias used
 | |
| 806 | +        for uri in reversed(project.get_alias_uris(alias)):
 | |
| 807 | +            new_source = source_kind(context, project, self.__meta,
 | |
| 808 | +                                     alias_override=(alias, uri))
 | |
| 809 | +            new_source._preflight()
 | |
| 810 | +            try:
 | |
| 811 | +                ref = new_source.track()
 | |
| 812 | +            # FIXME: Need to consider temporary vs. permanent failures,
 | |
| 813 | +            #        and how this works with retries.
 | |
| 814 | +            except BstError as e:
 | |
| 815 | +                last_error = e
 | |
| 816 | +                continue
 | |
| 817 | +            return ref
 | |
| 818 | +        raise last_error
 | |
| 819 | + | |
| 601 | 820 |      # Ensures a fully constructed path and returns it
 | 
| 602 | 821 |      def __ensure_directory(self, directory):
 | 
| 603 | 822 |  | 
| ... | ... | @@ -42,6 +42,10 @@ from . import _signals | 
| 42 | 42 |  from ._exceptions import BstError, ErrorDomain
 | 
| 43 | 43 |  | 
| 44 | 44 |  | 
| 45 | +# The separator we use for user specified aliases
 | |
| 46 | +_ALIAS_SEPARATOR = ':'
 | |
| 47 | + | |
| 48 | + | |
| 45 | 49 |  class UtilError(BstError):
 | 
| 46 | 50 |      """Raised by utility functions when system calls fail.
 | 
| 47 | 51 |  | 
| ... | ... | @@ -608,6 +612,27 @@ def _parse_size(size, volume): | 
| 608 | 612 |      return int(num) * 1024**units.index(unit)
 | 
| 609 | 613 |  | 
| 610 | 614 |  | 
| 615 | +# _pretty_size()
 | |
| 616 | +#
 | |
| 617 | +# Converts a number of bytes into a string representation in KB, MB, GB, TB
 | |
| 618 | +# represented as K, M, G, T etc.
 | |
| 619 | +#
 | |
| 620 | +# Args:
 | |
| 621 | +#   size (int): The size to convert in bytes.
 | |
| 622 | +#   dec_places (int): The number of decimal places to output to.
 | |
| 623 | +#
 | |
| 624 | +# Returns:
 | |
| 625 | +#   (str): The string representation of the number of bytes in the largest
 | |
| 626 | +def _pretty_size(size, dec_places=0):
 | |
| 627 | +    psize = size
 | |
| 628 | +    unit = 'B'
 | |
| 629 | +    for unit in ('B', 'K', 'M', 'G', 'T'):
 | |
| 630 | +        if psize < 1024:
 | |
| 631 | +            break
 | |
| 632 | +        else:
 | |
| 633 | +            psize /= 1024
 | |
| 634 | +    return "{size:g}{unit}".format(size=round(psize, dec_places), unit=unit)
 | |
| 635 | + | |
| 611 | 636 |  # A sentinel to be used as a default argument for functions that need
 | 
| 612 | 637 |  # to distinguish between a kwarg set to None and an unset kwarg.
 | 
| 613 | 638 |  _sentinel = object()
 | 
| 1 | + | |
| 2 | + | |
| 3 | +Creating and using a git mirror
 | |
| 4 | +'''''''''''''''''''''''''''''''
 | |
| 5 | +This is an example of how to create a git mirror using git's
 | |
| 6 | +`git-http-backend <https://git-scm.com/docs/git-http-backend>`_ and
 | |
| 7 | +`lighttpd <https://redmine.lighttpd.net/projects/1/wiki/TutorialConfiguration>`_.
 | |
| 8 | + | |
| 9 | + | |
| 10 | +Prerequisites
 | |
| 11 | +=============
 | |
| 12 | +You will need git installed, and git-http-backend must be present. It is assumed
 | |
| 13 | +that the git-http-backend binary exists at `/usr/lib/git-core/git-http-backend`.
 | |
| 14 | + | |
| 15 | +You will need `lighttpd` installed, and at the bare minimum has the modules
 | |
| 16 | +`mod_alias`, `mod_cgi`, and `mod_setenv`.
 | |
| 17 | + | |
| 18 | +I will be using gnome-modulesets as an example, which can be cloned from
 | |
| 19 | +`http://gnome7.codethink.co.uk/gnome-modulesets.git`.
 | |
| 20 | + | |
| 21 | + | |
| 22 | +Starting a git http server
 | |
| 23 | +==========================
 | |
| 24 | + | |
| 25 | + | |
| 26 | +1. Set up a directory containing mirrors
 | |
| 27 | +----------------------------------------
 | |
| 28 | +Choose a suitable directory to hold your mirrors, e.g. `/var/www/git`.
 | |
| 29 | + | |
| 30 | +Place the git repositories you want to use as mirrors in the mirror dir, e.g.
 | |
| 31 | +``git clone --mirror http://git.gnome.org/browse/yelp-xsl /var/www/git/yelp-xsl.git``.
 | |
| 32 | + | |
| 33 | + | |
| 34 | +2. Configure lighttpd
 | |
| 35 | +---------------------
 | |
| 36 | +Write out a lighttpd.conf as follows:
 | |
| 37 | + | |
| 38 | +::
 | |
| 39 | + | |
| 40 | +   server.document-root = "/var/www/git/" 
 | |
| 41 | +   server.port = 3000
 | |
| 42 | +   server.modules = (
 | |
| 43 | +        "mod_alias",
 | |
| 44 | +        "mod_cgi",
 | |
| 45 | +        "mod_setenv",
 | |
| 46 | +   )
 | |
| 47 | +   
 | |
| 48 | +   alias.url += ( "/git" => "/usr/lib/git-core/git-http-backend" )
 | |
| 49 | +   $HTTP["url"] =~ "^/git" {
 | |
| 50 | +        cgi.assign = ("" => "")
 | |
| 51 | +        setenv.add-environment = (
 | |
| 52 | +                "GIT_PROJECT_ROOT" => "/var/www/git",
 | |
| 53 | +                "GIT_HTTP_EXPORT_ALL" => ""
 | |
| 54 | +        )
 | |
| 55 | +   }
 | |
| 56 | + | |
| 57 | +.. note::
 | |
| 58 | + | |
| 59 | +   If you have your mirrors in another directory, replace /var/www/git/ with that directory.
 | |
| 60 | + | |
| 61 | + | |
| 62 | +3. Start lighttpd
 | |
| 63 | +-----------------
 | |
| 64 | +lighttpd can be invoked with the command-line ``lighttpd -D -f lighttpd.conf``.
 | |
| 65 | + | |
| 66 | + | |
| 67 | +4. Test that you can fetch from it
 | |
| 68 | +----------------------------------
 | |
| 69 | +We can then clone the mirrored repo using git via http with
 | |
| 70 | +``git clone http://127.0.0.1:3000/git/yelp-xsl``.
 | |
| 71 | + | |
| 72 | +.. note::
 | |
| 73 | + | |
| 74 | +   If you have set server.port to something other than the default, you will
 | |
| 75 | +   need to replace the '3000' in the command-line.
 | |
| 76 | + | |
| 77 | + | |
| 78 | +5. Configure the project to use the mirror
 | |
| 79 | +------------------------------------------
 | |
| 80 | +To add this local http server as a mirror, add the following to the project.conf:
 | |
| 81 | + | |
| 82 | +.. code:: yaml
 | |
| 83 | + | |
| 84 | +   mirrors:
 | |
| 85 | +   - name: local-mirror
 | |
| 86 | +     aliases:
 | |
| 87 | +       git_gnome_org:
 | |
| 88 | +       - http://127.0.0.1:3000/git/
 | |
| 89 | + | |
| 90 | + | |
| 91 | +6. Test that the mirror works
 | |
| 92 | +-----------------------------
 | |
| 93 | +We can make buildstream use the mirror by setting the alias to an invalid URL, e.g.
 | |
| 94 | + | |
| 95 | +.. code:: yaml
 | |
| 96 | + | |
| 97 | +   aliases:
 | |
| 98 | +     git_gnome_org: https://www.example.com/invalid/url/
 | |
| 99 | + | |
| 100 | +Now, if you build an element that uses the source you placed in the mirror
 | |
| 101 | +(e.g. ``bst build core-deps/yelp-xsl.bst``), you will see that it uses your mirror.
 | |
| 102 | + | |
| 103 | + | |
| 104 | +.. _lighttpd_git_tar_conf:
 | |
| 105 | + | |
| 106 | +Bonus: lighttpd conf for git and tar
 | |
| 107 | +====================================
 | |
| 108 | +For those who have also used the :ref:`tar-mirror tutorial <using_tar_mirror>`,
 | |
| 109 | +a combined lighttpd.conf is below:
 | |
| 110 | + | |
| 111 | +::
 | |
| 112 | + | |
| 113 | +   server.document-root = "/var/www/"
 | |
| 114 | +   server.port = 3000
 | |
| 115 | +   server.modules = (
 | |
| 116 | +           "mod_alias",
 | |
| 117 | +           "mod_cgi",
 | |
| 118 | +           "mod_setenv",
 | |
| 119 | +   )
 | |
| 120 | +   
 | |
| 121 | +   alias.url += ( "/git" => "/usr/lib/git-core/git-http-backend" )
 | |
| 122 | +   $HTTP["url"] =~ "^/git" {
 | |
| 123 | +           cgi.assign = ("" => "")
 | |
| 124 | +           setenv.add-environment = (
 | |
| 125 | +                   "GIT_PROJECT_ROOT" => "/var/www/git",
 | |
| 126 | +                   "GIT_HTTP_EXPORT_ALL" => ""
 | |
| 127 | +           )
 | |
| 128 | +   } else $HTTP["url"] =~ "^/tar" {
 | |
| 129 | +           dir-listing.activate = "enable"
 | |
| 130 | +   }
 | |
| 131 | + | |
| 132 | + | |
| 133 | +Further reading
 | |
| 134 | +===============
 | |
| 135 | +If this mirror isn't being used exclusively in a secure network, it is strongly
 | |
| 136 | +recommended you `use SSL <https://redmine.lighttpd.net/projects/1/wiki/HowToSimpleSSL>`_.
 | |
| 137 | + | |
| 138 | +This is the bare minimum required to set up a git mirror. A large, public project
 | |
| 139 | +would prefer to set it up using the
 | |
| 140 | +`git protocol <https://git-scm.com/book/en/v1/Git-on-the-Server-Git-Daemon>`_,
 | |
| 141 | +and a security-conscious project would be configured to use
 | |
| 142 | +`git over SSH <https://git-scm.com/book/en/v1/Git-on-the-Server-Getting-Git-on-a-Server#Small-Setups>`_.
 | |
| 143 | + | |
| 144 | +Lighttpd is documented on `its wiki <https://redmine.lighttpd.net/projects/lighttpd/wiki>`_. | 
| 1 | + | |
| 2 | + | |
| 3 | +.. _using_tar_mirror:
 | |
| 4 | + | |
| 5 | +Creating and using a tar mirror
 | |
| 6 | +'''''''''''''''''''''''''''''''
 | |
| 7 | +This is an example of how to create a tar mirror using 
 | |
| 8 | +`lighttpd <https://redmine.lighttpd.net/projects/1/wiki/TutorialConfiguration>`_.
 | |
| 9 | + | |
| 10 | + | |
| 11 | +Prerequisites
 | |
| 12 | +=============
 | |
| 13 | +You will need `lighttpd` installed.
 | |
| 14 | + | |
| 15 | + | |
| 16 | +I will be using gnome-modulesets as an example, which can be cloned from
 | |
| 17 | +`http://gnome7.codethink.co.uk/gnome-modulesets.git`.
 | |
| 18 | + | |
| 19 | + | |
| 20 | +Starting a tar server
 | |
| 21 | +=====================
 | |
| 22 | + | |
| 23 | + | |
| 24 | +1. Set up a directory containing mirrors
 | |
| 25 | +----------------------------------------
 | |
| 26 | +Choose a suitable directory to hold your mirrored tar files, e.g. `/var/www/tar`.
 | |
| 27 | + | |
| 28 | +Place the tar files you want to use as mirrors in your mirror dir, e.g.
 | |
| 29 | + | |
| 30 | +.. code::
 | |
| 31 | + | |
| 32 | +   mkdir -p /var/www/tar/gettext
 | |
| 33 | +   wget -O /var/www/tar/gettext/gettext-0.19.8.1.tar.xz https://ftp.gnu.org/gnu/gettext/gettext-0.19.8.1.tar.xz
 | |
| 34 | + | |
| 35 | + | |
| 36 | +2. Configure lighttpd
 | |
| 37 | +---------------------
 | |
| 38 | +Write out a lighttpd.conf as follows:
 | |
| 39 | + | |
| 40 | +::
 | |
| 41 | + | |
| 42 | +   server.document-root = "/var/www/tar/" 
 | |
| 43 | +   server.port = 3000
 | |
| 44 | +   
 | |
| 45 | +   dir-listing.activate = "enable"
 | |
| 46 | + | |
| 47 | +.. note::
 | |
| 48 | + | |
| 49 | +   If you have your mirrors in another directory, replace /var/www/tar/ with that directory.
 | |
| 50 | + | |
| 51 | +.. note::
 | |
| 52 | + | |
| 53 | +   An example lighttpd.conf that works for both git and tar services is available
 | |
| 54 | +   :ref:`here <lighttpd_git_tar_conf>`
 | |
| 55 | + | |
| 56 | + | |
| 57 | +3. Start lighttpd
 | |
| 58 | +-----------------
 | |
| 59 | +lighttpd can be invoked with the command-line ``lighttpd -D -f lighttpd.conf``.
 | |
| 60 | + | |
| 61 | + | |
| 62 | +4. Test that you can fetch from it
 | |
| 63 | +----------------------------------
 | |
| 64 | +We can then download the mirrored file with ``wget 127.0.0.1:3000/tar/gettext/gettext-0.19.8.1.tar.xz``.
 | |
| 65 | + | |
| 66 | +.. note::
 | |
| 67 | + | |
| 68 | +   If you have set server.port to something other than the default, you will need
 | |
| 69 | +   to replace the '3000' in the command-line.
 | |
| 70 | + | |
| 71 | + | |
| 72 | +5. Configure the project to use the mirror
 | |
| 73 | +------------------------------------------
 | |
| 74 | +To add this local http server as a mirror, add the following to the project.conf:
 | |
| 75 | + | |
| 76 | +.. code:: yaml
 | |
| 77 | + | |
| 78 | +   mirrors:
 | |
| 79 | +   - name: local-mirror
 | |
| 80 | +     aliases:
 | |
| 81 | +       ftp_gnu_org:
 | |
| 82 | +       - http://127.0.0.1:3000/tar/
 | |
| 83 | + | |
| 84 | + | |
| 85 | +6. Test that the mirror works
 | |
| 86 | +-----------------------------
 | |
| 87 | +We can make buildstream use the mirror by setting the alias to an invalid URL, e.g.
 | |
| 88 | + | |
| 89 | +.. code:: yaml
 | |
| 90 | + | |
| 91 | +   aliases:
 | |
| 92 | +     ftp_gnu_org: https://www.example.com/invalid/url/
 | |
| 93 | + | |
| 94 | +Now, if you build an element that uses the source you placed in the mirror
 | |
| 95 | +(e.g. ``bst build core-deps/gettext.bst``), you will see that it uses your mirror.
 | |
| 96 | + | |
| 97 | + | |
| 98 | +Further reading
 | |
| 99 | +===============
 | |
| 100 | +If this mirror isn't being used exclusively in a secure network, it is strongly
 | |
| 101 | +recommended you `use SSL <https://redmine.lighttpd.net/projects/1/wiki/HowToSimpleSSL>`_.
 | |
| 102 | + | |
| 103 | +Lighttpd is documented on `its wiki <https://redmine.lighttpd.net/projects/lighttpd/wiki>`_. | 
| ... | ... | @@ -198,6 +198,43 @@ You can also specify a list of caches here; earlier entries in the list | 
| 198 | 198 |  will have higher priority than later ones.
 | 
| 199 | 199 |  | 
| 200 | 200 |  | 
| 201 | +.. _project_essentials_mirrors:
 | |
| 202 | + | |
| 203 | +Mirrors
 | |
| 204 | +~~~~~~~
 | |
| 205 | +A list of mirrors can be defined that couple a location to a mapping of aliases to a
 | |
| 206 | +list of URIs, e.g.
 | |
| 207 | + | |
| 208 | +.. code:: yaml
 | |
| 209 | + | |
| 210 | +  mirrors:
 | |
| 211 | +  - name: middle-earth
 | |
| 212 | +    aliases:
 | |
| 213 | +      foo:
 | |
| 214 | +      - http://www.middle-earth.com/foo/1
 | |
| 215 | +      - http://www.middle-earth.com/foo/2
 | |
| 216 | +      bar:
 | |
| 217 | +      - http://www.middle-earth.com/bar/1
 | |
| 218 | +      - http://www.middle-earth.com/bar/2
 | |
| 219 | +  - name: oz
 | |
| 220 | +    aliases:
 | |
| 221 | +      foo:
 | |
| 222 | +      - http://www.oz.com/foo
 | |
| 223 | +      bar:
 | |
| 224 | +      - http://www.oz.com/bar
 | |
| 225 | + | |
| 226 | +The order that the mirrors (and the URIs therein) are consulted is in the order
 | |
| 227 | +they are defined when fetching, and in reverse-order when tracking.
 | |
| 228 | + | |
| 229 | +A default mirror to consult first can be defined via
 | |
| 230 | +:ref:`user config <config_default_mirror>`, or the command-line argument
 | |
| 231 | +:ref:`--default-mirror <invoking_bst>`.
 | |
| 232 | + | |
| 233 | +.. note::
 | |
| 234 | + | |
| 235 | +   The ``mirrors`` field is available since :ref:`format version 11 <project_format_version>`
 | |
| 236 | + | |
| 237 | + | |
| 201 | 238 |  .. _project_plugins:
 | 
| 202 | 239 |  | 
| 203 | 240 |  External plugins
 | 
| 1 | 1 |  Install
 | 
| 2 | 2 |  =======
 | 
| 3 | -This section covers how to install BuildStream onto your machine, how to run BuildStream inside a docker image and also how to configure an artifact server.
 | |
| 3 | +This section covers how to install BuildStream onto your machine, how to run
 | |
| 4 | +BuildStream inside a docker image and also how to configure an artifact server.
 | |
| 4 | 5 |  | 
| 6 | +.. note::
 | |
| 7 | + | |
| 8 | +   BuildStream is not currently supported natively on macOS and Windows. Windows
 | |
| 9 | +   and macOS users should refer to :ref:`docker`.
 | |
| 5 | 10 |  | 
| 6 | 11 |  .. toctree::
 | 
| 7 | 12 |     :maxdepth: 2
 | 
| ... | ... | @@ -89,6 +89,27 @@ modifying some low level component. | 
| 89 | 89 |     the ``--strict`` and ``--no-strict`` command line options.
 | 
| 90 | 90 |  | 
| 91 | 91 |  | 
| 92 | +.. _config_default_mirror:
 | |
| 93 | + | |
| 94 | +Default Mirror
 | |
| 95 | +~~~~~~~~~~~~~~
 | |
| 96 | +When using :ref:`mirrors <project_essentials_mirrors>`, a default mirror can
 | |
| 97 | +be defined to be fetched first.
 | |
| 98 | +The default mirror is defined by its name, e.g.
 | |
| 99 | + | |
| 100 | +.. code:: yaml
 | |
| 101 | + | |
| 102 | +  projects:
 | |
| 103 | +    project-name:
 | |
| 104 | +      default-mirror: oz
 | |
| 105 | + | |
| 106 | + | |
| 107 | +.. note::
 | |
| 108 | + | |
| 109 | +   It is possible to override this at invocation time using the
 | |
| 110 | +   ``--default-mirror`` command-line option.
 | |
| 111 | + | |
| 112 | + | |
| 92 | 113 |  Default configuration
 | 
| 93 | 114 |  ---------------------
 | 
| 94 | 115 |  The default BuildStream configuration is specified here for reference:
 | 
| ... | ... | @@ -10,3 +10,5 @@ maintained and work as expected. | 
| 10 | 10 |     :maxdepth: 1
 | 
| 11 | 11 |  | 
| 12 | 12 |     examples/flatpak-autotools
 | 
| 13 | +   examples/tar-mirror
 | |
| 14 | +   examples/git-mirror | 
| ... | ... | @@ -27,6 +27,7 @@ MAIN_OPTIONS = [ | 
| 27 | 27 |      "--colors ",
 | 
| 28 | 28 |      "--config ",
 | 
| 29 | 29 |      "--debug ",
 | 
| 30 | +    "--default-mirror ",
 | |
| 30 | 31 |      "--directory ",
 | 
| 31 | 32 |      "--error-lines ",
 | 
| 32 | 33 |      "--fetchers ",
 | 
| 1 | +import os
 | |
| 2 | +import pytest
 | |
| 3 | + | |
| 4 | +from tests.testutils import cli, create_repo, ALL_REPO_KINDS
 | |
| 5 | + | |
| 6 | +from buildstream import _yaml
 | |
| 7 | + | |
| 8 | + | |
| 9 | +# Project directory
 | |
| 10 | +TOP_DIR = os.path.dirname(os.path.realpath(__file__))
 | |
| 11 | +DATA_DIR = os.path.join(TOP_DIR, 'project')
 | |
| 12 | + | |
| 13 | + | |
| 14 | +def generate_element(output_file):
 | |
| 15 | +    element = {
 | |
| 16 | +        'kind': 'import',
 | |
| 17 | +        'sources': [
 | |
| 18 | +            {
 | |
| 19 | +                'kind': 'fetch_source',
 | |
| 20 | +                "output-text": output_file,
 | |
| 21 | +                "urls": ["foo:repo1", "bar:repo2"],
 | |
| 22 | +                "fetch-succeeds": {
 | |
| 23 | +                    "FOO/repo1": True,
 | |
| 24 | +                    "BAR/repo2": False,
 | |
| 25 | +                    "OOF/repo1": False,
 | |
| 26 | +                    "RAB/repo2": True,
 | |
| 27 | +                    "OFO/repo1": False,
 | |
| 28 | +                    "RBA/repo2": False,
 | |
| 29 | +                    "ooF/repo1": False,
 | |
| 30 | +                    "raB/repo2": False,
 | |
| 31 | +                }
 | |
| 32 | +            }
 | |
| 33 | +        ]
 | |
| 34 | +    }
 | |
| 35 | +    return element
 | |
| 36 | + | |
| 37 | + | |
| 38 | +def generate_project():
 | |
| 39 | +    project = {
 | |
| 40 | +        'name': 'test',
 | |
| 41 | +        'element-path': 'elements',
 | |
| 42 | +        'aliases': {
 | |
| 43 | +            'foo': 'FOO/',
 | |
| 44 | +            'bar': 'BAR/',
 | |
| 45 | +        },
 | |
| 46 | +        'mirrors': [
 | |
| 47 | +            {
 | |
| 48 | +                'name': 'middle-earth',
 | |
| 49 | +                'aliases': {
 | |
| 50 | +                    'foo': ['OOF/'],
 | |
| 51 | +                    'bar': ['RAB/'],
 | |
| 52 | +                },
 | |
| 53 | +            },
 | |
| 54 | +            {
 | |
| 55 | +                'name': 'arrakis',
 | |
| 56 | +                'aliases': {
 | |
| 57 | +                    'foo': ['OFO/'],
 | |
| 58 | +                    'bar': ['RBA/'],
 | |
| 59 | +                },
 | |
| 60 | +            },
 | |
| 61 | +            {
 | |
| 62 | +                'name': 'oz',
 | |
| 63 | +                'aliases': {
 | |
| 64 | +                    'foo': ['ooF/'],
 | |
| 65 | +                    'bar': ['raB/'],
 | |
| 66 | +                }
 | |
| 67 | +            },
 | |
| 68 | +        ],
 | |
| 69 | +        'plugins': [
 | |
| 70 | +            {
 | |
| 71 | +                'origin': 'local',
 | |
| 72 | +                'path': 'sources',
 | |
| 73 | +                'sources': {
 | |
| 74 | +                    'fetch_source': 0
 | |
| 75 | +                }
 | |
| 76 | +            }
 | |
| 77 | +        ]
 | |
| 78 | +    }
 | |
| 79 | +    return project
 | |
| 80 | + | |
| 81 | + | |
| 82 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 83 | +@pytest.mark.parametrize("kind", [(kind) for kind in ALL_REPO_KINDS])
 | |
| 84 | +def test_mirror_fetch(cli, tmpdir, datafiles, kind):
 | |
| 85 | +    bin_files_path = os.path.join(str(datafiles), 'files', 'bin-files', 'usr')
 | |
| 86 | +    dev_files_path = os.path.join(str(datafiles), 'files', 'dev-files', 'usr')
 | |
| 87 | +    upstream_repodir = os.path.join(str(tmpdir), 'upstream')
 | |
| 88 | +    mirror_repodir = os.path.join(str(tmpdir), 'mirror')
 | |
| 89 | +    project_dir = os.path.join(str(tmpdir), 'project')
 | |
| 90 | +    os.makedirs(project_dir)
 | |
| 91 | +    element_dir = os.path.join(project_dir, 'elements')
 | |
| 92 | + | |
| 93 | +    # Create repo objects of the upstream and mirror
 | |
| 94 | +    upstream_repo = create_repo(kind, upstream_repodir)
 | |
| 95 | +    upstream_ref = upstream_repo.create(bin_files_path)
 | |
| 96 | +    mirror_repo = upstream_repo.copy(mirror_repodir)
 | |
| 97 | +    mirror_ref = upstream_ref
 | |
| 98 | +    upstream_ref = upstream_repo.create(dev_files_path)
 | |
| 99 | + | |
| 100 | +    element = {
 | |
| 101 | +        'kind': 'import',
 | |
| 102 | +        'sources': [
 | |
| 103 | +            upstream_repo.source_config(ref=upstream_ref)
 | |
| 104 | +        ]
 | |
| 105 | +    }
 | |
| 106 | +    element_name = 'test.bst'
 | |
| 107 | +    element_path = os.path.join(element_dir, element_name)
 | |
| 108 | +    full_repo = element['sources'][0]['url']
 | |
| 109 | +    upstream_map, repo_name = os.path.split(full_repo)
 | |
| 110 | +    alias = 'foo-' + kind
 | |
| 111 | +    aliased_repo = alias + ':' + repo_name
 | |
| 112 | +    element['sources'][0]['url'] = aliased_repo
 | |
| 113 | +    full_mirror = mirror_repo.source_config()['url']
 | |
| 114 | +    mirror_map, _ = os.path.split(full_mirror)
 | |
| 115 | +    os.makedirs(element_dir)
 | |
| 116 | +    _yaml.dump(element, element_path)
 | |
| 117 | + | |
| 118 | +    project = {
 | |
| 119 | +        'name': 'test',
 | |
| 120 | +        'element-path': 'elements',
 | |
| 121 | +        'aliases': {
 | |
| 122 | +            alias: upstream_map + "/"
 | |
| 123 | +        },
 | |
| 124 | +        'mirrors': [
 | |
| 125 | +            {
 | |
| 126 | +                'name': 'middle-earth',
 | |
| 127 | +                'aliases': {
 | |
| 128 | +                    alias: [mirror_map + "/"],
 | |
| 129 | +                },
 | |
| 130 | +            },
 | |
| 131 | +        ]
 | |
| 132 | +    }
 | |
| 133 | +    project_file = os.path.join(project_dir, 'project.conf')
 | |
| 134 | +    _yaml.dump(project, project_file)
 | |
| 135 | + | |
| 136 | +    # No obvious ways of checking that the mirror has been fetched
 | |
| 137 | +    # But at least we can be sure it succeeds
 | |
| 138 | +    result = cli.run(project=project_dir, args=['fetch', element_name])
 | |
| 139 | +    result.assert_success()
 | |
| 140 | + | |
| 141 | + | |
| 142 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 143 | +def test_mirror_fetch_multi(cli, tmpdir, datafiles):
 | |
| 144 | +    output_file = os.path.join(str(tmpdir), "output.txt")
 | |
| 145 | +    project_dir = str(tmpdir)
 | |
| 146 | +    element_dir = os.path.join(project_dir, 'elements')
 | |
| 147 | +    os.makedirs(element_dir, exist_ok=True)
 | |
| 148 | +    element_name = "test.bst"
 | |
| 149 | +    element_path = os.path.join(element_dir, element_name)
 | |
| 150 | +    element = generate_element(output_file)
 | |
| 151 | +    _yaml.dump(element, element_path)
 | |
| 152 | + | |
| 153 | +    project_file = os.path.join(project_dir, 'project.conf')
 | |
| 154 | +    project = generate_project()
 | |
| 155 | +    _yaml.dump(project, project_file)
 | |
| 156 | + | |
| 157 | +    result = cli.run(project=project_dir, args=['fetch', element_name])
 | |
| 158 | +    result.assert_success()
 | |
| 159 | +    with open(output_file) as f:
 | |
| 160 | +        contents = f.read()
 | |
| 161 | +        assert "Fetch foo:repo1 succeeded from FOO/repo1" in contents
 | |
| 162 | +        assert "Fetch bar:repo2 succeeded from RAB/repo2" in contents
 | |
| 163 | + | |
| 164 | + | |
| 165 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 166 | +def test_mirror_fetch_default_cmdline(cli, tmpdir, datafiles):
 | |
| 167 | +    output_file = os.path.join(str(tmpdir), "output.txt")
 | |
| 168 | +    project_dir = str(tmpdir)
 | |
| 169 | +    element_dir = os.path.join(project_dir, 'elements')
 | |
| 170 | +    os.makedirs(element_dir, exist_ok=True)
 | |
| 171 | +    element_name = "test.bst"
 | |
| 172 | +    element_path = os.path.join(element_dir, element_name)
 | |
| 173 | +    element = generate_element(output_file)
 | |
| 174 | +    _yaml.dump(element, element_path)
 | |
| 175 | + | |
| 176 | +    project_file = os.path.join(project_dir, 'project.conf')
 | |
| 177 | +    project = generate_project()
 | |
| 178 | +    _yaml.dump(project, project_file)
 | |
| 179 | + | |
| 180 | +    result = cli.run(project=project_dir, args=['--default-mirror', 'arrakis', 'fetch', element_name])
 | |
| 181 | +    result.assert_success()
 | |
| 182 | +    with open(output_file) as f:
 | |
| 183 | +        contents = f.read()
 | |
| 184 | +        print(contents)
 | |
| 185 | +        # Success if fetching from arrakis' mirror happened before middle-earth's
 | |
| 186 | +        arrakis_str = "OFO/repo1"
 | |
| 187 | +        arrakis_pos = contents.find(arrakis_str)
 | |
| 188 | +        assert arrakis_pos != -1, "'{}' wasn't found".format(arrakis_str)
 | |
| 189 | +        me_str = "OOF/repo1"
 | |
| 190 | +        me_pos = contents.find(me_str)
 | |
| 191 | +        assert me_pos != -1, "'{}' wasn't found".format(me_str)
 | |
| 192 | +        assert arrakis_pos < me_pos, "'{}' wasn't found before '{}'".format(arrakis_str, me_str)
 | |
| 193 | + | |
| 194 | + | |
| 195 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 196 | +def test_mirror_fetch_default_userconfig(cli, tmpdir, datafiles):
 | |
| 197 | +    output_file = os.path.join(str(tmpdir), "output.txt")
 | |
| 198 | +    project_dir = str(tmpdir)
 | |
| 199 | +    element_dir = os.path.join(project_dir, 'elements')
 | |
| 200 | +    os.makedirs(element_dir, exist_ok=True)
 | |
| 201 | +    element_name = "test.bst"
 | |
| 202 | +    element_path = os.path.join(element_dir, element_name)
 | |
| 203 | +    element = generate_element(output_file)
 | |
| 204 | +    _yaml.dump(element, element_path)
 | |
| 205 | + | |
| 206 | +    project_file = os.path.join(project_dir, 'project.conf')
 | |
| 207 | +    project = generate_project()
 | |
| 208 | +    _yaml.dump(project, project_file)
 | |
| 209 | + | |
| 210 | +    userconfig = {
 | |
| 211 | +        'projects': {
 | |
| 212 | +            'test': {
 | |
| 213 | +                'default-mirror': 'oz'
 | |
| 214 | +            }
 | |
| 215 | +        }
 | |
| 216 | +    }
 | |
| 217 | +    cli.configure(userconfig)
 | |
| 218 | + | |
| 219 | +    result = cli.run(project=project_dir, args=['fetch', element_name])
 | |
| 220 | +    result.assert_success()
 | |
| 221 | +    with open(output_file) as f:
 | |
| 222 | +        contents = f.read()
 | |
| 223 | +        print(contents)
 | |
| 224 | +        # Success if fetching from Oz' mirror happened before middle-earth's
 | |
| 225 | +        oz_str = "ooF/repo1"
 | |
| 226 | +        oz_pos = contents.find(oz_str)
 | |
| 227 | +        assert oz_pos != -1, "'{}' wasn't found".format(oz_str)
 | |
| 228 | +        me_str = "OOF/repo1"
 | |
| 229 | +        me_pos = contents.find(me_str)
 | |
| 230 | +        assert me_pos != -1, "'{}' wasn't found".format(me_str)
 | |
| 231 | +        assert oz_pos < me_pos, "'{}' wasn't found before '{}'".format(oz_str, me_str)
 | |
| 232 | + | |
| 233 | + | |
| 234 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 235 | +def test_mirror_fetch_default_cmdline_overrides_config(cli, tmpdir, datafiles):
 | |
| 236 | +    output_file = os.path.join(str(tmpdir), "output.txt")
 | |
| 237 | +    project_dir = str(tmpdir)
 | |
| 238 | +    element_dir = os.path.join(project_dir, 'elements')
 | |
| 239 | +    os.makedirs(element_dir, exist_ok=True)
 | |
| 240 | +    element_name = "test.bst"
 | |
| 241 | +    element_path = os.path.join(element_dir, element_name)
 | |
| 242 | +    element = generate_element(output_file)
 | |
| 243 | +    _yaml.dump(element, element_path)
 | |
| 244 | + | |
| 245 | +    project_file = os.path.join(project_dir, 'project.conf')
 | |
| 246 | +    project = generate_project()
 | |
| 247 | +    _yaml.dump(project, project_file)
 | |
| 248 | + | |
| 249 | +    userconfig = {
 | |
| 250 | +        'projects': {
 | |
| 251 | +            'test': {
 | |
| 252 | +                'default-mirror': 'oz'
 | |
| 253 | +            }
 | |
| 254 | +        }
 | |
| 255 | +    }
 | |
| 256 | +    cli.configure(userconfig)
 | |
| 257 | + | |
| 258 | +    result = cli.run(project=project_dir, args=['--default-mirror', 'arrakis', 'fetch', element_name])
 | |
| 259 | +    result.assert_success()
 | |
| 260 | +    with open(output_file) as f:
 | |
| 261 | +        contents = f.read()
 | |
| 262 | +        print(contents)
 | |
| 263 | +        # Success if fetching from arrakis' mirror happened before middle-earth's
 | |
| 264 | +        arrakis_str = "OFO/repo1"
 | |
| 265 | +        arrakis_pos = contents.find(arrakis_str)
 | |
| 266 | +        assert arrakis_pos != -1, "'{}' wasn't found".format(arrakis_str)
 | |
| 267 | +        me_str = "OOF/repo1"
 | |
| 268 | +        me_pos = contents.find(me_str)
 | |
| 269 | +        assert me_pos != -1, "'{}' wasn't found".format(me_str)
 | |
| 270 | +        assert arrakis_pos < me_pos, "'{}' wasn't found before '{}'".format(arrakis_str, me_str)
 | |
| 271 | + | |
| 272 | + | |
| 273 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 274 | +@pytest.mark.parametrize("kind", [(kind) for kind in ALL_REPO_KINDS])
 | |
| 275 | +def test_mirror_track_upstream_present(cli, tmpdir, datafiles, kind):
 | |
| 276 | +    bin_files_path = os.path.join(str(datafiles), 'files', 'bin-files', 'usr')
 | |
| 277 | +    dev_files_path = os.path.join(str(datafiles), 'files', 'dev-files', 'usr')
 | |
| 278 | +    upstream_repodir = os.path.join(str(tmpdir), 'upstream')
 | |
| 279 | +    mirror_repodir = os.path.join(str(tmpdir), 'mirror')
 | |
| 280 | +    project_dir = os.path.join(str(tmpdir), 'project')
 | |
| 281 | +    os.makedirs(project_dir)
 | |
| 282 | +    element_dir = os.path.join(project_dir, 'elements')
 | |
| 283 | + | |
| 284 | +    # Create repo objects of the upstream and mirror
 | |
| 285 | +    upstream_repo = create_repo(kind, upstream_repodir)
 | |
| 286 | +    upstream_ref = upstream_repo.create(bin_files_path)
 | |
| 287 | +    mirror_repo = upstream_repo.copy(mirror_repodir)
 | |
| 288 | +    mirror_ref = upstream_ref
 | |
| 289 | +    upstream_ref = upstream_repo.create(dev_files_path)
 | |
| 290 | + | |
| 291 | +    element = {
 | |
| 292 | +        'kind': 'import',
 | |
| 293 | +        'sources': [
 | |
| 294 | +            upstream_repo.source_config(ref=upstream_ref)
 | |
| 295 | +        ]
 | |
| 296 | +    }
 | |
| 297 | + | |
| 298 | +    element['sources'][0]
 | |
| 299 | +    element_name = 'test.bst'
 | |
| 300 | +    element_path = os.path.join(element_dir, element_name)
 | |
| 301 | +    full_repo = element['sources'][0]['url']
 | |
| 302 | +    upstream_map, repo_name = os.path.split(full_repo)
 | |
| 303 | +    alias = 'foo-' + kind
 | |
| 304 | +    aliased_repo = alias + ':' + repo_name
 | |
| 305 | +    element['sources'][0]['url'] = aliased_repo
 | |
| 306 | +    full_mirror = mirror_repo.source_config()['url']
 | |
| 307 | +    mirror_map, _ = os.path.split(full_mirror)
 | |
| 308 | +    os.makedirs(element_dir)
 | |
| 309 | +    _yaml.dump(element, element_path)
 | |
| 310 | + | |
| 311 | +    project = {
 | |
| 312 | +        'name': 'test',
 | |
| 313 | +        'element-path': 'elements',
 | |
| 314 | +        'aliases': {
 | |
| 315 | +            alias: upstream_map + "/"
 | |
| 316 | +        },
 | |
| 317 | +        'mirrors': [
 | |
| 318 | +            {
 | |
| 319 | +                'name': 'middle-earth',
 | |
| 320 | +                'aliases': {
 | |
| 321 | +                    alias: [mirror_map + "/"],
 | |
| 322 | +                },
 | |
| 323 | +            },
 | |
| 324 | +        ]
 | |
| 325 | +    }
 | |
| 326 | +    project_file = os.path.join(project_dir, 'project.conf')
 | |
| 327 | +    _yaml.dump(project, project_file)
 | |
| 328 | + | |
| 329 | +    result = cli.run(project=project_dir, args=['track', element_name])
 | |
| 330 | +    result.assert_success()
 | |
| 331 | + | |
| 332 | +    # Tracking tries upstream first. Check the ref is from upstream.
 | |
| 333 | +    new_element = _yaml.load(element_path)
 | |
| 334 | +    source = new_element['sources'][0]
 | |
| 335 | +    if 'ref' in source:
 | |
| 336 | +        assert source['ref'] == upstream_ref
 | |
| 337 | + | |
| 338 | + | |
| 339 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 340 | +@pytest.mark.parametrize("kind", [(kind) for kind in ALL_REPO_KINDS])
 | |
| 341 | +def test_mirror_track_upstream_absent(cli, tmpdir, datafiles, kind):
 | |
| 342 | +    bin_files_path = os.path.join(str(datafiles), 'files', 'bin-files', 'usr')
 | |
| 343 | +    dev_files_path = os.path.join(str(datafiles), 'files', 'dev-files', 'usr')
 | |
| 344 | +    upstream_repodir = os.path.join(str(tmpdir), 'upstream')
 | |
| 345 | +    mirror_repodir = os.path.join(str(tmpdir), 'mirror')
 | |
| 346 | +    project_dir = os.path.join(str(tmpdir), 'project')
 | |
| 347 | +    os.makedirs(project_dir)
 | |
| 348 | +    element_dir = os.path.join(project_dir, 'elements')
 | |
| 349 | + | |
| 350 | +    # Create repo objects of the upstream and mirror
 | |
| 351 | +    upstream_repo = create_repo(kind, upstream_repodir)
 | |
| 352 | +    upstream_ref = upstream_repo.create(bin_files_path)
 | |
| 353 | +    mirror_repo = upstream_repo.copy(mirror_repodir)
 | |
| 354 | +    mirror_ref = upstream_ref
 | |
| 355 | +    upstream_ref = upstream_repo.create(dev_files_path)
 | |
| 356 | + | |
| 357 | +    element = {
 | |
| 358 | +        'kind': 'import',
 | |
| 359 | +        'sources': [
 | |
| 360 | +            upstream_repo.source_config(ref=upstream_ref)
 | |
| 361 | +        ]
 | |
| 362 | +    }
 | |
| 363 | + | |
| 364 | +    element['sources'][0]
 | |
| 365 | +    element_name = 'test.bst'
 | |
| 366 | +    element_path = os.path.join(element_dir, element_name)
 | |
| 367 | +    full_repo = element['sources'][0]['url']
 | |
| 368 | +    upstream_map, repo_name = os.path.split(full_repo)
 | |
| 369 | +    alias = 'foo-' + kind
 | |
| 370 | +    aliased_repo = alias + ':' + repo_name
 | |
| 371 | +    element['sources'][0]['url'] = aliased_repo
 | |
| 372 | +    full_mirror = mirror_repo.source_config()['url']
 | |
| 373 | +    mirror_map, _ = os.path.split(full_mirror)
 | |
| 374 | +    os.makedirs(element_dir)
 | |
| 375 | +    _yaml.dump(element, element_path)
 | |
| 376 | + | |
| 377 | +    project = {
 | |
| 378 | +        'name': 'test',
 | |
| 379 | +        'element-path': 'elements',
 | |
| 380 | +        'aliases': {
 | |
| 381 | +            alias: 'http://www.example.com/'
 | |
| 382 | +        },
 | |
| 383 | +        'mirrors': [
 | |
| 384 | +            {
 | |
| 385 | +                'name': 'middle-earth',
 | |
| 386 | +                'aliases': {
 | |
| 387 | +                    alias: [mirror_map + "/"],
 | |
| 388 | +                },
 | |
| 389 | +            },
 | |
| 390 | +        ]
 | |
| 391 | +    }
 | |
| 392 | +    project_file = os.path.join(project_dir, 'project.conf')
 | |
| 393 | +    _yaml.dump(project, project_file)
 | |
| 394 | + | |
| 395 | +    result = cli.run(project=project_dir, args=['track', element_name])
 | |
| 396 | +    result.assert_success()
 | |
| 397 | + | |
| 398 | +    # Check that tracking fell back to the mirror
 | |
| 399 | +    new_element = _yaml.load(element_path)
 | |
| 400 | +    source = new_element['sources'][0]
 | |
| 401 | +    if 'ref' in source:
 | |
| 402 | +        assert source['ref'] == mirror_ref | 
| 1 | +import os
 | |
| 2 | +import sys
 | |
| 3 | + | |
| 4 | +from buildstream import Source, Consistency, SourceError, SourceFetcher
 | |
| 5 | + | |
| 6 | +# Expected config
 | |
| 7 | +# sources:
 | |
| 8 | +# - output-text: $FILE
 | |
| 9 | +#   urls:
 | |
| 10 | +#   - foo:bar
 | |
| 11 | +#   - baz:quux
 | |
| 12 | +#   fetch-succeeds:
 | |
| 13 | +#     Foo/bar: true
 | |
| 14 | +#     ooF/bar: false
 | |
| 15 | + | |
| 16 | + | |
| 17 | +class FetchFetcher(SourceFetcher):
 | |
| 18 | +    def __init__(self, source, url):
 | |
| 19 | +        super().__init__()
 | |
| 20 | +        self.source = source
 | |
| 21 | +        self.original_url = url
 | |
| 22 | +        self.mark_download_url(url)
 | |
| 23 | + | |
| 24 | +    def fetch(self, alias_override=None):
 | |
| 25 | +        url = self.source.translate_url(self.original_url, alias_override=alias_override)
 | |
| 26 | +        with open(self.source.output_file, "a") as f:
 | |
| 27 | +            success = url in self.source.fetch_succeeds and self.source.fetch_succeeds[url]
 | |
| 28 | +            message = "Fetch {} {} from {}\n".format(self.original_url,
 | |
| 29 | +                                                     "succeeded" if success else "failed",
 | |
| 30 | +                                                     url)
 | |
| 31 | +            f.write(message)
 | |
| 32 | +            if not success:
 | |
| 33 | +                raise SourceError("Failed to fetch {}".format(url))
 | |
| 34 | + | |
| 35 | + | |
| 36 | +class FetchSource(Source):
 | |
| 37 | +    # Read config to know which URLs to fetch
 | |
| 38 | +    def configure(self, node):
 | |
| 39 | +        self.original_urls = self.node_get_member(node, list, 'urls')
 | |
| 40 | +        self.fetchers = [FetchFetcher(self, url) for url in self.original_urls]
 | |
| 41 | +        self.output_file = self.node_get_member(node, str, 'output-text')
 | |
| 42 | +        self.fetch_succeeds = {}
 | |
| 43 | +        if 'fetch-succeeds' in node:
 | |
| 44 | +            self.fetch_succeeds = {x[0]: x[1] for x in self.node_items(node['fetch-succeeds'])}
 | |
| 45 | + | |
| 46 | +    def get_source_fetchers(self):
 | |
| 47 | +        return self.fetchers
 | |
| 48 | + | |
| 49 | +    def preflight(self):
 | |
| 50 | +        output_dir = os.path.dirname(self.output_file)
 | |
| 51 | +        if not os.path.exists(output_dir):
 | |
| 52 | +            raise SourceError("Directory '{}' does not exist".format(output_dir))
 | |
| 53 | + | |
| 54 | +    def fetch(self):
 | |
| 55 | +        for fetcher in self.fetchers:
 | |
| 56 | +            fetcher.fetch()
 | |
| 57 | + | |
| 58 | +    def get_unique_key(self):
 | |
| 59 | +        return {"urls": self.original_urls, "output_file": self.output_file}
 | |
| 60 | + | |
| 61 | +    def get_consistency(self):
 | |
| 62 | +        if not os.path.exists(self.output_file):
 | |
| 63 | +            return Consistency.RESOLVED
 | |
| 64 | + | |
| 65 | +        with open(self.output_file, "r") as f:
 | |
| 66 | +            contents = f.read()
 | |
| 67 | +            for url in self.original_urls:
 | |
| 68 | +                if url not in contents:
 | |
| 69 | +                    return Consistency.RESOLVED
 | |
| 70 | + | |
| 71 | +        return Consistency.CACHED
 | |
| 72 | + | |
| 73 | +    # We dont have a ref, we're a local file...
 | |
| 74 | +    def load_ref(self, node):
 | |
| 75 | +        pass
 | |
| 76 | + | |
| 77 | +    def get_ref(self):
 | |
| 78 | +        return None  # pragma: nocover
 | |
| 79 | + | |
| 80 | +    def set_ref(self, ref, node):
 | |
| 81 | +        pass  # pragma: nocover
 | |
| 82 | + | |
| 83 | + | |
| 84 | +def setup():
 | |
| 85 | +    return FetchSource | 
| ... | ... | @@ -22,7 +22,7 @@ class Repo(): | 
| 22 | 22 |          # The directory the actual repo will be stored in
 | 
| 23 | 23 |          self.repo = os.path.join(self.directory, subdir)
 | 
| 24 | 24 |  | 
| 25 | -        os.makedirs(self.repo)
 | |
| 25 | +        os.makedirs(self.repo, exist_ok=True)
 | |
| 26 | 26 |  | 
| 27 | 27 |      # create():
 | 
| 28 | 28 |      #
 | 
| ... | ... | @@ -69,3 +69,22 @@ class Repo(): | 
| 69 | 69 |                  shutil.copytree(src_path, dest_path)
 | 
| 70 | 70 |              else:
 | 
| 71 | 71 |                  shutil.copy2(src_path, dest_path)
 | 
| 72 | + | |
| 73 | +    # copy():
 | |
| 74 | +    #
 | |
| 75 | +    # Creates a copy of this repository in the specified
 | |
| 76 | +    # destination.
 | |
| 77 | +    #
 | |
| 78 | +    # Args:
 | |
| 79 | +    #    dest (str): The destination directory
 | |
| 80 | +    #
 | |
| 81 | +    # Returns:
 | |
| 82 | +    #    (Repo): A Repo object for the new repository.
 | |
| 83 | +    def copy(self, dest):
 | |
| 84 | +        subdir = self.repo[len(self.directory):].lstrip(os.sep)
 | |
| 85 | +        new_dir = os.path.join(dest, subdir)
 | |
| 86 | +        os.makedirs(new_dir, exist_ok=True)
 | |
| 87 | +        self.copy_directory(self.repo, new_dir)
 | |
| 88 | +        repo_type = type(self)
 | |
| 89 | +        new_repo = repo_type(dest, subdir)
 | |
| 90 | +        return new_repo | 
