diff --git a/AGENTS.md b/AGENTS.md index 9aed26b..18fe973 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ Stubs are not allowed. Do **not**: - Add placeholder functions -- Add `todo!()` or `unimplemented!()` +- Add `todo!()` or `unimplemented!()` (unless asked to do so) - Leave partially implemented logic - Add empty modules “for later” - Return dummy values to satisfy the compiler diff --git a/Cargo.lock b/Cargo.lock index 3cee4a4..baf789d 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -418,7 +418,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" [[package]] name = "depot" -version = "0.21.1" +version = "0.22.0" dependencies = [ "anyhow", "ar", diff --git a/Cargo.toml b/Cargo.toml index 8a63b63..5188492 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "depot" -version = "0.21.1" +version = "0.22.0" edition = "2024" [lints.rust] diff --git a/meson.build b/meson.build index dff47c5..dea1412 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project( 'depot', - version: '0.21.1', + version: '0.22.0', meson_version: '>=0.60.0', default_options: ['buildtype=release'], ) diff --git a/src/commands.rs b/src/commands.rs index 9ce5bed..99cc287 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1034,23 +1034,10 @@ fn make_lib32_build_spec(base: &package::PackageSpec) -> package::PackageSpec { let mut spec = base.clone(); let flags = &mut spec.build.flags; flags.lib32_variant = true; - - if !flags.cflags_lib32.is_empty() { - flags.cflags.extend(flags.cflags_lib32.clone()); - } - if !flags.replace_cflags_lib32.is_empty() { - flags - .replace_cflags - .extend(flags.replace_cflags_lib32.clone()); - } - if !flags.cxxflags_lib32.is_empty() { - flags.cxxflags.extend(flags.cxxflags_lib32.clone()); - } - if !flags.replace_cxxflags_lib32.is_empty() { - flags - .replace_cxxflags - .extend(flags.replace_cxxflags_lib32.clone()); - } + flags.cflags = flags.cflags_lib32.clone(); + flags.replace_cflags = flags.replace_cflags_lib32.clone(); + flags.cxxflags = flags.cxxflags_lib32.clone(); + flags.replace_cxxflags = flags.replace_cxxflags_lib32.clone(); if !flags.configure_lib32.is_empty() { flags.configure = flags.configure_lib32.clone(); } @@ -3801,7 +3788,8 @@ fn run_direct_install_request( anyhow::bail!("Aborted"); } - // TODO(snapper): create pre-install snapshot before install work starts. + 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(|| { @@ -4070,10 +4058,11 @@ fn run_direct_install_request( install::hooks::HookPhase::Pre, &transaction_plans, )?; + let _snapper_post_install_snapshot_todo: fn() -> ! = + || todo!("snapper: create post-install snapshot after install commit succeeds"); let installed = install_planned_packages_to_rootfs(&transaction_plans, options.rootfs, config)?; for pkg in installed { log_install_success(&pkg); - // TODO(snapper): create post-install snapshot after install commit succeeds. } run_transaction_hooks_for_plans( options.rootfs, @@ -6921,7 +6910,7 @@ optional = [] } #[test] - fn make_lib32_build_spec_merges_replace_flag_rules() { + fn make_lib32_build_spec_uses_only_lib32_flag_rules() { let mut base = test_package_spec(package::BuildType::Custom, None, &[]); base.build.flags.cflags = vec!["-O2".into()]; base.build.flags.replace_cflags = vec!["-O2=>-O3".into()]; @@ -6935,15 +6924,15 @@ optional = [] let lib32 = make_lib32_build_spec(&base); assert!(lib32.build.flags.lib32_variant); - assert_eq!(lib32.build.flags.cflags, vec!["-O2", "-m32"]); + assert_eq!(lib32.build.flags.cflags, vec!["-m32"]); assert_eq!( lib32.build.flags.replace_cflags, - vec!["-O2=>-O3", "-m32=>-mstackrealign"] + vec!["-m32=>-mstackrealign"] ); - assert_eq!(lib32.build.flags.cxxflags, vec!["-O2", "-fno-rtti"]); + assert_eq!(lib32.build.flags.cxxflags, vec!["-fno-rtti"]); assert_eq!( lib32.build.flags.replace_cxxflags, - vec!["-O2=>-O3", "-fno-rtti=>-fno-exceptions"] + vec!["-fno-rtti=>-fno-exceptions"] ); } diff --git a/src/staging/mod.rs b/src/staging/mod.rs index 2290b3b..871469c 100755 --- a/src/staging/mod.rs +++ b/src/staging/mod.rs @@ -894,6 +894,7 @@ pub struct FsTransaction { tx_dir: PathBuf, backed_up: Vec, created: Vec, + relocated: Vec, removed: Vec, } @@ -929,6 +930,112 @@ fn backup_existing_path(src: &Path, backup_path: &Path, rel: &str) -> Result<()> Ok(()) } +fn move_directory_contents(src_dir: &Path, dst_dir: &Path) -> Result<()> { + fs::create_dir_all(dst_dir) + .with_context(|| format!("Failed to create directory {}", dst_dir.display()))?; + + for entry in + fs::read_dir(src_dir).with_context(|| format!("Failed to read {}", src_dir.display()))? + { + let entry = entry?; + move_tree_preserving_layout(&entry.path(), &dst_dir.join(entry.file_name()))?; + } + + fs::remove_dir(src_dir).with_context(|| format!("Failed to remove {}", src_dir.display()))?; + Ok(()) +} + +fn copy_tree_preserving_layout_no_overwrite( + src_root: &Path, + dst_root: &Path, + logical_root: &str, + created: &mut Vec, +) -> Result<()> { + for entry in WalkDir::new(src_root).follow_links(false) { + let entry = entry + .with_context(|| format!("Failed to walk relocation tree {}", src_root.display()))?; + let src_path = entry.path(); + let rel = src_path + .strip_prefix(src_root) + .with_context(|| format!("Failed to strip relocation root {}", src_root.display()))?; + if rel.as_os_str().is_empty() { + continue; + } + + let dst_path = dst_root.join(rel); + let metadata = src_path + .symlink_metadata() + .with_context(|| format!("Failed to inspect {}", src_path.display()))?; + let file_type = metadata.file_type(); + + if file_type.is_dir() { + match dst_path.symlink_metadata() { + Ok(dst_meta) => { + if !dst_meta.file_type().is_dir() { + anyhow::bail!( + "Failed to replay relocated directory into {}: destination exists and is not a directory", + dst_path.display() + ); + } + } + Err(err) if err.kind() == io::ErrorKind::NotFound => { + fs::create_dir_all(&dst_path).with_context(|| { + format!("Failed to create directory {}", dst_path.display()) + })?; + apply_unix_mode(&dst_path, &metadata)?; + } + Err(err) => { + return Err(err) + .with_context(|| format!("Failed to inspect {}", dst_path.display())); + } + } + continue; + } + + if let Some(parent) = dst_path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Failed to create directory {}", parent.display()))?; + } + + if dst_path.symlink_metadata().is_ok() { + anyhow::bail!( + "Failed to replay relocated path into {}: destination already exists", + dst_path.display() + ); + } + + if file_type.is_symlink() { + let target = fs::read_link(src_path) + .with_context(|| format!("Failed to read symlink {}", src_path.display()))?; + std::os::unix::fs::symlink(&target, &dst_path).with_context(|| { + format!( + "Failed to create relocated symlink {} -> {}", + dst_path.display(), + target.display() + ) + })?; + } else { + fs::copy(src_path, &dst_path).with_context(|| { + format!( + "Failed to copy relocated path {} to {}", + src_path.display(), + dst_path.display() + ) + })?; + apply_unix_mode(&dst_path, &metadata)?; + } + + let logical = Path::new(logical_root).join(rel); + let logical = logical + .to_str() + .context("Relocated install paths must be valid UTF-8")? + .to_string(); + created.push(logical); + } + + Ok(()) +} + fn remove_path_in_place(path: &Path, rel: &str) -> Result<()> { let metadata = path .symlink_metadata() @@ -1011,6 +1118,83 @@ impl FsTransaction { self.tx_dir.join("removed").join(rel) } + fn relocated_path(&self, rel: &str) -> PathBuf { + self.tx_dir.join("relocated").join(rel) + } + + fn relocate_directory_for_symlink_swap(&mut self, rel: &str) -> Result<()> { + let src = self.rootfs.join(rel); + let relocated = self.relocated_path(rel); + move_directory_contents(&src, &relocated)?; + self.relocated.push(rel.to_string()); + Ok(()) + } + + fn replay_relocated_dir_if_present(&mut self, rel: &str) -> Result<()> { + let relocated = self.relocated_path(rel); + match relocated.symlink_metadata() { + Ok(meta) if meta.file_type().is_dir() => {} + Ok(_) => { + anyhow::bail!( + "Relocation staging path is not a directory: {}", + relocated.display() + ); + } + Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()), + Err(err) => { + return Err(err) + .with_context(|| format!("Failed to inspect {}", relocated.display())); + } + } + + copy_tree_preserving_layout_no_overwrite( + &relocated, + &self.rootfs.join(rel), + rel, + &mut self.created, + ) + } + + fn restore_relocated_dir(&self, rel: &str) -> Result<()> { + let relocated = self.relocated_path(rel); + match relocated.symlink_metadata() { + Ok(meta) if meta.file_type().is_dir() => {} + Ok(_) => { + anyhow::bail!( + "Relocation staging path is not a directory: {}", + relocated.display() + ); + } + Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()), + Err(err) => { + return Err(err) + .with_context(|| format!("Failed to inspect {}", relocated.display())); + } + } + + let dst = self.rootfs.join(rel); + match dst.symlink_metadata() { + Ok(meta) if meta.file_type().is_dir() => {} + Ok(_) => { + anyhow::bail!( + "Failed to restore relocated directory contents into {}: destination is not a directory", + dst.display() + ); + } + Err(err) if err.kind() == io::ErrorKind::NotFound => { + fs::create_dir_all(&dst) + .with_context(|| format!("Failed to create directory {}", dst.display()))?; + } + Err(err) => { + return Err(err).with_context(|| format!("Failed to inspect {}", dst.display())); + } + } + + move_directory_contents(&relocated, &dst)?; + cleanup_empty_parent_dirs(&self.tx_dir, &relocated)?; + Ok(()) + } + fn restore_backup_entry(&self, src: &Path, dst: &Path) -> Result<()> { let metadata = src .symlink_metadata() @@ -1068,7 +1252,13 @@ impl FsTransaction { /// Roll back file operations performed by `install_atomic`. pub fn rollback(&self) -> Result<()> { - // Restore overwritten paths first so removed children have their original parent layout. + // Remove files that were newly created + for rel in &self.created { + let dst = self.rootfs.join(rel); + let _ = fs::remove_file(dst); + } + + // Restore overwritten paths first so relocated and removed children have their parent layout. for rel in &self.backed_up { let src = self.backup_path(rel); let dst = self.rootfs.join(rel); @@ -1077,6 +1267,10 @@ impl FsTransaction { } } + for rel in &self.relocated { + self.restore_relocated_dir(rel)?; + } + // Restore removed files/directories. for rel in &self.removed { let src = self.removed_backup_path(rel); @@ -1086,12 +1280,6 @@ impl FsTransaction { } } - // Remove files that were newly created - for rel in &self.created { - let dst = self.rootfs.join(rel); - let _ = fs::remove_file(dst); - } - Ok(()) } @@ -1147,6 +1335,7 @@ pub fn install_atomic( tx_dir, backed_up: Vec::new(), created: Vec::new(), + relocated: Vec::new(), removed: Vec::new(), }; let mut staged_paths = HashSet::new(); @@ -1227,15 +1416,21 @@ pub fn install_atomic( install_rel_path ); } + backup_existing_path(&dest_path, &backup_path, &install_rel_path)?; + tx.backed_up.push(install_rel_path.clone()); remove_obsolete_children_for_dir( &mut tx, rootfs, &install_rel_path, remove_paths, )?; + if file_type.is_symlink() { + tx.relocate_directory_for_symlink_swap(&install_rel_path)?; + } + } else { + backup_existing_path(&dest_path, &backup_path, &install_rel_path)?; + tx.backed_up.push(install_rel_path.clone()); } - backup_existing_path(&dest_path, &backup_path, &install_rel_path)?; - tx.backed_up.push(install_rel_path.clone()); } else { tx.created.push(install_rel_path.clone()); } @@ -1266,6 +1461,7 @@ pub fn install_atomic( let target = fs::read_link(src_path)?; std::os::unix::fs::symlink(target, &dest_path) .with_context(|| format!("Failed to create symlink: {}", install_rel_path))?; + tx.replay_relocated_dir_if_present(&install_rel_path)?; } else { fs::copy(src_path, &dest_path) .with_context(|| format!("Failed to install: {}", install_rel_path))?; @@ -1724,6 +1920,82 @@ mod tests { ); } + #[test] + #[cfg(unix)] + fn install_atomic_preserves_non_obsolete_directory_contents_when_replacing_with_symlink() { + let tmp = tempfile::tempdir().unwrap(); + let rootfs = tmp.path().join("root"); + let destdir = tmp.path().join("dest"); + let tx_base = tmp.path().join("tx"); + std::fs::create_dir_all(rootfs.join("usr/sbin")).unwrap(); + std::fs::create_dir_all(destdir.join("usr/bin")).unwrap(); + std::fs::create_dir_all(destdir.join("usr")).unwrap(); + std::fs::write(rootfs.join("usr/sbin/keep"), "keep-me").unwrap(); + std::fs::write(rootfs.join("usr/sbin/legacy"), "old").unwrap(); + std::fs::write(destdir.join("usr/bin/legacy"), "new").unwrap(); + std::os::unix::fs::symlink("bin", destdir.join("usr/sbin")).unwrap(); + + let remove_paths = vec!["usr/sbin/legacy".to_string(), "usr/sbin".to_string()]; + let tx = install_atomic(&destdir, &rootfs, &tx_base, &remove_paths, &[]).unwrap(); + + let sbin_meta = rootfs.join("usr/sbin").symlink_metadata().unwrap(); + assert!(sbin_meta.file_type().is_symlink()); + assert_eq!( + std::fs::read_to_string(rootfs.join("usr/bin/keep")).unwrap(), + "keep-me" + ); + assert_eq!( + std::fs::read_to_string(rootfs.join("usr/sbin/keep")).unwrap(), + "keep-me" + ); + assert_eq!( + std::fs::read_to_string(rootfs.join("usr/bin/legacy")).unwrap(), + "new" + ); + + tx.rollback().unwrap(); + let restored = rootfs.join("usr/sbin").symlink_metadata().unwrap(); + assert!(restored.file_type().is_dir()); + assert_eq!( + std::fs::read_to_string(rootfs.join("usr/sbin/keep")).unwrap(), + "keep-me" + ); + assert!(!rootfs.join("usr/bin/keep").exists()); + } + + #[test] + #[cfg(unix)] + fn install_atomic_rejects_symlink_swap_when_relocated_contents_conflict_with_target() { + let tmp = tempfile::tempdir().unwrap(); + let rootfs = tmp.path().join("root"); + let destdir = tmp.path().join("dest"); + let tx_base = tmp.path().join("tx"); + std::fs::create_dir_all(rootfs.join("usr/sbin")).unwrap(); + std::fs::create_dir_all(rootfs.join("usr/bin")).unwrap(); + std::fs::create_dir_all(destdir.join("usr")).unwrap(); + std::fs::write(rootfs.join("usr/sbin/keep"), "keep-me").unwrap(); + std::fs::write(rootfs.join("usr/bin/keep"), "target-conflict").unwrap(); + std::os::unix::fs::symlink("bin", destdir.join("usr/sbin")).unwrap(); + + let remove_paths = vec!["usr/sbin".to_string()]; + let err = install_atomic(&destdir, &rootfs, &tx_base, &remove_paths, &[]).unwrap_err(); + assert!( + err.to_string() + .contains("Failed to replay relocated path into") + ); + + let restored = rootfs.join("usr/sbin").symlink_metadata().unwrap(); + assert!(restored.file_type().is_dir()); + assert_eq!( + std::fs::read_to_string(rootfs.join("usr/sbin/keep")).unwrap(), + "keep-me" + ); + assert_eq!( + std::fs::read_to_string(rootfs.join("usr/bin/keep")).unwrap(), + "target-conflict" + ); + } + #[test] fn keep_glob_matches_question_mark_and_not_path_separator() { assert!(glob_match_path(