cargo-rake 0.5.3

A configuration-driven build tool that runs named targets declared in a Rakefile.toml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# cargo-rake

A configuration-driven build tool that runs named targets declared in a
`Rakefile.toml`, in dependency order. It ships as both a `cargo` subcommand
(`cargo rake`) and a standalone `rake` binary.

## Installation

The two binaries are distributed through different channels:

- **`cargo-rake`** (the `cargo rake` subcommand) — through cargo.
- **`rake`** (the standalone binary) — through system package managers.

### cargo (`cargo-rake`)

```bash
cargo binstall cargo-rake   # download a pre-built binary, no compile
cargo install cargo-rake    # build and install from crates.io
```

### Arch Linux (`rake`)

Available from the AUR in four mutually-exclusive flavors — pre-built or
built-from-source, each in a stable and a nightly `unstable`-feature variant:

```bash
paru -S rake-bin            # pre-built binary (recommended)
paru -S rake-unstable-bin   # pre-built, nightly unstable build
paru -S rake                # build from source
paru -S rake-unstable       # build from source, nightly unstable build
```

### Debian/Ubuntu (`rake`)

#### From the apt repository (recommended)

The signed apt repository at <https://rustyhorde.github.io/cargo-rake-packages/>
tracks every release, so `apt upgrade` keeps `rake` current. Packages are
available for `amd64` and `arm64`:

```bash
# Add the repository signing key
sudo install -d /etc/apt/keyrings
curl -fsSL https://rustyhorde.github.io/cargo-rake-packages/gpg.key \
    | sudo gpg --dearmor -o /etc/apt/keyrings/rake.gpg

# Add the apt source
echo "deb [arch=amd64,arm64 signed-by=/etc/apt/keyrings/rake.gpg] \
  https://rustyhorde.github.io/cargo-rake-packages/apt stable main" \
    | sudo tee /etc/apt/sources.list.d/rake.list

# Install
sudo apt update
sudo apt install rake
```

The same repository also carries the `rake-unstable` build (the `unstable`
feature is a functional no-op — it only toggles nightly-only lint gates).
`rake-unstable` conflicts with `rake`, so only one can be installed at a time.

#### From a downloaded `.deb`

