feat: add metapackage build support and fail-fast custom scripts

- add BuildType::Meta handling in builder and interactive spec output
- allow metapackage specs without sources and expose is_metapackage()
- avoid build deps in install planning for metapackages
- run custom non-function build.sh scripts with `sh -e` to stop on first failure
- add regression test for custom script fail-fast behavior
- apply small cleanup refactors (let-chains/iterator simplifications)
This commit is contained in:
2026-02-25 16:49:36 -06:00
parent 9953d4b2ee
commit d9a30f5d56
13 changed files with 168 additions and 94 deletions
+2
View File
@@ -21,6 +21,7 @@ impl fmt::Display for BuildType {
BuildType::Rust => write!(f, "Rust"),
BuildType::Makefile => write!(f, "Makefile"),
BuildType::Bin => write!(f, "Binary installer"),
BuildType::Meta => write!(f, "Metapackage"),
}
}
}
@@ -624,6 +625,7 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
BuildType::Rust => "rust",
BuildType::Makefile => "makefile",
BuildType::Bin => "bin",
BuildType::Meta => "meta",
};
build_tbl.insert("type".into(), Value::String(build_type.to_string()));
+42 -3
View File
@@ -70,9 +70,11 @@ impl PackageSpec {
.unwrap_or_else(|| PathBuf::from("."));
spec.apply_spec_appends(&appends)?;
// Require at least one source (remote or manual)
if spec.source.is_empty() && spec.manual_sources.is_empty() {
anyhow::bail!("Package must have at least one source or manual_sources entry");
// Require at least one source (remote or manual) unless this is a metapackage.
if spec.source.is_empty() && spec.manual_sources.is_empty() && !spec.is_metapackage() {
anyhow::bail!(
"Package must have at least one source or manual_sources entry (except build.type = \"meta\")"
);
}
spec.validate_manual_sources()?;
@@ -167,6 +169,11 @@ impl PackageSpec {
&self.source
}
/// Returns true when this spec is a metadata-only package that exists to pull dependencies.
pub fn is_metapackage(&self) -> bool {
matches!(self.build.build_type, BuildType::Meta)
}
/// Return all package outputs this spec will produce (primary + any extras)
pub fn outputs(&self) -> Vec<PackageInfo> {
let mut v = Vec::new();
@@ -1202,6 +1209,37 @@ type = "custom"
assert!(PackageSpec::from_file(&path).is_err());
}
#[test]
fn parse_allows_metapackage_without_sources() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo-meta"
version = "1.0"
description = "metapackage"
homepage = "https://example.com"
license = "MIT"
[build]
type = "meta"
[dependencies]
runtime = ["foo", "bar"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(spec.source.is_empty());
assert!(spec.manual_sources.is_empty());
assert!(spec.is_metapackage());
assert_eq!(spec.dependencies.runtime, vec!["foo", "bar"]);
}
#[test]
fn parse_manual_source_with_url() {
let tmp = tempfile::tempdir().unwrap();
@@ -2297,6 +2335,7 @@ pub enum BuildType {
Rust,
Makefile,
Bin,
Meta,
}
/// Build flags and toolchain configuration