diff --git a/packages/rust/Cargo.toml b/packages/rust/Cargo.toml index 91b439190..256b6403e 100644 --- a/packages/rust/Cargo.toml +++ b/packages/rust/Cargo.toml @@ -19,8 +19,14 @@ repository.workspace = true # `tests/` is the integration suite -- excluded so test files never ship in the # published `.crate` (enforced by the `packaging` gate in CI). Inline unit tests # stay (they are `#[cfg(test)]` modules inside `src/`, not separate files). +# The fragment dirs are repo inputs -- one file per change, assembled into +# nothing and read by no crate consumer. They need naming here because cargo +# ships whatever is not excluded; npm and maturin drop theirs structurally, via +# an allowlist and the wheel layout respectively. exclude = [ "tests/", + "changelog.d/", + "migrations.d/", "docs/.vitepress", "docs/.claude", "docs/tests", diff --git a/packages/rust/changelog.d/2026-08-20-crate-excludes-fragment-dirs.md b/packages/rust/changelog.d/2026-08-20-crate-excludes-fragment-dirs.md new file mode 100644 index 000000000..6febda98c --- /dev/null +++ b/packages/rust/changelog.d/2026-08-20-crate-excludes-fragment-dirs.md @@ -0,0 +1,9 @@ +**Fixed** — the published `.crate` archive no longer ships `changelog.d/` and +`migrations.d/`. `[package].exclude` named `tests/` and the VitePress tooling +under `docs/` but not the fragment dirs, and cargo ships whatever is not +excluded, so every release carried 60 fragment files no crate consumer reads — +more than half the archive's entries (119 files → 59). + +No API, CLI or behavior change; the crate is smaller and its file list now +matches the npm package (an allowlist) and the wheel (maturin's layout), which +already dropped their fragments structurally. diff --git a/packages/rust/tests/crate_contents.rs b/packages/rust/tests/crate_contents.rs new file mode 100644 index 000000000..6b80c17a9 --- /dev/null +++ b/packages/rust/tests/crate_contents.rs @@ -0,0 +1,89 @@ +//! What the published `.crate` archive ships. +//! +//! `changelog.d/` and `migrations.d/` are repo *inputs* -- one fragment file +//! per change, assembled by nothing and read by no consumer of the crate. +//! The npm `files` allowlist and maturin's wheel layout keep them out of +//! their artifacts structurally; cargo ships everything not named in +//! `[package].exclude`, so the crate needs the patterns spelled out. +//! +//! Asserted against `cargo package --list` -- the archive's own manifest -- +//! rather than against the `exclude` patterns, so a pattern that parses but +//! matches nothing still fails here. + +use std::process::Command; + +/// Every path `cargo package` would write into the `.crate` archive. +/// +/// `--allow-dirty` because the question is what the *working tree* would +/// ship: without it cargo refuses outright the moment a packaged file is +/// uncommitted, so editing `exclude` and re-running would abort before +/// listing anything -- failing for a reason that has nothing to do with the +/// assertions below. +fn packaged_paths() -> Vec { + let output = Command::new(env!("CARGO")) + .args([ + "package", + "--manifest-path", + concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"), + "--list", + "--allow-dirty", + ]) + .output() + .expect("cargo package --list should run"); + + assert!( + output.status.success(), + "cargo package --list failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + String::from_utf8(output.stdout) + .expect("cargo package --list emits utf-8") + .lines() + .map(str::to_string) + .collect() +} + +/// Archive paths under `prefix`. +fn shipped_under(prefix: &str) -> Vec { + packaged_paths() + .into_iter() + .filter(|path| path.starts_with(prefix)) + .collect() +} + +/// Guards the two assertions below against a vacuous pass: a `--list` that +/// silently returned nothing would otherwise satisfy every "does not ship" +/// check in this file. +#[test] +fn crate_archive_ships_the_library_source() { + let paths = packaged_paths(); + + assert!( + paths.iter().any(|p| p == "src/lib.rs"), + "expected src/lib.rs in the archive, got {} paths: {paths:?}", + paths.len() + ); +} + +#[test] +fn crate_archive_omits_changelog_fragments() { + let shipped = shipped_under("changelog.d/"); + + assert!( + shipped.is_empty(), + "changelog fragments are repo inputs and must not ship; \ + add `changelog.d/` to [package].exclude. Shipped: {shipped:?}" + ); +} + +#[test] +fn crate_archive_omits_migration_fragments() { + let shipped = shipped_under("migrations.d/"); + + assert!( + shipped.is_empty(), + "migration fragments are repo inputs and must not ship; \ + add `migrations.d/` to [package].exclude. Shipped: {shipped:?}" + ); +}