Targets and BUILD files

Metadata for your code.

Most goals require metadata about your code. For example, to run a test, you need to know about all the transitive dependencies of that test. You may also want to set a timeout on that test.

Targets are a set of metadata describing your code.

Some targets like files and python_library describe your first-party code, some like python_requirement_library describe your third-party requirements, and others like pex_binary and archive describe artifacts you'd like Pants to build.

BUILD files

Targets are defined in files with the name BUILD.

For example, this BUILD file defines two targets:

python_tests(
    name="tests",
    sources=["test_*.py"],
    timeout=120,
)

pex_binary(
    name="bin",
    entry_point="app.py",
    zip_safe=False,
)

Each target type has different fields, or individual metadata values. Run ./pants help $target to see which fields a particular target type has, e.g. ./pants help files. Most fields are optional and use sensible defaults.

All target types have a name field, which is used to identify the target. Target names must be unique within a directory. If you leave off the name field, it will default to the BUILD file's directory name.

Some target types are always available, but most require first registering backends. For example, the python_tests and pex_binary target types are provided by the pants.backend.python backend.

Target addresses

A target is identified by its address. A target address has the form path/to/directory:name or path/to/file.py:name. For example, the targets in the example above have the addresses helloworld/greet:tests and helloworld/greet:bin, respectively.

Addresses can be used as command-line arguments, such as ./pants fmt path/to:target (which will format all files that are in the sources field of :target), or ./pants fmt path/to/file.py (which will format path/to/file.py, owned by a default target named :to in the same directory).

Addresses are also used in the dependencies field to depend on other targets, as explained below.

Addresses are globally unique, which is guaranteed by the fact that target names must be unique within a directory.

πŸ“˜

Address abbreviation

If the target name is the same as the name of the directory containing it, then it is the "default" target in that directory, and you can omit it from the address.

For example: helloworld/greet is an abbreviated form of helloworld/greet:greet, and helloworld/greet/greeter.py is an abbreviated form of helloworld/greet/greeter.py:greet.

πŸ“˜

Addresses at at the "build root"

You may sometimes have a BUILD file at your build root, i.e. the top-level of your project. These addresses are prefixed with // for clarity.

For example, //:my_tgt refers to a a target defined in <build root>/BUILD with the field name="my_tgt".

Sources

Targets for first-party code have a sources field, which determines which source files belong to the target.

The value of the sources field is a list of names and/or glob patterns, relative to the BUILD file's directory. Sources must be in or below this directory, i.e., patterns containing ../ are not allowed.

A ! prefix excludes matching files that would otherwise be included. For example, ['*.py', '!exclude_*.py'] will include foo.py but not exclude_bar.py.

Use ** for a recursive glob, e.g. sources=['**/*.py'].

🚧

Overlapping sources fields can cause confusion

It's valid to include the same file in the sources for multiple targets. When would you do this? Sometimes you may have conflicting metadata for the same source file, such as toggling between a GPU vs. CPU version of a dependency:

python_library(
    name="lib_cpu",
    sources=["lib.py"],
    dependencies=["3rdparty:tensorflow"],
)

python_library(
    name="lib_gpu",
    sources=["lib.py"],
    dependencies=["3rdparty:tensorflow-gpu"],
)  

However, including the same file in the sources for multiple targets can result in two confusing behaviors:

  • Pants will no longer be able to infer dependencies on this file because it cannot disambiguate which of the targets you want to use. You must use explicit dependencies instead.
  • File arguments will run over all owning targets, e.g. ./pants test path/to/test.ext would run both test targets as two separate subprocesses, even though you might only expect a single subprocess.

You can run ./pants list path/to/file.ext to see all "owning" targets to check if >1 target has the file in its sources field.

Dependencies and dependency inference

Almost all targets have a dependencies field, which is used to determine which of your first-party code and third-party requirements to use.

Normally, you can leave off the dependencies field, thanks to dependency inference. Pants will read your import statements and map those imports back to your first-party code and your third-party requirements. You can run ./pants dependencies path/to/file.ext or ./pants dependencies path/to:target to see what dependencies Pants infers.

However, dependency inference cannot infer everything. For example, dependency inference does not know how to infer dependencies on resources() and files() targets, so you will need to sometimes explicitly add to the dependencies field.

To add a new explicit dependency, add the address to the target:

