diff --git a/src/cli.rs b/src/cli.rs index 773b3d8..8cf8599 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -53,12 +53,22 @@ pub struct Cli { pub enum Commands { /// Build and install a package from a spec file Install { - /// Path to package spec (.toml) or package archive (.tar.zst) - #[arg(value_name = "SPEC_OR_ARCHIVE")] - spec_or_archive: PathBuf, + /// One or more package names, spec paths (.toml), or package archives (.tar.zst) + #[arg( + value_name = "SPEC_OR_ARCHIVE", + num_args = 1.., + required_unless_present = "spec" + )] + spec_or_archive: Vec, /// Explicitly specify path to package spec (.toml file) - #[arg(short, long = "spec", visible_alias = "package", alias = "p")] + #[arg( + short, + long = "spec", + visible_alias = "package", + alias = "p", + conflicts_with = "spec_or_archive" + )] spec: Option, }, /// Remove an installed package @@ -220,3 +230,43 @@ pub enum RepoKindArg { Source, Binary, } + +#[cfg(test)] +mod tests { + use super::{Cli, Commands}; + use clap::Parser; + use std::path::PathBuf; + + #[test] + fn install_accepts_multiple_positional_targets() { + let cli = Cli::try_parse_from(["depot", "install", "base", "linux"]).unwrap(); + match cli.command { + Commands::Install { + spec_or_archive, + spec, + } => { + assert!(spec.is_none()); + assert_eq!( + spec_or_archive, + vec![PathBuf::from("base"), PathBuf::from("linux")] + ); + } + _ => panic!("expected install command"), + } + } + + #[test] + fn install_accepts_spec_flag_without_positional_target() { + let cli = Cli::try_parse_from(["depot", "install", "--spec", "pkg.toml"]).unwrap(); + match cli.command { + Commands::Install { + spec_or_archive, + spec, + } => { + assert!(spec_or_archive.is_empty()); + assert_eq!(spec, Some(PathBuf::from("pkg.toml"))); + } + _ => panic!("expected install command"), + } + } +} diff --git a/src/commands.rs b/src/commands.rs index f955afa..9b2446a 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1255,6 +1255,319 @@ fn execute_install_plan_with_child_commands( Ok(()) } +fn is_archive_install_request(spec_path: &Path) -> bool { + spec_path.exists() + && spec_path + .to_string_lossy() + .to_ascii_lowercase() + .ends_with(".tar.zst") +} + +fn shared_local_sibling_root(spec_paths: &[PathBuf]) -> Option { + let mut roots = spec_paths.iter().filter_map(|path| { + path.parent() + .and_then(|p| p.parent()) + .map(Path::to_path_buf) + }); + let first = roots.next()?; + if roots.all(|path| path == first) { + Some(first) + } else { + None + } +} + +#[derive(Clone, Copy)] +struct DirectInstallOptions<'a> { + rootfs: &'a Path, + no_deps: bool, + no_flags: bool, + cross_prefix: Option<&'a str>, + clean: bool, + dry_run: bool, + lib32_only: bool, +} + +fn run_direct_install_request( + options: DirectInstallOptions<'_>, + config: &config::Config, + mut spec_path: PathBuf, +) -> Result { + let mut install_lock = locking::open_lock(config)?; + let install_lock_path = locking::lock_path(config); + let _install_lock_guard = locking::try_write(&mut install_lock, &install_lock_path, "install")?; + + // Repo clone dir is available via `config.repo_clone_dir` and + // is passed explicitly to index builders below. + + // If the provided path doesn't exist, treat it as a package name and + // try to locate a spec under configured repo dir or local packages/. + if !spec_path.exists() { + let name = spec_path.to_string_lossy().to_string(); + ui::info(format!("Looking up package '{}' in local indexes...", name)); + let pkg_index = + index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone())); + if let Some(found) = pkg_index.find(&name) { + spec_path = found; + } else { + let host_arch = std::env::consts::ARCH; + let mut binary_repos: Vec<_> = config + .binary_repos + .iter() + .filter(|(_, repo)| repo.enabled && repo.supports_arch(host_arch)) + .collect(); + binary_repos.sort_by(|a, b| a.1.priority.cmp(&b.1.priority).then_with(|| a.0.cmp(b.0))); + + for (repo_name, repo_cfg) in binary_repos { + match db::repo::find_binary_repo_package( + repo_name, + repo_cfg, + options.rootfs, + &config.package_cache_dir, + &name, + ) { + Ok(Some(rec)) => { + let archive = db::repo::fetch_binary_package_archive( + repo_name, + repo_cfg, + options.rootfs, + &rec, + &config.package_cache_dir, + )?; + ui::info(format!( + "Resolved '{}' from binary repo '{}' as {}-{} (package {}) ({} bytes){} -> {}", + name, + repo_name, + rec.version, + rec.revision, + rec.name, + rec.size, + rec.description + .as_ref() + .map(|d| format!(" [{}]", d)) + .unwrap_or_default(), + archive.display() + )); + spec_path = archive; + break; + } + Ok(None) => {} + Err(e) => { + crate::log_warn!("Binary repo '{}': {}", repo_name, e); + } + } + } + } + } + + ui::info(format!("Installing package from: {}", spec_path.display())); + + let (mut pkg_spec, staging_dir): (package::PackageSpec, Option) = + if spec_path.to_string_lossy().ends_with(".tar.zst") { + // Install from archive + ui::info(format!("Detected package archive: {}", spec_path.display())); + let (spec, tmp_dir) = load_package_archive_into_staging(&spec_path)?; + (spec, Some(tmp_dir)) + } else { + // Install from spec (normal build) + let mut pkg_spec = package::PackageSpec::from_file(&spec_path)?; + pkg_spec.apply_config(config); + (pkg_spec, None) + }; + + if options.lib32_only && staging_dir.is_some() { + anyhow::bail!("--lib32-only is only supported when installing from a package spec"); + } + + ui::info(format!( + "Package: {} v{}-{}", + pkg_spec.package.name, pkg_spec.package.version, pkg_spec.package.revision + )); + + if options.dry_run { + ui::info("Dry run enabled, stopping before install/build work."); + return Ok(false); + } + + let install_targets = vec![format!( + "{} v{}-{}", + pkg_spec.package.name, pkg_spec.package.version, pkg_spec.package.revision + )]; + if !ui::prompt_package_action("installation", &install_targets, true)? { + anyhow::bail!("Aborted"); + } + + // TODO(snapper): create pre-install snapshot before install work starts. + + // Ensure database directory exists + std::fs::create_dir_all(&config.db_dir).with_context(|| { + format!( + "Failed to create database directory: {}", + config.db_dir.display() + ) + })?; + let db_path = config.db_dir.join("packages.db"); + + if staging_dir.is_none() { + maybe_disable_tests_for_missing_deps(&mut pkg_spec, &db_path)?; + } + + // Check dependencies and prompt for auto-install if needed + if !options.no_deps { + deps::print_dep_status(&pkg_spec, &db_path)?; + + // Collect all missing dependencies (build + runtime) + let missing = merge_missing_dependencies( + deps::check_build_deps(&pkg_spec, &db_path)?, + deps::check_runtime_deps(&pkg_spec, &db_path)?, + ); + if !missing.is_empty() { + // Check for dependency cycles via DEPOT_DEPCHAIN env var + let dep_chain = std::env::var("DEPOT_DEPCHAIN").unwrap_or_default(); + let chain_set: std::collections::HashSet<&str> = + dep_chain.split(',').filter(|s| !s.is_empty()).collect(); + + if chain_set.contains(pkg_spec.package.name.as_str()) { + anyhow::bail!( + "Dependency cycle detected! {} is already in chain: {}", + pkg_spec.package.name, + dep_chain + ); + } + + ui::warn(format!("Missing dependencies: {}", missing.join(", "))); + if ui::prompt_package_action("dependency installation", &missing, true)? { + // Build package index for fast lookups + let pkg_index = + index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone())); + + // Build new dep chain + let new_chain = if dep_chain.is_empty() { + pkg_spec.package.name.clone() + } else { + format!("{},{}", dep_chain, pkg_spec.package.name) + }; + + // Attempt to install missing deps + for dep in missing { + // Use package index for O(1) lookup + let candidate = pkg_index.find(&dep); + + if let Some(dep_spec_path) = candidate { + ui::info(format!("Installing dependency: {}...", dep)); + + let mut cmd = std::process::Command::new(std::env::current_exe()?); + cmd.arg("-r").arg(options.rootfs); + + if options.no_deps { + cmd.arg("--no-deps"); + } + if options.no_flags { + cmd.arg("--no-flags"); + } + if let Some(p) = options.cross_prefix { + cmd.arg("--cross-prefix").arg(p); + } + if options.clean { + cmd.arg("--clean"); + } + + cmd.arg("install").arg(&dep_spec_path); + cmd.env("DEPOT_DEPCHAIN", &new_chain); + + let status = cmd.status()?; + + if !status.success() { + anyhow::bail!("Failed to install dependency: {}", dep); + } + } else { + anyhow::bail!("Could not find package spec for dependency: {}", dep); + } + } + } + } + + // Enforce required dependencies before building/installing. + deps::require_build_deps(&pkg_spec, &db_path)?; + deps::require_runtime_deps(&pkg_spec, &db_path)?; + } + + let cross_config = options + .cross_prefix + .map(cross::CrossConfig::from_prefix) + .transpose()?; + let mut built_src_dir: Option = None; + + let destdir = if let Some(dir) = &staging_dir { + dir.path().to_path_buf() + } else { + // 1-2. Fetch + extract sources (supports archives and git URL#rev) + let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?; + built_src_dir = Some(src_dir.clone()); + + // 3. Build + let destdir = config + .build_dir + .join("destdir") + .join(&pkg_spec.package.name); + + if !options.lib32_only { + builder::build( + &pkg_spec, + &src_dir, + &destdir, + cross_config.as_ref(), + !options.no_flags, + )?; + + // 3.1 Copy license files into staged tree + staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?; + install::scripts::stage_scripts_from_spec_dir(&pkg_spec, &destdir)?; + } + + destdir + }; + + if !options.lib32_only { + if staging_dir.is_none() { + // Source-build path: apply staging transforms (strip/compress/static cleanup). + staging::process(&destdir, &pkg_spec)?; + } else { + // Binary archive path: install as-packaged without post-build transformations. + ui::info("Installing binary archive payload without staging transforms"); + } + + let installed = + install_package_outputs_to_rootfs(&pkg_spec, &destdir, options.rootfs, config)?; + for pkg in installed { + ui::success(format!( + "Successfully installed {} v{}", + pkg.name, pkg.version + )); + // TODO(snapper): create post-install snapshot after install commit succeeds. + } + } + + if let Some(src_dir) = built_src_dir.as_deref() + && let Some((lib32_spec, lib32_destdir)) = build_lib32_companion_package( + &pkg_spec, + src_dir, + config, + cross_config.as_ref(), + !options.no_flags, + options.lib32_only, + )? + { + install_staged_to_rootfs(&lib32_spec, &lib32_destdir, options.rootfs, config)?; + ui::success(format!( + "Successfully installed {} v{}", + lib32_spec.package.name, lib32_spec.package.version + )); + } + + Ok(true) +} + pub fn run(cli: Cli) -> Result<()> { ui::set_assume_yes(cli.yes); @@ -1266,340 +1579,95 @@ pub fn run(cli: Cli) -> Result<()> { if !crate::fakeroot::is_root() { anyhow::bail!("The 'install' command must be run as root"); } - let mut spec_path = spec.unwrap_or(spec_or_archive); - - // Load configuration early so we can use the configured repo clone dir - let config = config::Config::for_rootfs(&cli.rootfs); - - if !cli.no_deps { - let request_path_like = spec_path.exists(); - let is_archive_request = request_path_like - && spec_path - .to_string_lossy() - .to_ascii_lowercase() - .ends_with(".tar.zst"); - if !is_archive_request { - let target = if request_path_like { - planner::InstallTarget::SpecPath(spec_path.clone()) - } else { - planner::InstallTarget::PackageName(spec_path.to_string_lossy().to_string()) - }; - let local_sibling_root = spec_path - .parent() - .and_then(|p| p.parent()) - .map(Path::to_path_buf); - let plan = planner::build_install_plan( - &config, - &cli.rootfs, - target, - planner::PlannerOptions { - assume_yes: cli.yes, - prefer_binary: config.repo_settings.prefer_binary, - local_sibling_root, - }, - )?; - print_plan_summary(&plan); - execute_install_plan_with_child_commands( - &plan, - &cli.rootfs, - &config, - InstallPlanExecutionOptions { - no_flags: cli.no_flags, - cross_prefix: cli.cross_prefix.as_deref(), - clean: cli.clean, - dry_run: cli.dry_run, - confirm_installation: true, - }, - )?; - if cli.clean { - clean_build_workspace(&config)?; - } - return Ok(()); - } - } - - let mut install_lock = locking::open_lock(&config)?; - let install_lock_path = locking::lock_path(&config); - let _install_lock_guard = - locking::try_write(&mut install_lock, &install_lock_path, "install")?; - - // Repo clone dir is available via `config.repo_clone_dir` and - // is passed explicitly to index builders below. - - // If the provided path doesn't exist, treat it as a package name and - // try to locate a spec under configured repo dir or local packages/. - if !spec_path.exists() { - let name = spec_path.to_string_lossy().to_string(); - ui::info(format!("Looking up package '{}' in local indexes...", name)); - let pkg_index = - index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone())); - if let Some(found) = pkg_index.find(&name) { - spec_path = found; - } else { - let host_arch = std::env::consts::ARCH; - let mut binary_repos: Vec<_> = config - .binary_repos - .iter() - .filter(|(_, repo)| repo.enabled && repo.supports_arch(host_arch)) - .collect(); - binary_repos - .sort_by(|a, b| a.1.priority.cmp(&b.1.priority).then_with(|| a.0.cmp(b.0))); - - for (repo_name, repo_cfg) in binary_repos { - match db::repo::find_binary_repo_package( - repo_name, - repo_cfg, - &cli.rootfs, - &config.package_cache_dir, - &name, - ) { - Ok(Some(rec)) => { - let archive = db::repo::fetch_binary_package_archive( - repo_name, - repo_cfg, - &cli.rootfs, - &rec, - &config.package_cache_dir, - )?; - ui::info(format!( - "Resolved '{}' from binary repo '{}' as {}-{} (package {}) ({} bytes){} -> {}", - name, - repo_name, - rec.version, - rec.revision, - rec.name, - rec.size, - rec.description - .as_ref() - .map(|d| format!(" [{}]", d)) - .unwrap_or_default(), - archive.display() - )); - spec_path = archive; - break; - } - Ok(None) => {} - Err(e) => { - crate::log_warn!("Binary repo '{}': {}", repo_name, e); - } - } - } - } - } - - ui::info(format!("Installing package from: {}", spec_path.display())); - - let (mut pkg_spec, staging_dir): (package::PackageSpec, Option) = - if spec_path.to_string_lossy().ends_with(".tar.zst") { - // Install from archive - ui::info(format!("Detected package archive: {}", spec_path.display())); - let (spec, tmp_dir) = load_package_archive_into_staging(&spec_path)?; - (spec, Some(tmp_dir)) - } else { - // Install from spec (normal build) - let mut pkg_spec = package::PackageSpec::from_file(&spec_path)?; - pkg_spec.apply_config(&config); - (pkg_spec, None) - }; - - if cli.lib32_only && staging_dir.is_some() { - anyhow::bail!("--lib32-only is only supported when installing from a package spec"); - } - - ui::info(format!( - "Package: {} v{}-{}", - pkg_spec.package.name, pkg_spec.package.version, pkg_spec.package.revision - )); - - if cli.dry_run { - ui::info("Dry run enabled, stopping before install/build work."); - return Ok(()); - } - - let install_targets = vec![format!( - "{} v{}-{}", - pkg_spec.package.name, pkg_spec.package.version, pkg_spec.package.revision - )]; - if !ui::prompt_package_action("installation", &install_targets, true)? { - anyhow::bail!("Aborted"); - } - - // TODO(snapper): create pre-install snapshot before install work starts. - - // Ensure database directory exists - std::fs::create_dir_all(&config.db_dir).with_context(|| { - format!( - "Failed to create database directory: {}", - config.db_dir.display() - ) - })?; - let db_path = config.db_dir.join("packages.db"); - - if staging_dir.is_none() { - maybe_disable_tests_for_missing_deps(&mut pkg_spec, &db_path)?; - } - - // Check dependencies and prompt for auto-install if needed - if !cli.no_deps { - deps::print_dep_status(&pkg_spec, &db_path)?; - - // Collect all missing dependencies (build + runtime) - let missing = merge_missing_dependencies( - deps::check_build_deps(&pkg_spec, &db_path)?, - deps::check_runtime_deps(&pkg_spec, &db_path)?, - ); - if !missing.is_empty() { - // Check for dependency cycles via DEPOT_DEPCHAIN env var - let dep_chain = std::env::var("DEPOT_DEPCHAIN").unwrap_or_default(); - let chain_set: std::collections::HashSet<&str> = - dep_chain.split(',').filter(|s| !s.is_empty()).collect(); - - if chain_set.contains(pkg_spec.package.name.as_str()) { - anyhow::bail!( - "Dependency cycle detected! {} is already in chain: {}", - pkg_spec.package.name, - dep_chain - ); - } - - ui::warn(format!("Missing dependencies: {}", missing.join(", "))); - if ui::prompt_package_action("dependency installation", &missing, true)? { - // Build package index for fast lookups - let pkg_index = index::PackageIndex::build_with_repo_dir(Some( - config.repo_clone_dir.clone(), - )); - - // Build new dep chain - let new_chain = if dep_chain.is_empty() { - pkg_spec.package.name.clone() - } else { - format!("{},{}", dep_chain, pkg_spec.package.name) - }; - - // Attempt to install missing deps - for dep in missing { - // Use package index for O(1) lookup - let candidate = pkg_index.find(&dep); - - if let Some(dep_spec_path) = candidate { - ui::info(format!("Installing dependency: {}...", dep)); - - let mut cmd = std::process::Command::new(std::env::current_exe()?); - cmd.arg("-r").arg(&cli.rootfs); - - if cli.no_deps { - cmd.arg("--no-deps"); - } - if cli.no_flags { - cmd.arg("--no-flags"); - } - if let Some(ref p) = cli.cross_prefix { - cmd.arg("--cross-prefix").arg(p); - } - if cli.clean { - cmd.arg("--clean"); - } - - cmd.arg("install").arg(&dep_spec_path); - cmd.env("DEPOT_DEPCHAIN", &new_chain); - - let status = cmd.status()?; - - if !status.success() { - anyhow::bail!("Failed to install dependency: {}", dep); - } - } else { - anyhow::bail!( - "Could not find package spec for dependency: {}", - dep - ); - } - } - } - } - - // Enforce required dependencies before building/installing. - deps::require_build_deps(&pkg_spec, &db_path)?; - deps::require_runtime_deps(&pkg_spec, &db_path)?; - } - - let cross_config = cli - .cross_prefix - .as_ref() - .map(|p| cross::CrossConfig::from_prefix(p)) - .transpose()?; - let mut built_src_dir: Option = None; - - let destdir = if let Some(dir) = &staging_dir { - dir.path().to_path_buf() - } else { - // 1-2. Fetch + extract sources (supports archives and git URL#rev) - let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?; - built_src_dir = Some(src_dir.clone()); - - // 3. Build - let destdir = config - .build_dir - .join("destdir") - .join(&pkg_spec.package.name); - - if !cli.lib32_only { - builder::build( - &pkg_spec, - &src_dir, - &destdir, - cross_config.as_ref(), - !cli.no_flags, - )?; - - // 3.1 Copy license files into staged tree - staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?; - install::scripts::stage_scripts_from_spec_dir(&pkg_spec, &destdir)?; - } - - destdir + let install_requests = match spec { + Some(spec_path) => vec![spec_path], + None => spec_or_archive, }; - if !cli.lib32_only { - if staging_dir.is_none() { - // Source-build path: apply staging transforms (strip/compress/static cleanup). - staging::process(&destdir, &pkg_spec)?; + // Load configuration early so we can use configured repos/paths. + let config = config::Config::for_rootfs(&cli.rootfs); + let mut planned_targets = Vec::new(); + let mut planned_spec_paths = Vec::new(); + let mut direct_requests = Vec::new(); + + if cli.no_deps { + direct_requests = install_requests; + } else { + for request in install_requests { + if is_archive_install_request(&request) { + direct_requests.push(request); + continue; + } + if request.exists() { + planned_spec_paths.push(request.clone()); + planned_targets.push(planner::InstallTarget::SpecPath(request)); + } else { + planned_targets.push(planner::InstallTarget::PackageName( + request.to_string_lossy().to_string(), + )); + } + } + } + + let mut ran_plan_mode = false; + if !planned_targets.is_empty() { + ran_plan_mode = true; + let planner_opts = planner::PlannerOptions { + assume_yes: cli.yes, + prefer_binary: config.repo_settings.prefer_binary, + local_sibling_root: shared_local_sibling_root(&planned_spec_paths), + }; + let plan = if planned_targets.len() == 1 { + planner::build_install_plan( + &config, + &cli.rootfs, + planned_targets[0].clone(), + planner_opts, + )? } else { - // Binary archive path: install as-packaged without post-build transformations. - ui::info("Installing binary archive payload without staging transforms"); - } - - let installed = - install_package_outputs_to_rootfs(&pkg_spec, &destdir, &cli.rootfs, &config)?; - for pkg in installed { - ui::success(format!( - "Successfully installed {} v{}", - pkg.name, pkg.version - )); - // TODO(snapper): create post-install snapshot after install commit succeeds. - } - } - - if let Some(src_dir) = built_src_dir.as_deref() - && let Some((lib32_spec, lib32_destdir)) = build_lib32_companion_package( - &pkg_spec, - src_dir, + planner::build_install_plan_for_targets( + &config, + &cli.rootfs, + &planned_targets, + planner_opts, + )? + }; + print_plan_summary(&plan); + execute_install_plan_with_child_commands( + &plan, + &cli.rootfs, &config, - cross_config.as_ref(), - !cli.no_flags, - cli.lib32_only, - )? - { - install_staged_to_rootfs(&lib32_spec, &lib32_destdir, &cli.rootfs, &config)?; - ui::success(format!( - "Successfully installed {} v{}", - lib32_spec.package.name, lib32_spec.package.version - )); + InstallPlanExecutionOptions { + no_flags: cli.no_flags, + cross_prefix: cli.cross_prefix.as_deref(), + clean: cli.clean, + dry_run: cli.dry_run, + confirm_installation: true, + }, + )?; } - install::scripts::run_deferred_hooks_if_possible(&cli.rootfs)?; + let mut ran_direct_install = false; + for request in direct_requests { + ran_direct_install |= run_direct_install_request( + DirectInstallOptions { + rootfs: &cli.rootfs, + no_deps: cli.no_deps, + no_flags: cli.no_flags, + cross_prefix: cli.cross_prefix.as_deref(), + clean: cli.clean, + dry_run: cli.dry_run, + lib32_only: cli.lib32_only, + }, + &config, + request, + )?; + } + if ran_direct_install { + install::scripts::run_deferred_hooks_if_possible(&cli.rootfs)?; + } - if cli.clean { + if cli.clean && (ran_plan_mode || ran_direct_install) { clean_build_workspace(&config)?; } } diff --git a/src/install/hooks.rs b/src/install/hooks.rs index e021b9f..25790ee 100644 --- a/src/install/hooks.rs +++ b/src/install/hooks.rs @@ -429,7 +429,8 @@ fn run_hook_command( .env("DEPOT_PACKAGE", ctx.package) .env("DEPOT_ROOTFS", rootfs) .env("DEPOT_HOOK_FILE", &hook.file_path) - .env("DEPOT_HOOK_NAME", &hook_name); + .env("DEPOT_HOOK_NAME", &hook_name) + .env("PATH", crate::runtime_env::safe_script_path()); let status = run_command_with_optional_stdin(&mut command, stdin_payload.as_deref()) .with_context(|| { diff --git a/src/install/scripts.rs b/src/install/scripts.rs index 199d073..b7dc971 100644 --- a/src/install/scripts.rs +++ b/src/install/scripts.rs @@ -467,6 +467,7 @@ fn run_script_with_rootfs_context( .env("DEPOT_ROOTFS", &rootfs_env) .env("DEPOT_ACTION", hook.action()) .env("DEPOT_PHASE", hook.phase()) + .env("PATH", crate::runtime_env::safe_script_path()) .status() .with_context(|| { format!( @@ -497,7 +498,8 @@ fn run_hook_script_in_chroot( .env("DEPOT_PACKAGE", pkg_name) .env("DEPOT_ROOTFS", "/") .env("DEPOT_ACTION", hook.action()) - .env("DEPOT_PHASE", hook.phase()); + .env("DEPOT_PHASE", hook.phase()) + .env("PATH", crate::runtime_env::safe_script_path()); if quiet { cmd.stdout(Stdio::null()).stderr(Stdio::null()); } @@ -1068,6 +1070,30 @@ mod tests { ); } + #[test] + fn run_hook_if_present_uses_safe_script_path() { + let tmp = tempfile::tempdir().unwrap(); + let scripts = tmp.path().join("scripts"); + let rootfs = tmp.path().join("root"); + std::fs::create_dir_all(&scripts).unwrap(); + std::fs::create_dir_all(&rootfs).unwrap(); + + std::fs::write( + scripts.join("pre_install"), + "echo \"$PATH\" > \"$DEPOT_ROOTFS/path.out\"\n", + ) + .unwrap(); + + let ran = run_hook_if_present(&scripts, Hook::PreInstall, &rootfs, "foo").unwrap(); + assert!(ran); + assert_eq!( + std::fs::read_to_string(rootfs.join("path.out")) + .unwrap() + .trim_end(), + crate::runtime_env::safe_script_path() + ); + } + #[test] fn run_hook_if_present_accepts_compact_script_name() { let tmp = tempfile::tempdir().unwrap(); diff --git a/src/main.rs b/src/main.rs index c30a242..38dbf35 100755 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ mod install; mod locking; mod package; mod planner; +mod runtime_env; mod shell_helpers; mod signing; mod source; diff --git a/src/planner.rs b/src/planner.rs index 936d1fd..dc35ac3 100644 --- a/src/planner.rs +++ b/src/planner.rs @@ -179,15 +179,27 @@ impl<'a> Resolver<'a> { } fn plan_for_install_target(mut self, target: InstallTarget) -> Result { + self.resolve_install_target(&target)?; + self.finish_plan() + } + + fn plan_for_install_targets(mut self, targets: &[InstallTarget]) -> Result { + for target in targets { + self.resolve_install_target(target)?; + } + self.finish_plan() + } + + fn resolve_install_target(&mut self, target: &InstallTarget) -> Result<()> { match target { InstallTarget::PackageName(name) => { - if deps::is_dep_satisfied_in_db(&name, &self.db_path)? { + if deps::is_dep_satisfied_in_db(name, &self.db_path)? { if self.emitted_installed_roots.insert(name.clone()) { - self.add_installed_root_step(name, "requested package".to_string()); + self.add_installed_root_step(name.clone(), "requested package".to_string()); } } else { let root = - self.resolve_dep_node(&name, None, "requested package".to_string())?; + self.resolve_dep_node(name, None, "requested package".to_string())?; if let Some(root_idx) = root { self.mark_requested_by(root_idx, "requested package".to_string()); } @@ -195,11 +207,11 @@ impl<'a> Resolver<'a> { } InstallTarget::SpecPath(path) => { let root_idx = - self.ensure_source_spec_node(&path, true, true, "requested spec".to_string())?; + self.ensure_source_spec_node(path, true, true, "requested spec".to_string())?; self.mark_requested_by(root_idx, "requested spec".to_string()); } } - self.finish_plan() + Ok(()) } fn plan_for_deps(mut self, deps_to_install: &[String]) -> Result { @@ -702,6 +714,15 @@ pub(crate) fn build_install_plan( Resolver::new(config, rootfs, opts).plan_for_install_target(target) } +pub(crate) fn build_install_plan_for_targets( + config: &Config, + rootfs: &Path, + targets: &[InstallTarget], + opts: PlannerOptions, +) -> Result { + Resolver::new(config, rootfs, opts).plan_for_install_targets(targets) +} + pub(crate) fn build_dependency_install_plan( config: &Config, rootfs: &Path, diff --git a/src/runtime_env.rs b/src/runtime_env.rs new file mode 100644 index 0000000..57555f3 --- /dev/null +++ b/src/runtime_env.rs @@ -0,0 +1,16 @@ +//! Shared runtime environment defaults for script and hook execution. + +use std::path::Path; + +/// Deterministic command search path for shell scripts and hook commands. +pub const SAFE_SCRIPT_PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; + +/// Return the deterministic command search path used for script execution. +pub fn safe_script_path() -> &'static str { + SAFE_SCRIPT_PATH +} + +/// Prepend a helper binary directory before the deterministic script path. +pub fn prepend_helper_to_safe_path(helper_bin_dir: &Path) -> String { + format!("{}:{}", helper_bin_dir.display(), SAFE_SCRIPT_PATH) +} diff --git a/src/shell_helpers.rs b/src/shell_helpers.rs index 16182cf..dc03f6f 100644 --- a/src/shell_helpers.rs +++ b/src/shell_helpers.rs @@ -63,13 +63,7 @@ impl ShellHelpers { })?; } - let parent_path = std::env::var("PATH").unwrap_or_default(); - let helper_path = bin_dir.to_string_lossy().into_owned(); - let path_value = if parent_path.is_empty() { - helper_path - } else { - format!("{}:{}", helper_path, parent_path) - }; + let path_value = crate::runtime_env::prepend_helper_to_safe_path(&bin_dir); Ok(Self { _tempdir: tempdir, diff --git a/src/source/hooks.rs b/src/source/hooks.rs index 5868fe8..aaba6b3 100755 --- a/src/source/hooks.rs +++ b/src/source/hooks.rs @@ -61,6 +61,7 @@ fn apply_patches( // Apply with patch(1). Keep it simple: -p1 is the common case. let status = Command::new("patch") .current_dir(src_dir) + .env("PATH", crate::runtime_env::safe_script_path()) .arg("-p1") .arg("-i") .arg(&patch_path) @@ -93,6 +94,7 @@ fn run_post_extract_commands(spec: &PackageSpec, source: &Source, src_dir: &Path let status = Command::new("sh") .current_dir(src_dir) .env("DEPOT_SPECDIR", &spec.spec_dir) + .env("PATH", crate::runtime_env::safe_script_path()) .arg("-c") .arg(&cmd) .status()