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
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_
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 replaces the entire dependency entry with the
version and features from the spec. If you need to pin a specific
version or customize features for a dependency, use an explicit
entry instead:
# Managed: version and features come from the spec:
anyhow.bp-managed = true
# Explicit: left as-is during resolution:
clap = { version = "4", features = ["derive", "color"] }
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_templates() { battery_pack::testing::validate_templates(env!("CARGO_MANIFEST_DIR")).unwrap(); } } }
The built-in scaffolding template includes this test by default.