Creating a Battery Pack
A battery pack is a normal Rust crate published on crates.io. It has no real code — just a Cargo.toml that curates dependencies, plus documentation, examples, and optionally templates.
Scaffolding
The fastest way to start is:
cargo bp new battery-pack --name my-battery-pack
This creates a new battery pack project from the built-in template, complete with the right structure, a starter README, and license files.
Anatomy of a battery pack
Here's what a battery pack looks like:
my-battery-pack/
├── Cargo.toml
├── README.md
├── docs.handlebars.md
├── src/
│ └── lib.rs
├── examples/
│ ├── basic.rs
│ └── advanced.rs
└── templates/
└── default/
├── bp-template.toml
├── _Cargo.toml
└── src/
└── main.rs
The important parts:
- Cargo.toml — defines the curated crates as dependencies
- README.md — your prose documentation
- docs.handlebars.md — template for auto-generated docs on docs.rs
- src/lib.rs — just a doc include (no real code)
- examples/ — runnable examples showing the crates in action
- templates/ — project templates for
cargo bp new
Defining crates
Your curated crates are just regular Cargo dependencies. The dependency section they live in determines the default dependency kind for users:
[dependencies]
anyhow = "1"
thiserror = "2"
[dev-dependencies]
expect-test = "1.5"
[build-dependencies]
cc = "1"
When a user installs your battery pack:
anyhowandthiserrordefault to regular dependenciesexpect-testdefaults to a dev-dependencyccdefaults to a build-dependency
Users can override these in the TUI.
Features for grouping
Use Cargo's [features] to organize crates into groups:
[dev-dependencies]
clap = { version = "4", features = ["derive"] }
dialoguer = "0.11"
indicatif = { version = "0.17", optional = true }
console = { version = "0.15", optional = true }
[features]
default = ["clap", "dialoguer"]
indicators = ["indicatif", "console"]
fancy = ["clap", "indicatif", "console"]
The default feature
The default feature determines which crates a user gets with a plain
cargo bp add. Crates not in default are available but not installed
unless the user explicitly enables them (e.g., cargo bp add cli -F indicators).
If you don't define a default feature, all non-optional crates
are included by default.
Optional crates
Mark crates as optional = true if they shouldn't be part of the default
installation. These crates are available through named features or
individual selection in the TUI.
Feature augmentation
A feature can add Cargo features to a crate, not just toggle it on.
This uses Cargo's native dep/feature syntax:
[dependencies]
tokio = { version = "1", features = ["macros", "rt"] }
[features]
default = ["tokio"]
tokio-full = ["tokio/full"]
Enabling tokio-full keeps tokio but adds the full feature on top
of macros and rt. Feature merging is always additive.
Hidden dependencies
If your battery pack has dependencies that are internal tooling — not
something users would want to install — mark them as hidden. Every
battery pack should hide the battery-pack build dependency (used for
doc generation), along with any other internal crates:
[package.metadata.battery-pack]
hidden = ["battery-pack", "handlebars", "cargo-metadata"]
Hidden crates don't appear in the TUI or in cargo bp show output.
You can use globs:
[package.metadata.battery-pack]
hidden = ["serde*"]
Or hide everything (useful if your battery pack is purely templates and examples):
[package.metadata.battery-pack]
hidden = ["*"]
The lib.rs
A battery pack's lib.rs is minimal — it just includes auto-generated documentation:
#![allow(unused)] #![doc = include_str!(concat!(env!("OUT_DIR"), "/docs.md"))] fn main() { }
This makes the battery pack's documentation visible on docs.rs, including an auto-generated table of all curated crates. See Documentation and Examples for details on how the doc generation works.
Templates
Templates let users bootstrap new projects with cargo bp new.
They use MiniJinja templates
with a bp-template.toml configuration file.
A template lives in a subdirectory under templates/:
templates/
└── default/
├── bp-template.toml
├── _Cargo.toml
└── src/
└── main.rs
Note: Template
Cargo.tomlfiles must be named_Cargo.toml.cargo packagetreats any subdirectory containing aCargo.tomlas a separate crate and excludes it from the published tarball. The template engine automatically maps_Cargo.tomlback toCargo.tomlin the generated output.
The bp-template.toml configures template variables:
ignore = ["hooks"]
[placeholders.description]
type = "string"
prompt = "What does this project do?"
default = "A new project"
Placeholders should have default values so that cargo bp validate
can generate and check templates non-interactively. Placeholder names
must use snake_case (description, not my-description) because
MiniJinja treats - as the minus operator.
The template engine also provides built-in variables (no declaration needed):
{{ project_name }}— the project name passed via--name{{ crate_name }}— derived fromproject_namewith-replaced by_
Built-in functions
Templates can call these functions in MiniJinja expressions:
{{ pin_github_action("actions/checkout", "v6") }}— resolves a GitHub Action tag to a SHA-pinned reference at generation time (e.g.actions/checkout@abc123 # v6.0.2). Semver-aware: finds the latest version under the given major tag. Supports an optional subpath:{{ pin_github_action("github/codeql-action", "v3", "upload-sarif") }}.{{ rust_stable_version() }}— returns the current stable Rust version fromrustc --version(e.g.1.80.0).
Placeholder types
Placeholders support three types:
# String (default)
[placeholders.description]
type = "string"
prompt = "Project description"
default = "A new project"
# Bool — interactive: yes/no prompt, non-interactive: defaults to false
[placeholders.benchmarks]
type = "bool"
prompt = "Include benchmarks?"
# Select — interactive: arrow-key selection, requires explicit default
[placeholders.ci_platform]
type = "select"
prompt = "CI platform"
options = ["github", "none"]
default = "github"
Bool values are registered as actual booleans in MiniJinja, so
{% if benchmarks %} works naturally. Bare -d benchmarks on the
command line implies =true.
To include files from outside the template directory (e.g. shared
license files), use [[files]]:
[[files]]
src = "LICENSE-MIT" # relative to crate root
dest = "LICENSE-MIT" # relative to generated project
Register templates in your Cargo.toml metadata:
[package.metadata.battery.templates]
default = { path = "templates/default", description = "A basic starting point" }
subcmds = { path = "templates/subcmds", description = "Multi-command CLI" }
If you have multiple templates, users can choose:
cargo bp new my-pack --template subcmds
Managed dependencies
Use bp-managed = true on dependencies in your template's _Cargo.toml
instead of hardcoding versions. When someone generates a project from
your template, cargo bp resolves the actual versions from your
battery pack's spec:
[dependencies]
clap.bp-managed = true
[build-dependencies]
cli-battery-pack.bp-managed = true
[package.metadata.battery-pack]
cli-battery-pack = { features = ["default"] }
This way you don't need to update template files when you bump dependency versions. The template always picks up the current spec.
bp-managed = true resolves the version from the spec. If no explicit
features are given, the spec's features are used. You can override
features or add other keys like optional alongside bp-managed:
# Managed: version and features come from the spec:
anyhow.bp-managed = true
# Managed version, explicit features (overrides spec features):
clap = { bp-managed = true, features = ["derive", "env"] }
# Managed version with optional:
serde = { bp-managed = true, optional = true }
The only key that conflicts with bp-managed is version (since
bp-managed provides the version).
Validating templates
cargo bp validate automatically generates each template, runs
cargo check and cargo test on the result, and reports failures.
This catches broken templates before they reach users.
To run template validation in your CI tests, add a test in your src/lib.rs:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { #[test] fn validate() { ::battery_pack::testing::validate(env!("CARGO_MANIFEST_DIR")).unwrap(); } } }
The built-in scaffolding template includes this test by default.
Merge-friendly templates
Templates can also be applied to existing projects with cargo bp add <pack> -t <template>. This is especially useful for "micro" templates that add a single concern (CI workflow, fuzzing scaffold, spellcheck config, etc.).
When merged into an existing project, cargo bp handles files by type:
Cargo.toml: dependencies are merged (versions upgraded if behind, missing features added)- Other
.toml: new sections and keys added, existing ones left alone - YAML: top-level keys merged additively (new jobs added, existing ones preserved)
- Other files: the user is prompted to skip or overwrite if they already exist
Note: YAML merges don't preserve comments in existing files. For YAML, only jobs, on, and permissions are deep-merged; other top-level keys are atomic, so if the key already exists, the user's value wins. To avoid conflicts, use unique workflow filenames (e.g., typos.yml instead of ci.yml).
To help users with steps that can't be automated (like adding mod declarations or installing tools), declare hints in your bp-template.toml:
[[hints]]
message = "Add `mod errors;` to your lib.rs or main.rs"
[[hints]]
message = "Run `cargo install cargo-fuzz` if you haven't already"
Hints are printed after the merge summary. They're only shown for cargo bp add -t, not for cargo bp new.
Tips for writing merge-friendly templates:
- Keep template
Cargo.tomlfiles minimal; only include dependencies the template actually needs. - Use
bp-managed = truefor dependencies so versions stay current with the battery pack spec. - Use
[[hints]]for anything the user needs to do manually after the merge.