python_library(
    dependencies=[
        "3rdparty/python:ansicolors",
        "helloworld/util",  # shorthand for `helloworld/util:util`
        "helloworld/util:json_files,
    ],
)

Adding a dependency on a target describing first-party code, e.g. a python_library or files target, will add all of the files from that target's sources field. See the below tooltip "Explicit file addresses" for how to instead use more granular dependencies, which is what dependency inference uses.

You only need to declare direct dependencies; there is no need to include the dependencies of your dependencies. Pants will pull in those transitive dependencies for you.

πŸ“˜

Ignore dependencies with ! and !!

If you don't like that Pants inferred a certain dependency, you can tell Pants to ignore it with !. Run ./pants dependencies to find the address for the problematic dependency, then copy it into the dependencies field with a ! prefix.

python_library(
    dependencies=["!3rdparty/python:numpy"],
)

You can use the prefix !! to transitively exclude a dependency, meaning that even if a target's dependencies include the bad dependency, no matter what, the final result will not include the value.

Transitive excludes can only be used in target types that conventionally are not dependend upon by other targets, such as pex_binary and python_tests. This is meant to limit confusion, as using !! in something like a python_library could result in surprising behavior for everything that depends on it. If you use !! when not allowed to, Pants will print a helpful error message saying where you can use it.

πŸ“˜

Explicit "file addresses"

When you declare an explicit dependency on a target for first-party code, like helloworld/util:json_files, you end up depending on every file in the sources field of :json_files, even if you only use some of the files. Often, this is what you meant.

However, sometimes you want to be more granular, which results in finer-grained invalidation for caching. To do this, you can use a "file address", which tells Pants to only depend on a specific file from a certain target.

Pants's dependency inference automatically uses file addresses already, so you only need to use explicit file addresses when Pants cannot infer the dependency and you want more granular dependencies than normal target addresses.

To add an explicit file address, add the file path, followed by :target_name. For example, helloworld/util/f1.json:json_resources. If the target name is the default name, you can leave off the :target_name part. If the file is in the same directory or a subdirectory, you can use a relative file path like ./f1.json:json_resources.

python_library(
    dependencies=[
        "helloworld/util/f1.json:json_resources",
        "./f2.json:json_resources",  # Shorthand for "helloworld/util/f2.json:json_resources
        "helloworld/app.py",  # Shorthand for "helloworld/app.py:helloworld"
    ],
)

What if the file's target's BUILD file is in a different directory than that file? For example, consider this target definition:

resources(
  name="json_resources",
  sources=["**/*.json"],
)

If the file is in a subdirectory from the original target definition, use ../ in the target_name section, e.g. helloworld/util/f1.json:../json_resources.

The build graph and dependency cycles

The set of targets in a repo form the build graph. The vertices in this graph are the targets, and the (directed) edges are the dependencies.

The build graph must not contain directed cycles, i.e., it must form a DAG (a Directed Acyclic Graph). If you do have a cycle, Pants will print an error message explaining what caused the cycle and giving suggestions for how to fix it.

Target granularity for first-party code

A first-party target's sources field can be as fine-grained as a single file or as course-grained as an entire tree of files.

If you use dependency inference and explicit file addresses in the dependencies field, then target granularity has no implications for your caching and invalidation. You can, in theory, define one python_library target for your entire project, for example. (However, this usually doesn't scale well as your codebase evolves.)

In contrast, if you use normal target addresses in the dependencies field, then target granularity does have implications for caching and invalidation. Every time you add a new dependency to a target, you end up depending on every file from the sources field of the dependency, even if some of them are not used. So, more granular targets will result in better caching and invalidation.

In practice, we've found that having one library and/or one test target per-directory tends to work well. The default sources values for various target types reflect this.

For example, for a Python project with tests colocated with source code, we recommend defaulting to a BUILD file like this in every directory:

python_library()

python_tests(name="tests")

πŸ‘

Use ./pants tailor to cut down on manual work

The tailor goal will generate initial BUILD files for you! You may need to tweak them manually, but for the most part the auto-generated ones should get you off to a good start. You can run ./pants tailor again as you add new files in the future.

πŸ“˜

Tips on setting up initial BUILD files

See Adopting Pants in an Existing Repo for tips on setting up Pants, including where to add BUILD files.


Did this page help you?