refactor command flow and add signed package/hook safety
- split CLI definitions and command execution out of main.rs into new cli.rs and commands.rs modules\n- add transaction hook support via /etc/depot.d/hooks/*.toml with pre/post phases, operation/package/path filters, wildcard negation, and optional affected-path stdin\n- execute transaction hooks around install/update/remove operations and add package-action prompts for install/build/remove flows\n- require and verify detached minisign signatures for binary package archives when allow_unsigned=false, including cache handling and tests\n- persist optional dependencies from package metadata into repo index data and include optional deps in produced metadata\n- update dependency/planner logic to ignore dependencies provided by local split outputs and stop pulling test deps into install planning\n- auto-disable tests when required test dependencies are missing instead of hard-failing build/install paths\n- improve autotools defaults by injecting standard install directory flags when supported and preserving user overrides\n- remove static .a archives during staging by default with new build.flags.no_delete_static escape hatch\n- preserve symlinks and file metadata (permissions/timestamps) in extractor fallback copy path and add regression tests\n- refresh README docs for detached package signatures, optional dependencies, and transaction hooks\n- adjust dependency set: add filetime, disable reqwest default features, and simplify git2/OpenSSL linkage
This commit is contained in:
+101
-8
@@ -2,6 +2,7 @@
|
||||
|
||||
use crate::package::{PackageSpec, Source};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use filetime::FileTime;
|
||||
use flate2::read::GzDecoder;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Cursor, Read, Write};
|
||||
@@ -605,20 +606,61 @@ fn move_dir_contents(src: &Path, dest: &Path) -> Result<()> {
|
||||
let entry = entry?;
|
||||
let src_path = entry.path();
|
||||
let dest_path = dest.join(entry.file_name());
|
||||
let file_type = entry.file_type()?;
|
||||
if fs::rename(&src_path, &dest_path).is_err() {
|
||||
// fallback to copy-and-remove across filesystems
|
||||
if src_path.is_dir() {
|
||||
copy_dir_recursive_local(&src_path, &dest_path)?;
|
||||
fs::remove_dir_all(&src_path)?;
|
||||
} else {
|
||||
fs::copy(&src_path, &dest_path)?;
|
||||
fs::remove_file(&src_path)?;
|
||||
}
|
||||
copy_entry_fallback(&src_path, &dest_path, file_type)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_entry_fallback(src_path: &Path, dest_path: &Path, file_type: fs::FileType) -> Result<()> {
|
||||
if file_type.is_dir() {
|
||||
copy_dir_recursive_local(src_path, dest_path)?;
|
||||
fs::remove_dir_all(src_path)?;
|
||||
} else if file_type.is_symlink() {
|
||||
if let Some(parent) = dest_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let link_target = fs::read_link(src_path)
|
||||
.with_context(|| format!("Failed to read symlink {}", src_path.display()))?;
|
||||
unix_fs::symlink(&link_target, dest_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to create symlink {} -> {}",
|
||||
dest_path.display(),
|
||||
link_target.display()
|
||||
)
|
||||
})?;
|
||||
fs::remove_file(src_path)?;
|
||||
} else {
|
||||
copy_file_preserve_metadata(src_path, dest_path)?;
|
||||
fs::remove_file(src_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_file_preserve_metadata(src: &Path, dst: &Path) -> Result<()> {
|
||||
if let Some(parent) = dst.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
fs::copy(src, dst)
|
||||
.with_context(|| format!("Failed to copy {} -> {}", src.display(), dst.display()))?;
|
||||
|
||||
let src_meta = fs::metadata(src)
|
||||
.with_context(|| format!("Failed to read metadata for {}", src.display()))?;
|
||||
fs::set_permissions(dst, src_meta.permissions())
|
||||
.with_context(|| format!("Failed to set permissions for {}", dst.display()))?;
|
||||
|
||||
let atime = FileTime::from_last_access_time(&src_meta);
|
||||
let mtime = FileTime::from_last_modification_time(&src_meta);
|
||||
filetime::set_file_times(dst, atime, mtime)
|
||||
.with_context(|| format!("Failed to preserve file timestamps for {}", dst.display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_dir_recursive_local(src: &Path, dst: &Path) -> Result<()> {
|
||||
for entry in WalkDir::new(src) {
|
||||
let entry = entry?;
|
||||
@@ -633,7 +675,7 @@ fn copy_dir_recursive_local(src: &Path, dst: &Path) -> Result<()> {
|
||||
if let Some(parent) = target.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::copy(entry.path(), &target)?;
|
||||
copy_file_preserve_metadata(entry.path(), &target)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -643,6 +685,7 @@ fn copy_dir_recursive_local(src: &Path, dst: &Path) -> Result<()> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
@@ -690,6 +733,56 @@ mod tests {
|
||||
assert!(extract_dir.join("usr/bin/hello-deb").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_file_preserve_metadata_keeps_mtime() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let src = tmp.path().join("src");
|
||||
let dst = tmp.path().join("dst");
|
||||
std::fs::write(&src, b"hello").unwrap();
|
||||
|
||||
let fixed = SystemTime::UNIX_EPOCH + Duration::from_secs(946684800); // 2000-01-01 UTC
|
||||
let ts = FileTime::from_system_time(fixed);
|
||||
filetime::set_file_times(&src, ts, ts).unwrap();
|
||||
|
||||
copy_file_preserve_metadata(&src, &dst).unwrap();
|
||||
let src_meta = std::fs::metadata(&src).unwrap();
|
||||
let dst_meta = std::fs::metadata(&dst).unwrap();
|
||||
assert_eq!(
|
||||
FileTime::from_last_modification_time(&dst_meta),
|
||||
FileTime::from_last_modification_time(&src_meta)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_entry_fallback_preserves_symlink_when_target_is_missing() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let src_dir = tmp.path().join("src");
|
||||
let dst_dir = tmp.path().join("dst");
|
||||
std::fs::create_dir_all(&src_dir).unwrap();
|
||||
std::fs::create_dir_all(&dst_dir).unwrap();
|
||||
|
||||
let src_link = src_dir.join("RELEASE-NOTES");
|
||||
std::os::unix::fs::symlink("doc/RelNotes/v1.47.3.txt", &src_link).unwrap();
|
||||
|
||||
// Simulate target already moved/removed before copying this symlink entry.
|
||||
let src_target_dir = src_dir.join("doc");
|
||||
std::fs::create_dir_all(src_target_dir.join("RelNotes")).unwrap();
|
||||
std::fs::write(src_target_dir.join("RelNotes/v1.47.3.txt"), "notes").unwrap();
|
||||
std::fs::remove_dir_all(&src_target_dir).unwrap();
|
||||
|
||||
let dst_link = dst_dir.join("RELEASE-NOTES");
|
||||
let file_type = std::fs::symlink_metadata(&src_link).unwrap().file_type();
|
||||
copy_entry_fallback(&src_link, &dst_link, file_type).unwrap();
|
||||
|
||||
assert!(std::fs::symlink_metadata(&src_link).is_err());
|
||||
let dst_meta = std::fs::symlink_metadata(&dst_link).unwrap();
|
||||
assert!(dst_meta.file_type().is_symlink());
|
||||
assert_eq!(
|
||||
std::fs::read_link(&dst_link).unwrap(),
|
||||
PathBuf::from("doc/RelNotes/v1.47.3.txt")
|
||||
);
|
||||
}
|
||||
|
||||
fn write_cpio_newc_one_file(w: &mut Vec<u8>, name: &str, data: &[u8]) {
|
||||
// magic + 13 fields of 8 hex chars each
|
||||
fn h8(v: u64) -> String {
|
||||
|
||||
+1
-1
@@ -247,7 +247,7 @@ fn verify_file_hash(path: &Path, expected: &str) -> Result<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a blocking reqwest HTTP client using the platform-default TLS backend.
|
||||
/// Build a blocking reqwest HTTP client using the configured Cargo TLS backend.
|
||||
/// Any error building the client is returned directly (no fallback).
|
||||
pub(crate) fn build_blocking_client(
|
||||
user_agent: &str,
|
||||
|
||||
Reference in New Issue
Block a user