diff --git a/Cargo.lock b/Cargo.lock index 4d28ea4..8301c5a 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -418,7 +418,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" [[package]] name = "depot" -version = "0.13.3" +version = "0.14.0" dependencies = [ "anyhow", "ar", @@ -2228,9 +2228,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" dependencies = [ "indexmap", "serde_core", @@ -2243,9 +2243,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] diff --git a/Cargo.toml b/Cargo.toml index 8732229..445fcc5 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "depot" -version = "0.13.3" +version = "0.14.0" edition = "2024" [lints.rust] @@ -23,7 +23,7 @@ sha2 = "0.10.9" tar = "0.4.44" tempfile = "=3.26.0" thiserror = "2.0.18" -toml = "0.9.8" +toml = "1.0.6" url = "2.5.8" walkdir = "2.5.0" xz2 = "0.1.7" diff --git a/README.md b/README.md index 02b39fe..65280e6 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,12 @@ depot install zlib-1.2.11-1-x86_64.depot.pkg.tar.zst - Resolves a full dependency plan first (binary repos and/or source specs), then executes in dependency order. - Use `--yes` for non-interactive confirmation/provider selection. - Use `--dry-run` to print the plan without performing work. + - Use `--test-deps` to include declared test dependencies in dependency installation. - Binary package installs verify both checksums and detached minisign signatures (`.sig`). - `remove `: Remove an installed package. - `build `: Build a package and create an archive without installing. - Resolves and offers to install missing build dependencies before fetching/building. - - Missing test dependencies automatically disable test execution for that build. + - Missing test dependencies automatically disable test execution unless `--test-deps` or `[install].test_deps = true` is set. - `update [PACKAGE ...]`: Update installed packages from configured repositories. - With no package names, updates every installed package that has a newer repo version available. - Refreshes source repos first, compares installed package version/revision and UTC completion time against repo metadata, and installs any newly introduced runtime dependencies before applying updates. diff --git a/contrib/depot.toml b/contrib/depot.toml index e7ce2c5..df7daa9 100644 --- a/contrib/depot.toml +++ b/contrib/depot.toml @@ -36,3 +36,7 @@ carch = "x86_64" # Target triples (optional) #chost = "x86_64-pc-linux-gnu" #cbuild = "x86_64-pc-linux-gnu" + +[install] +# Install test dependencies along with build/runtime dependencies. +test_deps = false diff --git a/contrib/user.depot.toml.example b/contrib/user.depot.toml.example index 037707c..789178d 100644 --- a/contrib/user.depot.toml.example +++ b/contrib/user.depot.toml.example @@ -9,6 +9,9 @@ cflags = ["-O2"] cflags += ["-g"] carch = "x86_64" +[install] +test_deps = false + # Optional install directory overrides for supported builders #bindir = "/usr/local/bin" #sbindir = "/usr/local/sbin" diff --git a/meson.build b/meson.build index 0a495c9..33d1a4b 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project( 'depot', - version: '0.13.3', + version: '0.14.0', meson_version: '>=0.60.0', ) diff --git a/src/cli.rs b/src/cli.rs index a4c1084..cd439ad 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -41,6 +41,10 @@ pub struct Cli { #[arg(long, global = true)] pub dry_run: bool, + /// Install test dependencies alongside build/runtime dependencies + #[arg(long, global = true)] + pub test_deps: bool, + /// Build/install only the lib32 companion package path (skip primary package output) #[arg(long, global = true)] pub lib32_only: bool, @@ -310,4 +314,10 @@ mod tests { _ => panic!("expected check command"), } } + + #[test] + fn global_test_deps_flag_is_parsed() { + let cli = Cli::try_parse_from(["depot", "--test-deps", "install", "foo"]).unwrap(); + assert!(cli.test_deps); + } } diff --git a/src/commands.rs b/src/commands.rs index 33ccb01..cc4673d 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -40,6 +40,10 @@ fn should_delegate_live_rootfs_installs(rootfs: &Path) -> bool { !crate::fakeroot::is_root() && rootfs_is_system_root(rootfs) } +fn install_test_deps_enabled(cli_test_deps: bool, config: &config::Config) -> bool { + cli_test_deps || config.install_test_deps +} + fn maybe_reexec_with_sudo(cli: &Cli) -> Result { if !should_reexec_with_sudo(cli) { return Ok(false); @@ -67,6 +71,7 @@ struct ChildInstallCommandOptions<'a> { no_flags: bool, cross_prefix: Option<&'a str>, clean: bool, + install_test_deps: bool, dep_chain: Option<&'a str>, } @@ -105,6 +110,9 @@ fn run_install_command_with_program( if options.clean { cmd.arg("--clean"); } + if options.install_test_deps { + cmd.arg("--test-deps"); + } cmd.arg("install"); cmd.args(install_requests); if let Some(dep_chain) = options.dep_chain { @@ -144,6 +152,7 @@ fn run_child_install_command( no_flags: options.no_flags, cross_prefix: options.cross_prefix, clean: options.clean, + install_test_deps: options.install_test_deps, dep_chain: None, }, ) @@ -477,6 +486,10 @@ fn maybe_disable_tests_for_missing_deps( Ok(()) } +fn should_install_test_deps(pkg_spec: &package::PackageSpec, install_test_deps: bool) -> bool { + install_test_deps && !pkg_spec.build.flags.skip_tests && !pkg_spec.dependencies.test.is_empty() +} + fn clean_build_workspace(config: &config::Config) -> Result<()> { if config.build_dir.exists() { fs::remove_dir_all(&config.build_dir).with_context(|| { @@ -1249,6 +1262,7 @@ struct UpdateCommandOptions<'a> { clean: bool, dry_run: bool, assume_yes: bool, + install_test_deps: bool, } #[derive(Debug, Clone)] @@ -1782,6 +1796,7 @@ fn run_update_install_command( no_flags: options.no_flags, cross_prefix: options.cross_prefix, clean: options.clean, + install_test_deps: options.install_test_deps, dep_chain: None, }, ) @@ -1856,6 +1871,7 @@ fn run_update_command( assume_yes: options.assume_yes, prefer_binary: config.repo_settings.prefer_binary, local_sibling_root: None, + include_test_deps: options.install_test_deps, }, )?; print_plan_summary(&dep_plan); @@ -1875,6 +1891,7 @@ fn run_update_command( clean: options.clean, dry_run: false, confirm_installation: false, + install_test_deps: options.install_test_deps, }, )?; } else if options.dry_run { @@ -2350,6 +2367,7 @@ struct InstallPlanExecutionOptions<'a> { clean: bool, dry_run: bool, confirm_installation: bool, + install_test_deps: bool, } fn execute_install_plan_with_child_commands( @@ -2695,6 +2713,7 @@ struct DirectInstallOptions<'a> { clean: bool, dry_run: bool, lib32_only: bool, + install_test_deps: bool, } fn run_direct_archive_install_requests( @@ -2897,7 +2916,9 @@ fn run_direct_install_request( })?; let db_path = config.installed_db_path(options.rootfs); - if staging_dir.is_none() { + if staging_dir.is_none() + && (options.no_deps || !should_install_test_deps(&pkg_spec, options.install_test_deps)) + { maybe_disable_tests_for_missing_deps(&mut pkg_spec, &db_path)?; } @@ -2910,6 +2931,11 @@ fn run_direct_install_request( deps::check_build_deps(&pkg_spec, &db_path)?, deps::check_runtime_deps(&pkg_spec, &db_path)?, ); + let missing = if should_install_test_deps(&pkg_spec, options.install_test_deps) { + merge_missing_dependencies(missing, deps::check_test_deps(&pkg_spec, &db_path)?) + } else { + missing + }; if !missing.is_empty() { // Check for dependency cycles via DEPOT_DEPCHAIN env var let dep_chain = std::env::var("DEPOT_DEPCHAIN").unwrap_or_default(); @@ -2963,6 +2989,7 @@ fn run_direct_install_request( no_flags: options.no_flags, cross_prefix: options.cross_prefix, clean: options.clean, + install_test_deps: options.install_test_deps, dep_chain: Some(&new_chain), }, )?; @@ -2972,6 +2999,9 @@ fn run_direct_install_request( // Enforce required dependencies before building/installing. deps::require_build_deps(&pkg_spec, &db_path)?; deps::require_runtime_deps(&pkg_spec, &db_path)?; + if should_install_test_deps(&pkg_spec, options.install_test_deps) { + deps::require_test_deps(&pkg_spec, &db_path)?; + } } let cross_config = options @@ -3072,6 +3102,7 @@ pub fn run(cli: Cli) -> Result<()> { return Ok(()); } + let cli_test_deps = cli.test_deps; match cli.command { Commands::Install { spec_or_archive, @@ -3084,6 +3115,7 @@ pub fn run(cli: Cli) -> Result<()> { // Load configuration early so we can use configured repos/paths. let config = config::Config::for_rootfs(&cli.rootfs); + let install_test_deps = install_test_deps_enabled(cli_test_deps, &config); let mut planned_targets = Vec::new(); let mut planned_spec_paths = Vec::new(); let mut direct_requests = Vec::new(); @@ -3114,6 +3146,7 @@ pub fn run(cli: Cli) -> Result<()> { assume_yes: cli.yes, prefer_binary: config.repo_settings.prefer_binary, local_sibling_root: shared_local_sibling_root(&planned_spec_paths), + include_test_deps: install_test_deps, }; let plan = if planned_targets.len() == 1 { planner::build_install_plan( @@ -3141,6 +3174,7 @@ pub fn run(cli: Cli) -> Result<()> { clean: cli.clean, dry_run: cli.dry_run, confirm_installation: true, + install_test_deps, }, )?; } @@ -3154,6 +3188,7 @@ pub fn run(cli: Cli) -> Result<()> { clean: cli.clean, dry_run: cli.dry_run, lib32_only: cli.lib32_only, + install_test_deps, }; if direct_requests.len() > 1 && direct_requests @@ -3241,6 +3276,7 @@ pub fn run(cli: Cli) -> Result<()> { let mut pkg_spec = package::PackageSpec::from_file(&spec_path)?; let config = config::Config::for_rootfs(&cli.rootfs); + let install_test_deps = install_test_deps_enabled(cli_test_deps, &config); // Apply system overrides pkg_spec.apply_config(&config); @@ -3254,7 +3290,9 @@ pub fn run(cli: Cli) -> Result<()> { })?; let db_path = config.installed_db_path(&cli.rootfs); - maybe_disable_tests_for_missing_deps(&mut pkg_spec, &db_path)?; + if cli.no_deps || !should_install_test_deps(&pkg_spec, install_test_deps) { + maybe_disable_tests_for_missing_deps(&mut pkg_spec, &db_path)?; + } // Check build dependencies if !cli.no_deps { @@ -3263,6 +3301,11 @@ pub fn run(cli: Cli) -> Result<()> { deps::check_build_deps(&pkg_spec, &db_path)?, deps::check_runtime_deps(&pkg_spec, &db_path)?, ); + let missing = if should_install_test_deps(&pkg_spec, install_test_deps) { + merge_missing_dependencies(missing, deps::check_test_deps(&pkg_spec, &db_path)?) + } else { + missing + }; if !missing.is_empty() { ui::warn(format!("Missing dependencies: {}", missing.join(", "))); let local_sibling_root = spec_path @@ -3277,6 +3320,7 @@ pub fn run(cli: Cli) -> Result<()> { assume_yes: cli.yes, prefer_binary: config.repo_settings.prefer_binary, local_sibling_root, + include_test_deps: install_test_deps, }, )?; print_plan_summary(&dep_plan); @@ -3297,11 +3341,15 @@ pub fn run(cli: Cli) -> Result<()> { clean: cli.clean, dry_run: cli.dry_run, confirm_installation: false, + install_test_deps, }, )?; } deps::require_build_deps(&pkg_spec, &db_path)?; deps::require_runtime_deps(&pkg_spec, &db_path)?; + if should_install_test_deps(&pkg_spec, install_test_deps) { + deps::require_test_deps(&pkg_spec, &db_path)?; + } } else if cli.dry_run { ui::info("Dry run enabled, stopping before build."); return Ok(()); @@ -3444,6 +3492,7 @@ pub fn run(cli: Cli) -> Result<()> { clean: cli.clean, dry_run: cli.dry_run, confirm_installation: false, + install_test_deps, }, )?; if cli.clean { @@ -3536,6 +3585,7 @@ pub fn run(cli: Cli) -> Result<()> { clean: cli.clean, dry_run: cli.dry_run, assume_yes: cli.yes, + install_test_deps: install_test_deps_enabled(cli_test_deps, &config), }, )?; } @@ -3990,6 +4040,7 @@ pub fn run(cli: Cli) -> Result<()> { println!("Build Directory: {}", config.build_dir.display()); println!("Database Directory: {}", config.db_dir.display()); println!("Repo Clone Directory: {}", config.repo_clone_dir.display()); + println!("Install Test Deps: {}", config.install_test_deps); println!( "Configured Repos: {} source, {} binary", config.source_repos.len(), @@ -4228,6 +4279,7 @@ mod tests { clean: false, dry_run: false, lib32_only: false, + install_test_deps: false, }, &cfg, &[archive_a, archive_b], @@ -4831,6 +4883,7 @@ optional = [] no_flags: true, cross_prefix: Some("x86_64-linux-musl"), clean: true, + install_test_deps: true, dep_chain: Some("parent"), }, )?; @@ -4846,6 +4899,7 @@ optional = [] "--cross-prefix", "x86_64-linux-musl", "--clean", + "--test-deps", "install", "/tmp/pkg-a.toml", "/tmp/pkg-b.toml", diff --git a/src/config.rs b/src/config.rs index 940e666..fc7d6a0 100755 --- a/src/config.rs +++ b/src/config.rs @@ -218,6 +218,8 @@ pub struct Config { pub repo_clone_dir: PathBuf, /// Cache directory for binary packages and repo metadata. pub package_cache_dir: PathBuf, + /// Install test dependencies alongside build/runtime dependencies. + pub install_test_deps: bool, } impl Config { @@ -260,6 +262,7 @@ impl Config { mirrors: std::collections::HashMap::new(), repo_clone_dir: abs_rootfs.join("usr/src/depot"), package_cache_dir, + install_test_deps: false, }; if let Err(e) = config.load_system(&abs_rootfs) { @@ -313,6 +316,13 @@ impl Config { if let Some(pkg) = val.get("package") { merge_toml_values(&mut self.package_overrides, pkg); } + if let Some(include_test_deps) = val + .get("install") + .and_then(|v| v.get("test_deps")) + .and_then(|v| v.as_bool()) + { + self.install_test_deps = include_test_deps; + } for (k, v) in appends { self.appends.insert(k, v); @@ -673,6 +683,9 @@ cc = "clang" [build.flags] cflags = ["-O3"] + +[install] +test_deps = true "#, ) .unwrap(); @@ -700,6 +713,7 @@ cflags = ["-O3"] .map(|a| a.len()), Some(1) ); + assert!(config.install_test_deps); } #[test] diff --git a/src/deps.rs b/src/deps.rs index 71a2fac..585bef5 100755 --- a/src/deps.rs +++ b/src/deps.rs @@ -306,6 +306,20 @@ pub fn require_runtime_deps(spec: &PackageSpec, db_path: &Path) -> Result<()> { Ok(()) } +/// Verify all test dependencies are installed, error if not. +pub fn require_test_deps(spec: &PackageSpec, db_path: &Path) -> Result<()> { + let missing = check_test_deps(spec, db_path)?; + + if !missing.is_empty() { + anyhow::bail!( + "Missing test dependencies: {}\nInstall them first with: depot install --test-deps ", + missing.join(", ") + ); + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/package/spec.rs b/src/package/spec.rs index 77735a2..c086377 100755 --- a/src/package/spec.rs +++ b/src/package/spec.rs @@ -2877,6 +2877,7 @@ cbuild = "x86_64-pc-linux-gnu" mirrors: std::collections::HashMap::new(), repo_clone_dir: PathBuf::from("/tmp"), package_cache_dir: PathBuf::from("/tmp"), + install_test_deps: false, }; spec.apply_config(&config); diff --git a/src/planner.rs b/src/planner.rs index f9a0587..5b4aa9d 100644 --- a/src/planner.rs +++ b/src/planner.rs @@ -104,6 +104,7 @@ pub(crate) struct PlannerOptions { pub assume_yes: bool, pub prefer_binary: bool, pub local_sibling_root: Option, + pub include_test_deps: bool, } #[derive(Debug, Clone)] @@ -265,9 +266,13 @@ impl<'a> Resolver<'a> { local_sibling: bool, requested_by: String, ) -> Result { + let include_test_deps = self.opts.include_test_deps; let (package_name, deps_needed) = { let spec = self.load_spec(path)?; - (spec.package.name.clone(), source_deps_for_install(spec)) + ( + spec.package.name.clone(), + source_deps_for_install(spec, include_test_deps), + ) }; if let Some(&idx) = self.by_package.get(&package_name) { self.mark_requested_by(idx, requested_by); @@ -608,7 +613,7 @@ impl<'a> Resolver<'a> { } } -fn source_deps_for_install(spec: &PackageSpec) -> Vec { +fn source_deps_for_install(spec: &PackageSpec, include_test_deps: bool) -> Vec { let mut deps_all = Vec::new(); let local_provides = spec.local_dependency_provides(); if !spec.is_metapackage() { @@ -630,6 +635,11 @@ fn source_deps_for_install(spec: &PackageSpec) -> Vec { } } } + if include_test_deps && !spec.build.flags.skip_tests { + for dep in &spec.dependencies.test { + push_unique(&mut deps_all, dep.clone()); + } + } deps_all } @@ -876,7 +886,7 @@ mod tests { #[test] fn source_deps_for_install_excludes_local_runtime_outputs_and_provides() { let spec = mk_spec(); - let deps = source_deps_for_install(&spec); + let deps = source_deps_for_install(&spec, false); assert!(deps.contains(&"make".to_string())); assert!(deps.contains(&"zlib".to_string())); assert!(deps.contains(&"openssl".to_string())); @@ -887,10 +897,17 @@ mod tests { #[test] fn source_deps_for_install_does_not_include_test_deps() { let spec = mk_spec(); - let deps = source_deps_for_install(&spec); + let deps = source_deps_for_install(&spec, false); assert!(!deps.contains(&"bats".to_string())); } + #[test] + fn source_deps_for_install_includes_test_deps_when_enabled() { + let spec = mk_spec(); + let deps = source_deps_for_install(&spec, true); + assert!(deps.contains(&"bats".to_string())); + } + #[test] fn candidate_dedup_keeps_highest_priority_origin_for_same_package() { let candidates = vec![ @@ -937,6 +954,7 @@ mod tests { assume_yes: false, prefer_binary: true, local_sibling_root: None, + include_test_deps: false, }, ) .unwrap();