rust-fs-core 0.3.7

Pure-Rust block-device framework — BlockRead/BlockDevice traits + FileDevice + CallbackDevice + LRU cache. Foundation crate for the rust-fs-* drivers and rust-img-* containers.
Documentation
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: ['cargo build']

  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: ['cargo clean']

  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'