//! `flow deps check` (#101): whether every crate a `Cargo.lock` needs is //! vendored in the build image at that version. Jobs run with no network, //! so a crate the image lacks fails the run with "no matching package"; //! this says so before the push, naming the versions the image does have. use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; /// A crate at a version. pub type Crate = (String, String); /// The packages of a `Cargo.lock` that come from somewhere (a registry or /// git): the ones `cargo vendor` copies. A workspace's own members have no /// `source` and are left out. pub fn from_lock(text: &str) -> Result, String> { let lock: toml::Value = text.parse().map_err(|e| format!("not a Cargo.lock: {e}"))?; let mut out = BTreeSet::new(); for p in lock.get("package").and_then(|p| p.as_array()).into_iter().flatten() { let (Some(name), Some(version)) = (p.get("name").and_then(|v| v.as_str()), p.get("version").and_then(|v| v.as_str())) else { continue; }; if p.get("source").is_some() { out.insert((name.to_string(), version.to_string())); } } Ok(out) } /// A crate list as `flow deps list` and the image's `crates.txt` write it: /// `NAME VERSION` a line; blank lines and `#` comments are skipped. pub fn from_list(text: &str) -> BTreeSet { text.lines() .map(str::trim) .filter(|l| !l.is_empty() && !l.starts_with('#')) .filter_map(|l| { let mut w = l.split_whitespace(); Some((w.next()?.to_string(), w.next()?.to_string())) }) .collect() } /// The crates of a `cargo vendor` directory (the image's `/opt/vendor`): /// each subdirectory's `Cargo.toml` names its package and version. pub fn from_vendor(dir: &Path) -> Result, String> { let entries = std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))?; let mut out = BTreeSet::new(); for e in entries.flatten() { let manifest = e.path().join("Cargo.toml"); let Ok(text) = std::fs::read_to_string(&manifest) else { continue }; let Ok(v) = text.parse::() else { continue }; let pkg = v.get("package"); let field = |k: &str| pkg.and_then(|p| p.get(k)).and_then(|x| x.as_str()).map(str::to_string); if let (Some(n), Some(ver)) = (field("name"), field("version")) { out.insert((n, ver)); } } Ok(out) } /// What `need` lacks from `have`, each with the versions of that crate /// `have` does hold (empty when it holds none). pub fn missing(need: &BTreeSet, have: &BTreeSet) -> Vec<(Crate, Vec)> { let mut versions: BTreeMap<&str, Vec> = BTreeMap::new(); for (n, v) in have { versions.entry(n.as_str()).or_default().push(v.clone()); } need.iter().filter(|c| !have.contains(*c)).map(|c| (c.clone(), versions.get(c.0.as_str()).cloned().unwrap_or_default())).collect() } /// A crate list in the form [`from_list`] reads. pub fn render_list(crates: &BTreeSet) -> String { crates.iter().map(|(n, v)| format!("{n} {v}\n")).collect() } #[cfg(test)] mod tests { use super::*; const LOCK: &str = r#" version = 4 [[package]] name = "mine" version = "0.1.0" dependencies = ["rustls"] [[package]] name = "rustls" version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "tokio-rustls" version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" "#; #[test] fn names_what_the_image_lacks_and_what_it_has_instead() { let need = from_lock(LOCK).unwrap(); assert_eq!(need.len(), 3, "the workspace's own package is not vendored"); let have = from_list("# the image\nrustls 0.23.45\ntokio-rustls 0.26.4\n\n"); let m = missing(&need, &have); assert_eq!( m, vec![(("deranged".into(), "0.5.8".into()), vec![]), (("tokio-rustls".into(), "0.26.5".into()), vec!["0.26.4".into()]),] ); assert!(missing(&need, &need).is_empty()); assert_eq!(from_list(&render_list(&need)), need); assert!(from_lock("not = [toml").is_err()); } #[test] fn reads_a_vendor_directory() { let dir = tempfile::tempdir().unwrap(); 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")] { std::fs::create_dir(dir.path().join(d)).unwrap(); std::fs::write(dir.path().join(d).join("Cargo.toml"), format!("[package]\nname = \"{n}\"\nversion = \"{v}\"\n")).unwrap(); } std::fs::create_dir(dir.path().join("junk")).unwrap(); let got = from_vendor(dir.path()).unwrap(); assert_eq!(got, from_list("rustls 0.23.45\npre 1.0.0-rc.1\n")); } }