Pre-built `.deb` packages are also attached to each
[GitHub release](https://github.com/rustyhorde/cargo-rake/releases) if you prefer
not to add the repository. `dpkg -i` runs as root and works from any location:

```bash
sudo dpkg -i rake_*_amd64.deb
```

### Fedora/RHEL (`rake`)

#### From the dnf repository (recommended)

Pre-built `.rpm` packages for `x86_64` and `aarch64` are served from the signed
dnf repository at <https://rustyhorde.github.io/cargo-rake-packages/>, so
`dnf upgrade` keeps `rake` current:

```bash
# Add the repository (imports the signing key on first install)
sudo dnf config-manager \
    --add-repo https://rustyhorde.github.io/cargo-rake-packages/rpm/rake.repo

# Install
sudo dnf install rake
```

> On older releases the subcommand is `sudo dnf config-manager addrepo
> --from-repofile=…`, and on dnf 4 you may need `sudo dnf install
> dnf-plugins-core` first.

The `rake-unstable` build is available from the same repository by name; it
conflicts with `rake`, so only one can be installed at a time.

#### From a downloaded `.rpm`

`.rpm` files are also attached to each
[GitHub release](https://github.com/rustyhorde/cargo-rake/releases) for direct
installation:

```bash
sudo dnf install ./rake-*.x86_64.rpm   # or: sudo rpm -i rake-*.x86_64.rpm
```

### macOS (`rake`)

Pre-compiled binaries for **Apple Silicon (aarch64)** are published to the
[`rustyhorde/rake`](https://github.com/rustyhorde/homebrew-rake) Homebrew tap.
Intel Macs are not currently supported.

```bash
brew tap rustyhorde/rake
brew install rake
```

## Self-Update

`cargo-rake` (the `cargo rake` subcommand) checks crates.io for a newer version of itself on
every startup and installs it automatically via `cargo install cargo-rake` if one is found. After
a successful update, the binary relaunches itself with the original arguments so the newly
installed version handles the actual work.

`rake` (the standalone binary) is distributed through system package managers and does not
self-update via this mechanism — use your package manager to update it.

**Opt out** per-project by adding `update = false` to your `Rakefile.toml`:

```toml
update = false
```

When the key is absent the check is **enabled** (default `true`). The check is skipped entirely
in `--dry-run` mode. Network and version-check failures are non-fatal: a `Warning` line is
printed and the run continues normally.

## Rakefile.toml

A `Rakefile.toml` declares named **targets**. Each target owns an ordered array
of named **commands**, an optional `depends_on` list, and an optional `tools`
list. Tools are defined once in a top-level `[tool]` table. A complete example:

```toml
# toolchain = "stable"   # optional; pins both binaries to a specific Rust channel via rustup
# update    = false      # optional; set false to disable cargo-rake's self-update check

# Cargo-installable tools (cargo subcommands, etc.)
[tool.cargo.nextest]
crate   = "cargo-nextest"                         # crates.io name (required for update checks)
check   = ["cargo", "nextest", "--version"]       # probe: zero exit = installed
install = ["cargo", "install", "cargo-nextest", "--force", "--locked"]
update  = true                                    # re-install when a newer crates.io version exists

# OS-level tools that rake cannot install (docker, pkg-config, …)
[tool.os.docker]
check = ["docker", "--version"]
hint  = "install Docker: https://docs.docker.com/get-docker/"  # shown when absent and no install

# Fish shell function dependencies — probed with `fish -c "functions --query <name>"`
[tool.fish.puc]
hint = "define puc in ~/.config/fish/functions/puc.fish"

# ── Targets ──────────────────────────────────────────────────────────────────

[[target.build.command]]
name = "compile"           # label shown in `list` output and error messages (required)
cmd  = ["cargo", "build"]  # program + args, spawned directly — no shell involved

[target.test]
depends_on = ["build"]     # run these targets first, in order
tools      = ["nextest"]   # ensure these tools before the target's commands run

[[target.test.command]]
name = "run"
cmd  = ["cargo", "nextest", "run"]

[[target.check.command]]
name          = "fmt"
cmd           = ["cargo", "fmt", "--check"]
skip_on_error = true       # tolerate a non-zero exit and keep going

[target.package]
tools = ["docker"]

[[target.package.command]]
name     = "archive"
platform = ["linux", "macos"]               # skipped silently on other platforms
sh       = "tar czf dist.tgz \"$(pwd)\"/target/release/*"
fish     = "tar czf dist.tgz (pwd)/target/release/*"

[[target.package.command]]
name     = "zip"
platform = ["windows"]
ps       = "Compress-Archive target/release/* dist.zip"

[target.puc]
tools = ["puc"]

[[target.puc.command]]
name = "puc"
fish = "puc"

# macOS-specific variant — active only on macOS.  A base [target.notarize]
# is defined below as a cross-platform no-op so that `all` can depend on it
# without TargetNotAvailableOnPlatform on other hosts.
[target.notarize.macos]

[[target.notarize.macos.command]]
name = "submit"
cmd  = ["xcrun", "notarytool", "submit", "dist.pkg"]

[target.notarize]            # base: no-op on non-macOS hosts
[[target.notarize.command]]
name = "skip"
sh   = "true"
ps   = "exit 0"

[target.all]
depends_on = ["build", "test", "check", "package", "notarize"]

[target.default]
depends_on = ["test"]
```

- A command sets **one kind** of body: either `cmd`, or one or more shell
  variants (`sh` / `fish` / `ps`). `cmd` is mutually exclusive with the shell
  variants; the shell variants may coexist.
  - **`cmd`** is a program followed by its arguments, spawned directly — no
    shell is involved — so it behaves the same on every platform.
  - **`sh` / `fish` / `ps`** are each a single command line run through that
    shell, so shell features (`$(...)` substitution, `~`/`$VAR` expansion, globs,
    pipes) apply: `sh -c`, `fish -c`, and PowerShell `-Command` (`pwsh` if on
    `PATH`, else `powershell`) respectively. rake resolves the current shell in
    priority order: (0) `RAKE_SHELL` env variable — set it to any shell name
    (`fish`, `sh`, `bash`, `pwsh`, …) to pin the family, skipping all other
    detection; (1) a PowerShell environment variable
    (`POWERSHELL_DISTRIBUTION_CHANNEL` or `PSModulePath` on non-Windows) —
    checked before `$SHELL` because PowerShell does not set `$SHELL`; (2)
    `FISH_VERSION` env variable, exported by fish to all child processes —
    checked before `$SHELL` because `$SHELL` reflects the login shell, not the
    running shell; (3) `$SHELL`'s basename; (4) the platform default (`ps` on
    Windows, Posix otherwise).
    Selection is **strict**: if the detected shell has no matching variant, the
    run aborts with an error — so define a variant for every shell a command must
    run under. A `platform`/`arch` mismatch, by contrast, is a silent skip, not
    an error.
- **`[[target.<t>.command]]`** is a TOML array of tables. Each entry needs a
  `name` (a label used in `list` output and error messages) and a body (`cmd`
  or one or more of `sh`/`fish`/`ps`). Commands run in **array (declaration)
  order**. (TOML table headers are absolute, so the `target.<t>.command` prefix
  is required on each entry.)
- **`skip_on_error`** (per command, default `false`): when `true`, a non-zero
  exit from that command is tolerated and the target continues with its
  remaining commands instead of aborting.
- **`depends_on`** lists other targets that must run, in order, before this one. Prefix an
  entry with `^` (e.g. `depends_on = ["all", "^install"]`) to embed a skip for that
  dependency — the skipped target (and any dep reachable only through it) is pruned
  whenever this target is in the run. This complements the CLI `^target` syntax.
- **`tools`** — tools can be declared at **two levels**: on `[target.<name>]`
  (ensured before every command in that target) or on
  `[[target.<t>.command]]` (ensured immediately before that specific command,
  and only when it is not platform/arch-skipped). A name declared at both
  levels for the same target is a parse-time error. See [Tools]#tools.
- **Platform-specific targets** — add a platform token as a sub-table key to
  declare a platform-specific variant of a target (e.g. `[target.sign.macos]`,
  `[target.sign.linux]`). The most specific match wins: an OS token (e.g.
  `linux`) beats a family token (e.g. `unix`) when both variants exist for the
  current host. If no variant matches and no base `[target.sign]` exists, any
  target that `depends_on` it fails with `TargetNotAvailableOnPlatform` — add a
  base variant as a cross-platform fallback (or no-op) when needed. The
  `notarize` target above shows this pattern.

  Valid **OS tokens** (match `std::env::consts::OS`):
  `linux`, `macos`, `windows`, `freebsd`, `netbsd`, `openbsd`, `dragonfly`,
  `solaris`, `illumos`, `android`, `ios`, `tvos`, `watchos`, `visionos`,
  `fuchsia`, `redox`, `haiku`, `hurd`, `aix`, `nto`, `emscripten`, `wasi`

  Valid **family tokens** (match `std::env::consts::FAMILY`): `unix`, `windows`, `wasm`

  An unknown token, or any key other than `depends_on`/`tools`/`command` and the
  platform tokens above, is a hard parse-time error.
- **`platform`** (optional list, per command) names OS or family tokens
  (`linux`/`macos`/`windows`/`unix`/…). **`arch`** (optional list, per command
  only) names architecture tokens (`x86_64`/`aarch64`/…). Both lists are
  validated at parse time; an empty list or an unknown token is a hard error. A
  command runs only when every declared dimension matches (AND across
  `platform`/`arch`, OR within each list). A non-matching command is **silently
  skipped** — a `Skipped` status line is printed and the target's remaining
  commands continue.
- **Skipping targets**: prefix a target name with `^` to exclude it from the run
  (e.g. `rake all ^clean`). The skipped target and any dependency reachable only
  through it are pruned. A skip is not allowed if another non-root target that
  still runs `depends_on` the skipped target — the run fails fast. With only
  skip tokens (e.g. `rake ^clean`), the `default` target runs.

### Tools

Tools are defined in a top-level `[tool]` table, split into three categories:
**`[tool.cargo.<name>]`** for cargo-installable tools (cargo subcommands and
the like), **`[tool.os.<name>]`** for OS-level dependencies (`docker`,
`pkg-config`, …) that `rake` cannot `cargo install`, and **`[tool.fish.<name>]`**
for fish shell function dependencies. The three categories share one flat
reference namespace, so a name must be unique across all of them.

Tools can be referenced at **two levels**:

- **Target-level** (`tools = [...]` on `[target.<name>]`): the named tools are
  ensured before any of the target's commands run.
- **Command-level** (`tools = [...]` on `[[target.<t>.command]]`): the named
  tools are ensured immediately before *that specific command* runs — and only
  when the command is not already skipped by its `platform`/`arch` gates. This
  lets platform-specific commands declare their own tool dependencies without
  requiring the same tools on every platform.

A tool name may not appear in both a target's `tools` list and a command's
`tools` list within the same target — that is a parse-time error. Use
target-level `tools` when a dependency is shared by all commands in the target,
and command-level `tools` when only a specific (often platform-gated) command
needs it:

```toml
[tool.os.gpg]
check = ["gpg", "--version"]

[tool.os.xcrun]
check = ["xcrun", "--version"]

[target.sign]
# No target-level tools here — each platform needs a different signing tool.

[[target.sign.command]]
name     = "gpg-sign"
platform = ["linux"]
sh       = "gpg --detach-sign dist.tgz"
tools    = ["gpg"]                  # only ensured on linux

[[target.sign.command]]
name     = "notarize"
platform = ["macos"]
cmd      = ["xcrun", "notarytool", "submit", "dist.pkg"]
tools    = ["xcrun"]                # only ensured on macOS
```

Tools are ensured lazily: a tool's `check` only runs when a target (or command)
that references it is actually run (never at parse time or for unrelated targets),
and at most once per run across both levels. Before a target's commands run, each
tool it references is ensured:

- The **`check`** command probes whether the tool is already installed: it must
  **exit 0 when the tool is present**. For a cargo subcommand this means
  `["cargo", "<sub>", "--version"]` — the bare `cargo-<sub>` binary rejects
  `--version` and exits non-zero, which would make the tool look perpetually
  absent.

**Cargo tools** (`[tool.cargo.<name>]`):

- When absent, `rake` prints an `Installing` notice and runs **`install`** (both
  `check` and `install` are required).
- **`update`** (default `false`): when `true`, the installed version (parsed
  from `check`'s output) is compared against the latest reported by the tool's
  **`semver_check`** mode and re-installed if a newer one exists. The only mode
  today is `"crates-io"` (the default), which queries the crates.io API and so
  needs a **`crate`** name; a failed version check — or a `check` whose output
  has no parseable version — is non-fatal and keeps the installed version. With
  `update = false`, the already-installed version is used as-is.
- A failed `install` aborts the run (unlike a tolerated command failure).

**OS tools** (`[tool.os.<name>]`): only `check` is required; there is no update
support.

- When absent and an **`install`** is declared, `rake` runs it (like a cargo
  tool). When absent and no `install` is declared, the run **aborts** with a
  message stating the requirement, plus any **`hint`**`rake` does not try to
  install OS dependencies itself.

**Fish tools** (`[tool.fish.<name>]`): the TOML key is the fish function name to
check. No `check` or `install` fields are needed — `rake` always probes with
`fish -c "functions --query <name>"`, covering user-defined, autoloaded, and
builtin functions. When absent the run aborts with the requirement and any
**`hint`**. Fish tools have no update support.

### Execution

A target runs after its transitive dependencies. Within a target, commands run
in array order and execution **stops at the first command that exits non-zero**,
aborting the dependency chain — unless that command sets `skip_on_error = true`,
in which case the failure is tolerated and execution continues. The process
exits with the code of the command that stopped the run (or the last command if
all succeeded).

When multiple roots are given (e.g. `rake build test`), each root's dependency
graph runs in full in the order given. A target shared by two roots runs once per
root (no cross-root deduplication). Tools, however, are ensured at most once for
the whole run regardless of how many roots reference them.

## Usage

```bash
cargo rake <target>              # run a target (and its dependencies)
cargo rake all ^clean            # skip 'clean' (and any dep reachable only through it)
cargo rake --dry-run <target>    # preview what would run without executing anything
cargo rake list                  # list all targets and their commands
cargo rake syntax                # parse + validate the Rakefile, reporting any errors
cargo rake --file path/to/Rakefile.toml <target>

rake <target>                    # the standalone binary, same interface
```

`list` and `syntax` are reserved subcommand names: a target sharing one of
those names cannot be run by name (run it via a parent target instead).

When no target is named, the `default` target runs.