SKILL.md
Pants Plugin Creator
Guide users through creating production-grade Pants build system plugins following official best practices.
What This Skill Does
- Gathers requirements through focused questions
- Generates complete plugin structure using native file tools
- Customizes targets, rules, and goals for specific use cases
- Provides testing templates and guidance
- Explains development and publishing workflow
Workflow
Phase 1: Requirements Gathering
Ask these questions (adapt based on responses):
1. Plugin name: What should your plugin be called?
- Must be lowercase with hyphens (e.g.,
my-linter-plugin) - Will create package name with underscores (e.g.,
mylinterplugin)
2. Purpose: What does your plugin do?
- Lint/format code (integrates with external linter)
- Generate code from definitions (protobuf, thrift, etc.)
- Build/compile custom assets
- Integrate with external tools
- Other custom processing
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?
- If yes, ask what metadata users will specify in BUILD files
6. Configuration: What options should users configure in pants.toml?
Phase 2: Generate Plugin Structure
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
Phase 3: File Generation Templates
Use the templates from reference/templates.md for each file. Key files:
pyproject.toml
[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"]
register.py (REQUIRED - Pants loads this)
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]
subsystem.py (Configuration)
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.")
targets.py (What data you need)
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)
rules.py (How you process data)
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()
goals.py (User-facing commands)
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)
Phase 4: Customization Based on Plugin Type
For Linter Plugins
Read reference/patterns/linter.md for complete pattern:
- Define FieldSet with SourcesField
- Use Process to invoke external linter tool
- Return LintResult with exit_code and output
For Codegen Plugins
Read reference/patterns/codegen.md for complete pattern:
- Define input target with definition field
- Use CreateDigest for output files
- Return CodegenResult with Digest
For Custom Target Plugins
Read reference/patterns/custom-target.md for complete pattern:
- Define Field classes for each metadata piece
- Create Target with core_fields tuple
- Register in target_types()
Phase 5: Testing Guidance
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
Phase 6: Development Workflow
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
Phase 7: Publishing Options
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}"]
Critical Reminders
When generating plugin code, always ensure:
- Rules must be pure - No side effects, no print(), no direct file I/O
- Use frozen dataclasses - All rule outputs:
@dataclass(frozen=True) - Type hints required - All parameters and returns must be typed
- Use Get() for dependencies - Never call rules directly
- Deterministic outputs - Same inputs must produce same outputs
- Use Process for subprocesses - Hermetic execution via the engine
v2.30+ Gotchas
Critical issues specific to Pants 2.30+:
- Namespace subsystem scopes - Use
myplugin-ruffnotruffto avoid conflicts with built-in backends - One goal definition per scope - Define each
GoalSubsystem.namein exactly one place - Use 3-arg Get() syntax -
Get(Output, Input, value)not dict syntax (it's broken) - No
from future import annotations- Breaks Pants runtime type inference - Subsystems auto-register - Don't call
.rules()on Subsystem classes - Bump version for local testing - Pants caches wheels by filename
See develop-pants-plugin skill's reference/gotchas.md for detailed fixes.
Reference Documentation
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 pattern
Example Session
User: "I want to create a Pants plugin that runs shellcheck on shell scripts"
Response:
- Ask for plugin name, author, org
- Generate plugin structure with:
- Target: shellcheck_sources with sources field - Rule: Runs shellcheck process on source files - Goal: shellcheck command for users - Subsystem: Configuration for shellcheck version, args
- Generate tests
- Provide development instructions