Create production-grade Pants build system plugins. Use when the user wants to create a Pants plugin, scaffold a Pants backend, or build custom build system functionality for Pants.
Guide users through creating production-grade Pants build system plugins following official best practices.
Ask these questions (adapt based on responses):
1. Plugin name: What should your plugin be called?
my-linter-plugin)my_linter_plugin)2. Purpose: What does your plugin do?
3. Author info: Name and email for pyproject.toml
4. Organization: GitHub org/username for repository URLs
5. Custom targets: Does your plugin need custom target types?
6. Configuration: What options should users configure in pants.toml?
Create the following directory structure:
{plugin-name}/
āāā pyproject.toml
āāā README.md
āāā LICENSE
āāā Makefile
āāā .gitignore
āāā src/{package}/
ā āāā __init__.py
ā āāā version.py
ā āāā register.py # CRITICAL: Entry point
ā āāā subsystem.py # Configuration
ā āāā targets.py # Target definitions
ā āāā rules.py # Business logic
ā āāā goals.py # User commands
āāā tests/
ā āāā conftest.py
ā āāā unit/
ā ā āāā __init__.py
ā ā āāā test_subsystem.py
ā āāā integration/
ā āāā __init__.py
āāā docs/
āāā index.md
Use the templates from reference/templates.md for each file. Key files:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "{plugin-name}"
version = "0.1.0"
description = "{description}"
requires-python = ">=3.12,<4"
authors = [{name = "{author}", email = "{email}"}]
[tool.hatch.build.targets.wheel]
packages = ["src/{package}"]
[tool.hatch.envs.default.scripts]
test = "pytest {args:tests}"
lint = ["black --check src tests", "mypy src"]
fmt = ["black src tests", "isort src tests"]
from pants.engine.rules import collect_rules
from {package} import rules as plugin_rules
from {package}.subsystem import PluginSubsystem
from {package}.targets import CustomTarget
def rules():
return [*collect_rules(plugin_rules), *PluginSubsystem.rules()]
def target_types():
return [CustomTarget]
from pants.option.subsystem import Subsystem
from pants.option.option_types import BoolOption, StrOption
class PluginSubsystem(Subsystem):
options_scope = "{scope}"
help = "Options for {plugin-name}"
enabled = BoolOption(default=True, help="Whether to enable this plugin.")
from pants.engine.target import Target, COMMON_TARGET_FIELDS, StringSequenceField
class SourcesField(StringSequenceField):
alias = "sources"
help = "Source files for this target"
default = ()
class CustomTarget(Target):
alias = "custom_target"
help = "A custom target type"
core_fields = (*COMMON_TARGET_FIELDS, SourcesField)
from dataclasses import dataclass
from pants.engine.rules import rule, collect_rules
@dataclass(frozen=True)
class ProcessedOutput:
target_name: str
exit_code: int = 0
@rule
async def process_target(wrapped: WrappedTarget) -> ProcessedOutput:
target = wrapped.target
return ProcessedOutput(target_name=target.name, exit_code=0)
def rules():
return collect_rules()
from pants.engine.goal import Goal, GoalSubsystem, goal_rule
from pants.engine.console import Console
class PluginGoal(Goal):
subsystem_cls = PluginGoalSubsystem
@goal_rule
async def run_plugin(console: Console, targets: Targets) -> PluginGoal:
console.print_stdout(f"Processing {len(targets)} targets")
return PluginGoal(exit_code=0)
Read reference/patterns/linter.md for complete pattern:
Read reference/patterns/codegen.md for complete pattern:
Read reference/patterns/custom-target.md for complete pattern:
Generate test files using RuleRunner:
from pants.testutil.rule_runner import RuleRunner, QueryRule
@pytest.fixture
def rule_runner():
from {package}.register import rules, target_types
return RuleRunner(rules=rules(), target_types=target_types())
def test_process_target(rule_runner):
rule_runner.write_files({
"BUILD": 'custom_target(name="test", sources=["*.txt"])',
"file.txt": "content",
})
# Test your rules here
After generating the plugin, instruct the user:
cd {plugin-name}
hatch env create # Create dev environment
hatch run test # Run tests
hatch run fmt # Format code
hatch run lint # Check code quality
Option A: Publish to PyPI
hatch build
hatch publish
Users install with:
[GLOBAL]
plugins = ["{plugin-name}==1.0.0"]
backend_packages = ["{package}"]
Option B: In-repo plugin
[GLOBAL]
pythonpath = ["%(buildroot)s/pants-plugins/{plugin-name}/src"]
backend_packages = ["{package}"]
When generating plugin code, always ensure:
@dataclass(frozen=True)Critical issues specific to Pants 2.30+:
myplugin-ruff not ruff to avoid conflicts with built-in backendsGoalSubsystem.name in exactly one placeGet(Output, Input, value) not dict syntax (it's broken)from __future__ import annotations - Breaks Pants runtime type inference.rules() on Subsystem classesSee develop-pants-plugin skill's reference/gotchas.md for detailed fixes.
For detailed information, read these supporting files:
reference/quickstart.md - 5-minute intro to Pants pluginsreference/api-reference.md - Complete API documentation (Target API, Rules API)reference/developer-guide.md - Best practices, caching, common pitfallsreference/templates.md - Complete file templates with full contentreference/patterns/linter.md - Complete linter plugin patternreference/patterns/codegen.md - Complete code generation patternreference/patterns/custom-target.md - Custom target type patternUser: "I want to create a Pants plugin that runs shellcheck on shell scripts"
Response:
shellcheck_sources with sources fieldshellcheck command for users