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    })
50}
51
52/// Whether the receipt names `flake.nix`: a pair release-kit landed is its
53/// own and is never withheld.
54#[must_use]
55pub fn flake_recorded(recorded: Option<&Manifest>) -> bool {
56    recorded.is_some_and(|record| record.file("flake.nix").is_some())
57}
58
59/// The crate facts at `target` the Nix seed relies on. An unreadable
60/// `Cargo.toml` is `None`, which the judgment names.
61#[must_use]
62pub fn crate_shape(target: &Utf8Path) -> CrateShape {
63    CrateShape {
64        cargo_toml: std::fs::read_to_string(target.join("Cargo.toml")).ok(),
65        cargo_lock: target.join("Cargo.lock").is_file(),
66        main_rs: target.join("src/main.rs").is_file(),
67    }
68}
69
70/// Whether `flake.nix` and `flake.lock` are present at `target`, a link
71/// or any other entry counted as present: a seed lock beside anything
72/// bearing that name describes the wrong input graph.
73///
74/// # Errors
75///
76/// Any read failure other than the entry being absent.
77pub fn flake_presence(target: &Utf8Path) -> std::io::Result<(bool, bool)> {
78    let present = |name: &str| match std::fs::symlink_metadata(target.join(name).as_std_path()) {
79        Ok(_) => Ok(true),
80        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
81        Err(e) => Err(e),
82    };
83    Ok((present("flake.nix")?, present("flake.lock")?))
84}
85
86/// The bytes at `path`, or `None` where no file exists.
87fn read_optional(path: &Utf8Path) -> std::io::Result<Option<Vec<u8>>> {
88    match std::fs::read(path) {
89        Ok(bytes) => Ok(Some(bytes)),
90        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
91        Err(e) => Err(e),
92    }
93}