Tom Pollard pushed to branch tpollard/774 at BuildStream / buildstream
Commits:
- 
406f546f
by Tom Pollard at 2018-11-30T11:46:58Z
4 changed files:
- buildstream/_frontend/cli.py
- buildstream/_stream.py
- buildstream/element.py
- tests/integration/pullbuildtrees.py
Changes:
| ... | ... | @@ -469,6 +469,10 @@ def push(app, elements, deps, remote): | 
| 469 | 469 |      The default destination is the highest priority configured cache. You can
 | 
| 470 | 470 |      override this by passing a different cache URL with the `--remote` flag.
 | 
| 471 | 471 |  | 
| 472 | +    If bst has been configured to include build trees on artifact pulls,
 | |
| 473 | +    an attempt will be made to pull any required build trees to avoid the
 | |
| 474 | +    skipping of partial artifacts being pushed.
 | |
| 475 | + | |
| 472 | 476 |      Specify `--deps` to control which artifacts to push:
 | 
| 473 | 477 |  | 
| 474 | 478 |      \b
 | 
| ... | ... | @@ -327,6 +327,10 @@ class Stream(): | 
| 327 | 327 |      # If `remote` specified as None, then regular configuration will be used
 | 
| 328 | 328 |      # to determine where to push artifacts to.
 | 
| 329 | 329 |      #
 | 
| 330 | +    # If any of the given targets are missing their expected buildtree artifact,
 | |
| 331 | +    # a pull queue will be created if user context and available remotes allow for
 | |
| 332 | +    # attempting to fetch them.
 | |
| 333 | +    #
 | |
| 330 | 334 |      def push(self, targets, *,
 | 
| 331 | 335 |               selection=PipelineSelection.NONE,
 | 
| 332 | 336 |               remote=None):
 | 
| ... | ... | @@ -345,8 +349,17 @@ class Stream(): | 
| 345 | 349 |              raise StreamError("No artifact caches available for pushing artifacts")
 | 
| 346 | 350 |  | 
| 347 | 351 |          self._pipeline.assert_consistent(elements)
 | 
| 348 | -        self._add_queue(PushQueue(self._scheduler))
 | |
| 349 | -        self._enqueue_plan(elements)
 | |
| 352 | + | |
| 353 | +        # Check if we require a pull queue, with given artifact state and context
 | |
| 354 | +        require_buildtrees = self._buildtree_pull_required(elements)
 | |
| 355 | +        if require_buildtrees:
 | |
| 356 | +            self._message(MessageType.INFO, "Attempting to fetch missing artifact buildtrees")
 | |
| 357 | +            self._add_queue(PullQueue(self._scheduler))
 | |
| 358 | +            self._enqueue_plan(require_buildtrees)
 | |
| 359 | + | |
| 360 | +        push_queue = PushQueue(self._scheduler)
 | |
| 361 | +        self._add_queue(push_queue)
 | |
| 362 | +        self._enqueue_plan(elements, queue=push_queue)
 | |
| 350 | 363 |          self._run()
 | 
| 351 | 364 |  | 
| 352 | 365 |      # checkout()
 | 
| ... | ... | @@ -1237,3 +1250,28 @@ class Stream(): | 
| 1237 | 1250 |              parts.append(element.normal_name)
 | 
| 1238 | 1251 |  | 
| 1239 | 1252 |          return os.path.join(directory, *reversed(parts))
 | 
| 1253 | + | |
| 1254 | +    # _buildtree_pull_required()
 | |
| 1255 | +    #
 | |
| 1256 | +    # Check if current task, given config, requires element buildtree artifact
 | |
| 1257 | +    #
 | |
| 1258 | +    # Args:
 | |
| 1259 | +    #    elements (list): elements to check if buildtrees are required
 | |
| 1260 | +    #
 | |
| 1261 | +    # Returns:
 | |
| 1262 | +    #    (list): elements requiring buildtrees
 | |
| 1263 | +    #
 | |
| 1264 | +    def _buildtree_pull_required(self, elements):
 | |
| 1265 | +        required_list = []
 | |
| 1266 | + | |
| 1267 | +        # If context is set to not pull buildtrees, or no fetch remotes, return empty list
 | |
| 1268 | +        if not (self._context.pull_buildtrees or self._artifacts.has_fetch_remotes()):
 | |
| 1269 | +            return required_list
 | |
| 1270 | + | |
| 1271 | +        for element in elements:
 | |
