forked from pantsbuild/pants
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Ensure packaged artefacts are fully replaced in dist/ (pantsbuild#18930)
This patch has the package goal clear out the artefact(s) it is about to write to `dist/`, if they already exists. For instance, if running ``pants package path/to:target`` on a `pex_binary` (outputting `path.to/target.pex`), pants will now first remove anything that's already at `dist/path.to/target.pex` before writing the new output. This resolves two problems: - if there's existing contents of a different kind (e.g. a directory in `dist/` and writing a file), the package call would explode. For instance, switching a target like `pex_binary(..., format="zipapp")` (file) to `pex_binary(..., format="packed")` (directory). - if the package output is directory, stale files already in that location in `dist/` would remain. For instance, a `pex_binary(..., format="packed")` where a file was removed. This fixes pantsbuild#17758 and fixes pantsbuild#18849, respectively. This only fixes `package`, not any other goals that also write to fixed paths (like `export` and `export-codegen`). In pantsbuild#18871, I start on `export-codegen`, but it's a bit fiddlier (requires propagating "this is the artefact" paths around) and it's best to land the infrastructure in this PR first. I'll file follow-up issues covering them specifically.
- Loading branch information
Showing
8 changed files
with
268 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
from __future__ import annotations | ||
|
||
from dataclasses import dataclass | ||
from pathlib import Path | ||
from textwrap import dedent | ||
|
||
import pytest | ||
|
||
from pants.core.goals import package | ||
from pants.core.goals.package import BuiltPackage, BuiltPackageArtifact, Package, PackageFieldSet | ||
from pants.engine.fs import CreateDigest, Digest, FileContent | ||
from pants.engine.internals.selectors import Get | ||
from pants.engine.rules import rule | ||
from pants.engine.target import StringField, Target | ||
from pants.engine.unions import UnionRule | ||
from pants.testutil.rule_runner import RuleRunner | ||
|
||
|
||
class MockTypeField(StringField): | ||
alias = "type" | ||
|
||
def synth(self, base_path: Path) -> tuple[CreateDigest, tuple[Path, ...]]: | ||
if self.value == "single_file": | ||
return (CreateDigest([FileContent(str(base_path), b"single")]), (base_path,)) | ||
|
||
elif self.value == "multiple_files": | ||
a = base_path / "a" | ||
b = base_path / "b" | ||
return ( | ||
CreateDigest( | ||
[FileContent(str(a), b"multiple: a"), FileContent(str(b), b"multiple: b")] | ||
), | ||
(a, b), | ||
) | ||
|
||
elif self.value == "directory": | ||
a = base_path / "a" | ||
b = base_path / "b" | ||
return ( | ||
CreateDigest( | ||
[ | ||
FileContent(str(a), b"directory: a"), | ||
FileContent(str(b), b"directory: b"), | ||
] | ||
), | ||
(base_path,), | ||
) | ||
|
||
raise ValueError(f"don't understand {self.value}") | ||
|
||
|
||
class MockTarget(Target): | ||
alias = "mock" | ||
core_fields = (MockTypeField,) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class MockPackageFieldSet(PackageFieldSet): | ||
required_fields = (MockTypeField,) | ||
|
||
type: MockTypeField | ||
|
||
|
||
@rule | ||
async def package_mock_target(field_set: MockPackageFieldSet) -> BuiltPackage: | ||
base_path = Path(f"base/{field_set.address.target_name}") | ||
create_digest, relpaths = field_set.type.synth(base_path) | ||
digest = await Get(Digest, CreateDigest, create_digest) | ||
return BuiltPackage( | ||
digest, tuple(BuiltPackageArtifact(relpath=str(relpath)) for relpath in relpaths) | ||
) | ||
|
||
|
||
@pytest.fixture | ||
def rule_runner() -> RuleRunner: | ||
return RuleRunner( | ||
rules=[ | ||
*package.rules(), | ||
package_mock_target, | ||
UnionRule(PackageFieldSet, MockPackageFieldSet), | ||
], | ||
target_types=[MockTarget], | ||
) | ||
|
||
|
||
@pytest.fixture | ||
def dist_base(rule_runner) -> Path: | ||
return Path(rule_runner.build_root, "dist/base") | ||
|
||
|
||
def test_package_single_file_artifact(rule_runner: RuleRunner, dist_base: Path) -> None: | ||
rule_runner.write_files({"src/BUILD": "mock(name='x', type='single_file')"}) | ||
result = rule_runner.run_goal_rule( | ||
Package, | ||
args=("src:x",), | ||
env_inherit={"HOME", "PATH", "PYENV_ROOT"}, | ||
) | ||
|
||
assert result.exit_code == 0 | ||
assert (dist_base / "x").read_text() == "single" | ||
|
||
|
||
def test_package_directory_artifact(rule_runner: RuleRunner, dist_base: Path) -> None: | ||
rule_runner.write_files({"src/BUILD": "mock(name='x', type='directory')"}) | ||
result = rule_runner.run_goal_rule( | ||
Package, | ||
args=("src:x",), | ||
env_inherit={"HOME", "PATH", "PYENV_ROOT"}, | ||
) | ||
|
||
assert result.exit_code == 0 | ||
assert (dist_base / "x/a").read_text() == "directory: a" | ||
assert (dist_base / "x/b").read_text() == "directory: b" | ||
|
||
|
||
def test_package_multiple_artifacts(rule_runner: RuleRunner, dist_base: Path) -> None: | ||
rule_runner.write_files({"src/BUILD": "mock(name='x', type='multiple_files')"}) | ||
result = rule_runner.run_goal_rule( | ||
Package, | ||
args=("src:x",), | ||
env_inherit={"HOME", "PATH", "PYENV_ROOT"}, | ||
) | ||
|
||
assert result.exit_code == 0 | ||
assert (dist_base / "x/a").read_text() == "multiple: a" | ||
assert (dist_base / "x/b").read_text() == "multiple: b" | ||
|
||
|
||
def test_package_multiple_targets(rule_runner: RuleRunner, dist_base: Path) -> None: | ||
rule_runner.write_files( | ||
{ | ||
"src/BUILD": dedent( | ||
"""\ | ||
mock(name='x', type='single_file') | ||
mock(name='y', type='single_file') | ||
""" | ||
) | ||
} | ||
) | ||
result = rule_runner.run_goal_rule( | ||
Package, | ||
args=("src:x", "src:y"), | ||
env_inherit={"HOME", "PATH", "PYENV_ROOT"}, | ||
) | ||
|
||
assert result.exit_code == 0 | ||
assert (dist_base / "x").read_text() == "single" | ||
assert (dist_base / "y").read_text() == "single" | ||
|
||
|
||
@pytest.mark.parametrize("existing", ["file", "directory"]) | ||
@pytest.mark.parametrize("type", ["single_file", "directory"]) | ||
def test_package_replace_existing( | ||
existing: str, type: str, rule_runner: RuleRunner, dist_base: Path | ||
) -> None: | ||
"""All combinations of having existing contents (either file or directory) in dist/ and | ||
replacing it with file or directory package contents: the final result should be exactly the | ||
same as clearing dist/ and running package from scratch: | ||
- works | ||
- no extraneous files remaining within an artifact | ||
""" | ||
existing_contents = ( | ||
{"dist/base/x": "existing"} | ||
if existing == "file" | ||
else {"dist/base/x/a": "existing: a", "dist/base/x/c": "existing: c"} | ||
) | ||
rule_runner.write_files({**existing_contents, "src/BUILD": f"mock(name='x', type='{type}')"}) | ||
result = rule_runner.run_goal_rule( | ||
Package, | ||
args=("src:x",), | ||
env_inherit={"HOME", "PATH", "PYENV_ROOT"}, | ||
) | ||
|
||
assert result.exit_code == 0 | ||
|
||
if type == "single_file": | ||
assert (dist_base / "x").read_text() == "single" | ||
else: | ||
a = dist_base / "x/a" | ||
b = dist_base / "x/b" | ||
assert set((dist_base / "x").iterdir()) == {a, b} | ||
assert a.read_text() == "directory: a" | ||
assert b.read_text() == "directory: b" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters