From e984ee282f0f3b17394795d67690da93bb6a02c7 Mon Sep 17 00:00:00 2001 From: SFG545 Date: Mon, 9 Mar 2026 00:26:26 -0500 Subject: [PATCH] feat: implement root filesystem checks and staging directory management for package extraction --- src/commands.rs | 164 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 154 insertions(+), 10 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 4af6ca5..a5b43b5 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -10,6 +10,45 @@ use std::fs; use std::io::IsTerminal; use std::path::{Path, PathBuf}; +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 { .. }) +} + +fn should_reexec_with_sudo(cli: &Cli) -> bool { + !crate::fakeroot::is_root() + && rootfs_is_system_root(&cli.rootfs) + && command_requires_live_root(&cli.command) +} + +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"); + 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 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()]; @@ -38,10 +77,34 @@ fn parse_dependency_list(metadata: &toml::Value, kind: &str) -> Vec { .unwrap_or_default() } +fn staging_temp_root(config: &config::Config) -> PathBuf { + config.build_dir.join("staging") +} + +fn create_archive_staging_dir( + config: &config::Config, + archive_path: &Path, +) -> Result { + let staging_root = staging_temp_root(config); + fs::create_dir_all(&staging_root) + .with_context(|| format!("Failed to create staging root {}", staging_root.display()))?; + tempfile::Builder::new() + .prefix("archive-") + .tempdir_in(&staging_root) + .with_context(|| { + format!( + "Failed to create staging dir for {} under {}", + archive_path.display(), + staging_root.display() + ) + }) +} + fn load_package_archive_into_staging( + config: &config::Config, archive_path: &Path, ) -> Result<(package::PackageSpec, tempfile::TempDir)> { - let tmp_dir = tempfile::TempDir::new().with_context(|| { + let tmp_dir = create_archive_staging_dir(config, archive_path).with_context(|| { format!( "Failed to create staging dir for {}", archive_path.display() @@ -167,8 +230,11 @@ fn load_package_archive_into_staging( Ok((spec, tmp_dir)) } -fn extract_package_archive_to_staging(archive_path: &Path) -> Result { - let tmp_dir = tempfile::TempDir::new().with_context(|| { +fn extract_package_archive_to_staging( + config: &config::Config, + archive_path: &Path, +) -> Result { + let tmp_dir = create_archive_staging_dir(config, archive_path).with_context(|| { format!( "Failed to create staging dir for {}", archive_path.display() @@ -1261,7 +1327,7 @@ fn execute_install_plan_with_child_commands( record.filename, repo_name ) })?; - let staged = extract_package_archive_to_staging(&cached.package_path)?; + let staged = extract_package_archive_to_staging(config, &cached.package_path)?; let spec = package_spec_from_repo_record(record); let plans = plan_package_outputs_for_install(&spec, staged.path(), config)?; binary_pre_hook_plans.extend(plans); @@ -1317,7 +1383,7 @@ fn execute_install_plan_with_child_commands( record.filename, repo_name ) })?; - let staged = extract_package_archive_to_staging(&cached.package_path)?; + let staged = extract_package_archive_to_staging(config, &cached.package_path)?; let spec = package_spec_from_repo_record(record); let plans = plan_package_outputs_for_install(&spec, staged.path(), config)?; let installed = install_planned_packages_to_rootfs(&plans, rootfs, config)?; @@ -1450,7 +1516,7 @@ fn run_direct_install_request( 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)?; + let (spec, tmp_dir) = load_package_archive_into_staging(config, &spec_path)?; (spec, Some(tmp_dir)) } else { // Install from spec (normal build) @@ -1669,15 +1735,15 @@ fn run_direct_install_request( pub fn run(cli: Cli) -> Result<()> { ui::set_assume_yes(cli.yes); + if maybe_reexec_with_sudo(&cli)? { + return Ok(()); + } match cli.command { Commands::Install { spec_or_archive, spec, } => { - if !crate::fakeroot::is_root() { - anyhow::bail!("The 'install' command must be run as root"); - } let install_requests = match spec { Some(spec_path) => vec![spec_path], None => spec_or_archive, @@ -2648,7 +2714,7 @@ mod tests { cfg.build_dir = rootfs.path().join("var/cache/depot/build"); cfg.db_dir = rootfs.path().join("var/lib/depot"); - let staged = extract_package_archive_to_staging(&archive_path)?; + let staged = extract_package_archive_to_staging(&cfg, &archive_path)?; let record = db::repo::BinaryRepoPackageRecord { repo_name: "core".into(), name: "pkg".into(), @@ -2685,6 +2751,58 @@ mod tests { Ok(()) } + #[test] + fn binary_archive_staging_uses_config_build_dir_instead_of_process_tmpdir() -> Result<()> { + let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?; + let pkg_dir = tempfile::tempdir().context("Failed to create temp package dir")?; + let archive_path = pkg_dir.path().join("pkg-1.0-1-x86_64.depot.pkg.tar.zst"); + + let file = fs::File::create(&archive_path) + .with_context(|| format!("Failed to create {}", archive_path.display()))?; + let encoder = + zstd::stream::write::Encoder::new(file, 3).context("Failed to create zstd encoder")?; + let mut tar = tar::Builder::new(encoder); + let payload = b"hello"; + let mut header = tar::Header::new_gnu(); + header.set_path("usr/bin/hello").unwrap(); + header.set_size(payload.len() as u64); + header.set_mode(0o755); + header.set_cksum(); + tar.append(&header, &payload[..]).unwrap(); + let encoder = tar.into_inner().unwrap(); + encoder.finish().unwrap(); + + let mut cfg = config::Config::for_rootfs(rootfs.path()); + cfg.build_dir = rootfs.path().join("var/cache/depot/build"); + + let blocked_tmp = rootfs.path().join("blocked-tmp"); + fs::create_dir_all(&blocked_tmp) + .with_context(|| format!("Failed to create {}", blocked_tmp.display()))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&blocked_tmp, fs::Permissions::from_mode(0o555)) + .with_context(|| format!("Failed to chmod {}", blocked_tmp.display()))?; + } + + let previous_tmpdir = std::env::var_os("TMPDIR"); + unsafe { + std::env::set_var("TMPDIR", &blocked_tmp); + } + let staged = extract_package_archive_to_staging(&cfg, &archive_path)?; + unsafe { + if let Some(value) = previous_tmpdir { + std::env::set_var("TMPDIR", value); + } else { + std::env::remove_var("TMPDIR"); + } + } + + assert!(staged.path().starts_with(staging_temp_root(&cfg))); + assert!(staged.path().join("usr/bin/hello").exists()); + Ok(()) + } + #[test] fn merge_missing_dependencies_preserves_order_and_uniqueness() { let merged = merge_missing_dependencies( @@ -2698,4 +2816,30 @@ mod tests { ); assert_eq!(merged, vec!["make", "pkgconf", "glibc", "openssl", "zlib"]); } + + #[test] + fn rootfs_is_system_root_detects_live_rootfs() { + assert!(rootfs_is_system_root(Path::new("/"))); + assert!(!rootfs_is_system_root(Path::new("/tmp/depot-test-rootfs"))); + } + + #[test] + fn command_requires_live_root_only_for_install_and_remove() { + assert!(command_requires_live_root(&Commands::Install { + spec_or_archive: vec![PathBuf::from("foo")], + spec: None, + })); + assert!(command_requires_live_root(&Commands::Remove { + package: "foo".to_string(), + })); + assert!(!command_requires_live_root(&Commands::Build { + spec_pos: Some(PathBuf::from("foo.toml")), + spec: None, + install: false, + })); + assert!(!command_requires_live_root(&Commands::Search { + query: "foo".to_string(), + files: false, + })); + } }