| 1272 | +            # Check if element is partially cached without its buildtree, as the element
 | |
| 1273 | +            # artifact may not be cached at all
 | |
| 1274 | +            if element._cached() and not element._cached_buildtree():
 | |
| 1275 | +                required_list.append(element)
 | |
| 1276 | + | |
| 1277 | +        return required_list | 
| ... | ... | @@ -1422,7 +1422,7 @@ class Element(Plugin): | 
| 1422 | 1422 |                                                   .format(workspace.get_absolute_path())):
 | 
| 1423 | 1423 |                              workspace.stage(temp_staging_directory)
 | 
| 1424 | 1424 |                  # Check if we have a cached buildtree to use
 | 
| 1425 | -                elif self.__cached_buildtree():
 | |
| 1425 | +                elif self._cached_buildtree():
 | |
| 1426 | 1426 |                      artifact_base, _ = self.__extract()
 | 
| 1427 | 1427 |                      import_dir = os.path.join(artifact_base, 'buildtree')
 | 
| 1428 | 1428 |                  else:
 | 
| ... | ... | @@ -1808,7 +1808,7 @@ class Element(Plugin): | 
| 1808 | 1808 |  | 
| 1809 | 1809 |          # Do not push elements that aren't cached, or that are cached with a dangling buildtree
 | 
| 1810 | 1810 |          # artifact unless element type is expected to have an an empty buildtree directory
 | 
| 1811 | -        if not self.__cached_buildtree():
 | |
| 1811 | +        if not self._cached_buildtree():
 | |
| 1812 | 1812 |              return True
 | 
| 1813 | 1813 |  | 
| 1814 | 1814 |          # Do not push tainted artifact
 | 
| ... | ... | @@ -1998,6 +1998,29 @@ class Element(Plugin): | 
| 1998 | 1998 |      def _get_source_element(self):
 | 
| 1999 | 1999 |          return self
 | 
| 2000 | 2000 |  | 
| 2001 | +    # _cached_buildtree()
 | |
| 2002 | +    #
 | |
| 2003 | +    # Check if element artifact contains expected buildtree. An
 | |
| 2004 | +    # element's buildtree artifact will not be present if the rest
 | |
| 2005 | +    # of the partial artifact is not cached.
 | |
| 2006 | +    #
 | |
| 2007 | +    # Returns:
 | |
| 2008 | +    #     (bool): True if artifact cached with buildtree, False if
 | |
| 2009 | +    #             element not cached or missing expected buildtree.
 | |
| 2010 | +    #
 | |
| 2011 | +    def _cached_buildtree(self):
 | |
| 2012 | +        context = self._get_context()
 | |
| 2013 | + | |
| 2014 | +        if not self._cached():
 | |
| 2015 | +            return False
 | |
| 2016 | + | |
| 2017 | +        key_strength = _KeyStrength.STRONG if context.get_strict() else _KeyStrength.WEAK
 | |
| 2018 | +        if not self.__artifacts.contains_subdir_artifact(self, self._get_cache_key(strength=key_strength),
 | |
| 2019 | +                                                         'buildtree'):
 | |
| 2020 | +            return False
 | |
| 2021 | + | |
| 2022 | +        return True
 | |
| 2023 | + | |
| 2001 | 2024 |      #############################################################
 | 
| 2002 | 2025 |      #                   Private Local Methods                   #
 | 
| 2003 | 2026 |      #############################################################
 | 
| ... | ... | @@ -2764,27 +2787,6 @@ class Element(Plugin): | 
| 2764 | 2787 |  | 
| 2765 | 2788 |          return True
 | 
| 2766 | 2789 |  | 
| 2767 | -    # __cached_buildtree():
 | |
| 2768 | -    #
 | |
| 2769 | -    # Check if cached element artifact contains expected buildtree
 | |
| 2770 | -    #
 | |
| 2771 | -    # Returns:
 | |
| 2772 | -    #     (bool): True if artifact cached with buildtree, False if
 | |
| 2773 | -    #             element not cached or missing expected buildtree
 | |
| 2774 | -    #
 | |
| 2775 | -    def __cached_buildtree(self):
 | |
| 2776 | -        context = self._get_context()
 | |
| 2777 | - | |
| 2778 | -        if not self._cached():
 | |
| 2779 | -            return False
 | |
| 2780 | -        elif context.get_strict():
 | |
