Hey! These docs are for version 2.0, which is no longer officially supported. Click here for the latest version, 2.7!

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.

📘

Example repository

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 Fields 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


Did this page help you?