use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
const CARGO_UNRESOLVED_DEP: &str = "error: failed to get `velox-core` as a dependency of package";
#[derive(Debug)]
enum Pin {
Path(String),
Git { url: String, rev: String },
}
impl Pin {
fn crate_name_of(line: &str) -> Option<&str> {
let (name, rest) = line.split_once(" = {")?;
if name.starts_with("velox-") || name == "veloxc" {
Some(name)
} else {
let _ = rest;
None
}
}
}
fn velox_pins(cargo_toml: &str) -> Vec<(String, Pin)> {
let mut out = Vec::new();
for raw in cargo_toml.lines() {
let line = raw.trim();
let Some(name) = Pin::crate_name_of(line) else {
continue;
};
let field = |key: &str| -> Option<String> {
let needle = format!("{key} = \"");
let start = line.find(&needle)? + needle.len();
let rest = &line[start..];
let end = rest.find('"')?;
Some(rest[..end].to_string())
};
let pin = if let Some(p) = field("path") {
Pin::Path(p)
} else if let Some(url) = field("git") {
let rev = field("rev").unwrap_or_default();
Pin::Git { url, rev }
} else {
continue;
};
out.push((name.to_string(), pin));
}
out
}
struct Scaffold {
project_dir: PathBuf,
cargo_toml: String,
stdout: String,
}
impl Scaffold {
fn init_outside_the_checkout(tag: &str, extra_env: &[(&str, &str)]) -> Self {
let scratch =
std::env::temp_dir().join(format!("velox-dep-src-{}-{}", std::process::id(), tag));
let _ = fs::remove_dir_all(&scratch);
fs::create_dir_all(&scratch).expect("create scratch dir");
assert!(
!Self::velox_checkout()
.starts_with(fs::canonicalize(&scratch).unwrap_or(scratch.clone())),
"the scratch dir must live outside the velox checkout or `init` finds \
path deps by walking up from the cwd and the git pin is never exercised"
);
let mut cmd = Command::new(env!("CARGO_BIN_EXE_veloxc"));
cmd.arg("init")
.arg("myapp")
.current_dir(&scratch)
.env_remove("VELOX_PATH")
.env_remove("VELOX_DEP_SOURCE");
for (k, v) in extra_env {
cmd.env(k, v);
}
let out = cmd.output().expect("run velox init");
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
assert!(
out.status.success(),
"velox init failed outside the checkout: {}\n--- stdout ---\n{stdout}",
String::from_utf8_lossy(&out.stderr)
);
let project_dir = scratch.join("myapp");
let cargo_toml =
fs::read_to_string(project_dir.join("Cargo.toml")).expect("init wrote a Cargo.toml");
Self {
project_dir,
cargo_toml,
stdout,
}
}
fn velox_checkout() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("veloxc has a parent")
.to_path_buf()
}
}
impl Drop for Scaffold {
fn drop(&mut self) {
if let Some(scratch) = self.project_dir.parent() {
let _ = fs::remove_dir_all(scratch);
}
}
}
fn git_remote_refs_resolve(rev: &str) -> Result<bool, String> {
let checkout = Scaffold::velox_checkout();
let out = Command::new("git")
.arg("-C")
.arg(&checkout)
.args(["branch", "-r", "--contains", rev])
.output()
.map_err(|e| format!("could not run git in {}: {e}", checkout.display()))?;
if !out.status.success() {
return Err(format!(
"git branch -r --contains {rev} failed: {}",
String::from_utf8_lossy(&out.stderr).trim()
));
}
let refs = String::from_utf8_lossy(&out.stdout);
Ok(refs.lines().any(|l| !l.trim().is_empty()))
}
#[test]
fn scaffolded_velox_dependencies_are_resolvable() {
let s = Scaffold::init_outside_the_checkout("repro", &[]);
let pins = velox_pins(&s.cargo_toml);
assert!(
pins.len() >= 5,
"expected all five velox dependency crates (velox-core, velox-dom, \
velox-style, velox-renderer, veloxc) to be pinned, found {} in:\n{}",
pins.len(),
s.cargo_toml
);
let mut unresolvable = Vec::new();
for (name, pin) in &pins {
match pin {
Pin::Path(rel) => {
let dir = s.project_dir.join(rel);
let manifest = dir.join("Cargo.toml");
if !manifest.is_file() {
unresolvable.push(format!(
"{name}: path = \"{rel}\" -> {} is not a crate directory \
(no Cargo.toml). Cargo would fail with \"{CARGO_UNRESOLVED_DEP}\".",
dir.display()
));
continue;
}
let text = fs::read_to_string(&manifest).unwrap_or_default();
let declared = text
.lines()
.find_map(|l| l.trim().strip_prefix("name"))
.and_then(|l| l.split('"').nth(1))
.unwrap_or("");
if declared != name {
unresolvable.push(format!(
"{name}: path = \"{rel}\" -> {} declares package name \
\"{declared}\", not \"{name}\".",
dir.display()
));
}
}
Pin::Git { url, rev } => match git_remote_refs_resolve(rev) {
Ok(true) => {}
Ok(false) => unresolvable.push(format!(
"{name}: git = \"{url}\", rev = \"{rev}\" — no remote branch of the \
velox repository contains that commit, so nobody can check it out. \
Cargo fails with \"{CARGO_UNRESOLVED_DEP}\". That rev is the commit \
the velox binary was built from, baked in by build.rs; if it was \
never pushed, every `velox init` on this machine is unbuildable."
)),
Err(e) => panic!("{e}"),
},
}
}
assert!(
unresolvable.is_empty(),
"`velox init` produced a Cargo.toml whose dependencies cannot be resolved — \
{} of {} pins point at nothing:\n {}\n--- generated Cargo.toml ---\n{}\n\
--- init stdout ---\n{}",
unresolvable.len(),
pins.len(),
unresolvable.join("\n "),
s.cargo_toml,
s.stdout,
);
}