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
version: "3"
chore_min_version: 0.6.0
# THE CONTRACT THIS FILE EXISTS TO OWN.
#
# How to build rust-fs-core is knowledge that belongs to rust-fs-core. Every
# sibling that depends on it — rust-fs-{ext4,ntfs,squashfs,erofs,xfs,btrfs},
# rust-partitions, rust-img-{qcow2,vhd,vhdx,vmdk} — resolves it as
# `rust-fs-core = { path = "../rust-fs-core" }`, and each of them ships
# `include/fs_core.h` alongside its own header because the header it emits
# `#include`s it.
#
# WHAT CONSUMERS ACTUALLY TAKE FROM HERE IS THE HEADER, NOT THE ARCHIVE.
# fs_core is linked into every driver's staticlib already — that is what the
# `crate-type = ["staticlib", "rlib"]` in Cargo.toml produces. A consumer that
# ALSO linked libfs_core.a would present ld with two strong definitions of
# every `fs_core_*` C export. So `staticlib` below exists for uniformity with
# the sibling libraries and for anyone building this crate on its own; no
# consumer links its output directly.
#
# IT TAKES NO OUTPUT DIRECTORY. `staticlib` builds into this crate's own
# dist/; `chore artifact` prints the absolute path of that DIRECTORY, and a
# consumer copies its contents. A library does not write into its consumer.
#
# `artifact` PRINTS A DIRECTORY, NOT A FILE. That is the one place this
# differs from rust-blk-probe, whose single binary lets its `artifact` name a
# file: this crate produces libfs_core.a AND include/fs_core.h, so there is
# no single path to name.
#
# QUIET TEST TIERS, WITH MEASURED BUDGETS AND EXECUTED-TEST FLOORS.
#
# Each tier keeps its complete transcript in tmp/logs/, prints one verdict on
# success, prints the tail on failure, and exits 65 when a passing run exceeds
# either budget. `chore test -- --verbose` or AM_FS_CORE_VERBOSE=1 streams the
# transcript without lifting the budget.
#
# Measured from a clean Linux checkout on 2026-09-21. Debug produced 557 lines
# / 31,250 bytes and executed 365 tests; the 750-line / 50,000-byte ceiling
# leaves room for cold-build and platform variation, while the floor of 330
# catches a tier that silently stops running a material part of the suite.
# Release uses the same selection and measured the same 557 lines / 31,295
# bytes after its cold LTO build. Coverage measured 575 lines / 32,607 bytes
# and executed the same 374 tests as the post-change debug run:
#
# tier measured locally budget floor
# debug 557 / 31,250 bytes 1000 / 60,000 330
# release 751 lines (CI, cold) 1000 / 60,000 330
# coverage 575 / 32,607 bytes 800 / 60,000 330
# semver (see below) 40 / 4,000 -
#
# THE DEBUG AND RELEASE ROWS MOVED ON 2026-10-06, from 750 / 50,000: the v0.3.5
# release's test (release) tier printed 751 lines on CI (run 37490111355), the
# suite having grown with the family scripts' tests since the measurement
# above. That plus about a third; debug runs the same selection.
#
# The semver row is rust-fs-ext4's measurement of the same script: 11 lines /
# 489 bytes passing with the baseline's rustdoc cached, one line more when it
# is built. Its lines are fixed, not per item, so 40 / 4,000 leaves room for a
# warning block without hiding a flood. It has no floor: it runs one check.
#
# scripts/output-budget.sh is the one canonical neutral family source. See
# docs/output-budget.md; consumers invoke it through their pinned
# ../rust-fs-core sibling and own their adapters, budgets, floors and artifacts.
vars:
TRIPLE: aarch64-apple-darwin
LIBNAME: fs_core
tasks:
staticlib:
desc: Build the static library and its headers into this crate's dist/
# NO `out` ARGUMENT, deliberately. A library does not write into its
# consumer. It builds into its own dist/ and a consumer asks where that
# is — `chore artifact` prints the path. Inverting it that way means this
# crate can change its output layout without every consumer knowing, and
# it stays independently usable with no consumer at all.
vars:
OUT: 'dist'
# These patterns are LITERAL on purpose. chore records a fingerprint by
# calling fingerprint.Save WITHOUT a renderer (internal/run/run.go:313 ->
# fingerprint.SaveWith with a nil Renderer), so a pattern containing
# {{ }} is expanded against the raw text, matches nothing, and stores the
# empty-set hash — the task then never registers as up to date.
sources:
- Cargo.toml
- 'src/**/*.rs'
- 'include/*.h'
# THE GUARD IS PART OF THIS TASK, SO IT IS PART OF THE FINGERPRINT.
#
# The first command below runs
# tests/header_names_the_built_library.rs, and neither that file
# nor this one was a source -- so editing the guard left the task
# up to date and the guard did not run. Measured on the sibling
# this task is shared with: force its body to `false`, change
# nothing else, and `chore staticlib` prints "task: staticlib is
# up to date" and executes nothing.
#
# A check whose result nothing re-reads, in the step that decides
# what ships. chores.yml is here for the same reason: the command
# list, the artefact paths and the guard's invocation all live in
# this file, and a change to any of them changes the task.
#
# THE FILE THE TASK RUNS, NOT THE DIRECTORY IT LIVES IN. The
# sibling closed this with 'tests/**/*.rs', which fingerprints
# every file in tests/ while `cmds:` runs exactly one target -- so
# editing an unrelated test re-runs the whole task including the
# cross-target release build it could not have affected. That is
# rust-img-vhdx#85, and copying the fix verbatim reproduces it.
#
# DO NOT ANSWER EITHER BY DROPPING THE ENTRY. The rule is that
# `sources:` names what the task READS -- so if a second `--test`
# target is ever added below, its file belongs here beside this
# one.
# `the_staticlib_task_fingerprints_the_tests_it_runs_and_no_others`
# in tests/header_names_the_built_library.rs refuses both
# directions.
- 'tests/header_names_the_built_library.rs'
- chores.yml
# Templated, which is fine here where it is not fine above: UpToDate
# renders these before expanding them, and every entry is an exact path
# rather than a glob, so a missing artifact is caught by the pattern
# failing to match.
generates:
- '{{.OUT}}/lib{{.LIBNAME}}.a'
- '{{.OUT}}/include/{{.LIBNAME}}.h'
cmds:
# THE HEADER CHECK IS THE TEST, NOT A SECOND COPY OF IT.
#
# This was a shell fragment that grepped the header and compared
# what it found against `lib{{.LIBNAME}}.a` -- a value derived
# from the same hand-maintained LIBNAME variable it was checking.
# A LIBNAME/Cargo.toml drift, which is precisely what it existed
# to catch, passed it silently: rename `[lib] name` and cargo
# builds a different artefact while the guard compares LIBNAME
# against itself and agrees. A check whose two sides come from one
# source cannot fail.
#
# tests/header_names_the_built_library.rs derives the expected
# name from Cargo.toml's `[lib] name` -- the thing that actually
# decides the artefact -- with a real TOML parser rather than a
# line scan. Running it here means one implementation instead of
# two, and the one that is already exercised by CI.
#
# It stays FIRST so a mismatch costs no release build.
- 'cargo test --locked --test header_names_the_built_library'
# Idempotent: rustup answers "up to date" without touching the network
# when the target is already installed. It runs here rather than in the
# consumer because the triple is ours.
- 'rustup target add {{.TRIPLE}}'
- 'cargo build --release --target {{.TRIPLE}}'
- 'mkdir -p "{{.OUT}}/include"'
- 'cp target/{{.TRIPLE}}/release/lib{{.LIBNAME}}.a "{{.OUT}}/lib{{.LIBNAME}}.a"'
- 'cp include/{{.LIBNAME}}.h "{{.OUT}}/include/{{.LIBNAME}}.h"'
lint:
desc: Formatting and clippy, exactly as CI runs them
# `--locked` is not decoration. It makes the build fail rather than
# silently update Cargo.lock, which is what keeps a local gate and
# CI checking the same dependency versions — and it is what the
# release process relies on.
#
# This exists so the CI gate can be reproduced locally. If the two
# ever differ, this is the copy that is wrong.
cmds:
- scripts/agents-core-check.sh
- cargo fmt --check
- cargo clippy --locked --all-targets -- -D warnings
- cargo clippy --locked --all-targets --features cli -- -D warnings
build:
desc: Debug build
cmds:
test:debug:
desc: Debug-profile suite, including every target
cmds:
- 'AM_FS_CORE_ALLOW_UNPRIVILEGED_SKIP=1 EXPECT_OVERFLOW_CHECKS=1 bash scripts/tier.sh "test (debug)" debug 1000 60000 -- cargo test --locked --all-targets --features cli'
- 'bash scripts/core.sh test-floor debug 330'
test:release:
desc: Release-profile suite, including every target
cmds:
- 'AM_FS_CORE_ALLOW_UNPRIVILEGED_SKIP=1 bash scripts/tier.sh "test (release)" release 1000 60000 -- cargo test --locked --release --all-targets --features cli'
- 'bash scripts/core.sh test-floor release 330'
test:
desc: Debug and release test tiers
cmds:
- task: test:debug
- task: test:release
coverage:
desc: Instrumented suite with the same 90% line-coverage gate as CI
cmds:
- 'AM_FS_CORE_ALLOW_UNPRIVILEGED_SKIP=1 bash scripts/tier.sh coverage coverage 800 60000 -- cargo llvm-cov --features cli --html --output-dir target/llvm-cov --fail-under-lines 90'
- 'bash scripts/core.sh test-floor coverage 330'
clean:
desc: Remove this crate's cargo output
cmds:
artifact:
desc: Print the absolute path of the directory holding the built outputs
# `silent: true` is what makes this usable as a VALUE — without it chore
# echoes each command and a caller capturing stdout gets the echo too.
# PATTERNS.md: "a value — a flag string, a container status, a path" is a
# task called through {{.CHORE_EXE}}.
silent: true
# NO `deps: [staticlib]`. chore prints "task: staticlib is up to date" on
# STDOUT, which a caller capturing this as a value receives as PART OF THE
# VALUE — measured on rust-blk-probe, where the capture came back as two
# lines and the caller's path check failed. A value-returning task returns
# a value and does nothing else; the caller builds first, then asks. Two
# calls, on purpose.
#
# IT PRINTS A DIRECTORY, NOT A FILE, and that is the one place this differs
# from rust-blk-probe's `artifact`. That crate produces a single binary, so
# it can name a file. This one produces libfs_core.a AND include/fs_core.h,
# so there is no single path to name — A CONSUMER COPIES THE CONTENTS of
# what this prints. A consumer that treats it as a file path will silently
# copy the wrong thing.
cmds:
- 'cd "{{.OUT}}" && pwd -P'
vars:
OUT: 'dist'
check:ci-gate:
desc: The one required check stands for every job in ci.yml
# A chore task naming a script, and nothing else -- the script is what can
# be tested, reviewed and run without chore at all.
#
# This was tests/ci_aggregate_gate.rs. It was the wrong container twice
# over: it parses a YAML file and compares strings, exercising nothing this
# crate ships, and as a cargo test it counted towards the executed-test
# floor the gate itself enforces -- so the suite could satisfy its floor
# partly by checking its own CI config.
cmds:
- bash scripts/core.sh ci-gate
check:semver:
desc: Refuse a public-API break the version in Cargo.toml does not declare
# Against the newest version on crates.io, with cargo-semver-checks: a
# break needs the 0.x minor to move, an addition the patch. Why, and what
# it cannot see, is at the top of the script. No `sources:` on purpose --
# the baseline is the registry, which moves without this tree changing,
# so a fingerprint would call a stale answer up to date.
cmds:
- 'bash scripts/tier.sh semver semver 40 4000 -- bash scripts/core.sh semver-check'
test:scripts:
desc: The shell tests (tests/scripts/*.sh), exactly as CI runs them
cmds:
- 'for t in tests/scripts/*.sh; do bash "$t" || exit 1; done'