Goal rules
How to create new goals.
For many plugin tasks, you will be extending pre-existing goals, such as adding a new linter to the lint
goal. However, you may instead want to create a new goal, such as a publish
goal. This page explains how to create a new goal.
As explained in Concepts, @goal_rule
s are the entry-point into the rule graph. When a user runs ./pants my-goal
, the Pants engine will look for the respective @goal_rule
. That @goal_rule
will usually request other types, either as parameters in the @goal_rule
signature or through await Get
.
Often, you can keep all of your logic inline in the @goal_rule
. As your @goal_rule
gets more complex, you may end up factoring out helper @rule
s, but you do not need to start with writing helper @rule
s.
How to register a new goal
There are four steps to creating a new goal with Pants:
- Define a subclass of
GoalSubsystem
. This is the API to your goal.- Give a description in the docstring.
- Set the class property
name
to the name of your goal. - You may register options through the class method
register_options()
. See Options and subsystems.
- Define a subclass of
Goal
. When a user runs./pants my-goal
, the engine will request your subclass, which is what causes the@goal_rule
to run.- Set the class property
subsystem_cls
to theGoalSubsystem
from the previous step. - A
Goal
takes a single argument in its constructor,exit_code: int
. Pants will use this to determine what its own exit code should be.
- Set the class property
- Define an
@goal_rule
, which must return theGoal
from the previous step and set itsexit_code
.- For most goals, simply return
MyGoal(exit_code=0)
. Some goals likelint
andtest
will instead propagate the error code from the tools they run.
- For most goals, simply return
- Register the
@goal_rule
in aregister.py
file.
- pants-plugins/example/hello_world.py
- pants-plugins/example/register.py
from pants.engine.goal import Goal, GoalSubsystem
from pants.engine.rules import collect_rules, goal_rule
class HelloWorldSubsystem(GoalSubsystem):
"""An example goal."""
name = "hello-world"
class HelloWorld(Goal):
subsystem_cls = HelloWorldSubsystem
@goal_rule
async def hello_world() -> HelloWorld:
return HelloWorld(exit_code=1)
def rules():
return collect_rules()
from example import hello_world
def rules():
return [*hello_world.rules()]
You may now run ./pants hello-world
, which should cause Pants to return with an error code of 1 (run echo $?
to verify). Precisely, this causes the engine to request the type HelloWorld
, which results in running the @goal_rule
hello_world
.
Console
: output to stdout/stderr
To output to the user, request the type Console
as a parameter in your @goal_rule
. This is a special type that may only be requested in @goal_rules
and allows you to output to stdout and stderr.
from pants.engine.console import Console
...
@goal_rule
async def hello_world(console: Console) -> HelloWorld:
console.print_stdout("Hello!")
console.print_stderr("Uh oh, an error.")
return HelloWorld(exit_code=1)
Using colors
You may output in color by using the methods .blue()
, .cyan()
, .green()
, .magenta()
, .red()
, and .yellow()
. The colors will only be used if the global option --colors
is True.
console.print_stderr(f"{console.red('𐄂')} Error encountered.")
Outputting
mixin (optional)
If your goal's purpose is to emit output, it may be helpful to use the mixin Outputting
. This mixin will register the output --output-file
, which allows the user to redirect the goal's stdout.
from pants.engine.goal import Goal, GoalSubsystem, Outputting
from pants.engine.rules import goal_rule
class HelloWorldSubsystem(Outputting, GoalSubystem):
"""An example goal."""
name = "hello-world"
...
@goal_rule
async def hello_world(
console: Console, hello_world_subsystem: HelloWorldSubsystem
) -> HelloWorld:
with hello_world_subsystem.output(console) as write_stdout:
write_stdout("Hello world!")
return HelloWorld(exit_code=0)
LineOriented
mixin (optional)
If your goal's purpose is to emit output—and that output is naturally split by new lines—it may be helpful to use the mixin LineOriented
. This subclasses Outputting
, so will register both the options --output-file
and --sep
, which allows the user to change the separator to not be \n
.
from pants.engine.goal import Goal, GoalSubsystem, LineOriented
from pants.engine.rules import goal_rule
class HelloWorldSubsystem(LineOriented, GoalSubystem):
"""An example goal."""
name = "hello-world"
...
@goal_rule
async def hello_world(
console: Console, hello_world_subsystem: HelloWorldSubsystem
) -> HelloWorld:
with hello_world_subsystem.line_oriented(console) as print_stdout:
print_stdout("0")
print_stdout("1")
return HelloWorld(exit_code=0)
How to operate on Targets
Most goals will want to operate on targets. To do this, specify Targets
as a parameter of your goal rule.
from pants.engine.target import Targets
...
@goal_rule
async def hello_world(console: Console, targets: Targets) -> HelloWorld:
for target in targets:
console.print_stdout(target.address.spec)
return HelloWorld(exit_code=0)
This example will print the address of any targets specified by the user, just as the list
goal behaves.
$ ./pants hello-world helloworld/util::
helloworld/util
helloworld/util:tests
See Rules and the Target API for detailed information on how to use these targets in your rules, including accessing the metadata specified in BUILD files.
You may alternatively request Addresses
from pants.engine.addresses
if you do not care about the metadata of the targets. You may also alternatively request TransitiveTargets
from pants.engine.target
if you need all of the transitive dependencies included.
@goal_rule
signatureFor example, if you are writing a publish
goal, and you expect to operate on python_distribution
targets, you might think to request PythonDistribution
in your @goal_rule
signature:
@goal_rule
def publish(distribution: PythonDistribution, console: Console) -> Publish:
...
This will not work because the engine has no path in the rule graph to resolve a PythonDistribution
type given the initial input types to the rule graph (the "roots").
Instead, request Targets
, which will give you all of the targets that the user specified on the command line. The engine knows how to resolve this type because it can go from AddressSpecs
and FilesystemSpecs
-> Specs
-> Addresses
-> Targets
.
From here, filter out the relevant targets you want using the Target API (see Rules and the Target API.
from pants.engine.target import Targets
@goal_rule
def publish(targets: Targets, console: Console) -> Publish:
relevant_targets = [
tgt for tgt in targets
if tgt.has_fields([PythonSources, PythonPublishDestination])
]
Only care about source files?
If you only care about files, and you don't need any metadata from BUILD files, then you can request SourcesSnapshot
instead of Targets
.
from pants.engine.fs import SourcesSnapshot
...
@goal_rule
async def hello_world(console: Console, sources_snapshot: SourcesSnapshot) -> HelloWorld:
for f in sources_snapshot.snapshot.files:
console.print_stdout(f)
return HelloWorld(exit_code=0)
When users use address arguments like ::
, you will get all the sources belonging to the matched targets. When users use file arguments like '**'
, you will get all matching files, even if the file doesn't have any owning target.