bake
Write project tasks as ordinary Rust functions, then run them with cargo bake.
Task functions have typed arguments, generated help, automatic discovery, and a
shared project context. Reusable task libraries are ordinary Cargo dependencies.
This is an initial implementation inspired by Ruby Bake, Bake Releases, and Cargo's xtask pattern.
Try this repository
From a checkout, the included Cargo alias bootstraps the launcher:
The task crate is bake/. Cargo compiles it on demand and caches
the build. --offline and --locked are available before the task name.
To install the launcher locally:
The executable is cargo-bake; Cargo makes it available as cargo bake.
The core library package is bake, and the launcher package is
socketry-cargo-bake:
The earlier socketry-bake package remains available for existing projects; use
bake for new projects.
Add tasks to a project
Add an unpublished bake binary crate to your workspace:
Cargo.toml
src/
bake/
Cargo.toml
src/main.rs
In the project's Cargo.toml:
[]
= ["bake"]
In bake/Cargo.toml, depend on Bake by its published crate version:
[]
= "project-tasks"
= "0.0.0"
= "2024"
= false
[]
= "0.17"
In bake/src/main.rs:
use ;
/// Greet someone, optionally with extra enthusiasm.
#[bake::task] preserves greet and generates greet_task(), which describes
the arguments and adapts command-line input to the original function. It also adds
the descriptor to Bake's link-time registration table. Registry::discover()
collects tasks from the executable and linked task libraries. Nested Rust modules
form namespaces, so a function in releases:: becomes releases:notes.
Arguments and results
| Rust parameter | Command-line behavior |
|---|---|
name: String |
Required positional, also accepts --name value |
#[bake(named)] name: String |
Required named argument |
#[bake(default = 3)] count: usize |
Optional named argument with a typed default |
#[bake(default = "releases.md")] path: PathBuf |
String literal converted to the parameter type |
output: Option<PathBuf> |
Optional named argument, defaults to None |
labels: Vec<String> |
Repeatable named argument, defaults to an empty vector |
#[bake(default = false)] verbose: bool |
--verbose true or --verbose false |
context: &mut Context |
Injected execution context, omitted from command-line arguments |
#[bake(input)] input: Value |
Injected result from the preceding task in a chain |
Values implement FromStr, with a displayable error. Custom argument types can
implement that trait. Defaults other than string literals must produce the
parameter's type. Defaults are evaluated when invoking the task. Parameter help
comes from #[bake(help = "...")]; task help comes from Rust documentation comments.
An explicitly marked #[bake(context)] parameter may have another name.
Named arguments use two tokens: --name value. This also applies to boolean and
repeatable arguments. Equals signs are not a named-argument separator; flag names
accept hyphens in place of underscores. Use -- before positional values that
look like options. :: is reserved as a task separator. UTF-8 task arguments are
required.
Task functions return Result<Output, Error> where Output implements
serde::Serialize and the error implements Display. bake::Result is a
convenience alias. After the final task, Bake invokes its registered output
task unless that task handled output itself. The default output task prints
strings as text, structured values as pretty JSON, and () silently. A leading
--json selects JSON, including for strings and null. Tasks should use stderr
for diagnostics when callers need machine-readable stdout.
The built-in output task also works in a chain. Its input is the previous
task's result, and it returns that result for further processing:
Use --format raw, --format json, or --format ndjson; JSON and NDJSON file
extensions also select a format. Raw text is the default for other file extensions.
Output files are relative to the project root, and their parent directories must exist.
The null task consumes a result without printing it. Mark a custom task with
#[bake::task(output)] if it handles output, or replace the default formatter
with registry.replace("output", custom_output_task()).
Composition and hooks
Chain tasks with ::. Bare task names also start a new task once the preceding
task's positional arguments are filled. Explicit separators make intent clearer:
The entire chain is parsed and supplied values are type-checked before the first
task runs. Execution stops at the first error. Validation calls FromStr before
the adapter converts values again; argument parsers should be free of side effects.
Each invocation receives the same Context. It provides:
root()— the project root determined by the launcher.previous()— the previous successful task's structured result.insert,get,get_mut— shared state indexed by Rust type.call("task:name", &["--argument", "value"])— invoke one task by its full registered name.call_if_registered("task:name", &[...])— invoke an optional task, returningNoneif it is not registered.command("cargo")— astd::process::Commandconfigured to run in the project root.
Hooks are ordinary calls around an operation. For example, this repository's
release:prepare task calls build:check, then releases:notes. Direct Rust
function calls are also available when registry dispatch is unnecessary; they
do not automatically update previous().
Reusable tasks can invoke project-local hooks through the shared registry. The
bake-cargo version tasks optionally call cargo:after_version_bump, passing
the new workspace version. A project can define that task in its private
bake/ crate; if it is absent, the version bump continues without a hook.
Reusable task libraries
Task functions in a library are discovered with the same attribute. Put them in a semantic module to give them a namespace:
Add the library as a Cargo dependency and reference it from the task binary so Rust includes its registration entries in the link:
use bake_releases as _;
discover?.run
This removes per-task registration and namespace boilerplate. Explicit
Registry::register and Registry::include remain available when a project needs
to assemble names dynamically. Duplicate discovered names are errors. The
Bake Releases library provides:
update renames the Unreleased heading in the file. It does not change package
versions, commit, tag, or publish. Release headings use the documented ATX format
such as ## v0.1.0.
The companion Bake Cargo library
provides Cargo workspace tasks, GitHub release creation, publishing workflow
generation, GitHub release protections, and crates.io trusted publishers. Its
shared version tasks optionally invoke cargo:after_version_bump with the new
version. This repository defines the hook to run license:update and
releases:update. cargo:release validates and packages a candidate for a
reviewed release pull request. After the pull request merges, the workflow waits
for the crates-io environment approval, publishes the workspace through
trusted publishing, and creates the vVERSION tag after all uploads succeed.
The initial publish can be followed by
trusted-publisher setup with the explicit cargo:bootstrap PACKAGE task. Review
its effects and package contents before invoking it.
The separately reusable Bake License
library tracks Git authorship, refreshes license.md, removes the README License
section, and updates Rust source copyright headers.
Discovery and configuration
The launcher uses cargo metadata --format-version 1 --no-deps. From a workspace
member it defaults to the workspace's bake/Cargo.toml. Override the path with:
[]
= "development/Cargo.toml"
[package.metadata.bake] takes precedence for the selected package; its path and
execution root are relative to that package. Workspace configuration is relative
to the workspace root. --manifest-path PATH selects the project manifest.
Options that take values use a separate following argument.
The task package must have one binary, or select it with package.default-run.
It can belong to the project workspace, or be a separate workspace excluded from
the parent. A separate task workspace has its own dependency resolution and lockfile.
Launcher options (--manifest-path PATH, --offline, --locked, --release) go
before the task name. Everything from the first task argument onward is forwarded intact.
cargo bake --help explains the launcher without compiling tasks; --list and
TASK --help compile and query the project's task registry. Child exit codes
are preserved. Process arguments are passed directly, without a shell.
Packages
| Published package | Rust library / executable | Purpose |
|---|---|---|
bake |
bake |
Registry, arguments, context, task result handling |
bake-macros |
bake_macros |
Function attribute, re-exported by bake |
socketry-cargo-bake |
cargo-bake |
Project discovery and Cargo launcher |
bake-releases |
bake_releases |
Release-document tasks (repository) |
bake-cargo |
bake_cargo |
Cargo project and release tasks (repository) |
bake-license |
bake_license |
License and copyright maintenance tasks (repository) |
bake-agent-context |
bake_agent_context |
Dependency context tasks (repository) |
For local development of the task binary, check out the task repositories beside
this repository as ../bake-releases-rust, ../bake-cargo-rust, and
../bake-license-rust.
Tasks are synchronous in this initial implementation. An individual task can start a runtime or a subprocess; Bake imposes no async runtime dependency.
Releasing
Prepare a release with cargo bake cargo:version:patch (or minor, major,
or bump --version X.Y.Z), then run cargo bake cargo:release and open a
pull request. After review and merge, GitHub Actions publishes the release
when the configured crates-io environment approves it. See the
Cargo publishing guide.
Context
This crate includes development context, a
design overview, and a guide to
structuring and using task libraries. The local
task executable also includes Bake Agent Context, so run
cargo bake agent:context:install to install context from its dependencies.
The generated .agents/context/ directory is ignored by Git.
For repository-only conventions, see .agents/conventions.md. The shared release instructions are included in the Bake Cargo agent context; see the Cargo publishing guide.
Releases
See releases.md for the full release history.
v0.17.2
- Add
cargo bake --regenerateto create and synchronize a project's private task crate. - Integrate project release, license, agent-context, Readme, and external-test tasks.
- Add external tests for six downstream Socketry projects.
v0.17.1
- Create or update GitHub Releases after successful crates.io publication.
- Resolve the local task crate during version updates.
v0.17.0
- Publish the core library under the
bakepackage name. - Publish task authoring and development guides under
context/. - Add Bake Agent Context tasks to the project task executable.
- Move repository-only conventions and release instructions under
.agents/.
Contributing
Please open an issue or pull request on GitHub.
Agent Context
Before contributing, read agents.md and the relevant context files it links. If agents.md is missing or out of date, run cargo bake agent:context:install to install context from dependencies and update the index.