feat: add support for metapackages in interactive prompts and update git2 dependency features

This commit is contained in:
2026-02-25 20:09:19 -06:00
parent d9a30f5d56
commit 631ac73b3f
2 changed files with 268 additions and 210 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ ar = "0.9.0"
bzip2 = "0.6.1" bzip2 = "0.6.1"
clap = { version = "4.5.60", features = ["derive"] } clap = { version = "4.5.60", features = ["derive"] }
flate2 = "1.1.9" flate2 = "1.1.9"
git2 = { version = "0.20.4", features = ["vendored-libgit2"] } git2 = { version = "0.20.4", features = ["openssl-sys"] }
indicatif = "0.18.4" indicatif = "0.18.4"
nix = { version = "0.31.1", features = ["user"] } nix = { version = "0.31.1", features = ["user"] }
reqwest = { version = "0.13.2", features = ["blocking", "native-tls"] } reqwest = { version = "0.13.2", features = ["blocking", "native-tls"] }
+267 -209
View File
@@ -243,11 +243,13 @@ pub fn create_interactive() -> Result<PackageSpec> {
BuildType::Rust, BuildType::Rust,
BuildType::Custom, BuildType::Custom,
BuildType::Bin, BuildType::Bin,
BuildType::Meta,
]; ];
let build_type = Select::new("Build System:", build_types) let build_type = Select::new("Build System:", build_types)
.with_help_message("Select the build system used by the package (common choices)") .with_help_message("Select the build system used by the package (common choices)")
.prompt()?; .prompt()?;
let is_metapackage = matches!(build_type, BuildType::Meta);
// If the build system is Autotools, offer a convenience prompt for GNU-hosted tarballs. // If the build system is Autotools, offer a convenience prompt for GNU-hosted tarballs.
let mut gnu_source_default = String::new(); let mut gnu_source_default = String::new();
@@ -268,232 +270,251 @@ pub fn create_interactive() -> Result<PackageSpec> {
} }
// Ask whether to show advanced fields. // Ask whether to show advanced fields.
let show_advanced = Confirm::new("Show advanced options?") let show_advanced = if is_metapackage {
.with_help_message("Includes source subdir, toolchain overrides, hooks, and build tuning") false
.with_default(false) } else {
.prompt()?; Confirm::new("Show advanced options?")
.with_help_message(
"Includes source subdir, toolchain overrides, hooks, and build tuning",
)
.with_default(false)
.prompt()?
};
let source_url = Text::new("Source URL:") let mut sources = Vec::new();
.with_help_message("URL to the source tarball or git repository") if !is_metapackage {
.with_default(gnu_source_default.as_str()) let source_url = Text::new("Source URL:")
.prompt()?; .with_help_message("URL to the source tarball or git repository")
.with_default(gnu_source_default.as_str())
.prompt()?;
if source_url.trim().is_empty() { if source_url.trim().is_empty() {
anyhow::bail!("Source URL cannot be empty"); anyhow::bail!("Source URL cannot be empty");
}
// Attempt to compute SHA256 automatically when online — use as the default if available.
let checksum_source_url = expand_known_package_vars(&source_url, &name, &version);
let computed_sha_default = match compute_sha256_for_url(&checksum_source_url) {
Ok(hex) => {
// Use raw hex as the default so pressing Enter accepts it
hex
} }
Err(_) => "skip".to_string(),
};
let source_sha256 = Text::new("Source checksum:") // Attempt to compute SHA256 automatically when online — use as the default if available.
.with_help_message( let checksum_source_url = expand_known_package_vars(&source_url, &name, &version);
"Accepts sha256:, sha512:, md5:, or raw SHA256 hex (use 'skip' to bypass)", let computed_sha_default = match compute_sha256_for_url(&checksum_source_url) {
) Ok(hex) => {
.with_default(computed_sha_default.as_str()) // Use raw hex as the default so pressing Enter accepts it
.prompt()?; hex
}
Err(_) => "skip".to_string(),
};
let extract_dir = Text::new("Extract Directory:") let source_sha256 = Text::new("Source checksum:")
.with_help_message("Directory created after extraction (supports $name, $version)") .with_help_message(
.with_default("$name-$version") "Accepts sha256:, sha512:, md5:, or raw SHA256 hex (use 'skip' to bypass)",
.prompt()?; )
.with_default(computed_sha_default.as_str())
.prompt()?;
let mut source = Source { let extract_dir = Text::new("Extract Directory:")
url: source_url, .with_help_message("Directory created after extraction (supports $name, $version)")
sha256: source_sha256, .with_default("$name-$version")
extract_dir, .prompt()?;
patches: Vec::new(),
post_extract: Vec::new(),
};
if show_advanced { let mut source = Source {
source.patches = prompt_repeating_list( url: source_url,
"Patch path/URL", sha256: source_sha256,
"Patch file path (relative to spec dir) or direct URL", extract_dir,
)?; patches: Vec::new(),
source.post_extract = prompt_repeating_list( post_extract: Vec::new(),
"post_extract command", };
"Runs in extracted source directory using sh -c",
)?; if show_advanced {
source.patches = prompt_repeating_list(
"Patch path/URL",
"Patch file path (relative to spec dir) or direct URL",
)?;
source.post_extract = prompt_repeating_list(
"post_extract command",
"Runs in extracted source directory using sh -c",
)?;
}
sources.push(source);
} else {
crate::log_info!(
"Metapackage selected: skipping source/build prompts. Define runtime dependencies to pull in."
);
} }
let mut flags = BuildFlags::default(); let mut flags = BuildFlags::default();
if !is_metapackage {
if !matches!(build_type, BuildType::Bin) { if !matches!(build_type, BuildType::Bin) {
flags.prefix = Text::new("Install prefix:") flags.prefix = Text::new("Install prefix:")
.with_help_message("Most packages should use /usr") .with_help_message("Most packages should use /usr")
.with_default(flags.prefix.as_str()) .with_default(flags.prefix.as_str())
.prompt()?; .prompt()?;
}
let supports_separate_build_dir = matches!(
build_type,
BuildType::Autotools | BuildType::CMake | BuildType::Meson | BuildType::Custom
);
if supports_separate_build_dir {
let default_bdir = if matches!(build_type, BuildType::Meson) {
"builddir"
} else {
"build"
};
let default_enabled = matches!(build_type, BuildType::CMake | BuildType::Meson);
if Confirm::new("Use separate build directory?")
.with_help_message("Recommended for CMake/Meson and often useful for Autotools")
.with_default(default_enabled)
.prompt()?
{
flags.build_dir = Some(
Text::new("Build directory name:")
.with_default(default_bdir)
.prompt()?,
);
} }
}
if show_advanced { let supports_separate_build_dir = matches!(
flags.source_subdir = prompt_optional_text( build_type,
"Source subdirectory (optional):", BuildType::Autotools | BuildType::CMake | BuildType::Meson | BuildType::Custom
"Use for monorepos (e.g. llvm-project/clang, src)", );
)?; if supports_separate_build_dir {
flags.cc = Text::new("CC:") let default_bdir = if matches!(build_type, BuildType::Meson) {
.with_help_message("C compiler") "builddir"
.with_default(flags.cc.as_str()) } else {
.prompt()?; "build"
flags.cxx = Text::new("CXX:") };
.with_help_message("C++ compiler") let default_enabled = matches!(build_type, BuildType::CMake | BuildType::Meson);
.with_default(flags.cxx.as_str()) if Confirm::new("Use separate build directory?")
.prompt()?; .with_help_message("Recommended for CMake/Meson and often useful for Autotools")
flags.ar = Text::new("AR:") .with_default(default_enabled)
.with_help_message("Archiver tool") .prompt()?
.with_default(flags.ar.as_str()) {
.prompt()?; flags.build_dir = Some(
flags.chost = prompt_optional_text( Text::new("Build directory name:")
"CHOST target triple (optional):", .with_default(default_bdir)
"Example: x86_64-sfg-linux-gnu", .prompt()?,
)?; );
flags.cbuild = prompt_optional_text( }
"CBUILD build triple (optional):",
"Example: x86_64-pc-linux-gnu",
)?;
flags.carch = Text::new("CARCH:")
.with_help_message("CPU architecture short name")
.with_default(flags.carch.as_str())
.prompt()?;
flags.cflags = prompt_repeating_list(
"CFLAG",
"One flag per entry, e.g. -O2, -fPIC, -D_GNU_SOURCE",
)?;
flags.ldflags = prompt_repeating_list("LDFLAG", "One flag per entry, e.g. -Wl,-z,relro")?;
}
if matches!(
build_type,
BuildType::Autotools | BuildType::CMake | BuildType::Meson
) {
let help = match build_type {
BuildType::Autotools => "Examples: --disable-static, --enable-nls, --with-zlib",
BuildType::CMake => "Examples: -DENABLE_TESTS=OFF, -DUSE_SYSTEM_LIBS=ON",
BuildType::Meson => "Examples: -Dtests=false, -Ddefault_library=static",
_ => "",
};
if Confirm::new("Add configure/setup options?")
.with_help_message("Adds entries to build.flags.configure")
.with_default(show_advanced)
.prompt()?
{
flags.configure = prompt_repeating_list("Configure option", help)?;
} }
}
if show_advanced && matches!(build_type, BuildType::Autotools) {
flags.configure_file = prompt_optional_text(
"Configure script path (optional):",
"Relative to source root, e.g. build-aux/configure",
)?;
flags.make_dirs = prompt_repeating_list(
"Make dir (build phase)",
"Relative to build dir, e.g. lib, libelf (empty = build root)",
)?;
flags.make_test_dirs = prompt_repeating_list(
"Make dir (test phase)",
"Relative to build dir, e.g. tests (empty = build root)",
)?;
flags.make_install_dirs = prompt_repeating_list(
"Make dir (install phase)",
"Relative to build dir, e.g. lib, apps (empty = build root)",
)?;
flags.skip_tests = Confirm::new("Skip automatic tests (make check/test)?")
.with_help_message(
"If enabled, Depot will not auto-run detected Autotools test targets",
)
.with_default(false)
.prompt()?;
}
if matches!(build_type, BuildType::Makefile) {
crate::log_info!("Define Makefile build and install commands.");
flags.makefile_commands = prompt_repeating_list(
"makefile build command",
"Examples: make -j$(nproc), make all",
)?;
if flags.makefile_commands.is_empty() {
anyhow::bail!("Makefile build type requires at least one build command");
}
flags.makefile_install_commands = prompt_repeating_list(
"makefile install command",
"Examples: make DESTDIR=$DESTDIR install",
)?;
if flags.makefile_install_commands.is_empty() {
anyhow::bail!("Makefile build type requires at least one install command");
}
}
if matches!(build_type, BuildType::Rust) {
let profiles = vec!["release", "debug"];
let profile = Select::new("Cargo profile:", profiles)
.with_help_message("Release is recommended for production packages")
.prompt()?;
flags.profile = profile.to_string();
flags.target = prompt_optional_text(
"Rust target triple (optional):",
"Example: x86_64-unknown-linux-gnu",
)?;
if show_advanced { if show_advanced {
flags.rustflags = flags.source_subdir = prompt_optional_text(
prompt_repeating_list("RUSTFLAG", "One flag per entry, e.g. -Ctarget-cpu=native")?; "Source subdirectory (optional):",
flags.cargs = prompt_repeating_list( "Use for monorepos (e.g. llvm-project/clang, src)",
"Extra cargo arg", )?;
"Examples: --locked, --features, serde, --no-default-features", flags.cc = Text::new("CC:")
.with_help_message("C compiler")
.with_default(flags.cc.as_str())
.prompt()?;
flags.cxx = Text::new("CXX:")
.with_help_message("C++ compiler")
.with_default(flags.cxx.as_str())
.prompt()?;
flags.ar = Text::new("AR:")
.with_help_message("Archiver tool")
.with_default(flags.ar.as_str())
.prompt()?;
flags.chost = prompt_optional_text(
"CHOST target triple (optional):",
"Example: x86_64-sfg-linux-gnu",
)?;
flags.cbuild = prompt_optional_text(
"CBUILD build triple (optional):",
"Example: x86_64-pc-linux-gnu",
)?;
flags.carch = Text::new("CARCH:")
.with_help_message("CPU architecture short name")
.with_default(flags.carch.as_str())
.prompt()?;
flags.cflags = prompt_repeating_list(
"CFLAG",
"One flag per entry, e.g. -O2, -fPIC, -D_GNU_SOURCE",
)?;
flags.ldflags =
prompt_repeating_list("LDFLAG", "One flag per entry, e.g. -Wl,-z,relro")?;
}
if matches!(
build_type,
BuildType::Autotools | BuildType::CMake | BuildType::Meson
) {
let help = match build_type {
BuildType::Autotools => "Examples: --disable-static, --enable-nls, --with-zlib",
BuildType::CMake => "Examples: -DENABLE_TESTS=OFF, -DUSE_SYSTEM_LIBS=ON",
BuildType::Meson => "Examples: -Dtests=false, -Ddefault_library=static",
_ => "",
};
if Confirm::new("Add configure/setup options?")
.with_help_message("Adds entries to build.flags.configure")
.with_default(show_advanced)
.prompt()?
{
flags.configure = prompt_repeating_list("Configure option", help)?;
}
}
if show_advanced && matches!(build_type, BuildType::Autotools) {
flags.configure_file = prompt_optional_text(
"Configure script path (optional):",
"Relative to source root, e.g. build-aux/configure",
)?;
flags.make_dirs = prompt_repeating_list(
"Make dir (build phase)",
"Relative to build dir, e.g. lib, libelf (empty = build root)",
)?;
flags.make_test_dirs = prompt_repeating_list(
"Make dir (test phase)",
"Relative to build dir, e.g. tests (empty = build root)",
)?;
flags.make_install_dirs = prompt_repeating_list(
"Make dir (install phase)",
"Relative to build dir, e.g. lib, apps (empty = build root)",
)?;
flags.skip_tests = Confirm::new("Skip automatic tests (make check/test)?")
.with_help_message(
"If enabled, Depot will not auto-run detected Autotools test targets",
)
.with_default(false)
.prompt()?;
}
if matches!(build_type, BuildType::Makefile) {
crate::log_info!("Define Makefile build and install commands.");
flags.makefile_commands = prompt_repeating_list(
"makefile build command",
"Examples: make -j$(nproc), make all",
)?;
if flags.makefile_commands.is_empty() {
anyhow::bail!("Makefile build type requires at least one build command");
}
flags.makefile_install_commands = prompt_repeating_list(
"makefile install command",
"Examples: make DESTDIR=$DESTDIR install",
)?;
if flags.makefile_install_commands.is_empty() {
anyhow::bail!("Makefile build type requires at least one install command");
}
}
if matches!(build_type, BuildType::Rust) {
let profiles = vec!["release", "debug"];
let profile = Select::new("Cargo profile:", profiles)
.with_help_message("Release is recommended for production packages")
.prompt()?;
flags.profile = profile.to_string();
flags.target = prompt_optional_text(
"Rust target triple (optional):",
"Example: x86_64-unknown-linux-gnu",
)?;
if show_advanced {
flags.rustflags = prompt_repeating_list(
"RUSTFLAG",
"One flag per entry, e.g. -Ctarget-cpu=native",
)?;
flags.cargs = prompt_repeating_list(
"Extra cargo arg",
"Examples: --locked, --features, serde, --no-default-features",
)?;
}
flags.bindir = Text::new("Binary install dir:")
.with_help_message("Destination inside package image")
.with_default(flags.bindir.as_str())
.prompt()?;
}
if matches!(build_type, BuildType::Bin) {
flags.binary_type = prompt_optional_text(
"Binary type (optional):",
"Metadata only, e.g. deb, rpm, tarball",
)?; )?;
} }
flags.bindir = Text::new("Binary install dir:")
.with_help_message("Destination inside package image")
.with_default(flags.bindir.as_str())
.prompt()?;
}
if matches!(build_type, BuildType::Bin) { if show_advanced {
flags.binary_type = prompt_optional_text( flags.post_configure = prompt_repeating_list(
"Binary type (optional):", "post_configure command",
"Metadata only, e.g. deb, rpm, tarball", "Runs after configure/setup, before build",
)?; )?;
} flags.post_compile =
prompt_repeating_list("post_compile command", "Runs after build, before install")?;
if show_advanced { flags.post_install =
flags.post_configure = prompt_repeating_list( prompt_repeating_list("post_install command", "Runs after install step")?;
"post_configure command", }
"Runs after configure/setup, before build",
)?;
flags.post_compile =
prompt_repeating_list("post_compile command", "Runs after build, before install")?;
flags.post_install =
prompt_repeating_list("post_install command", "Runs after install step")?;
} }
let runtime_deps = let runtime_deps =
@@ -517,7 +538,7 @@ pub fn create_interactive() -> Result<PackageSpec> {
packages: Vec::new(), packages: Vec::new(),
alternatives: Alternatives::default(), alternatives: Alternatives::default(),
manual_sources: Vec::new(), manual_sources: Vec::new(),
source: vec![source], source: sources,
build: Build { build_type, flags }, build: Build { build_type, flags },
dependencies: Dependencies { dependencies: Dependencies {
build: build_deps, build: build_deps,
@@ -1382,6 +1403,43 @@ mod tests {
assert_eq!(test_deps[1].as_str(), Some("bats")); assert_eq!(test_deps[1].as_str(), Some("bats"));
} }
#[test]
fn spec_to_minimal_toml_supports_metapackage_without_sources() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo-meta".into(),
version: "1.0".into(),
revision: 1,
description: "Meta package".into(),
homepage: "".into(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Meta,
flags: BuildFlags::default(),
},
dependencies: Dependencies {
build: Vec::new(),
runtime: vec!["foo".into(), "bar".into()],
test: Vec::new(),
},
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("type = \"meta\""));
assert!(!toml.contains("[[source]]"));
let val: toml::Value = toml::from_str(&toml).unwrap();
assert!(val.get("source").is_none());
}
#[test] #[test]
fn compute_sha256_for_local_path_and_file_url() { fn compute_sha256_for_local_path_and_file_url() {
use sha2::Digest as TestDigest; use sha2::Digest as TestDigest;