-
Notifications
You must be signed in to change notification settings - Fork 43
cli: add buildtree checkout command #2129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -333,23 +333,8 @@ def shell( | |||||||||||||||||||||||||||||
| reason="shell-missing-deps", | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Check if we require a pull queue attempt, with given artifact state and context | ||||||||||||||||||||||||||||||
| if usebuildtree: | ||||||||||||||||||||||||||||||
| if not element._cached_buildroot(): | ||||||||||||||||||||||||||||||
| if not element._cached(): | ||||||||||||||||||||||||||||||
| message = "Artifact not cached locally or in available remotes" | ||||||||||||||||||||||||||||||
| reason = "missing-buildtree-artifact-not-cached" | ||||||||||||||||||||||||||||||
| elif element._buildroot_exists(): | ||||||||||||||||||||||||||||||
| message = "Buildtree is not cached locally or in available remotes" | ||||||||||||||||||||||||||||||
| reason = "missing-buildtree-artifact-buildtree-not-cached" | ||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
| message = "Artifact was created without buildtree" | ||||||||||||||||||||||||||||||
| reason = "missing-buildtree-artifact-created-without-buildtree" | ||||||||||||||||||||||||||||||
| raise StreamError(message, reason=reason) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Raise warning if the element is cached in a failed state | ||||||||||||||||||||||||||||||
| if element._cached_failure(): | ||||||||||||||||||||||||||||||
| self._context.messenger.warn("using a buildtree from a failed build.") | ||||||||||||||||||||||||||||||
| self._check_buildtree(element) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Ensure we have our sources if we are launching a build shell | ||||||||||||||||||||||||||||||
| if scope == _Scope.BUILD and not usebuildtree: | ||||||||||||||||||||||||||||||
|
|
@@ -916,6 +901,74 @@ def artifact_delete(self, targets, *, selection=_PipelineSelection.NONE): | |||||||||||||||||||||||||||||
| if not ref_removed: | ||||||||||||||||||||||||||||||
| self._context.messenger.info("No artifacts were removed") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # buildtree_checkout() | ||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||
| # Checkout target buildtree artifact to the specified location | ||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||
| # Args: | ||||||||||||||||||||||||||||||
| # target: Target to checkout | ||||||||||||||||||||||||||||||
| # location: Location to checkout the artifact to | ||||||||||||||||||||||||||||||
| # force: Whether files can be overwritten if necessary | ||||||||||||||||||||||||||||||
| # hardlinks: Whether checking out files hardlinked to | ||||||||||||||||||||||||||||||
| # their artifacts is acceptable | ||||||||||||||||||||||||||||||
| # tar: If true, a tarball from the artifact contents will | ||||||||||||||||||||||||||||||
| # be created, otherwise the file tree of the artifact | ||||||||||||||||||||||||||||||
| # will be placed at the given location. If true and | ||||||||||||||||||||||||||||||
| # location is '-', the tarball will be dumped on the | ||||||||||||||||||||||||||||||
| # standard output. | ||||||||||||||||||||||||||||||
| # artifact_remotes: Artifact cache remotes specified on the commmand line | ||||||||||||||||||||||||||||||
| # ignore_project_artifact_remotes: Whether to ignore artifact remotes specified by projects | ||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||
| def buildtree_checkout( | ||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||
| target: str, | ||||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||||
| location: Optional[str] = None, | ||||||||||||||||||||||||||||||
| buildroot: bool = False, | ||||||||||||||||||||||||||||||
| force: bool = False, | ||||||||||||||||||||||||||||||
| hardlinks: bool = False, | ||||||||||||||||||||||||||||||
| compression: str = "", | ||||||||||||||||||||||||||||||
| tar: bool = False, | ||||||||||||||||||||||||||||||
| artifact_remotes: Iterable[RemoteSpec] = (), | ||||||||||||||||||||||||||||||
| ignore_project_artifact_remotes: bool = False, | ||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| elements = self._load( | ||||||||||||||||||||||||||||||
| (target,), | ||||||||||||||||||||||||||||||
| selection=_PipelineSelection.NONE, | ||||||||||||||||||||||||||||||
| load_artifacts=True, | ||||||||||||||||||||||||||||||
| attempt_artifact_metadata=True, | ||||||||||||||||||||||||||||||
| connect_artifact_cache=True, | ||||||||||||||||||||||||||||||
| artifact_remotes=artifact_remotes, | ||||||||||||||||||||||||||||||
| ignore_project_artifact_remotes=ignore_project_artifact_remotes, | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # self.targets contains a list of the loaded target objects | ||||||||||||||||||||||||||||||
| # if we specify --deps build, Stream._load() will return a list | ||||||||||||||||||||||||||||||
| # of build dependency objects, however, we need to prepare a sandbox | ||||||||||||||||||||||||||||||
| # with the target (which has had its appropriate dependencies loaded) | ||||||||||||||||||||||||||||||
| element: Element = self.targets[0] | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| self._check_location_writable(location, force=force, tar=tar) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Check whether the required elements are cached, and then | ||||||||||||||||||||||||||||||
| # try to pull them if they are not already cached. | ||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||
| self.query_cache(elements) | ||||||||||||||||||||||||||||||
| self._pull_missing_artifacts(elements) | ||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we only need to pull the artifact for the element in question. No need for dependencies here. as an aside, I wonder if we should try to override the |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| self._check_buildtree(element) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||
| artifact = element._get_artifact() | ||||||||||||||||||||||||||||||
| virdir = artifact.get_buildroot() if buildroot else artifact.get_buildtree() | ||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another thing we need to be aware of (and also give a suggestion about to the user) is that not all elements have a build tree: only build elements do. So it's not enough to check for the build root, we also need to check for the build tree. Then depending on the availability and the user request, we can suggest an alternative to the user.
|
||||||||||||||||||||||||||||||
| self._export_artifact(tar, location, compression, element, hardlinks, virdir) | ||||||||||||||||||||||||||||||
| except BstError as e: | ||||||||||||||||||||||||||||||
| raise StreamError( | ||||||||||||||||||||||||||||||
| "Error while exporting buildtree artifacts" ": '{}'".format(e), detail=e.detail, reason=e.reason | ||||||||||||||||||||||||||||||
| ) from e | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # source_checkout() | ||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||
| # Checkout sources of the target element to the specified location | ||||||||||||||||||||||||||||||
|
|
@@ -1907,6 +1960,33 @@ def _check_location_writable(self, location, force=False, tar=False): | |||||||||||||||||||||||||||||
| if not force and os.path.exists(location): | ||||||||||||||||||||||||||||||
| raise StreamError("Output file '{}' already exists".format(location)) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # _check_buildtree() | ||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||
| # Check if we require a pull queue attempt, with given artifact state and context. | ||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||
| # Args: | ||||||||||||||||||||||||||||||
| # element (Element): Destination path | ||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||
| # Raises: | ||||||||||||||||||||||||||||||
| # (StreamError): If the no buildroot found | ||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||
| def _check_buildtree(self, element): | ||||||||||||||||||||||||||||||
| if not element._cached_buildroot(): | ||||||||||||||||||||||||||||||
| if not element._cached(): | ||||||||||||||||||||||||||||||
| message = "Artifact not cached locally or in available remotes" | ||||||||||||||||||||||||||||||
| reason = "missing-buildtree-artifact-not-cached" | ||||||||||||||||||||||||||||||
| elif element._buildroot_exists(): | ||||||||||||||||||||||||||||||
| message = "Buildtree is not cached locally or in available remotes" | ||||||||||||||||||||||||||||||
| reason = "missing-buildtree-artifact-buildtree-not-cached" | ||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
| message = "Artifact was created without buildtree" | ||||||||||||||||||||||||||||||
| reason = "missing-buildtree-artifact-created-without-buildtree" | ||||||||||||||||||||||||||||||
| raise StreamError(message, reason=reason) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Raise warning if the element is cached in a failed state | ||||||||||||||||||||||||||||||
| if element._cached_failure(): | ||||||||||||||||||||||||||||||
| self._context.messenger.warn("using a buildtree from a failed build.") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Helper function for source_checkout() | ||||||||||||||||||||||||||||||
| def _source_checkout( | ||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,133 @@ | ||||||
| # | ||||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| # you may not use this file except in compliance with the License. | ||||||
| # You may obtain a copy of the License at | ||||||
| # | ||||||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||||||
| # | ||||||
| # Unless required by applicable law or agreed to in writing, software | ||||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
| # See the License for the specific language governing permissions and | ||||||
| # limitations under the License. | ||||||
| # | ||||||
|
|
||||||
| # Pylint doesn't play well with fixtures and dependency injection from pytest | ||||||
| # pylint: disable=redefined-outer-name | ||||||
|
|
||||||
| import os | ||||||
| import tarfile | ||||||
| import shutil | ||||||
|
|
||||||
| import pytest | ||||||
|
|
||||||
| from buildstream._testing import cli, cli_integration, Cli # pylint: disable=unused-import | ||||||
| from buildstream.exceptions import ErrorDomain | ||||||
| from buildstream._testing._utils.site import HAVE_SANDBOX | ||||||
|
|
||||||
| pytestmark = pytest.mark.integration | ||||||
|
|
||||||
|
|
||||||
| DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "project") | ||||||
|
|
||||||
| # | ||||||
| # Verify fail cases when checkout buildtree | ||||||
| # | ||||||
| @pytest.mark.datafiles(DATA_DIR) | ||||||
| @pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox") | ||||||
| def test_buildtree_checkout_fail(cli, datafiles): | ||||||
| project = str(datafiles) | ||||||
| element_name = "build-shell/buildtree.bst" | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should also test the buildtree of a script element (using |
||||||
| checkout = os.path.join(cli.directory, "checkout") | ||||||
| tar = os.path.join(cli.directory, "source-checkout.tar") | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| res = cli.run(project=project, args=["--cache-buildtrees", "never", "build", element_name]) | ||||||
| res.assert_success() | ||||||
|
|
||||||
| res = cli.run(project=project, args=["buildtree", "checkout", "--directory", checkout, element_name]) | ||||||
| res.assert_main_error(ErrorDomain.STREAM, "missing-buildtree-artifact-created-without-buildtree") | ||||||
|
|
||||||
| res = cli.run(project=project, args=["buildtree", "checkout", "--compression", "gz", "--directory", checkout, element_name]) | ||||||
| assert res.exit_code != 0 | ||||||
| assert "ERROR: --compression can only be provided if --tar is provided" in res.stderr | ||||||
|
|
||||||
|
|
||||||
| res = cli.run(project=project, args=["buildtree", "checkout", "--tar", tar, "--directory", checkout, element_name]) | ||||||
| assert res.exit_code != 0 | ||||||
| assert "ERROR: options --directory and --tar conflict" in res.stderr | ||||||
|
|
||||||
| res = cli.run(project=project, args=["buildtree", "checkout", "--tar", tar, "--hardlinks", element_name]) | ||||||
| assert res.exit_code != 0 | ||||||
| assert "ERROR: options --hardlinks and --tar conflict" in res.stderr | ||||||
|
|
||||||
| # | ||||||
| # Verify checkout buildtree | ||||||
| # | ||||||
| @pytest.mark.datafiles(DATA_DIR) | ||||||
| @pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox") | ||||||
| def test_buildtree_checkout(cli, datafiles): | ||||||
| project = str(datafiles) | ||||||
| element_name = "build-shell/buildtree.bst" | ||||||
| checkout = os.path.join(cli.directory, "checkout") | ||||||
| tar = os.path.join(cli.directory, "source-checkout.tar") | ||||||
|
|
||||||
| # Build only once with cached buildtree | ||||||
| res = cli.run(project=project, args=["--cache-buildtrees", "always", "build", element_name]) | ||||||
| res.assert_success() | ||||||
|
|
||||||
| # verify checkout buildtree only | ||||||
| res = cli.run(project=project, args=["buildtree", "checkout", "--directory", checkout, element_name]) | ||||||
| res.assert_success() | ||||||
|
|
||||||
| expect_buildtree = ['test'] | ||||||
| assert expect_buildtree == os.listdir(checkout) | ||||||
| shutil.rmtree(checkout) | ||||||
|
|
||||||
| # verify checkout buildtree only tar | ||||||
| res = cli.run(project=project, args=["buildtree", "checkout", "--tar", tar, element_name]) | ||||||
| res.assert_success() | ||||||
|
|
||||||
| assert os.path.exists(tar) | ||||||
| with tarfile.open(tar) as tf: | ||||||
| expected_content = [os.path.join(".", expect) for expect in expect_buildtree] | ||||||
| tar_members = [f.name for f in tf] | ||||||
| assert tar_members == expected_content | ||||||
| os.remove(tar) | ||||||
|
|
||||||
| # verify checkout buildtree only tar in different compression formats | ||||||
| for compression in ["gz", "xz", "bz2"]: | ||||||
| res = cli.run(project=project, args=["buildtree", "checkout", "--tar", "-", "--compression", compression, element_name], binary_capture=True) | ||||||
| res.assert_success() | ||||||
|
|
||||||
| with open(tar, "wb") as f: | ||||||
| f.write(res.output) | ||||||
|
|
||||||
| with tarfile.open(tar, "r:" + compression) as tf: | ||||||
| expected_content = [os.path.join(".", expect) for expect in expect_buildtree] | ||||||
| tar_members = [f.name for f in tf] | ||||||
| assert tar_members == expected_content | ||||||
|
|
||||||
| os.remove(tar) | ||||||
|
|
||||||
| # verify checkout whole buildroot | ||||||
| res = cli.run(project=project, args=["buildtree", "checkout", "--buildroot", "--directory", checkout, element_name]) | ||||||
| res.assert_success() | ||||||
|
|
||||||
| expect_buildroot = ['lib', 'media', 'proc', 'usr', 'home', 'buildstream-install', 'dev', 'var', 'sys', 'bin', 'run', 'buildstream', 'tmp', 'sbin', 'etc', 'mnt', 'srv', 'root'] | ||||||
| assert expect_buildroot == os.listdir(checkout) | ||||||
|
|
||||||
| # verify checkout whole buildroot with force flag | ||||||
| res = cli.run(project=project, args=["buildtree", "checkout", "--force", "--directory", checkout, element_name]) | ||||||
| res.assert_success() | ||||||
|
|
||||||
| assert sorted([*expect_buildtree, *expect_buildroot]) == sorted(os.listdir(checkout)) | ||||||
|
|
||||||
| shutil.rmtree(checkout) | ||||||
|
|
||||||
| # verify checkout whole buildroot with hardlinks | ||||||
| res = cli.run(project=project, args=["buildtree", "checkout", "--buildroot", "--hardlinks", "--directory", checkout, element_name]) | ||||||
| res.assert_success() | ||||||
|
|
||||||
| expect_buildroot = ['lib', 'media', 'proc', 'usr', 'home', 'buildstream-install', 'dev', 'var', 'sys', 'bin', 'run', 'buildstream', 'tmp', 'sbin', 'etc', 'mnt', 'srv', 'root'] | ||||||
| assert expect_buildroot == os.listdir(checkout) | ||||||
| shutil.rmtree(checkout) | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this make sense for the build tree? Either we're checking out just the buildtree, and then dependencies don't make sense, or we check out the whole buildroot and it already contains the build dependencies.
Am I missing something?