Skip to content

Commit 9183ca7

Browse files
mdittmerjoshlf
authored andcommitted
[anneal][v2][exocrate] Add install fixup hook
Teach exocrate to run a one-time fixup after fresh archive extraction while preserving archive permission bits. The hook lets Anneal rewrite install-root placeholders in Lake trace files and restore the installed toolchain to read-only form before normal use. gherrit-pr-id: G4a5griqkxk56ry227bqqe3kfzp7s2dmz
1 parent fca6bd9 commit 9183ca7

2 files changed

Lines changed: 161 additions & 16 deletions

File tree

exocrate/src/lib.rs

Lines changed: 150 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,29 @@ pub enum Source {
186186
Local(PathBuf),
187187
}
188188

189+
/// Paths passed to an installation fixup hook.
190+
///
191+
/// The staging directory is the mutable, freshly extracted archive directory.
192+
/// The final directory is the stable path where the staging directory will be
193+
/// atomically published after the hook returns successfully.
194+
#[derive(Clone, Copy)]
195+
pub struct FixupPaths<'a> {
196+
staging_dir: &'a Path,
197+
final_dir: &'a Path,
198+
}
199+
200+
impl<'a> FixupPaths<'a> {
201+
/// The mutable directory containing freshly extracted archive contents.
202+
pub fn staging_dir(&self) -> &'a Path {
203+
self.staging_dir
204+
}
205+
206+
/// The stable installation path that will exist after publication.
207+
pub fn final_dir(&self) -> &'a Path {
208+
self.final_dir
209+
}
210+
}
211+
189212
impl Config {
190213
/// Resolves the dependency directory, failing if it doesn't exist.
191214
pub fn resolve_installation_dir(&self, location: Location) -> IoResult<PathBuf> {
@@ -199,13 +222,29 @@ impl Config {
199222
&self,
200223
location: Location,
201224
source: Source,
225+
) -> IoResult<PathBuf> {
226+
self.resolve_installation_dir_or_install_with_fixup(location, source, |_| Ok(()))
227+
}
228+
229+
/// Resolves the dependency directory, installing and fixing it up if needed.
230+
///
231+
/// The `fixup` hook runs once after a fresh archive extraction, but before
232+
/// the managed directory is published. Mutate files through
233+
/// [`FixupPaths::staging_dir`]; use [`FixupPaths::final_dir`] only for
234+
/// fixups that need to write stable install-root references into those
235+
/// staged files.
236+
pub fn resolve_installation_dir_or_install_with_fixup(
237+
&self,
238+
location: Location,
239+
source: Source,
240+
fixup: impl FnOnce(FixupPaths<'_>) -> IoResult<()>,
202241
) -> IoResult<PathBuf> {
203242
let dir_path = self.dir_path(location);
204243
if ManagedDirName::new(&dir_path).check_exists().is_ok() {
205244
return Ok(dir_path);
206245
}
207246
let (reader, expected_sha) = self.open_source(source)?;
208-
install(reader, &dir_path, expected_sha)?;
247+
install_with_fixup(reader, &dir_path, expected_sha, fixup)?;
209248
Ok(dir_path)
210249
}
211250

@@ -268,7 +307,17 @@ impl Config {
268307

269308
/// Extracts the `.tar.zst` from `reader` and installs it at `dst`, optionally
270309
/// validating its hash.
310+
#[cfg(test)]
271311
fn install(mut reader: impl Read, dst: &Path, expected_sha256: Option<[u8; 32]>) -> IoResult<()> {
312+
install_with_fixup(&mut reader, dst, expected_sha256, |_| Ok(()))
313+
}
314+
315+
fn install_with_fixup(
316+
mut reader: impl Read,
317+
dst: &Path,
318+
expected_sha256: Option<[u8; 32]>,
319+
fixup: impl FnOnce(FixupPaths<'_>) -> IoResult<()>,
320+
) -> IoResult<()> {
272321
struct HashingReader<R> {
273322
reader: R,
274323
hasher: sha2::Sha256,
@@ -282,14 +331,20 @@ fn install(mut reader: impl Read, dst: &Path, expected_sha256: Option<[u8; 32]>)
282331
}
283332
}
284333

285-
sync::ManagedDirName::new(dst)
286-
.check_exists_or_create(|target_dir| {
334+
fn unpack_tar_zst(reader: impl Read, target_dir: &Path) -> IoResult<()> {
335+
let decoder = zstd::stream::read::Decoder::new(reader)?;
336+
let mut archive = tar::Archive::new(decoder);
337+
archive.set_mask(0);
338+
archive.set_preserve_permissions(true);
339+
archive.unpack(target_dir)
340+
}
341+
342+
let (_dir, _created) =
343+
sync::ManagedDirName::new(dst).check_exists_or_create_with_status(|target_dir| {
287344
if let Some(expected) = expected_sha256 {
288345
let mut hash_reader = HashingReader { reader, hasher: sha2::Sha256::new() };
289346
{
290-
let decoder = zstd::stream::read::Decoder::new(&mut hash_reader)?;
291-
let mut archive = tar::Archive::new(decoder);
292-
archive.unpack(target_dir)?;
347+
unpack_tar_zst(&mut hash_reader, target_dir)?;
293348
}
294349

295350
// Ensure any remaining trailing bytes in the stream are read
@@ -308,13 +363,13 @@ fn install(mut reader: impl Read, dst: &Path, expected_sha256: Option<[u8; 32]>)
308363
));
309364
}
310365
} else {
311-
let decoder = zstd::stream::read::Decoder::new(&mut reader)?;
312-
let mut archive = tar::Archive::new(decoder);
313-
archive.unpack(target_dir)?;
366+
unpack_tar_zst(&mut reader, target_dir)?;
314367
}
368+
fixup(FixupPaths { staging_dir: target_dir, final_dir: dst })?;
315369
Ok(())
316-
})
317-
.map(|_| ())
370+
})?;
371+
372+
Ok(())
318373
}
319374

320375
/// Parses a [`RemoteArchive`] from the `Cargo.toml` at `$cargo_toml_path`.
@@ -545,13 +600,19 @@ mod tests {
545600
use super::*;
546601

547602
fn create_dummy_tar_zst(files: &[(&str, &[u8])]) -> Vec<u8> {
603+
let files =
604+
files.iter().map(|(name, content)| (*name, *content, 0o644)).collect::<Vec<_>>();
605+
create_dummy_tar_zst_with_modes(&files)
606+
}
607+
608+
fn create_dummy_tar_zst_with_modes(files: &[(&str, &[u8], u32)]) -> Vec<u8> {
548609
let mut zstd_enc = zstd::stream::write::Encoder::new(Vec::new(), 0).unwrap();
549610
{
550611
let mut tar_builder = tar::Builder::new(&mut zstd_enc);
551-
for (name, content) in files {
612+
for (name, content, mode) in files {
552613
let mut header = tar::Header::new_gnu();
553614
header.set_size(content.len() as u64);
554-
header.set_mode(0o644);
615+
header.set_mode(*mode);
555616
header.set_cksum();
556617
tar_builder.append_data(&mut header, name, *content).unwrap();
557618
}
@@ -595,6 +656,82 @@ mod tests {
595656
assert_eq!(fs::read(dst.join("nested/dir/data.bin")).unwrap(), b"\x01\x02\x03");
596657
}
597658

659+
#[test]
660+
fn test_install_with_fixup() {
661+
let temp = tempfile::tempdir().unwrap();
662+
let dst = temp.path().join("install_target");
663+
let tar_zst = create_dummy_tar_zst(&[("hello.txt", b"hello world")]);
664+
665+
install_with_fixup(tar_zst.as_slice(), &dst, None, |paths| {
666+
assert_ne!(paths.staging_dir(), paths.final_dir());
667+
assert!(
668+
!paths.final_dir().exists(),
669+
"final directory should not be visible until fixup completes"
670+
);
671+
fs::write(paths.staging_dir().join("fixed.txt"), "fixed")
672+
})
673+
.unwrap();
674+
675+
assert_eq!(fs::read_to_string(dst.join("hello.txt")).unwrap(), "hello world");
676+
assert_eq!(fs::read_to_string(dst.join("fixed.txt")).unwrap(), "fixed");
677+
}
678+
679+
#[test]
680+
fn test_install_fixup_not_rerun_for_existing_dir() {
681+
let temp = tempfile::tempdir().unwrap();
682+
let dst = temp.path().join("install_target");
683+
fs::create_dir_all(&dst).unwrap();
684+
fs::write(dst.join("existing.txt"), "existing content").unwrap();
685+
686+
let bad_data = b"invalid archive data";
687+
install_with_fixup(&bad_data[..], &dst, None, |_| {
688+
panic!("fixup should not run for an existing installation");
689+
})
690+
.unwrap();
691+
692+
assert_eq!(fs::read_to_string(dst.join("existing.txt")).unwrap(), "existing content");
693+
}
694+
695+
#[test]
696+
fn test_install_fixup_failure_removes_installation() {
697+
let temp = tempfile::tempdir().unwrap();
698+
let dst = temp.path().join("install_target");
699+
let tar_zst = create_dummy_tar_zst(&[("hello.txt", b"hello world")]);
700+
701+
let result = install_with_fixup(tar_zst.as_slice(), &dst, None, |_| {
702+
Err(std::io::Error::new(std::io::ErrorKind::Other, "fixup failed"))
703+
});
704+
705+
assert!(result.is_err());
706+
assert!(!dst.exists());
707+
let entries: Vec<_> = fs::read_dir(temp.path())
708+
.unwrap()
709+
.map(|e| e.unwrap().file_name())
710+
.filter(|n| n != "install_target.lock")
711+
.collect();
712+
assert!(entries.is_empty());
713+
}
714+
715+
#[cfg(unix)]
716+
#[test]
717+
fn test_install_preserves_read_only_permissions() {
718+
use std::os::unix::fs::PermissionsExt as _;
719+
720+
let temp = tempfile::tempdir().unwrap();
721+
let dst = temp.path().join("install_target");
722+
let tar_zst = create_dummy_tar_zst_with_modes(&[
723+
("readonly.txt", b"locked", 0o444),
724+
("bin/tool", b"tool", 0o555),
725+
]);
726+
727+
install(tar_zst.as_slice(), &dst, None).unwrap();
728+
729+
let readonly_mode = fs::metadata(dst.join("readonly.txt")).unwrap().permissions().mode();
730+
let tool_mode = fs::metadata(dst.join("bin/tool")).unwrap().permissions().mode();
731+
assert_eq!(readonly_mode & 0o777, 0o444);
732+
assert_eq!(tool_mode & 0o777, 0o555);
733+
}
734+
598735
#[test]
599736
fn test_install_without_hash_validation() {
600737
let temp = tempfile::tempdir().unwrap();

exocrate/src/sync.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,20 @@ impl<'a> ManagedDirName<'a> {
6666
///
6767
/// `check_exists_or_create` is **not** concurrency-safe if multiple
6868
/// concurrent calls are made *from the same process*.
69+
#[cfg(test)]
6970
pub(crate) fn check_exists_or_create(
7071
self,
7172
populate: impl FnOnce(&Path) -> IoResult<()>,
7273
) -> IoResult<ManagedDir<'a>> {
74+
self.check_exists_or_create_with_status(populate).map(|(dir, _created)| dir)
75+
}
76+
77+
pub(crate) fn check_exists_or_create_with_status(
78+
self,
79+
populate: impl FnOnce(&Path) -> IoResult<()>,
80+
) -> IoResult<(ManagedDir<'a>, bool)> {
7381
if let Ok(dir) = self.check_exists() {
74-
return Ok(dir);
82+
return Ok((dir, false));
7583
}
7684

7785
if let Some(parent) = self.path.parent() {
@@ -117,7 +125,7 @@ impl<'a> ManagedDirName<'a> {
117125
// another process populated the directory.
118126
if let Ok(dir) = self.check_exists() {
119127
guard.completed = true;
120-
return Ok(dir);
128+
return Ok((dir, false));
121129
}
122130

123131
// Clean up from any previous failed attempt to populate the directory.
@@ -142,7 +150,7 @@ impl<'a> ManagedDirName<'a> {
142150
})?;
143151

144152
guard.completed = true;
145-
Ok(ManagedDir { path: self.path })
153+
Ok((ManagedDir { path: self.path }, true))
146154
}
147155

148156
fn staging(&self) -> PathBuf {

0 commit comments

Comments
 (0)