OXYGENHUB
◆ v0.5.3 oxygenflow / crates / flow / src / deps.rs
4.8 KiB · Rust · last changed flow deps check: is every crate of a Cargo.lock in the image (closes #101) 1 day ago source blame history raw
f3b5b86e Andy McCall ↶ 1
//! `flow deps check` (#101): whether every crate a `Cargo.lock` needs is
2
//! vendored in the build image at that version. Jobs run with no network,
3
//! so a crate the image lacks fails the run with "no matching package";
4
//! this says so before the push, naming the versions the image does have.
5
6
use std::collections::{BTreeMap, BTreeSet};
7
use std::path::Path;
8
9
/// A crate at a version.
10
pub type Crate = (String, String);
11
12
/// The packages of a `Cargo.lock` that come from somewhere (a registry or
13
/// git): the ones `cargo vendor` copies. A workspace's own members have no
14
/// `source` and are left out.
15
pub fn from_lock(text: &str) -> Result<BTreeSet<Crate>, String> {
16
    let lock: toml::Value = text.parse().map_err(|e| format!("not a Cargo.lock: {e}"))?;
17
    let mut out = BTreeSet::new();
18
    for p in lock.get("package").and_then(|p| p.as_array()).into_iter().flatten() {
19
        let (Some(name), Some(version)) = (p.get("name").and_then(|v| v.as_str()), p.get("version").and_then(|v| v.as_str())) else {
20
            continue;
21
        };
22
        if p.get("source").is_some() {
23
            out.insert((name.to_string(), version.to_string()));
24
        }
25
    }
26
    Ok(out)
27
}
28
29
/// A crate list as `flow deps list` and the image's `crates.txt` write it:
30
/// `NAME VERSION` a line; blank lines and `#` comments are skipped.
31
pub fn from_list(text: &str) -> BTreeSet<Crate> {
32
    text.lines()
33
        .map(str::trim)
34
        .filter(|l| !l.is_empty() && !l.starts_with('#'))
35
        .filter_map(|l| {
36
            let mut w = l.split_whitespace();
37
            Some((w.next()?.to_string(), w.next()?.to_string()))
38
        })
39
        .collect()
40
}
41
42
/// The crates of a `cargo vendor` directory (the image's `/opt/vendor`):
43
/// each subdirectory's `Cargo.toml` names its package and version.
44
pub fn from_vendor(dir: &Path) -> Result<BTreeSet<Crate>, String> {
45
    let entries = std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
46
    let mut out = BTreeSet::new();
47
    for e in entries.flatten() {
48
        let manifest = e.path().join("Cargo.toml");
49
        let Ok(text) = std::fs::read_to_string(&manifest) else { continue };
50
        let Ok(v) = text.parse::<toml::Value>() else { continue };
51
        let pkg = v.get("package");
52
        let field = |k: &str| pkg.and_then(|p| p.get(k)).and_then(|x| x.as_str()).map(str::to_string);
53
        if let (Some(n), Some(ver)) = (field("name"), field("version")) {
54
            out.insert((n, ver));
55
        }
56
    }
57
    Ok(out)
58
}
59
60
/// What `need` lacks from `have`, each with the versions of that crate
61
/// `have` does hold (empty when it holds none).
62
pub fn missing(need: &BTreeSet<Crate>, have: &BTreeSet<Crate>) -> Vec<(Crate, Vec<String>)> {
63
    let mut versions: BTreeMap<&str, Vec<String>> = BTreeMap::new();
64
    for (n, v) in have {
65
        versions.entry(n.as_str()).or_default().push(v.clone());
66
    }
67
    need.iter().filter(|c| !have.contains(*c)).map(|c| (c.clone(), versions.get(c.0.as_str()).cloned().unwrap_or_default())).collect()
68
}
69
70
/// A crate list in the form [`from_list`] reads.
71
pub fn render_list(crates: &BTreeSet<Crate>) -> String {
72
    crates.iter().map(|(n, v)| format!("{n} {v}\n")).collect()
73
}
74
75
#[cfg(test)]
76
mod tests {
77
    use super::*;
78
79
    const LOCK: &str = r#"
80
version = 4
81
82
[[package]]
83
name = "mine"
84
version = "0.1.0"
85
dependencies = ["rustls"]
86
87
[[package]]
88
name = "rustls"
89
version = "0.23.45"
90
source = "registry+https://github.com/rust-lang/crates.io-index"
91
92
[[package]]
93
name = "tokio-rustls"
94
version = "0.26.5"
95
source = "registry+https://github.com/rust-lang/crates.io-index"
96
97
[[package]]
98
name = "deranged"
99
version = "0.5.8"
100
source = "registry+https://github.com/rust-lang/crates.io-index"
101
"#;
102
103
    #[test]
104
    fn names_what_the_image_lacks_and_what_it_has_instead() {
105
        let need = from_lock(LOCK).unwrap();
106
        assert_eq!(need.len(), 3, "the workspace's own package is not vendored");
107
        let have = from_list("# the image\nrustls 0.23.45\ntokio-rustls 0.26.4\n\n");
108
        let m = missing(&need, &have);
109
        assert_eq!(
110
            m,
111
            vec![(("deranged".into(), "0.5.8".into()), vec![]), (("tokio-rustls".into(), "0.26.5".into()), vec!["0.26.4".into()]),]
112
        );
113
        assert!(missing(&need, &need).is_empty());
114
        assert_eq!(from_list(&render_list(&need)), need);
115
        assert!(from_lock("not = [toml").is_err());
116
    }
117
118
    #[test]
119
    fn reads_a_vendor_directory() {
120
        let dir = tempfile::tempdir().unwrap();
121
        for (d, n, v) in [("rustls-0.23.45", "rustls", "0.23.45"), ("pre-1.0.0-rc.1", "pre", "1.0.0-rc.1")] {
122
            std::fs::create_dir(dir.path().join(d)).unwrap();
123
            std::fs::write(dir.path().join(d).join("Cargo.toml"), format!("[package]\nname = \"{n}\"\nversion = \"{v}\"\n")).unwrap();
124
        }
125
        std::fs::create_dir(dir.path().join("junk")).unwrap();
126
        let got = from_vendor(dir.path()).unwrap();
127
        assert_eq!(got, from_list("rustls 0.23.45\npre 1.0.0-rc.1\n"));
128
    }
129
}