use crate::cli::{ BuildArgs, Cli, Commands, ConfigArgs, ConvertArgs, InfoArgs, InstallArgs, InternalCommands, ListArgs, OwnsArgs, RemoveArgs, RepoCommands, RepoKindArg, SearchArgs, SetArgs, SignArgs, SystemArgs, UpdateArgs, }; use crate::{ bootstrap, builder, cli_assets, config, cross, db, deps, index, install, locking, package, planner, signing, source, staging, ui, }; use anyhow::{Context, Result}; use git2::Direction; use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fs; use std::io::IsTerminal; use std::path::{Path, PathBuf}; use std::time::Duration; use url::Url; use walkdir::WalkDir; use build_cmd::support::{ automatic_tests_disabled_for_outputs, build_lib32_companion_package, clean_build_source_dirs, clean_build_workspace, effective_lib32_only, ensure_requested_development_package_installed, make_lib32_package_spec, maybe_disable_tests_for_missing_deps, maybe_prompt_to_skip_tests_for_missing_requested_deps, merge_missing_dependencies, requested_outputs, should_install_test_deps, }; use install_cmd::archive::{ extract_package_archive_to_staging, load_package_archive_into_staging, load_package_spec_from_staging_or_repo_record, }; use repo::groups::{binary_arch_from_filename, human_bytes}; use update::candidates::{collect_update_candidates, compare_completed_at}; use update::versions::compare_versions_for_updates; #[cfg(test)] use build_cmd::support::{build_type_runs_automatic_tests, make_lib32_build_spec}; #[cfg(test)] use install_cmd::archive::{package_spec_from_repo_record, staging_temp_root}; #[cfg(test)] use misc::internal::run_internal_command; #[cfg(test)] use repo::groups::{expand_install_requests_for_groups, expand_installed_group_targets}; #[cfg(test)] use update::candidates::{ SourceUpdateCandidate, UpdateCandidate, UpdateOrigin, collect_best_source_update_candidates, collect_missing_update_dependencies, select_update_candidate, }; #[cfg(test)] use update::versions::{ ArchiveListingProbe, VersionPattern, archive_listing_probe, best_newer_version, candidate_versions_from_listing, candidate_versions_from_refs, extract_version_patterns, list_archive_versions, remote_git_repository_from_source_url, }; fn rootfs_is_system_root(rootfs: &Path) -> bool { if rootfs == Path::new("/") { return true; } fs::canonicalize(rootfs) .map(|path| path == Path::new("/")) .unwrap_or(false) } fn command_requires_live_root(command: &Commands) -> bool { matches!( command, Commands::Install(_) | Commands::Remove(_) | Commands::Update(_) | Commands::Set(_) ) } fn repo_command_rootfs(command: &RepoCommands) -> &Path { match command { RepoCommands::Create { args, .. } | RepoCommands::Sync { args } | RepoCommands::Update { args, .. } | RepoCommands::Index { args, .. } | RepoCommands::List { args } | RepoCommands::Add { args, .. } | RepoCommands::Remove { args, .. } | RepoCommands::Enable { args, .. } | RepoCommands::Disable { args, .. } | RepoCommands::Owns { args, .. } | RepoCommands::Status { args } => &args.rootfs_args.rootfs, } } fn command_rootfs(command: &Commands) -> Option<&Path> { match command { Commands::Install(args) => Some(&args.rootfs_args.rootfs), Commands::Remove(args) => Some(&args.rootfs_args.rootfs), Commands::Build(args) => Some(&args.rootfs_args.rootfs), Commands::Bootstrap(args) => Some(&args.sysroot), Commands::Update(args) => Some(&args.rootfs_args.rootfs), Commands::Info(args) => Some(&args.rootfs_args.rootfs), Commands::Search(args) => Some(&args.rootfs_args.rootfs), Commands::Owns(args) => Some(&args.rootfs_args.rootfs), Commands::List(args) => Some(&args.rootfs_args.rootfs), Commands::Sign(args) => Some(&args.rootfs_args.rootfs), Commands::Repo(args) => Some(repo_command_rootfs(&args.command)), Commands::Config(args) => Some(&args.rootfs_args.rootfs), Commands::Set(args) => Some(&args.rootfs_args.rootfs), Commands::System(args) => Some(&args.rootfs_args.rootfs), Commands::Check(_) | Commands::Convert(_) | Commands::GenerateArtifacts(_) | Commands::MakeSpec(_) | Commands::Internal(_) => None, } } fn command_assume_yes(command: &Commands) -> bool { match command { Commands::Install(args) => args.prompt_args.yes, Commands::Remove(args) => args.prompt_args.yes, Commands::Build(args) => args.prompt_args.yes, Commands::Bootstrap(_) => false, Commands::Update(args) => args.prompt_args.yes, Commands::Check(_) | Commands::Convert(_) | Commands::Info(_) | Commands::Search(_) | Commands::Owns(_) | Commands::List(_) | Commands::Sign(_) | Commands::Repo(_) | Commands::Config(_) | Commands::Set(_) | Commands::System(_) | Commands::GenerateArtifacts(_) | Commands::MakeSpec(_) | Commands::Internal(_) => false, } } fn should_reexec_with_sudo(cli: &Cli) -> bool { !crate::fakeroot::is_root() && command_rootfs(&cli.command).is_some_and(rootfs_is_system_root) && command_requires_live_root(&cli.command) } fn should_delegate_live_rootfs_installs(rootfs: &Path) -> bool { !crate::fakeroot::is_root() && rootfs_is_system_root(rootfs) } fn compare_package_release( left_version: &str, left_revision: u32, right_version: &str, right_revision: u32, ) -> Ordering { compare_versions_for_updates(left_version, right_version) .then_with(|| left_revision.cmp(&right_revision)) } const DEPOT_INSTALL_CONTEXT_ENV: &str = "DEPOT_INSTALL_CONTEXT"; const INSTALL_CONTEXT_UPDATE: &str = "update"; const INSTALL_CONTEXT_PLANNED: &str = "planned"; const DEPOT_PACKAGE_NAME: &str = "depot"; #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum InstallInvocationContext { Default, Update, Planned, } fn current_install_invocation_context() -> InstallInvocationContext { match std::env::var(DEPOT_INSTALL_CONTEXT_ENV).as_deref() { Ok(INSTALL_CONTEXT_UPDATE) => InstallInvocationContext::Update, Ok(INSTALL_CONTEXT_PLANNED) => InstallInvocationContext::Planned, _ => InstallInvocationContext::Default, } } fn suppress_nested_install_output() -> bool { matches!( current_install_invocation_context(), InstallInvocationContext::Update | InstallInvocationContext::Planned ) } fn install_test_deps_enabled(cli_test_deps: bool, config: &config::Config) -> bool { cli_test_deps || config.install_test_deps } fn current_argv0() -> String { std::env::args_os() .next() .filter(|arg| !arg.is_empty()) .map(|arg| PathBuf::from(arg).display().to_string()) .or_else(|| { std::env::current_exe() .ok() .map(|path| path.display().to_string()) }) .unwrap_or_else(|| DEPOT_PACKAGE_NAME.to_string()) } fn is_explicit_depot_self_update_request(packages: &[String]) -> bool { packages.len() == 1 && packages .first() .is_some_and(|package| package == DEPOT_PACKAGE_NAME) } fn ensure_depot_self_update_not_required(config: &config::Config, rootfs: &Path) -> Result<()> { if current_install_invocation_context() == InstallInvocationContext::Update { return Ok(()); } let db_path = config.installed_db_path(rootfs); if db::get_package_version(&db_path, DEPOT_PACKAGE_NAME) .with_context(|| { format!( "Failed to query installed package database at {}", db_path.display() ) })? .is_none() { return Ok(()); } let requested = [DEPOT_PACKAGE_NAME.to_string()]; let updates = collect_update_candidates(config, rootfs, &requested) .context("Failed to check for pending depot self-update")?; if updates.is_empty() { return Ok(()); } anyhow::bail!( "An update for '{}' is available. Run '{} update {}' before continuing.", DEPOT_PACKAGE_NAME, current_argv0(), DEPOT_PACKAGE_NAME ); } fn maybe_reexec_with_sudo(cli: &Cli) -> Result { if !should_reexec_with_sudo(cli) { return Ok(false); } let exe = std::env::current_exe().context("Failed to locate depot executable")?; let mut cmd = std::process::Command::new("sudo"); if let Some(preserve_arg) = sudo_preserve_env_arg() { cmd.arg(preserve_arg); } cmd.arg(exe); cmd.args(std::env::args_os().skip(1)); let status = cmd .status() .context("Failed to re-execute depot via sudo for live-system install/remove")?; if status.success() { Ok(true) } else { anyhow::bail!("sudo depot command failed with status {}", status); } } fn sudo_preserve_env_arg() -> Option { let mut keys = Vec::new(); for key in [DEPOT_INSTALL_CONTEXT_ENV, "DEPOT_DEPCHAIN"] { if std::env::var_os(key).is_some() { keys.push(key); } } (!keys.is_empty()).then(|| format!("--preserve-env={}", keys.join(","))) } #[derive(Clone, Copy)] struct ChildInstallCommandOptions<'a> { no_deps: bool, assume_yes: bool, no_flags: bool, cross_prefix: Option<&'a str>, clean: bool, lib32_only: bool, install_test_deps: bool, install_context: Option<&'a str>, dep_chain: Option<&'a str>, } fn install_request_display(install_requests: &[PathBuf]) -> String { install_requests .iter() .map(|path| path.display().to_string()) .collect::>() .join(", ") } fn run_install_command_with_program( program: &Path, install_requests: &[PathBuf], rootfs: &Path, options: ChildInstallCommandOptions<'_>, ) -> Result<()> { if install_requests.is_empty() { return Ok(()); } let mut cmd = std::process::Command::new(program); cmd.arg("install"); cmd.arg("-r").arg(rootfs); if options.no_deps { cmd.arg("--no-deps"); } if options.assume_yes { cmd.arg("--yes"); } 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"); } if options.lib32_only { cmd.arg("--lib32-only"); } if options.install_test_deps { cmd.arg("--test-deps"); } cmd.args(install_requests); if let Some(context) = options.install_context { cmd.env(DEPOT_INSTALL_CONTEXT_ENV, context); } if let Some(dep_chain) = options.dep_chain { cmd.env("DEPOT_DEPCHAIN", dep_chain); } let status = command_status_with_sh_fallback(&mut cmd).with_context(|| { format!( "Failed to spawn child install for {}", install_request_display(install_requests) ) })?; if status.success() { Ok(()) } else { anyhow::bail!( "Child install failed for {} with status {}", install_request_display(install_requests), status ); } } fn command_status_with_sh_fallback( cmd: &mut std::process::Command, ) -> std::io::Result { match crate::interrupts::command_status(cmd) { Ok(status) => Ok(status), Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied || err.raw_os_error() == Some(26) => { let program = cmd.get_program(); let contents = fs::read(program); let is_script = contents.ok().is_some_and(|bytes| bytes.starts_with(b"#!")); if !is_script { return Err(err); } let mut fallback = std::process::Command::new("sh"); fallback.arg(program); fallback.args(cmd.get_args()); if let Some(dir) = cmd.get_current_dir() { fallback.current_dir(dir); } for (key, value) in cmd.get_envs() { match value { Some(value) => { fallback.env(key, value); } None => { fallback.env_remove(key); } } } crate::interrupts::command_status(&mut fallback) } Err(err) => Err(err), } } fn run_child_install_command( install_requests: &[PathBuf], rootfs: &Path, options: InstallPlanExecutionOptions<'_>, ) -> Result<()> { let exe = std::env::current_exe().context("Failed to locate depot executable")?; run_install_command_with_program( &exe, install_requests, rootfs, ChildInstallCommandOptions { no_deps: true, assume_yes: true, no_flags: options.no_flags, cross_prefix: options.cross_prefix, clean: options.clean, lib32_only: false, install_test_deps: options.install_test_deps, install_context: Some(INSTALL_CONTEXT_PLANNED), dep_chain: None, }, ) } #[derive(Clone)] struct InterruptWatcher; impl InterruptWatcher { fn install() -> Result { crate::interrupts::install()?; crate::interrupts::reset(); Ok(Self) } fn was_interrupted(&self) -> bool { crate::interrupts::was_interrupted() } fn check(&self) -> Result<()> { crate::interrupts::check() } } #[derive(Debug, Clone, Copy, Eq, PartialEq)] enum AutoInstalledDependencyKind { Build, Runtime, Test, } #[derive(Debug, Default, Clone)] struct AutoInstalledDependencyTracker { install_order: Vec, build: BTreeSet, runtime: BTreeSet, test: BTreeSet, } impl AutoInstalledDependencyTracker { fn record_plan( &mut self, plan: &planner::ExecutionPlan, requested_deps: &[String], kind: AutoInstalledDependencyKind, ) { if requested_deps.is_empty() { return; } for step in plan.actionable_steps() { if !self.install_order.contains(&step.package) { self.install_order.push(step.package.clone()); } } let closure = plan_dependency_closure_for_requested_deps(plan, requested_deps); let target = match kind { AutoInstalledDependencyKind::Build => &mut self.build, AutoInstalledDependencyKind::Runtime => &mut self.runtime, AutoInstalledDependencyKind::Test => &mut self.test, }; target.extend(closure); } fn cleanup_targets(&self, include_runtime: bool) -> Vec { let mut remove = self.build.clone(); remove.extend(self.test.iter().cloned()); if include_runtime { remove.extend(self.runtime.iter().cloned()); } else { remove.retain(|package| !self.runtime.contains(package)); } self.install_order .iter() .rev() .filter(|package| remove.contains(*package)) .cloned() .collect() } fn is_empty(&self) -> bool { self.build.is_empty() && self.runtime.is_empty() && self.test.is_empty() } } fn plan_dependency_closure_for_requested_deps( plan: &planner::ExecutionPlan, requested_deps: &[String], ) -> HashSet { let requested: HashSet<_> = requested_deps.iter().map(String::as_str).collect(); let mut roots = Vec::new(); let mut children_by_parent: HashMap> = HashMap::new(); for step in plan.actionable_steps() { if step.requested_by.iter().any(|reason| { reason .strip_prefix("dependency ") .is_some_and(|dep| requested.contains(dep)) }) { roots.push(step.package.clone()); } for reason in &step.requested_by { if let Some((parent, _dep)) = reason.split_once(" needs ") { let children = children_by_parent.entry(parent.to_string()).or_default(); if !children.contains(&step.package) { children.push(step.package.clone()); } } } } let mut stack = roots; let mut closure = HashSet::new(); while let Some(package) = stack.pop() { if !closure.insert(package.clone()) { continue; } if let Some(children) = children_by_parent.get(&package) { stack.extend(children.iter().cloned()); } } closure } fn prompt_for_dependency_cleanup(packages: &[String]) -> Result { let assume_yes = ui::assume_yes_enabled(); ui::set_assume_yes(false); let result = ui::prompt_package_action("dependency cleanup", packages, true); ui::set_assume_yes(assume_yes); result } fn cleanup_auto_installed_dependencies( tracker: &AutoInstalledDependencyTracker, rootfs: &Path, config: &config::Config, include_runtime: bool, prompt: bool, ) -> Result<()> { let db_path = config.installed_db_path(rootfs); let installed = db::get_installed_packages(&db_path)?; let targets: Vec = tracker .cleanup_targets(include_runtime) .into_iter() .filter(|package| installed.contains(package)) .collect(); if targets.is_empty() { return Ok(()); } if prompt && !prompt_for_dependency_cleanup(&targets)? { return Ok(()); } ui::info(format!( "Removing auto-installed dependencies: {}", targets.join(", ") )); for package in targets { remove_installed_package_with_hooks(&package, rootfs, config)?; } Ok(()) } fn parse_licenses_from_toml(metadata: &toml::Value) -> Vec { if let Some(s) = metadata.get("license").and_then(|v| v.as_str()) { return vec![s.to_string()]; } if let Some(arr) = metadata.get("license").and_then(|v| v.as_array()) { return arr .iter() .filter_map(|v| v.as_str()) .map(String::from) .collect(); } Vec::new() } fn parse_dependency_list(metadata: &toml::Value, kind: &str) -> Vec { metadata .get("dependencies") .and_then(|v| v.get(kind)) .and_then(|v| v.as_array()) .map(|deps| { deps.iter() .filter_map(|v| v.as_str()) .map(String::from) .collect() }) .unwrap_or_default() } fn parse_keep_list(metadata: &toml::Value) -> Vec { if let Some(s) = metadata.get("keep").and_then(|v| v.as_str()) { return vec![s.to_string()]; } if let Some(arr) = metadata.get("keep").and_then(|v| v.as_array()) { return arr .iter() .filter_map(|v| v.as_str()) .map(String::from) .collect(); } Vec::new() } fn output_destdir_for(base_destdir: &Path, primary_pkg: &str, output_pkg: &str) -> PathBuf { if output_pkg == primary_pkg { base_destdir.to_path_buf() } else { staging::output_staging_dir(base_destdir, output_pkg) } } fn spec_for_output( pkg_spec: &package::PackageSpec, output: package::PackageInfo, ) -> package::PackageSpec { let output_name = output.name.clone(); let mut spec_for_out = pkg_spec.clone(); spec_for_out.package = output; spec_for_out.alternatives = pkg_spec.alternatives_for_output(&output_name); spec_for_out.dependencies = pkg_spec.dependencies_for_output(&output_name); spec_for_out } fn destdir_has_packagable_content(destdir: &Path) -> Result { if !destdir.exists() { return Ok(false); } let manifest = staging::generate_manifest_with_dirs(destdir)?; Ok(!manifest.files.is_empty() || !manifest.directories.is_empty()) } fn staged_output_specs( pkg_spec: &package::PackageSpec, destdir: &Path, ) -> Result> { let declared_outputs = pkg_spec.outputs(); let declared_names: HashSet = declared_outputs .iter() .map(|output| output.name.clone()) .collect(); let mut outputs = Vec::new(); let mut seen = HashSet::new(); for output in declared_outputs { let output_name = output.name.clone(); let out_destdir = output_destdir_for(destdir, &pkg_spec.package.name, &output_name); outputs.push((spec_for_output(pkg_spec, output.clone()), out_destdir)); seen.insert(output_name.clone()); if !pkg_spec.build.flags.split_docs || output_name.ends_with("-docs") { continue; } let docs_pkg = pkg_spec.docs_package_for_output(&output); if declared_names.contains(&docs_pkg.name) || seen.contains(&docs_pkg.name) { continue; } let docs_destdir = output_destdir_for(destdir, &pkg_spec.package.name, &docs_pkg.name); if !destdir_has_packagable_content(&docs_destdir)? { continue; } seen.insert(docs_pkg.name.clone()); outputs.push((spec_for_output(pkg_spec, docs_pkg), docs_destdir)); } Ok(outputs) } #[derive(Debug, Clone)] struct PlannedStagedInstall { is_update: bool, remove_paths: Vec, replacement_removals: Vec, renamed_transition: Option, hook_context: install::hooks::HookExecutionContextOwned, } #[derive(Debug, Clone)] struct RenamedPackageTransition { replaced: db::InstalledPackageRecord, retained_files: Vec, retained_directories: Vec, } impl RenamedPackageTransition { fn replacement(&self) -> db::PackageReplacement { db::PackageReplacement { old_name: self.replaced.name.clone(), retained_files: self.retained_files.clone(), retained_directories: self.retained_directories.clone(), } } fn retains_old_package(&self) -> bool { !self.retained_files.is_empty() || !self.retained_directories.is_empty() } } #[derive(Debug, Clone)] struct PlannedPackageInstall { spec: package::PackageSpec, destdir: PathBuf, staged: PlannedStagedInstall, } #[derive(Clone, Copy)] struct PendingLifecycleHook { hook: install::scripts::Hook, } #[cfg(test)] #[derive(Debug, Clone)] struct InstalledPackageOutcome { package: package::PackageInfo, is_update: bool, } #[derive(Debug, Clone)] struct InstallConflictSubject { package: String, provides: Vec, conflicts: Vec, } #[derive(Debug, Clone)] struct InstalledConflictPackage { name: String, provides: Vec, } fn install_conflict_subjects_for_output_spec( spec: &package::PackageSpec, ) -> Vec { spec.outputs() .into_iter() .map(|output| { let alternatives = spec.alternatives_for_output(&output.name); InstallConflictSubject { package: output.name, provides: alternatives.provides, conflicts: alternatives.conflicts, } }) .collect() } fn install_conflict_subjects_for_spec( spec: &package::PackageSpec, include_primary: bool, include_lib32: bool, ) -> Vec { let mut subjects = Vec::new(); if include_primary { subjects.extend(install_conflict_subjects_for_output_spec(spec)); } if include_lib32 { subjects.extend(install_conflict_subjects_for_output_spec( &make_lib32_package_spec(spec), )); } subjects } fn install_conflict_subject_for_binary_record( record: &db::repo::BinaryRepoPackageRecord, ) -> InstallConflictSubject { InstallConflictSubject { package: record.name.clone(), provides: record.provides.clone(), conflicts: record.conflicts.clone(), } } fn matching_conflict_names( conflicts: &[String], package_name: &str, provides: &[String], ) -> Vec { let mut matches = Vec::new(); for conflict in conflicts { if conflict == package_name || provides.iter().any(|provided| provided == conflict) { matches.push(conflict.clone()); } } matches.sort(); matches.dedup(); matches } fn validate_no_transaction_conflicts(subjects: &[InstallConflictSubject]) -> Result<()> { let mut violations = BTreeSet::new(); for (idx, left) in subjects.iter().enumerate() { for right in subjects.iter().skip(idx + 1) { let left_hits = matching_conflict_names(&left.conflicts, &right.package, &right.provides); if !left_hits.is_empty() { violations.insert(format!( "{} conflicts with {} via {}", left.package, right.package, left_hits.join(", ") )); } let right_hits = matching_conflict_names(&right.conflicts, &left.package, &left.provides); if !right_hits.is_empty() { violations.insert(format!( "{} conflicts with {} via {}", right.package, left.package, right_hits.join(", ") )); } } } if violations.is_empty() { return Ok(()); } let mut message = String::from("Cannot install conflicting packages in the same transaction:\n"); for violation in violations { message.push_str(" "); message.push_str(&violation); message.push('\n'); } anyhow::bail!(message.trim_end().to_string()); } fn collect_installed_conflict_packages(db_path: &Path) -> Result> { let mut installed = Vec::new(); for record in db::list_installed_package_records(db_path)? { installed.push(InstalledConflictPackage { provides: db::get_package_provides(db_path, &record.name)?, name: record.name, }); } Ok(installed) } fn collect_conflicting_installed_packages( subjects: &[InstallConflictSubject], installed: &[InstalledConflictPackage], ) -> Result>> { validate_no_transaction_conflicts(subjects)?; let planned_packages: HashSet<_> = subjects .iter() .map(|subject| subject.package.clone()) .collect(); let mut removals: BTreeMap> = BTreeMap::new(); for subject in subjects { for installed_pkg in installed { if installed_pkg.name == subject.package { continue; } let matched = matching_conflict_names( &subject.conflicts, &installed_pkg.name, &installed_pkg.provides, ); if matched.is_empty() { continue; } if planned_packages.contains(&installed_pkg.name) { anyhow::bail!( "Cannot install conflicting packages in the same transaction: {} conflicts with {}", subject.package, installed_pkg.name ); } removals .entry(installed_pkg.name.clone()) .or_default() .insert(subject.package.clone()); } } Ok(removals) } fn collect_installed_replacement_packages( db_path: &Path, pkg_spec: &package::PackageSpec, ) -> Result> { let installed = db::get_installed_packages(db_path)?; let mut replacements: Vec = pkg_spec .alternatives .replaces .iter() .filter(|name| *name != &pkg_spec.package.name) .filter(|name| installed.contains(*name)) .cloned() .collect(); replacements.sort(); replacements.dedup(); Ok(replacements) } pub(crate) fn remove_installed_package_with_hooks( package: &str, rootfs: &Path, config: &config::Config, ) -> Result<()> { let db_path = config.installed_db_path(rootfs); let affected_paths = db::get_package_files(&db_path, package)?; install::hooks::run_transaction_hooks( rootfs, &install::hooks::HookExecutionContext { phase: install::hooks::HookPhase::Pre, operation: install::hooks::HookOperation::Remove, package, affected_paths: &affected_paths, }, )?; let script_dir = install::scripts::installed_scripts_dir(rootfs, package); let _ = install::scripts::run_hook_if_present( &script_dir, install::scripts::Hook::PreRemove, rootfs, package, )?; db::remove_package(&db_path, package, rootfs)?; let post_remove = install::scripts::run_hook_if_present( &script_dir, install::scripts::Hook::PostRemove, rootfs, package, ); let cleanup_scripts = install::scripts::remove_installed_scripts(rootfs, package); post_remove?; cleanup_scripts?; install::hooks::run_transaction_hooks( rootfs, &install::hooks::HookExecutionContext { phase: install::hooks::HookPhase::Post, operation: install::hooks::HookOperation::Remove, package, affected_paths: &affected_paths, }, )?; ui::success(format!("Successfully removed {}", package)); Ok(()) } fn resolve_installed_conflicts_for_subjects( subjects: &[InstallConflictSubject], rootfs: &Path, config: &config::Config, dry_run: bool, ) -> Result<()> { if subjects.is_empty() { return Ok(()); } let db_path = config.installed_db_path(rootfs); let installed = collect_installed_conflict_packages(&db_path)?; let removals = collect_conflicting_installed_packages(subjects, &installed)?; if removals.is_empty() { return Ok(()); } let prompt_entries: Vec = removals .iter() .map(|(package, conflicted_by)| { format!( "{} (conflicts with {})", package, conflicted_by.iter().cloned().collect::>().join(", ") ) }) .collect(); if dry_run { ui::info(format!( "Dry run: would remove conflicting installed package(s): {}", prompt_entries.join(", ") )); return Ok(()); } if !ui::prompt_package_action("conflict removal", &prompt_entries, true)? { anyhow::bail!("Aborted"); } for package in removals.keys() { ui::info(format!("Removing conflicting package: {}", package)); remove_installed_package_with_hooks(package, rootfs, config)?; } Ok(()) } fn is_versioned_shared_library_path(path: &str) -> bool { let Some(file_name) = Path::new(path).file_name().and_then(|name| name.to_str()) else { return false; }; let Some(version_suffix) = file_name.split(".so.").nth(1) else { return false; }; !version_suffix.is_empty() && version_suffix .chars() .all(|ch| ch.is_ascii_digit() || ch == '.') && version_suffix.chars().any(|ch| ch.is_ascii_digit()) } fn retained_abi_files_for_replacement( old_files: &[String], new_manifest: &staging::Manifest, ) -> Vec { let new_files: HashSet<&str> = new_manifest.files.iter().map(String::as_str).collect(); let mut retained: Vec = old_files .iter() .filter(|path| is_versioned_shared_library_path(path)) .filter(|path| !new_files.contains(path.as_str())) .cloned() .collect(); retained.sort(); retained } fn retained_directories_for_files( old_directories: &[String], retained_files: &[String], ) -> Vec { let retained_files: HashSet<&str> = retained_files.iter().map(String::as_str).collect(); let mut directories: Vec = old_directories .iter() .filter(|directory| { let prefix = format!("{}/", directory); retained_files .iter() .any(|file| *file == directory.as_str() || file.starts_with(&prefix)) }) .cloned() .collect(); directories.sort_by_key(|path| std::cmp::Reverse(path.matches('/').count())); directories } fn compare_installed_records_for_stream( left: &db::InstalledPackageRecord, right: &db::InstalledPackageRecord, ) -> Ordering { compare_package_release(&left.version, left.revision, &right.version, right.revision) .then_with(|| compare_completed_at(left.completed_at, right.completed_at)) .then_with(|| left.name.cmp(&right.name)) } fn select_primary_installed_record<'a>( records: impl IntoIterator, ) -> Option<&'a db::InstalledPackageRecord> { let mut best: Option<&db::InstalledPackageRecord> = None; for record in records { if best.as_ref().is_none_or(|current| { compare_installed_records_for_stream(record, current) == Ordering::Greater }) { best = Some(record); } } best } fn build_renamed_package_transition( db_path: &Path, pkg_spec: &package::PackageSpec, new_manifest: &staging::Manifest, ) -> Result> { let installed = db::list_installed_package_records(db_path)?; if installed .iter() .any(|record| record.name == pkg_spec.package.name) { return Ok(None); } let stream_name = pkg_spec.package.effective_real_name(); let Some(replaced) = select_primary_installed_record( installed .iter() .filter(|record| record.effective_real_name() == stream_name) .filter(|record| record.name != pkg_spec.package.name), ) .cloned() else { return Ok(None); }; let old_files = db::get_package_files(db_path, &replaced.name)?; let old_directories = db::get_package_directories(db_path, &replaced.name)?; let retained_files = if replaced.abi_breaking { retained_abi_files_for_replacement(&old_files, new_manifest) } else { Vec::new() }; let retained_directories = if retained_files.is_empty() { Vec::new() } else { retained_directories_for_files(&old_directories, &retained_files) }; Ok(Some(RenamedPackageTransition { replaced, retained_files, retained_directories, })) } fn plan_staged_install( pkg_spec: &package::PackageSpec, destdir: &Path, rootfs: &Path, config: &config::Config, ) -> Result { std::fs::create_dir_all(&config.db_dir).with_context(|| { format!( "Failed to create database directory: {}", config.db_dir.display() ) })?; let db_path = config.installed_db_path(rootfs); let new_manifest = staging::generate_manifest_with_dirs(destdir)?; let replacement_removals = collect_installed_replacement_packages(&db_path, pkg_spec)?; let renamed_transition = build_renamed_package_transition(&db_path, pkg_spec, &new_manifest)?; let is_update = db::get_package_version(&db_path, &pkg_spec.package.name)?.is_some() || renamed_transition.is_some() || !replacement_removals.is_empty(); let mut remove_paths = db::calculate_upgrade_paths(&db_path, &pkg_spec.package.name, &new_manifest)?; if let Some(transition) = &renamed_transition { let old_files = db::get_package_files(&db_path, &transition.replaced.name)?; let old_directories = db::get_package_directories(&db_path, &transition.replaced.name)?; let retained_files: HashSet<&str> = transition .retained_files .iter() .map(String::as_str) .collect(); let retained_directories: HashSet<&str> = transition .retained_directories .iter() .map(String::as_str) .collect(); remove_paths.extend( old_files .into_iter() .filter(|path| !retained_files.contains(path.as_str())), ); remove_paths.extend( old_directories .into_iter() .filter(|path| !retained_directories.contains(path.as_str())), ); remove_paths.sort_by_key(|path| std::cmp::Reverse(path.matches('/').count())); remove_paths.dedup(); } let operation = if is_update { install::hooks::HookOperation::Update } else { install::hooks::HookOperation::Install }; let mut affected_paths = new_manifest.files.clone(); affected_paths.extend(remove_paths.iter().cloned()); affected_paths.sort(); affected_paths.dedup(); Ok(PlannedStagedInstall { is_update, remove_paths, replacement_removals, renamed_transition, hook_context: install::hooks::HookExecutionContextOwned { operation, package: pkg_spec.package.name.clone(), affected_paths, }, }) } fn plan_package_outputs_for_install( pkg_spec: &package::PackageSpec, destdir: &Path, rootfs: &Path, config: &config::Config, ) -> Result> { let mut plans = Vec::new(); for (spec_for_out, out_destdir) in staged_output_specs(pkg_spec, destdir)? { let staged = plan_staged_install(&spec_for_out, &out_destdir, rootfs, config)?; plans.push(PlannedPackageInstall { spec: spec_for_out, destdir: out_destdir, staged, }); } Ok(plans) } fn run_transaction_hooks_for_plans( rootfs: &Path, phase: install::hooks::HookPhase, plans: &[PlannedPackageInstall], ) -> Result { let contexts: Vec<_> = plans .iter() .map(|plan| plan.staged.hook_context.clone()) .collect(); install::hooks::run_transaction_hooks_batch(rootfs, phase, &contexts) } fn install_staged_to_rootfs( pkg_spec: &package::PackageSpec, destdir: &Path, rootfs: &Path, config: &config::Config, plan: &PlannedStagedInstall, ) -> Result> { let staged_scripts_dir = install::scripts::staged_scripts_dir(destdir); let installed_scripts_dir = install::scripts::installed_scripts_dir(rootfs, &pkg_spec.package.name); if plan.is_update { let has_staged_pre = install::scripts::run_hook_if_present( &staged_scripts_dir, install::scripts::Hook::PreUpdate, rootfs, &pkg_spec.package.name, )?; if !has_staged_pre { let _ = install::scripts::run_hook_if_present( &installed_scripts_dir, install::scripts::Hook::PreUpdate, rootfs, &pkg_spec.package.name, )?; } } else { let _ = install::scripts::run_hook_if_present( &staged_scripts_dir, install::scripts::Hook::PreInstall, rootfs, &pkg_spec.package.name, )?; } let tx_base = config.build_dir.join("tx"); let tx = staging::install_atomic( destdir, rootfs, &tx_base, &plan.remove_paths, &pkg_spec.build.flags.keep, )?; let db_path = config.installed_db_path(rootfs); let replacement = plan .renamed_transition .as_ref() .map(RenamedPackageTransition::replacement); let register_result = if let Some(replacement) = replacement.as_ref() { db::register_package_with_replacement(&db_path, pkg_spec, destdir, Some(replacement)) } else { db::register_package(&db_path, pkg_spec, destdir) }; if let Err(e) = register_result { let _ = tx.rollback(); return Err(e); } tx.commit()?; if let Some(transition) = &plan.renamed_transition && !transition.retains_old_package() { install::scripts::remove_installed_scripts(rootfs, &transition.replaced.name)?; } install::scripts::sync_staged_scripts_to_rootfs( &staged_scripts_dir, rootfs, &pkg_spec.package.name, )?; Ok(Some(PendingLifecycleHook { hook: if plan.is_update { install::scripts::Hook::PostUpdate } else { install::scripts::Hook::PostInstall }, })) } fn install_planned_packages_to_rootfs( plans: &[PlannedPackageInstall], rootfs: &Path, config: &config::Config, ) -> Result<()> { let mut removed_replacements = HashSet::new(); let mut pending_post_hooks = Vec::new(); for (idx, plan) in plans.iter().enumerate() { ui::info(format!( "{}/{} Installing package {}-{}-{}", idx + 1, plans.len(), plan.spec.package.name, plan.spec.package.version, plan.spec.package.revision )); for package in &plan.staged.replacement_removals { if removed_replacements.insert(package.clone()) { remove_installed_package_with_hooks(package, rootfs, config)?; } } if let Some(hook) = install_staged_to_rootfs(&plan.spec, &plan.destdir, rootfs, config, &plan.staged)? { pending_post_hooks.push((plan.spec.package.name.clone(), hook)); } } for (pkg_name, pending_hook) in pending_post_hooks { let installed_scripts_dir = install::scripts::installed_scripts_dir(rootfs, &pkg_name); let _ = install::scripts::run_hook_if_present_or_defer( &installed_scripts_dir, pending_hook.hook, rootfs, &pkg_name, )?; } install::scripts::run_deferred_hooks_if_possible(rootfs)?; Ok(()) } #[cfg(test)] fn install_package_outputs_to_rootfs( pkg_spec: &package::PackageSpec, destdir: &Path, rootfs: &Path, config: &config::Config, ) -> Result> { let plans = plan_package_outputs_for_install(pkg_spec, destdir, rootfs, config)?; let installed = plans .iter() .map(|plan| InstalledPackageOutcome { package: plan.spec.package.clone(), is_update: plan.staged.is_update, }) .collect(); run_transaction_hooks_for_plans(rootfs, install::hooks::HookPhase::Pre, &plans)?; install_planned_packages_to_rootfs(&plans, rootfs, config)?; run_transaction_hooks_for_plans(rootfs, install::hooks::HookPhase::Post, &plans)?; Ok(installed) } fn print_plan_summary(plan: &planner::ExecutionPlan) { let summary = plan.summary(); ui::info(format!( "Plan summary: packages={} actions={} (binary_install={}, source_build_install={}, skip_installed={}) known_download={}", summary.total_packages, summary.binary_installs + summary.source_build_installs, summary.binary_installs, summary.source_build_installs, summary.skipped_installed, human_bytes(summary.known_download_bytes) )); if std::env::var_os("DEPOT_VERBOSE_PLAN").is_none() { return; } for step in &plan.steps { let (action, origin) = match &step.action { planner::PlanAction::SkipInstalled => ("skip", "installed".to_string()), planner::PlanAction::BuildAndInstall => match &step.origin { planner::PlanOrigin::Source { path, local_sibling, } => ( "build+install", if *local_sibling { format!("source:local-sibling ({})", path.display()) } else { format!("source ({})", path.display()) }, ), _ => ("build+install", "source".to_string()), }, planner::PlanAction::InstallBinary => match &step.origin { planner::PlanOrigin::Binary { repo_name, record } => ( "install", format!( "binary:{} {}-{} size={}", repo_name, record.version, record.revision, human_bytes(record.size) ), ), _ => ("install", "binary".to_string()), }, }; ui::info(format!(" {} [{}] {}", step.package, action, origin)); } } fn actionable_plan_packages(plan: &planner::ExecutionPlan) -> Vec { plan.actionable_steps() .map(|step| step.package.clone()) .collect() } fn source_build_reason(reason: &str) -> String { if let Some(dep) = reason.strip_prefix("dependency ") { format!("requested dependency '{dep}'") } else if let Some((requester, _)) = reason.split_once(" needs ") { format!("needed by '{requester}'") } else if reason == "requested spec" { "requested spec".to_string() } else if reason == "requested package" { "requested package".to_string() } else { reason.to_string() } } fn source_build_warning_messages(plan: &planner::ExecutionPlan) -> Vec { let mut lines = Vec::new(); for step in plan.actionable_steps() { if !matches!(step.action, planner::PlanAction::BuildAndInstall) { continue; } let mut reasons = Vec::new(); for reason in &step.requested_by { let label = source_build_reason(reason); if !reasons.contains(&label) { reasons.push(label); } } if reasons.is_empty() { lines.push(step.package.clone()); } else { lines.push(format!("{} ({})", step.package, reasons.join(", "))); } } lines } fn warn_source_build_plan(plan: &planner::ExecutionPlan) { let lines = source_build_warning_messages(plan); if lines.is_empty() { return; } ui::warn(format!( "{} package(s) will be built from source before installation.", lines.len() )); for line in lines { ui::warn(format!(" {line}")); } } fn validate_source_build_prereqs_for_plan( plan: &planner::ExecutionPlan, rootfs: &Path, config: &config::Config, ) -> Result<()> { let db_path = config.installed_db_path(rootfs); let mut checked_development_package = false; for step in plan.actionable_steps() { let planner::PlanOrigin::Source { path, .. } = &step.origin else { continue; }; if !matches!(step.action, planner::PlanAction::BuildAndInstall) { continue; } let mut spec = package::PackageSpec::from_file(path) .with_context(|| format!("Failed to parse spec {}", path.display()))?; spec.apply_config(config); source::preflight_local_manual_sources(&spec)?; if !checked_development_package && !spec.is_metapackage() { ensure_requested_development_package_installed(&db_path)?; checked_development_package = true; } } Ok(()) } #[derive(Clone, Copy)] struct InstallPlanExecutionOptions<'a> { no_flags: bool, cross_prefix: Option<&'a str>, clean: bool, dry_run: bool, confirm_installation: bool, lib32_only_requested_specs: bool, install_test_deps: bool, } #[derive(Debug, Clone, PartialEq, Eq)] struct ChildInstallBatch { requests: Vec, lib32_only: bool, } fn step_requests_only_lib32( step: &planner::PlannedStep, options: &InstallPlanExecutionOptions<'_>, ) -> bool { options.lib32_only_requested_specs && step .requested_by .iter() .any(|reason| reason.starts_with("requested ")) } fn build_live_rootfs_child_install_batches( steps: &[&planner::PlannedStep], options: &InstallPlanExecutionOptions<'_>, binary_archives: &HashMap<(String, String), db::repo::BinaryRepoCachedArchive>, ) -> Result> { let mut batches = Vec::new(); let mut pending_binary_requests = Vec::new(); for step in steps { match &step.origin { planner::PlanOrigin::Source { path, .. } => { if !pending_binary_requests.is_empty() { batches.push(ChildInstallBatch { requests: std::mem::take(&mut pending_binary_requests), lib32_only: false, }); } batches.push(ChildInstallBatch { requests: vec![path.clone()], lib32_only: step_requests_only_lib32(step, options), }); } planner::PlanOrigin::Binary { repo_name, record } => { let cached = binary_archives .get(&(repo_name.clone(), record.filename.clone())) .with_context(|| { format!( "Cached archive missing for planned binary step '{}' from repo '{}'", record.filename, repo_name ) })?; pending_binary_requests.push(cached.package_path.clone()); } planner::PlanOrigin::Installed => {} } } if !pending_binary_requests.is_empty() { batches.push(ChildInstallBatch { requests: pending_binary_requests, lib32_only: false, }); } Ok(batches) } fn flush_binary_install_batch( pending_plans: &mut Vec, pending_staging_dirs: &mut Vec, rootfs: &Path, config: &config::Config, ) -> Result<()> { if pending_plans.is_empty() { return Ok(()); } install_planned_packages_to_rootfs(pending_plans, rootfs, config)?; pending_plans.clear(); pending_staging_dirs.clear(); Ok(()) } fn execute_install_plan_with_child_commands( plan: &planner::ExecutionPlan, rootfs: &Path, config: &config::Config, options: InstallPlanExecutionOptions<'_>, ) -> Result<()> { #[derive(Clone)] struct BinaryPhaseItem { repo_name: String, record: db::repo::BinaryRepoPackageRecord, } let actionable_steps: Vec<_> = plan.actionable_steps().collect(); if actionable_steps.is_empty() { ui::info("Nothing to do."); return Ok(()); } validate_source_build_prereqs_for_plan(plan, rootfs, config)?; warn_source_build_plan(plan); let planned_packages = actionable_plan_packages(plan); if options.confirm_installation && !ui::prompt_package_action("installation", &planned_packages, true)? { anyhow::bail!("Aborted"); } let mut conflict_subjects = Vec::new(); for step in &actionable_steps { match &step.origin { planner::PlanOrigin::Source { path, .. } => { let mut spec = package::PackageSpec::from_file(path) .with_context(|| format!("Failed to parse spec {}", path.display()))?; spec.apply_config(config); let lib32_only = effective_lib32_only(&spec, step_requests_only_lib32(step, &options)); conflict_subjects.extend(install_conflict_subjects_for_spec( &spec, !lib32_only, spec.builds_lib32_output() || lib32_only, )); } planner::PlanOrigin::Binary { record, .. } => { conflict_subjects.push(install_conflict_subject_for_binary_record(record)); } planner::PlanOrigin::Installed => {} } } resolve_installed_conflicts_for_subjects(&conflict_subjects, rootfs, config, options.dry_run)?; if options.dry_run { ui::info("Dry run enabled, no install/build actions executed."); return Ok(()); } let mut binary_archives: HashMap<(String, String), db::repo::BinaryRepoCachedArchive> = HashMap::new(); let mut binary_phase_items = Vec::new(); for step in &actionable_steps { if let planner::PlanOrigin::Binary { repo_name, record } = &step.origin { binary_phase_items.push(BinaryPhaseItem { repo_name: repo_name.clone(), record: (**record).clone(), }); } } if !binary_phase_items.is_empty() { ui::info(format!( "Downloading {} binary package(s) and detached signatures...", binary_phase_items.len() )); let use_tty_progress = std::io::stderr().is_terminal(); for item in &binary_phase_items { let label = format!( "{}-{}-{}", item.record.name, item.record.version, binary_arch_from_filename(&item.record.filename) ); let pb = ProgressBar::new(item.record.size.max(1)); pb.set_draw_target(if use_tty_progress { ProgressDrawTarget::stderr() } else { ProgressDrawTarget::hidden() }); pb.set_style( ProgressStyle::default_bar() .template("{prefix:.bold} [{bar:40.cyan/blue}] {eta}") .unwrap_or_else(|_| ProgressStyle::default_bar()) .progress_chars("#>-"), ); pb.set_prefix(label); let repo_cfg = config .binary_repos .get(&item.repo_name) .with_context(|| format!("Binary repo '{}' not found in config", item.repo_name))?; let mut progress_cb = |downloaded: u64, total: Option| { if let Some(t) = total && t > 0 { pb.set_length(t); } pb.set_position(downloaded); }; let cached = db::repo::cache_binary_package_archive_with_progress( &item.repo_name, repo_cfg, &item.record, &config.package_cache_dir, Some(&mut progress_cb), ) .with_context(|| { format!( "Failed to cache binary package '{}' from repo '{}'", item.record.filename, item.repo_name ) })?; pb.finish_and_clear(); binary_archives.insert( (item.repo_name.clone(), item.record.filename.clone()), cached, ); } ui::info(format!( "Verifying checksums for {} binary package(s)...", binary_phase_items.len() )); let checksum_pb = ProgressBar::new(binary_phase_items.len() as u64); checksum_pb.set_draw_target(if use_tty_progress { ProgressDrawTarget::stderr() } else { ProgressDrawTarget::hidden() }); checksum_pb.set_style( ProgressStyle::default_bar() .template("{prefix:.bold} [{bar:40.cyan/blue}] {pos}/{len} {eta}") .unwrap_or_else(|_| ProgressStyle::default_bar()) .progress_chars("#>-"), ); checksum_pb.set_prefix("checksums"); for item in &binary_phase_items { let cached = binary_archives .get(&(item.repo_name.clone(), item.record.filename.clone())) .with_context(|| { format!( "Cached archive missing for {} from repo '{}'", item.record.filename, item.repo_name ) })?; db::repo::verify_binary_package_archive_checksums(&cached.package_path, &item.record) .with_context(|| { format!( "Checksum verification failed for {} from repo '{}'", item.record.filename, item.repo_name ) })?; checksum_pb.inc(1); } checksum_pb.finish_and_clear(); ui::info(format!( "Verifying detached signatures for {} binary package(s)...", binary_phase_items.len() )); let signature_pb = ProgressBar::new(binary_phase_items.len() as u64); signature_pb.set_draw_target(if use_tty_progress { ProgressDrawTarget::stderr() } else { ProgressDrawTarget::hidden() }); signature_pb.set_style( ProgressStyle::default_bar() .template("{prefix:.bold} [{bar:40.cyan/blue}] {pos}/{len} {eta}") .unwrap_or_else(|_| ProgressStyle::default_bar()) .progress_chars("#>-"), ); signature_pb.set_prefix("signatures"); for item in &binary_phase_items { let repo_cfg = config .binary_repos .get(&item.repo_name) .with_context(|| format!("Binary repo '{}' not found in config", item.repo_name))?; let cached = binary_archives .get(&(item.repo_name.clone(), item.record.filename.clone())) .with_context(|| { format!( "Cached archive missing for {} from repo '{}'", item.record.filename, item.repo_name ) })?; db::repo::verify_binary_package_archive_signature( &item.repo_name, repo_cfg, rootfs, &cached.package_path, &cached.signature_path, ) .with_context(|| { format!( "Detached signature verification failed for {} from repo '{}'", item.record.filename, item.repo_name ) })?; signature_pb.inc(1); } signature_pb.finish_and_clear(); } if should_delegate_live_rootfs_installs(rootfs) { let exe = std::env::current_exe().context("Failed to locate depot executable")?; let batches = build_live_rootfs_child_install_batches(&actionable_steps, &options, &binary_archives)?; for batch in batches { run_install_command_with_program( &exe, &batch.requests, rootfs, ChildInstallCommandOptions { no_deps: true, assume_yes: true, no_flags: options.no_flags, cross_prefix: options.cross_prefix, clean: options.clean, lib32_only: batch.lib32_only, install_test_deps: options.install_test_deps, install_context: Some(INSTALL_CONTEXT_PLANNED), dep_chain: None, }, )?; } return Ok(()); } let mut binary_pre_hook_plans = Vec::new(); for step in &actionable_steps { if let planner::PlanOrigin::Binary { repo_name, record } = &step.origin { let cached = binary_archives .get(&(repo_name.clone(), record.filename.clone())) .with_context(|| { format!( "Cached archive missing for planned binary step '{}' from repo '{}'", record.filename, repo_name ) })?; let staged = extract_package_archive_to_staging(config, &cached.package_path)?; let spec = load_package_spec_from_staging_or_repo_record(staged.path(), record)?; let plans = plan_package_outputs_for_install(&spec, staged.path(), rootfs, config)?; binary_pre_hook_plans.extend(plans); } } run_transaction_hooks_for_plans( rootfs, install::hooks::HookPhase::Pre, &binary_pre_hook_plans, )?; let exe = std::env::current_exe().context("Failed to locate depot executable")?; let total_steps = actionable_steps.len(); let mut binary_post_hook_plans = Vec::new(); let mut pending_binary_install_plans = Vec::new(); let mut pending_binary_install_staging_dirs = Vec::new(); for (idx, step) in actionable_steps.into_iter().enumerate() { match &step.origin { planner::PlanOrigin::Source { path, .. } => { flush_binary_install_batch( &mut pending_binary_install_plans, &mut pending_binary_install_staging_dirs, rootfs, config, )?; ui::info(format!( "[{}/{}] building+installing {} from source", idx + 1, total_steps, step.package )); run_install_command_with_program( &exe, std::slice::from_ref(path), rootfs, ChildInstallCommandOptions { no_deps: true, assume_yes: true, no_flags: options.no_flags, cross_prefix: options.cross_prefix, clean: options.clean, lib32_only: step_requests_only_lib32(step, &options), install_test_deps: options.install_test_deps, install_context: Some(INSTALL_CONTEXT_PLANNED), dep_chain: None, }, ) .with_context(|| { format!("Failed to spawn planned install step '{}'", step.package) })?; } planner::PlanOrigin::Binary { repo_name, record } => { let cached = binary_archives .get(&(repo_name.clone(), record.filename.clone())) .with_context(|| { format!( "Cached archive missing for planned binary step '{}' from repo '{}'", record.filename, repo_name ) })?; let staged = extract_package_archive_to_staging(config, &cached.package_path)?; let spec = load_package_spec_from_staging_or_repo_record(staged.path(), record)?; let plans = plan_package_outputs_for_install(&spec, staged.path(), rootfs, config)?; binary_post_hook_plans.extend(plans.iter().cloned()); pending_binary_install_plans.extend(plans); pending_binary_install_staging_dirs.push(staged); } planner::PlanOrigin::Installed => {} } } flush_binary_install_batch( &mut pending_binary_install_plans, &mut pending_binary_install_staging_dirs, rootfs, config, )?; run_transaction_hooks_for_plans( rootfs, install::hooks::HookPhase::Post, &binary_post_hook_plans, )?; install::scripts::run_deferred_hooks_if_possible(rootfs)?; 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, install_test_deps: bool, } fn run_direct_archive_install_requests( options: DirectInstallOptions<'_>, config: &config::Config, archive_paths: &[PathBuf], confirm_installation: bool, ) -> Result { if archive_paths.is_empty() { return Ok(false); } 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")?; let mut staged_dirs = Vec::with_capacity(archive_paths.len()); let mut pkg_specs = Vec::with_capacity(archive_paths.len()); let mut install_targets = Vec::with_capacity(archive_paths.len()); let suppress_output = suppress_nested_install_output(); for archive_path in archive_paths { if !suppress_output { ui::info(format!( "Installing package from: {}", archive_path.display() )); } let (pkg_spec, staging_dir) = load_package_archive_into_staging(config, archive_path)?; if options.lib32_only { anyhow::bail!("--lib32-only is only supported when installing from a package spec"); } install_targets.push(format!( "{} v{}-{}", pkg_spec.package.name, pkg_spec.package.version, pkg_spec.package.revision )); pkg_specs.push(pkg_spec); staged_dirs.push(staging_dir); } let mut conflict_subjects = Vec::new(); for pkg_spec in &pkg_specs { conflict_subjects.extend(install_conflict_subjects_for_spec(pkg_spec, true, false)); } resolve_installed_conflicts_for_subjects( &conflict_subjects, options.rootfs, config, options.dry_run, )?; if options.dry_run { ui::info("Dry run enabled, stopping before install/build work."); return Ok(false); } if confirm_installation && !suppress_output && !ui::prompt_package_action("installation", &install_targets, true)? { anyhow::bail!("Aborted"); } if !suppress_output { ui::info(format!( "Installing {} binary archive payload(s)", archive_paths.len() )); } let mut transaction_plans = Vec::new(); for (pkg_spec, staging_dir) in pkg_specs.iter().zip(staged_dirs.iter()) { let output_plans = plan_package_outputs_for_install(pkg_spec, staging_dir.path(), options.rootfs, config)?; transaction_plans.extend(output_plans); } run_transaction_hooks_for_plans( options.rootfs, install::hooks::HookPhase::Pre, &transaction_plans, )?; install_planned_packages_to_rootfs(&transaction_plans, options.rootfs, config)?; run_transaction_hooks_for_plans( options.rootfs, install::hooks::HookPhase::Post, &transaction_plans, )?; Ok(true) } struct SourceBuildCleanupGuard<'a> { config: &'a config::Config, enabled: bool, } impl<'a> SourceBuildCleanupGuard<'a> { fn new(config: &'a config::Config, enabled: bool) -> Self { Self { config, enabled } } } impl Drop for SourceBuildCleanupGuard<'_> { fn drop(&mut self) { if self.enabled && let Err(err) = clean_build_source_dirs(self.config) { crate::log_warn!("Failed to clean build source dirs: {}", err); } } } 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); } } } } } let suppress_output = suppress_nested_install_output(); if !suppress_output { 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 let (spec, tmp_dir) = load_package_archive_into_staging(config, &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.build.flags.rootfs = build_cmd::build_env_rootfs(options.rootfs); (pkg_spec, None) }; let built_from_source = staging_dir.is_none(); let _source_cleanup_guard = SourceBuildCleanupGuard::new(config, built_from_source); if options.lib32_only && staging_dir.is_some() { anyhow::bail!("--lib32-only is only supported when installing from a package spec"); } let lib32_only = effective_lib32_only(&pkg_spec, options.lib32_only); if staging_dir.is_none() && !suppress_output { ui::info(format!( "Package: {} v{}-{}", pkg_spec.package.name, pkg_spec.package.version, pkg_spec.package.revision )); } let requested_outputs = requested_outputs(&pkg_spec, lib32_only); let db_path = config.installed_db_path(options.rootfs); if staging_dir.is_none() { source::preflight_local_manual_sources(&pkg_spec)?; if !pkg_spec.is_metapackage() { ensure_requested_development_package_installed(&db_path)?; } } let mut conflict_subjects = install_conflict_subjects_for_spec( &pkg_spec, !lib32_only, staging_dir.is_none() && (lib32_only || pkg_spec.builds_lib32_output()), ); if staging_dir.is_some() { conflict_subjects = install_conflict_subjects_for_spec(&pkg_spec, true, false); } resolve_installed_conflicts_for_subjects( &conflict_subjects, options.rootfs, config, options.dry_run, )?; 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 !suppress_output && !ui::prompt_package_action("installation", &install_targets, true)? { anyhow::bail!("Aborted"); } let _snapper_pre_install_snapshot_todo: fn() -> ! = || 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() ) })?; if staging_dir.is_none() { if options.no_deps && should_install_test_deps(&pkg_spec, options.install_test_deps, requested_outputs) { let missing_test = deps::check_test_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?; if !missing_test.is_empty() && !maybe_prompt_to_skip_tests_for_missing_requested_deps( &mut pkg_spec, &missing_test, "Requested test dependencies are missing", )? { anyhow::bail!("Missing test dependencies: {}", missing_test.join(", ")); } } else if options.no_deps || !should_install_test_deps(&pkg_spec, options.install_test_deps, requested_outputs) { maybe_disable_tests_for_missing_deps(&mut pkg_spec, &db_path, requested_outputs)?; } } // Check dependencies and prompt for auto-install if needed if !options.no_deps { deps::print_dep_status_for_outputs(&pkg_spec, &db_path, requested_outputs)?; let missing_required = merge_missing_dependencies( deps::check_build_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?, deps::check_runtime_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?, ); if !missing_required.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_required.join(", ") )); let local_sibling_root = spec_path.parent().and_then(|path| path.parent()); let dep_plan = planner::build_dependency_install_plan( config, options.rootfs, &missing_required, planner::PlannerOptions { assume_yes: ui::assume_yes_enabled(), prefer_binary: config.repo_settings.prefer_binary, local_sibling_root: local_sibling_root.map(Path::to_path_buf), include_test_deps: options.install_test_deps, lib32_only_requested_specs: false, }, )?; let dep_plan_packages = actionable_plan_packages(&dep_plan); warn_source_build_plan(&dep_plan); let dep_prompt_packages = if dep_plan_packages.is_empty() { missing_required.clone() } else { dep_plan_packages }; if ui::prompt_package_action("dependency installation", &dep_prompt_packages, 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) }; let mut dep_spec_paths = Vec::new(); for dep in missing_required { // Use package index for O(1) lookup let candidate = pkg_index.find(&dep); if let Some(dep_spec_path) = candidate { dep_spec_paths.push(dep_spec_path); } else { anyhow::bail!("Could not find package spec for dependency: {}", dep); } } ui::info(format!( "Installing dependencies: {}", install_request_display(&dep_spec_paths) )); let exe = std::env::current_exe().context("Failed to locate depot executable")?; run_install_command_with_program( &exe, &dep_spec_paths, options.rootfs, ChildInstallCommandOptions { no_deps: options.no_deps, assume_yes: true, no_flags: options.no_flags, cross_prefix: options.cross_prefix, clean: options.clean, lib32_only: false, install_test_deps: options.install_test_deps, install_context: Some(INSTALL_CONTEXT_PLANNED), dep_chain: Some(&new_chain), }, )?; } } // Enforce required dependencies before building/installing. deps::require_build_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?; deps::require_runtime_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?; if should_install_test_deps(&pkg_spec, options.install_test_deps, requested_outputs) { let missing_test = deps::check_test_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?; if !missing_test.is_empty() { let pkg_index = index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone())); let mut dep_spec_paths = Vec::new(); let mut unavailable_test = Vec::new(); for dep in &missing_test { if let Some(dep_spec_path) = pkg_index.find(dep) { dep_spec_paths.push(dep_spec_path); } else { unavailable_test.push(dep.clone()); } } if !unavailable_test.is_empty() && !maybe_prompt_to_skip_tests_for_missing_requested_deps( &mut pkg_spec, &unavailable_test, "Requested test dependencies could not be resolved", )? { anyhow::bail!("Missing test dependencies: {}", unavailable_test.join(", ")); } if !automatic_tests_disabled_for_outputs(&pkg_spec, requested_outputs) && !dep_spec_paths.is_empty() { ui::warn(format!( "Missing test dependencies: {}", missing_test.join(", ") )); let local_sibling_root = spec_path.parent().and_then(|path| path.parent()); let dep_plan = planner::build_dependency_install_plan( config, options.rootfs, &missing_test, planner::PlannerOptions { assume_yes: ui::assume_yes_enabled(), prefer_binary: config.repo_settings.prefer_binary, local_sibling_root: local_sibling_root.map(Path::to_path_buf), include_test_deps: options.install_test_deps, lib32_only_requested_specs: false, }, )?; let dep_plan_packages = actionable_plan_packages(&dep_plan); warn_source_build_plan(&dep_plan); let dep_prompt_packages = if dep_plan_packages.is_empty() { missing_test.clone() } else { dep_plan_packages }; if ui::prompt_package_action( "dependency installation", &dep_prompt_packages, true, )? { ui::info(format!( "Installing test dependencies: {}", install_request_display(&dep_spec_paths) )); let exe = std::env::current_exe().context("Failed to locate depot executable")?; run_install_command_with_program( &exe, &dep_spec_paths, options.rootfs, ChildInstallCommandOptions { no_deps: options.no_deps, assume_yes: true, no_flags: options.no_flags, cross_prefix: options.cross_prefix, clean: options.clean, lib32_only: false, install_test_deps: options.install_test_deps, install_context: Some(INSTALL_CONTEXT_PLANNED), dep_chain: None, }, )?; } else if !maybe_prompt_to_skip_tests_for_missing_requested_deps( &mut pkg_spec, &missing_test, "Requested test dependencies were not installed", )? { anyhow::bail!("Aborted"); } } } } if should_install_test_deps(&pkg_spec, options.install_test_deps, requested_outputs) { let missing_test = deps::check_test_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?; if !missing_test.is_empty() && !maybe_prompt_to_skip_tests_for_missing_requested_deps( &mut pkg_spec, &missing_test, "Requested test dependencies are still missing", )? { deps::require_test_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?; } } } 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) clean_build_source_dirs(config)?; source::preflight_manual_sources(&pkg_spec, &config.cache_dir)?; let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?; built_src_dir = Some(src_dir.clone()); let host_build_dir = builder::ensure_host_build( &pkg_spec, &src_dir, cross_config.as_ref(), !options.no_flags, builder::TargetBuildKind::Primary, )?; if let Some(host_dir) = host_build_dir.as_ref() { pkg_spec.build.flags.host_build_dir = Some(host_dir.to_string_lossy().into_owned()); } // 3. Build let destdir = config .build_dir .join("destdir") .join(&pkg_spec.package.name); if !lib32_only { builder::build( &pkg_spec, &src_dir, &destdir, cross_config.as_ref(), !options.no_flags, host_build_dir.as_deref(), )?; // 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 mut transaction_plans = Vec::new(); if !lib32_only { if staging_dir.is_none() { // Source-build path: apply staging transforms (strip/compress/static cleanup). staging::process(&destdir, &pkg_spec)?; if let Some(src_dir) = built_src_dir.as_deref() { staging::stage_split_package_licenses(src_dir, &destdir, &pkg_spec)?; } } else { // Binary archive path: install as-packaged without post-build transformations. if !suppress_output { ui::info("Installing binary archive payload"); } } let output_plans = plan_package_outputs_for_install(&pkg_spec, &destdir, options.rootfs, config)?; transaction_plans.extend(output_plans); } 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, lib32_only, )? { let staged = plan_staged_install(&lib32_spec, &lib32_destdir, options.rootfs, config)?; transaction_plans.push(PlannedPackageInstall { spec: lib32_spec, destdir: lib32_destdir, staged, }); } run_transaction_hooks_for_plans( options.rootfs, install::hooks::HookPhase::Pre, &transaction_plans, )?; let _snapper_post_install_snapshot_todo: fn() -> ! = || todo!("snapper: create post-install snapshot after install commit succeeds"); install_planned_packages_to_rootfs(&transaction_plans, options.rootfs, config)?; run_transaction_hooks_for_plans( options.rootfs, install::hooks::HookPhase::Post, &transaction_plans, )?; Ok(true) } mod build_cmd; mod check; mod install_cmd; mod misc; mod repo; mod set; mod update; pub fn run(cli: Cli) -> Result<()> { crate::interrupts::install()?; crate::interrupts::reset(); ui::set_assume_yes(command_assume_yes(&cli.command)); if maybe_reexec_with_sudo(&cli)? { return Ok(()); } let cli_test_deps = match &cli.command { Commands::Install(args) => args.build_exec_args.test_deps, Commands::Build(args) => args.build_exec_args.test_deps, Commands::Update(args) => args.build_exec_args.test_deps, Commands::Bootstrap(_) | Commands::Check(_) | Commands::Remove(_) | Commands::Info(_) | Commands::Search(_) | Commands::Owns(_) | Commands::List(_) | Commands::Sign(_) | Commands::Repo(_) | Commands::Config(_) | Commands::Set(_) | Commands::System(_) | Commands::GenerateArtifacts(_) | Commands::Convert(_) | Commands::MakeSpec(_) | Commands::Internal(_) => false, }; match cli.command { Commands::Install(args) => install_cmd::run_install(args, cli_test_deps)?, Commands::Remove(args) => install_cmd::run_remove(args)?, Commands::Build(args) => build_cmd::run_build(args, cli_test_deps)?, Commands::Bootstrap(args) => bootstrap::run(args)?, Commands::Update(args) => update::run_update(args, cli_test_deps)?, Commands::Check(args) => check::run_check(args)?, Commands::Info(args) => misc::run_info(args)?, Commands::Search(args) => repo::run_search(args)?, Commands::Owns(args) => misc::run_owns(args)?, Commands::List(args) => misc::run_list(args)?, Commands::Sign(args) => misc::run_sign(args)?, Commands::Repo(args) => repo::run_repo(args.command)?, Commands::GenerateArtifacts(args) => misc::run_generate_artifacts(args)?, Commands::Config(args) => misc::run_config(args)?, Commands::Set(args) => set::run_set(args)?, Commands::System(args) => misc::run_system(args)?, Commands::MakeSpec(args) => misc::run_make_spec(args)?, Commands::Convert(args) => misc::run_convert(args)?, Commands::Internal(args) => misc::run_internal(args)?, } Ok(()) } #[cfg(test)] mod tests;