Skip to main content

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;