feat: implement root filesystem checks and staging directory management for package extraction
This commit is contained in:
+154
-10
@@ -10,6 +10,45 @@ use std::fs;
|
|||||||
use std::io::IsTerminal;
|
use std::io::IsTerminal;
|
||||||
use std::path::{Path, PathBuf};
|
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<bool> {
|
||||||
|
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<String> {
|
fn parse_licenses_from_toml(metadata: &toml::Value) -> Vec<String> {
|
||||||
if let Some(s) = metadata.get("license").and_then(|v| v.as_str()) {
|
if let Some(s) = metadata.get("license").and_then(|v| v.as_str()) {
|
||||||
return vec![s.to_string()];
|
return vec![s.to_string()];
|
||||||
@@ -38,10 +77,34 @@ fn parse_dependency_list(metadata: &toml::Value, kind: &str) -> Vec<String> {
|
|||||||
.unwrap_or_default()
|
.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<tempfile::TempDir> {
|
||||||
|
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(
|
fn load_package_archive_into_staging(
|
||||||
|
config: &config::Config,
|
||||||
archive_path: &Path,
|
archive_path: &Path,
|
||||||
) -> Result<(package::PackageSpec, tempfile::TempDir)> {
|
) -> 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!(
|
format!(
|
||||||
"Failed to create staging dir for {}",
|
"Failed to create staging dir for {}",
|
||||||
archive_path.display()
|
archive_path.display()
|
||||||
@@ -167,8 +230,11 @@ fn load_package_archive_into_staging(
|
|||||||
Ok((spec, tmp_dir))
|
Ok((spec, tmp_dir))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_package_archive_to_staging(archive_path: &Path) -> Result<tempfile::TempDir> {
|
fn extract_package_archive_to_staging(
|
||||||
let tmp_dir = tempfile::TempDir::new().with_context(|| {
|
config: &config::Config,
|
||||||
|
archive_path: &Path,
|
||||||
|
) -> Result<tempfile::TempDir> {
|
||||||
|
let tmp_dir = create_archive_staging_dir(config, archive_path).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to create staging dir for {}",
|
"Failed to create staging dir for {}",
|
||||||
archive_path.display()
|
archive_path.display()
|
||||||
@@ -1261,7 +1327,7 @@ fn execute_install_plan_with_child_commands(
|
|||||||
record.filename, repo_name
|
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 spec = package_spec_from_repo_record(record);
|
||||||
let plans = plan_package_outputs_for_install(&spec, staged.path(), config)?;
|
let plans = plan_package_outputs_for_install(&spec, staged.path(), config)?;
|
||||||
binary_pre_hook_plans.extend(plans);
|
binary_pre_hook_plans.extend(plans);
|
||||||
@@ -1317,7 +1383,7 @@ fn execute_install_plan_with_child_commands(
|
|||||||
record.filename, repo_name
|
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 spec = package_spec_from_repo_record(record);
|
||||||
let plans = plan_package_outputs_for_install(&spec, staged.path(), config)?;
|
let plans = plan_package_outputs_for_install(&spec, staged.path(), config)?;
|
||||||
let installed = install_planned_packages_to_rootfs(&plans, rootfs, 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") {
|
if spec_path.to_string_lossy().ends_with(".tar.zst") {
|
||||||
// Install from archive
|
// Install from archive
|
||||||
ui::info(format!("Detected package archive: {}", spec_path.display()));
|
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))
|
(spec, Some(tmp_dir))
|
||||||
} else {
|
} else {
|
||||||
// Install from spec (normal build)
|
// Install from spec (normal build)
|
||||||
@@ -1669,15 +1735,15 @@ fn run_direct_install_request(
|
|||||||
|
|
||||||
pub fn run(cli: Cli) -> Result<()> {
|
pub fn run(cli: Cli) -> Result<()> {
|
||||||
ui::set_assume_yes(cli.yes);
|
ui::set_assume_yes(cli.yes);
|
||||||
|
if maybe_reexec_with_sudo(&cli)? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Install {
|
Commands::Install {
|
||||||
spec_or_archive,
|
spec_or_archive,
|
||||||
spec,
|
spec,
|
||||||
} => {
|
} => {
|
||||||
if !crate::fakeroot::is_root() {
|
|
||||||
anyhow::bail!("The 'install' command must be run as root");
|
|
||||||
}
|
|
||||||
let install_requests = match spec {
|
let install_requests = match spec {
|
||||||
Some(spec_path) => vec![spec_path],
|
Some(spec_path) => vec![spec_path],
|
||||||
None => spec_or_archive,
|
None => spec_or_archive,
|
||||||
@@ -2648,7 +2714,7 @@ mod tests {
|
|||||||
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
|
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
|
||||||
cfg.db_dir = rootfs.path().join("var/lib/depot");
|
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 {
|
let record = db::repo::BinaryRepoPackageRecord {
|
||||||
repo_name: "core".into(),
|
repo_name: "core".into(),
|
||||||
name: "pkg".into(),
|
name: "pkg".into(),
|
||||||
@@ -2685,6 +2751,58 @@ mod tests {
|
|||||||
Ok(())
|
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]
|
#[test]
|
||||||
fn merge_missing_dependencies_preserves_order_and_uniqueness() {
|
fn merge_missing_dependencies_preserves_order_and_uniqueness() {
|
||||||
let merged = merge_missing_dependencies(
|
let merged = merge_missing_dependencies(
|
||||||
@@ -2698,4 +2816,30 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(merged, vec!["make", "pkgconf", "glibc", "openssl", "zlib"]);
|
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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user