ic_build/artifacts/staging/mod.rs
1//! Invocation-owned artifact staging without cache publication or reuse.
2//!
3//! Callers own the parent directory, recipes and publication boundary. This
4//! module allocates only its own child and removes it after the last owner is
5//! dropped. It never prunes existing artifacts or publishes a product release.
6
7use std::{
8 fs, io,
9 path::{Path, PathBuf},
10 sync::atomic::{AtomicU64, Ordering},
11};
12
13use ic_host_fs::{durable::write_bytes, read::read_file};
14
15use super::cache_fs::remove_path_if_present;
16
17const STAGE_ATTEMPTS: usize = 64;
18static STAGE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
19
20/// Temporary artifact directory retained until its owner is closed or dropped.
21///
22/// The caller keeps custody of the parent and allocated child against unrelated
23/// writers. Drop performs best-effort cleanup of this invocation-owned child;
24/// use [`Self::close`] when cleanup failures must reach the caller. No shared
25/// cache entry is created, reused or removed.
26#[derive(Debug)]
27pub struct ArtifactStagingDirectory {
28 root: PathBuf,
29 armed: bool,
30}
31
32impl ArtifactStagingDirectory {
33 /// Allocate a distinct staging child beneath an existing caller-owned directory.
34 ///
35 /// The parent is not created. Existing children are never reused or removed.
36 ///
37 /// # Errors
38 /// Returns the filesystem error for a missing or unwritable parent, or
39 /// `AlreadyExists` when every allocation attempt collides.
40 pub fn new(parent: &Path) -> io::Result<Self> {
41 for _ in 0..STAGE_ATTEMPTS {
42 let sequence = STAGE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
43 let root = parent.join(format!(
44 ".ic-build-artifact-stage-{}-{sequence}",
45 std::process::id()
46 ));
47 match fs::create_dir(&root) {
48 Ok(()) => return Ok(Self { root, armed: true }),
49 Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {}
50 Err(source) => return Err(source),
51 }
52 }
53 Err(io::Error::new(
54 io::ErrorKind::AlreadyExists,
55 format!(
56 "failed to allocate artifact staging beneath {}",
57 parent.display()
58 ),
59 ))
60 }
61
62 /// Borrow the owned staging directory through artifact production and consumption.
63 #[must_use]
64 pub fn path(&self) -> &Path {
65 &self.root
66 }
67
68 /// Remove this owned staging child, reporting cleanup failure without a hidden retry.
69 ///
70 /// On failure, the caller owns recovery of the remaining staging child.
71 /// It does not remove the parent or any other artifact directory.
72 ///
73 /// # Errors
74 /// Returns the filesystem error if removing the owned child fails.
75 pub fn close(mut self) -> io::Result<()> {
76 self.armed = false;
77 remove_path_if_present(&self.root)
78 }
79}
80
81impl Drop for ArtifactStagingDirectory {
82 fn drop(&mut self) {
83 if self.armed {
84 let _ = remove_path_if_present(&self.root);
85 }
86 }
87}
88
89/// Owned copy of one completed producer output, independent of later source replacement.
90///
91/// Keep this record alive through every use of [`Self::path`]. The copy is
92/// temporary rather than a reusable cache entry; dropping its owner removes it.
93#[derive(Debug)]
94pub struct CapturedArtifact {
95 staging: ArtifactStagingDirectory,
96 path: PathBuf,
97}
98
99impl CapturedArtifact {
100 /// Capture an output synchronously before another producer can overwrite it.
101 ///
102 /// The caller keeps the source stable during capture and selects the maximum
103 /// readable byte count. Read/publication failures retain their typed causes
104 /// inside the returned I/O error. Existing public destinations are untouched.
105 ///
106 /// # Errors
107 /// Returns the source read/byte-limit, staging allocation or durable-write
108 /// error. An unsuccessful capture performs best-effort staging cleanup.
109 pub fn capture(source: &Path, parent: &Path, maximum_bytes: usize) -> io::Result<Self> {
110 let bytes = read_file(source, maximum_bytes).map_err(io::Error::from)?;
111 let staging = ArtifactStagingDirectory::new(parent)?;
112 let path = staging.path().join("input.artifact");
113 write_bytes(&path, &bytes).map_err(io::Error::other)?;
114 Ok(Self { staging, path })
115 }
116
117 /// Borrow the exact captured bytes while this record owns their lifetime.
118 #[must_use]
119 pub fn path(&self) -> &Path {
120 &self.path
121 }
122
123 /// Remove the captured output and its owned staging child, reporting cleanup failure.
124 ///
125 /// # Errors
126 /// Returns the filesystem error if removing the owned child fails.
127 pub fn close(self) -> io::Result<()> {
128 self.staging.close()
129 }
130}
131
132#[cfg(test)]
133mod tests;