Skip to content

Commit 9e4732e

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 4948425 commit 9e4732e

2 files changed

Lines changed: 131 additions & 16 deletions

File tree

exocrate/src/lib.rs

Lines changed: 120 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,27 @@ impl Config {
199199
&self,
200200
location: Location,
201201
source: Source,
202+
) -> IoResult<PathBuf> {
203+
self.resolve_installation_dir_or_install_with_fixup(location, source, |_| Ok(()))
204+
}
205+
206+
/// Resolves the dependency directory, installing and fixing it up if needed.
207+
///
208+
/// The `fixup` hook runs once after a fresh archive extraction. It receives
209+
/// the installed exocrate directory path and can rewrite files, adjust
210+
/// permissions, or perform other environment-local repairs.
211+
pub fn resolve_installation_dir_or_install_with_fixup(
212+
&self,
213+
location: Location,
214+
source: Source,
215+
fixup: impl FnOnce(&Path) -> IoResult<()>,
202216
) -> IoResult<PathBuf> {
203217
let dir_path = self.dir_path(location);
204218
if ManagedDirName::new(&dir_path).check_exists().is_ok() {
205219
return Ok(dir_path);
206220
}
207221
let (reader, expected_sha) = self.open_source(source)?;
208-
install(reader, &dir_path, expected_sha)?;
222+
install_with_fixup(reader, &dir_path, expected_sha, fixup)?;
209223
Ok(dir_path)
210224
}
211225

@@ -268,7 +282,17 @@ impl Config {
268282

269283
/// Extracts the `.tar.zst` from `reader` and installs it at `dst`, optionally
270284
/// validating its hash.
285+
#[cfg(test)]
271286
fn install(mut reader: impl Read, dst: &Path, expected_sha256: Option<[u8; 32]>) -> IoResult<()> {
287+
install_with_fixup(&mut reader, dst, expected_sha256, |_| Ok(()))
288+
}
289+
290+
fn install_with_fixup(
291+
mut reader: impl Read,
292+
dst: &Path,
293+
expected_sha256: Option<[u8; 32]>,
294+
fixup: impl FnOnce(&Path) -> IoResult<()>,
295+
) -> IoResult<()> {
272296
struct HashingReader<R> {
273297
reader: R,
274298
hasher: sha2::Sha256,
@@ -282,14 +306,20 @@ fn install(mut reader: impl Read, dst: &Path, expected_sha256: Option<[u8; 32]>)
282306
}
283307
}
284308

285-
sync::ManagedDirName::new(dst)
286-
.check_exists_or_create(|target_dir| {
309+
fn unpack_tar_zst(reader: impl Read, target_dir: &Path) -> IoResult<()> {
310+
let decoder = zstd::stream::read::Decoder::new(reader)?;
311+
let mut archive = tar::Archive::new(decoder);
312+
archive.set_mask(0);
313+
archive.set_preserve_permissions(true);
314+
archive.unpack(target_dir)
315+
}
316+
317+
let (_dir, created) =
318+
sync::ManagedDirName::new(dst).check_exists_or_create_with_status(|target_dir| {
287319
if let Some(expected) = expected_sha256 {
288320
let mut hash_reader = HashingReader { reader, hasher: sha2::Sha256::new() };
289321
{
290-
let decoder = zstd::stream::read::Decoder::new(&mut hash_reader)?;
291-
let mut archive = tar::Archive::new(decoder);
292-
archive.unpack(target_dir)?;
322+
unpack_tar_zst(&mut hash_reader, target_dir)?;
293323
}
294324

295325
// Ensure any remaining trailing bytes in the stream are read
@@ -308,13 +338,19 @@ fn install(mut reader: impl Read, dst: &Path, expected_sha256: Option<[u8; 32]>)
308338
));
309339
}
310340
} else {
311-
let decoder = zstd::stream::read::Decoder::new(&mut reader)?;
312-
let mut archive = tar::Archive::new(decoder);
313-
archive.unpack(target_dir)?;
341+
unpack_tar_zst(&mut reader, target_dir)?;
314342
}
315343
Ok(())
316-
})
317-
.map(|_| ())
344+
})?;
345+
346+
if created {
347+
if let Err(e) = fixup(dst) {
348+
let _ = std::fs::remove_dir_all(dst);
349+
return Err(e);
350+
}
351+
}
352+
353+
Ok(())
318354
}
319355