| 2781 | -            if not self.__artifacts.contains_subdir_artifact(self, self.__strict_cache_key, 'buildtree'):
 | |
| 2782 | -                return False
 | |
| 2783 | -        elif not self.__artifacts.contains_subdir_artifact(self, self.__weak_cache_key, 'buildtree'):
 | |
| 2784 | -            return False
 | |
| 2785 | - | |
| 2786 | -        return True
 | |
| 2787 | - | |
| 2788 | 2790 |      # __pull_directories():
 | 
| 2789 | 2791 |      #
 | 
| 2790 | 2792 |      # Which directories to include or exclude given the current
 | 
| ... | ... | @@ -38,7 +38,8 @@ def test_pullbuildtrees(cli, tmpdir, datafiles, integration_cache): | 
| 38 | 38 |  | 
| 39 | 39 |      # Create artifact shares for pull & push testing
 | 
| 40 | 40 |      with create_artifact_share(os.path.join(str(tmpdir), 'share1')) as share1,\
 | 
| 41 | -        create_artifact_share(os.path.join(str(tmpdir), 'share2')) as share2:
 | |
| 41 | +        create_artifact_share(os.path.join(str(tmpdir), 'share2')) as share2,\
 | |
| 42 | +        create_artifact_share(os.path.join(str(tmpdir), 'share3')) as share3:
 | |
| 42 | 43 |          cli.configure({
 | 
| 43 | 44 |              'artifacts': {'url': share1.repo, 'push': True},
 | 
| 44 | 45 |              'artifactdir': os.path.join(str(tmpdir), 'artifacts')
 | 
| ... | ... | @@ -123,6 +124,32 @@ def test_pullbuildtrees(cli, tmpdir, datafiles, integration_cache): | 
| 123 | 124 |          assert share2.has_artifact('test', element_name, cli.get_element_key(project, element_name))
 | 
| 124 | 125 |          default_state(cli, tmpdir, share1)
 | 
| 125 | 126 |  | 
| 127 | +        # Assert that bst push will automatically attempt to pull a missing buildtree
 | |
| 128 | +        # if pull-buildtrees is set, however as share3 is the only defined remote and is empty,
 | |
| 129 | +        # assert that no element artifact buildtrees are pulled (no available remote buildtree) and thus the
 | |
| 130 | +        # artifact cannot be pushed.
 | |
| 131 | +        result = cli.run(project=project, args=['pull', element_name])
 | |
| 132 | +        assert element_name in result.get_pulled_elements()
 | |
| 133 | +        cli.configure({'artifacts': {'url': share3.repo, 'push': True}})
 | |
| 134 | +        result = cli.run(project=project, args=['--pull-buildtrees', 'push', element_name])
 | |
| 135 | +        assert "Attempting to fetch missing artifact buildtrees" in result.stderr
 | |
| 136 | +        assert element_name not in result.get_pulled_elements()
 | |
| 137 | +        assert not os.path.isdir(buildtreedir)
 | |
| 138 | +        assert element_name not in result.get_pushed_elements()
 | |
| 139 | +        assert not share3.has_artifact('test', element_name, cli.get_element_key(project, element_name))
 | |
| 140 | + | |
| 141 | +        # Assert that if we add an extra remote that has the buildtree artfact cached, bst push will
 | |
| 142 | +        # automatically attempt to pull it and will be successful, leading to the full artifact being pushed
 | |
| 143 | +        # to the empty share3. This gives the ability to attempt push currently partial artifacts to a remote,
 | |
| 144 | +        # without exlipictly requiring a bst pull.
 | |
| 145 | +        cli.configure({'artifacts': [{'url': share1.repo, 'push': False}, {'url': share3.repo, 'push': True}]})
 | |
| 146 | +        result = cli.run(project=project, args=['--pull-buildtrees', 'push', element_name])
 | |
| 147 | +        assert "Attempting to fetch missing artifact buildtrees" in result.stderr
 | |
| 148 | +        assert element_name in result.get_pulled_elements()
 | |
| 149 | +        assert os.path.isdir(buildtreedir)
 | |
| 150 | +        assert element_name in result.get_pushed_elements()
 | |
| 151 | +        assert share3.has_artifact('test', element_name, cli.get_element_key(project, element_name))
 | |
| 152 | + | |
| 126 | 153 |  | 
| 127 | 154 |  # Ensure that only valid pull-buildtrees boolean options make it through the loading
 | 
| 128 | 155 |  # process.
 | 
