Skip to content

Commit 030cacc

Browse files
fix: include crates/contracts/* in the workspace (#10550) (#10551)
* fix: include crates/contracts/* in the workspace (#10550) cargo's `exclude` matches by path prefix and outranks the `members` globs, so `exclude = ["crates/contracts"]` silently dropped all 18 `crates/contracts/*` crates out of the workspace -- the `"crates/contracts/*"` members entry never had any effect. cargo resolved 25 members against 43 crates on disk. Nothing warned, because every contract crate is a path dependency of a member: they still compiled, so `cargo build` stayed green while `--workspace` package selection never saw them. 199 `#[test]` functions across 12 contract crates were never compiled, let alone run, by `cargo check --workspace --tests`, `cargo test --workspace`, or clippy. The exclude was not gratuitous -- a bare `crates/*` glob also matches the `crates/contracts` grouping directory, which has no manifest, and cargo hard errors on it. Prefix-qualifying both globs as `homeboy-*` keeps the grouping directory out of the match set, so the exclude is no longer needed. All 42 crates already carry that prefix. Cargo.lock is unchanged and `--locked` still resolves. Expect fallout: this compiles and runs 199 tests for the first time. * test: fail closed when a crate is missing from workspace members (#10550) The prefix-qualified members globs fix the contracts drop, but trade one silent failure mode for another: a crate added under `crates/` without the `homeboy-` prefix would be just as invisible to every `--workspace` gate as the contract crates were. Compare crate directories on disk against what the globs can match, and assert the `exclude = ["crates/contracts"]` workaround is not reintroduced. --------- Co-authored-by: chubes-bot <266378653+homeboy-ci[bot]@users.noreply.github.com>
1 parent 7e3c664 commit 030cacc

2 files changed

Lines changed: 98 additions & 4 deletions

File tree

Cargo.toml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,19 @@
33
# incremental compilation and lower peak build memory. They are path-only
44
# dependencies of the `homeboy` crate. The entire workspace is private; the
55
# shipped CLI artifact remains the single `homeboy` binary.
6-
members = ["crates/*", "crates/contracts/*"]
7-
# `crates/*` also globs the `crates/contracts` grouping directory, which is not
8-
# itself a crate; exclude it so only the contract crates under it are members.
9-
exclude = ["crates/contracts"]
6+
#
7+
# Both globs are prefix-qualified (`homeboy-*`) rather than a bare `crates/*`.
8+
# A bare `crates/*` also matches the `crates/contracts` grouping directory,
9+
# which is not itself a crate, so cargo fails to load its (nonexistent)
10+
# manifest. The previous workaround was `exclude = ["crates/contracts"]`, but
11+
# cargo's `exclude` matches by path PREFIX and outranks the members globs, so it
12+
# silently dropped all 18 `crates/contracts/*` crates from the workspace too
13+
# (#10550). Keeping the grouping directory out of the glob in the first place is
14+
# what makes the membership honest. Every crate under `crates/` and
15+
# `crates/contracts/` is named `homeboy-*`; `workspace_membership_is_complete`
16+
# in `crates/homeboy-paths` fails closed if a crate is ever added that these
17+
# globs would miss.
18+
members = ["crates/homeboy-*", "crates/contracts/homeboy-*"]
1019

1120
[package]
1221
name = "homeboy"

crates/homeboy-paths/src/lib.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,3 +447,88 @@ pub fn authorize_remote_artifact_path(
447447
Err(RemotePathAuthorizationError::OutsideAllowedRoots)
448448
}
449449
}
450+
451+
/// Fails closed when a crate on disk is not enumerated as a workspace member.
452+
///
453+
/// The root manifest globs members as `crates/homeboy-*` and
454+
/// `crates/contracts/homeboy-*` rather than a bare `crates/*`, because a bare
455+
/// glob also matches the `crates/contracts` grouping directory (not itself a
456+
/// crate) and cargo then fails to load its manifest. The historical workaround
457+
/// was `exclude = ["crates/contracts"]`, but cargo's `exclude` matches by path
458+
/// PREFIX and outranks the members globs -- it silently dropped all 18
459+
/// `crates/contracts/*` crates out of the workspace. Nothing warned: they still
460+
/// compiled as path dependencies, so `cargo build` was green while
461+
/// `--workspace` selection (test, clippy, `check --tests`) never saw them and
462+
/// their unit tests never ran (#10550).
463+
///
464+
/// The prefix globs remove the need for `exclude`, but they trade one silent
465+
/// failure for another: a crate added under `crates/` without the `homeboy-`
466+
/// prefix would be just as invisible. This test closes that hole by comparing
467+
/// the manifests on disk against the members cargo actually resolved.
468+
#[cfg(test)]
469+
mod workspace_membership {
470+
use std::collections::BTreeSet;
471+
use std::path::{Path, PathBuf};
472+
473+
fn workspace_root() -> PathBuf {
474+
// crates/homeboy-paths -> crates -> <root>
475+
Path::new(env!("CARGO_MANIFEST_DIR"))
476+
.ancestors()
477+
.nth(2)
478+
.expect("homeboy-paths should live at <root>/crates/homeboy-paths")
479+
.to_path_buf()
480+
}
481+
482+
/// Every directory under `dir` that contains a `Cargo.toml`.
483+
fn crate_dirs(dir: &Path) -> BTreeSet<String> {
484+
let mut found = BTreeSet::new();
485+
let Ok(entries) = std::fs::read_dir(dir) else {
486+
return found;
487+
};
488+
for entry in entries.flatten() {
489+
let path = entry.path();
490+
if path.join("Cargo.toml").is_file() {
491+
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
492+
found.insert(name.to_string());
493+
}
494+
}
495+
}
496+
found
497+
}
498+
499+
#[test]
500+
fn workspace_membership_is_complete() {
501+
let root = workspace_root();
502+
let manifest = std::fs::read_to_string(root.join("Cargo.toml")).expect("root manifest");
503+
504+
// Guard the mechanism itself: `exclude` outranks the members globs, so
505+
// reintroducing it silently re-orphans crates.
506+
assert!(
507+
!manifest.contains("\nexclude = [\"crates/contracts\"]"),
508+
"root manifest must not exclude `crates/contracts`: cargo's exclude \
509+
matches by path prefix and would drop every crates/contracts/* \
510+
member from the workspace (#10550)"
511+
);
512+
513+
for (dir, glob) in [
514+
(root.join("crates"), "crates/homeboy-*"),
515+
(
516+
root.join("crates").join("contracts"),
517+
"crates/contracts/homeboy-*",
518+
),
519+
] {
520+
for name in crate_dirs(&dir) {
521+
assert!(
522+
name.starts_with("homeboy-"),
523+
"crate `{}` in {} is not matched by the `{}` members glob, so it \
524+
is invisible to every `--workspace` gate (test, clippy, \
525+
`check --tests`). Rename it with the `homeboy-` prefix or widen \
526+
the glob in the root Cargo.toml.",
527+
name,
528+
dir.display(),
529+
glob
530+
);
531+
}
532+
}
533+
}
534+
}

0 commit comments

Comments
 (0)