Skip to main content

release_kit/projection/
evidence.rs

1//! Gathering the target evidence a projection consumes.
2//!
3//! The one place on the projection path that reads a target. It runs
4//! before [`Projection::compute`](super::Projection::compute) and returns
5//! values, so the projection itself stays pure and a source scan can hold
6//! `src/projection.rs` to that boundary without excluding this file's
7//! reads by name.
8
9use std::collections::BTreeMap;
10
11use camino::Utf8Path;
12
13use super::{BLOCK_DESTINATIONS, CrateShape, TargetEvidence};
14use crate::landing::manifest::Manifest;
15
16impl TargetEvidence {
17    /// The evidence at `target`, read once, with `recorded` answering
18    /// whether the receipt already names the flake pair.
19    ///
20    /// # Errors
21    ///
22    /// Any read failure other than a file being absent.
23    pub fn gather(target: &Utf8Path, recorded: Option<&Manifest>) -> std::io::Result<Self> {
24        gather(target, recorded)
25    }
26}
27
28/// The evidence at `target`: every block destination's existing document,
29/// the crate shape, the flake pair's presence, and whether `recorded`
30/// names `flake.nix`.
31///
32/// # Errors
33///
34/// Any read failure other than a file being absent.
35pub fn gather(target: &Utf8Path, recorded: Option<&Manifest>) -> std::io::Result<TargetEvidence> {
36    let mut documents = BTreeMap::new();
37    for destination in BLOCK_DESTINATIONS {
38        if let Some(bytes) = read_optional(&target.join(destination))? {
39            documents.insert(destination.to_owned(), bytes);
40        }
41    }
42    let (flake_nix_present, flake_lock_present) = flake_presence(target)?;
43    Ok(TargetEvidence {
44        documents,
45        crate_shape: crate_shape(target),
46        flake_nix_present,
47        flake_lock_present,
48        flake_recorded: flake_recorded(recorded),
49        root_pipeline_present: present(target, super::GITLAB_ROOT_PIPELINE)?,
50        root_pipeline_recorded: recorded
51            .is_some_and(|record| record.file(super::GITLAB_ROOT_PIPELINE).is_some()),
52    })
53}
54
55/// Whether the receipt names `flake.nix`: a pair release-kit landed is its
56/// own and is never withheld.
57#[must_use]
58pub fn flake_recorded(recorded: Option<&Manifest>) -> bool {
59    recorded.is_some_and(|record| record.file("flake.nix").is_some())
60}
61
62/// The crate facts at `target` the Nix seed relies on. An unreadable
63/// `Cargo.toml` is `None`, which the judgment names.
64#[must_use]
65pub fn crate_shape(target: &Utf8Path) -> CrateShape {
66    CrateShape {
67        cargo_toml: std::fs::read_to_string(target.join("Cargo.toml")).ok(),
68        cargo_lock: target.join("Cargo.lock").is_file(),
69        main_rs: target.join("src/main.rs").is_file(),
70    }
71}
72
73/// Whether `flake.nix` and `flake.lock` are present at `target`, a link
74/// or any other entry counted as present: a seed lock beside anything
75/// bearing that name describes the wrong input graph.
76///
77/// # Errors
78///
79/// Any read failure other than the entry being absent.
80pub fn flake_presence(target: &Utf8Path) -> std::io::Result<(bool, bool)> {
81    Ok((
82        present(target, "flake.nix")?,
83        present(target, "flake.lock")?,
84    ))
85}
86
87/// Whether `name` is present at `target`, a link or any other entry
88/// counted as present.
89fn present(target: &Utf8Path, name: &str) -> std::io::Result<bool> {
90    match std::fs::symlink_metadata(target.join(name).as_std_path()) {
91        Ok(_) => Ok(true),
92        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
93        Err(e) => Err(e),
94    }
95}
96
97/// The bytes at `path`, or `None` where no file exists.
98fn read_optional(path: &Utf8Path) -> std::io::Result<Option<Vec<u8>>> {
99    match std::fs::read(path) {
100        Ok(bytes) => Ok(Some(bytes)),
101        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
102        Err(e) => Err(e),
103    }
104}