Goal rules¶
For many plugin tasks, you will be extending 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_rules 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. But unlike a @rule, a @goal_rule may also trigger side effects (such as running interactive processes, writing to the filesystem, etc) via await Effect.
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 @rules, but you do not need to start with writing helper @rules.
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. - Set the class property
nameto the name of your goal. - Set the class property
help, which is used bypants help. - You may register options through attributes of
pants.option.option_typestypes. See Options and subsystems. - Define a subclass of
Goal. When a user runspants my-goal, the engine will request your subclass, which is what causes the@goal_ruleto run. - Set the class property
subsystem_clsto theGoalSubsystemfrom the previous step. - A
Goaltakes a single argument in its constructor,exit_code: int. Pants will use this to determine what its own exit code should be. - Define an
@goal_rule, which must return theGoalfrom the previous step and set itsexit_code. - For most goals, simply return
MyGoal(exit_code=0). Some goals likelintandtestwill instead propagate the error code from the tools they run. - Register the
@goal_rulein aregister.pyfile.
from pants.engine.goal import Goal, GoalSubsystem
from pants.engine.rules import collect_rules, goal_rule
class HelloWorldSubsystem(GoalSubsystem):
name = "hello-world"
help = "An example goal."
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.
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, GoalSubsystem):
name = "hello-world"
help = "An example goal."
...
@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, GoalSubsystem):
name = "hello-world"
help = "An example goal."
...
@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.
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.
Common mistake: requesting the type of target you want in the @goal_rule signature
For 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:
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 the targets that the user specified on the command line. The engine knows how to resolve this type because it can go from Specs -> Addresses -> Targets.
From here, filter out the relevant targets you want using the Target API (see Rules and the Target API).
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 SpecsPaths instead of Targets.
from pants.engine.fs import SpecsPaths
...
@goal_rule
async def hello_world(console: Console, specs_paths: SpecsPaths) -> HelloWorld:
for f in specs_paths.files:
console.print_stdout(f)
return HelloWorld(exit_code=0)
SpecsPaths.files will list all files matched by the specs, e.g. :: will match every file in the project (regardless of if targets own the files).
To convert SpecsPaths into a Digest, use await Get(Digest, PathGlobs(globs=specs_paths.files)).
Name clashing
It is very unlikely, but is still possible that adding a custom goal with an unfortunate name may cause issues when certain existing Pants options are passed in the command line. For instance, executing a goal named local with a particular option (in this case, the global local_cache option), e.g. pants --no-local-cache local ... would fail since there's no --no-cache flag defined for the local goal.