diff --git a/src/cook/cook_build.rs b/src/cook/cook_build.rs index 10b2946b29..ef94935f6a 100644 --- a/src/cook/cook_build.rs +++ b/src/cook/cook_build.rs @@ -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| {