cook: fail a build that stages no files
A recipe whose script exits 0 but installs nothing currently reports success, and the damage lands far from the cause. qtspeech produced a 136-byte package because upstream `return()`s with only a NOTICE when Qt6::Multimedia is absent; that surfaced much later as an unresolved symbol in a consumer, not as a qtspeech failure. Zero regular files staged is unambiguous -- every real package installs at least one file -- so this needs no per-recipe size threshold and cannot false-positive on a small-but-valid package. The error names the likely cause (a configure step soft-bailing on a missing optional dependency) rather than just reporting emptiness. Metapackages that genuinely stage nothing opt out by mentioning COOKBOOK_ALLOW_EMPTY_PACKAGE in their script. This is the same failure shape as the libudev/libpciaccess API-surface risk: a gap that reports success locally and only becomes visible as a link error in something downstream.
This commit is contained in:
@@ -1179,6 +1179,49 @@ pub fn build(
|
||||
globs.push((glob.compile_matcher(), stage_dir.clone()));
|
||||
}
|
||||
}
|
||||
// EMPTY-PACKAGE GATE. A recipe whose script exits 0 but installs nothing
|
||||
// is a build failure that currently reports success, and the damage
|
||||
// lands far away: qtspeech produced a 136-byte package because upstream
|
||||
// `return()`s with only a NOTICE when Qt6::Multimedia is missing, and
|
||||
// that only surfaced much later as an unresolved symbol in a consumer.
|
||||
//
|
||||
// Zero regular files is unambiguous -- every real package installs at
|
||||
// least one file -- so this needs no per-recipe threshold and cannot
|
||||
// false-positive on a small-but-valid package. Recipes that legitimately
|
||||
// stage nothing (metapackages) set COOKBOOK_ALLOW_EMPTY_PACKAGE=1.
|
||||
{
|
||||
fn count_files(dir: &Path) -> usize {
|
||||
let mut n = 0;
|
||||
if let Ok(entries) = fs::read_dir(dir) {
|
||||
for e in entries.flatten() {
|
||||
match e.file_type() {
|
||||
Ok(t) if t.is_dir() => n += count_files(&e.path()),
|
||||
Ok(_) => n += 1,
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
let staged_files = count_files(&stage_dir_tmp);
|
||||
let allow_empty = matches!(
|
||||
&recipe.build.kind,
|
||||
BuildKind::Custom { script } if script.contains("COOKBOOK_ALLOW_EMPTY_PACKAGE")
|
||||
);
|
||||
if staged_files == 0 && !allow_empty {
|
||||
return Err(format!(
|
||||
"{name}: build reported success but staged NO files.\n \
|
||||
A package that installs nothing is a silent failure -- the error \
|
||||
surfaces later as a missing header or unresolved symbol in a \
|
||||
consumer, far from the cause.\n \
|
||||
Check the recipe's configure step for a soft bail-out (a missing \
|
||||
optional dependency that turns the build into a no-op).\n \
|
||||
If this package genuinely stages nothing, mention \
|
||||
COOKBOOK_ALLOW_EMPTY_PACKAGE in its script."
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
move_dir_all_fn(
|
||||
&stage_dir_tmp,
|
||||
&|path: PathBuf| {
|
||||
|
||||
Reference in New Issue
Block a user