Package code
How to add a new implementation to the package
goal.
The package
goal bundles all the relevant code and third-party dependencies into a single asset, such as a JAR, PEX, or zip file.
Often, the asset is executable, but it need not be.
This guide walks through adding a simple package
implementation for Bash that simply puts all the relevant source files into a .zip
file.
This duplicates the archive
target type, and is solely implemented for instructional purposes. See here for the final implementation.
1. Set up a package target type (recommended)
Usually, you will want to add a new target type for your implementation, such as bash_binary
or python_distribution
.
If your package has a specific file as its entry point, you may want to subclass the Sources
field and set the class property expected_num_files = 1
.
Usually, you will want to include OutputPathField
from pants.core.goals.package
in your target's fields, which will allow the user to change where the package is built to.
See Creating new targets for a guide on how to define new target types.
from pants.core.goals.package import OutputPathField
from pants.engine.target import COMMON_TARGET_FIELDS, Dependencies, Sources, Target
class BashSources(Sources):
expected_file_extensions = (".sh",)
class BashBinarySources(BashSources):
required = True
expected_num_files = 1
class BashBinary(Target):
"""A Bash file that may be directly run."""
alias = "bash_binary"
core_fields = (*COMMON_TARGET_FIELDS, OutputPathField, Dependencies, BashBinarySources)
2. Set up a subclass of PackageFieldSet
As described in Rules and the Target API, a FieldSet
is a way to tell Pants which Field
s you care about targets having for your plugin to work.
Create a new dataclass that subclasses PackageFieldSet
. Set the class property required_fields
to the fields your target must have registered to work. Usually, this is a field like BashEntryPoint
or BashBinarySources
.
from dataclasses import dataclass
from pants.core.goals.package import OutputPathField, PackageFieldSet
@dataclass(frozen=True)
class BashBinaryFieldSet(PackageFieldSet):
required_fields = (BashBinarySources,)
sources: BashBinarySources
output_path: OutputPathField
Then, register your new PackageFieldSet
with a UnionRule
so that Pants knows your binary implementation exists:
from pants.engine.rules import collect_rules
from pants.engine.unions import UnionRule
...
def rules():
return [
*collect_rules(),
UnionRule(PackageFieldSet, BashBinaryFieldSet),
]
3. Create a rule for your logic
Your rule should take as a parameter the PackageFieldSet
from Step 2. It should return BuiltPackage
, which has the fields digest: Digest
and artifacts: Tuple[BuiltPackageArtifact, ...]
, where each BuiltPackageArtifact
has the field relpath: str
and optional extra_log_lines: Tuple[str, ...]
.
Your package rule can have whatever logic you'd like to create a package. All that Pants cares about is that you return a valid BuiltPackage
object.
In this example, we simply create a .zip
file with the bash_binary
and all of its dependencies.
from dataclasses import dataclass
from pants.core.goals.package import (
BuiltPackage,
BuiltPackageArtifact,
OutputPathField,
PackageFieldSet,
)
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
from pants.engine.addresses import Addresses
from pants.engine.process import BinaryPathRequest, BinaryPaths, Process, ProcessResult
from pants.engine.rules import Get, rule
from pants.engine.target import TransitiveTargets
from pants.util.logging import LogLevel
from examples.bash.target_types import BashBinarySources, BashSources
...
@rule(level=LogLevel.DEBUG)
async def package_bash_binary(field_set: BashBinaryFieldSet) -> BuiltPckage:
zip_program_paths = await Get(
BinaryPaths,
BinaryPathRequest(binary_name="zip", search_path=["/bin", "/usr/bin"]),
)
if not zip_program_paths.first_path:
raise ValueError(
"Could not find the `zip` program on `/bin` or `/usr/bin`, so cannot create a package "
f"for {field_set.address}."
)
transitive_targets = await Get(TransitiveTargets, Addresses([field_set.address]))
sources = await Get(
SourceFiles,
SourceFilesRequest(
tgt[BashSources]
for tgt in transitive_targets.closure
if tgt.has_field(BashSources)
),
)
output_filename = field_set.output_path.value_or_default(
field_set.address, file_ending="zip"
)
result = await Get(
ProcessResult,
Process(
argv=(
zip_program_paths.first_path,
output_filename,
*sources.snapshot.files,
),
input_digest=sources.snapshot.digest,
description=f"Zip {field_set.address} and its dependencies.",
output_files=(output_filename,),
),
)
return BuiltPackage(
result.output_digest, artifacts=(BuiltPackageArtifact(output_filename),)
)
Note that we use field_set.output_path.value_or_default
to determine the output filename, which will use the output_path
field if defined, and will default to an unambiguous value otherwise.
Finally, update your plugin's register.py
to activate this file's rules.
from bash import package_binary
def rules():
return [*package_binary.rules()]
Now, when you run ./pants package ::
, Pants should create packages for all your package target types in the --pants-distdir
(defaults to dist/
).
4. Add tests (optional)
Refer to Testing rules. TODO