320356
/// Parses a [`RemoteArchive`] from the `Cargo.toml` at `$cargo_toml_path`.
@@ -545,13 +581,19 @@ mod tests {
545581
use super::*;
546582

547583
fn create_dummy_tar_zst(files: &[(&str, &[u8])]) -> Vec<u8> {
584+
let files =
585+
files.iter().map(|(name, content)| (*name, *content, 0o644)).collect::<Vec<_>>();
586+
create_dummy_tar_zst_with_modes(&files)
587+
}
588+
589+
fn create_dummy_tar_zst_with_modes(files: &[(&str, &[u8], u32)]) -> Vec<u8> {
548590
let mut zstd_enc = zstd::stream::write::Encoder::new(Vec::new(), 0).unwrap();
549591
{
550592
let mut tar_builder = tar::Builder::new(&mut zstd_enc);
551-
for (name, content) in files {
593+
for (name, content, mode) in files {
552594
let mut header = tar::Header::new_gnu();
553595
header.set_size(content.len() as u64);
554-
header.set_mode(0o644);
596+
header.set_mode(*mode);
555597
header.set_cksum();
556598
tar_builder.append_data(&mut header, name, *content).unwrap();
557599
}
@@ -595,6 +637,71 @@ mod tests {
595637
assert_eq!(fs::read(dst.join("nested/dir/data.bin")).unwrap(), b"\x01\x02\x03");
596638
}
597639

640+
#[test]
641+
fn test_install_with_fixup() {
642+
let temp = tempfile::tempdir().unwrap();
643+
let dst = temp.path().join("install_target");
644+
let tar_zst = create_dummy_tar_zst(&[("hello.txt", b"hello world")]);
645+
646+
install_with_fixup(tar_zst.as_slice(), &dst, None, |dir| {
647+
fs::write(dir.join("fixed.txt"), "fixed")
648+
})
649+
.unwrap();
650+
651+
assert_eq!(fs::read_to_string(dst.join("hello.txt")).unwrap(), "hello world");
652+
assert_eq!(fs::read_to_string(dst.join("fixed.txt")).unwrap(), "fixed");
653+
}
654+
655+
#[test]
656+
fn test_install_fixup_not_rerun_for_existing_dir() {
657+
let temp = tempfile::tempdir().unwrap();
658+
let dst = temp.path().join("install_target");
659+
fs::create_dir_all(&dst).unwrap();
660+
fs::write(dst.join("existing.txt"), "existing content").unwrap();
661+
662+
let bad_data = b"invalid archive data";
663+
install_with_fixup(&bad_data[..], &dst, None, |_| {
664+
panic!("fixup should not run for an existing installation");
665+
})
666+
.unwrap();
667+
668+
assert_eq!(fs::read_to_string(dst.join("existing.txt")).unwrap(), "existing content");
669+
}
670+
671+
#[test]
672+
fn test_install_fixup_failure_removes_installation() {
673+
let temp = tempfile::tempdir().unwrap();
674+
let dst = temp.path().join("install_target");
675+
let tar_zst = create_dummy_tar_zst(&[("hello.txt", b"hello world")]);
676+
677+
let result = install_with_fixup(tar_zst.as_slice(), &dst, None, |_| {
678+
Err(std::io::Error::new(std::io::ErrorKind::Other, "fixup failed"))
679+
});
680+
681+
assert!(result.is_err());
682+
assert!(!dst.exists());
683+
}
684+
685+
#[cfg(unix)]
686+
#[test]
687+
fn test_install_preserves_read_only_permissions() {
688+
use std::os::unix::fs::PermissionsExt as _;
689+
690+
let temp = tempfile::tempdir().unwrap();
691+
let dst = temp.path().join("install_target");
692+
let tar_zst = create_dummy_tar_zst_with_modes(&[
693+
("readonly.txt", b"locked", 0o444),
694+
("bin/tool", b"tool", 0o555),
695+
]);
696+
697+
install(tar_zst.as_slice(), &dst, None).unwrap();
698+
699+
let readonly_mode = fs::metadata(dst.join("readonly.txt")).unwrap().permissions().mode();
700+
let tool_mode = fs::metadata(dst.join("bin/tool")).unwrap().permissions().mode();
701+
assert_eq!(readonly_mode & 0o777, 0o444);
702+
assert_eq!(tool_mode & 0o777, 0o555);
703+
}
704+
598705
#[test]
599706
fn test_install_without_hash_validation() {
600707
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)