use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShimPaths {
pub rustc_shim: PathBuf,
pub linker_shim: PathBuf,
}
pub fn expected_shim_paths(workspace_root: &Path) -> ShimPaths {
let target_dir = std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| workspace_root.join("target"));
let bin = |name: &str| target_dir.join("debug").join(exe_name(name));
ShimPaths {
rustc_shim: bin("whisker-rustc-shim"),
linker_shim: bin("whisker-linker-shim"),
}
}
pub fn exe_name(name: &str) -> String {
if cfg!(windows) {
format!("{name}.exe")
} else {
name.to_string()
}
}
pub fn resolve_shim_paths(workspace_root: &Path) -> Result<ShimPaths> {
let paths = expected_shim_paths(workspace_root);
if paths.rustc_shim.is_file() && paths.linker_shim.is_file() {
return Ok(paths);
}
build_shims(workspace_root).context("build whisker-cli shim binaries")?;
let paths = expected_shim_paths(workspace_root);
anyhow::ensure!(
paths.rustc_shim.is_file(),
"expected `{}` to exist after `cargo build` of the shims",
paths.rustc_shim.display(),
);
anyhow::ensure!(
paths.linker_shim.is_file(),
"expected `{}` to exist after `cargo build` of the shims",
paths.linker_shim.display(),
);
Ok(paths)
}
fn build_shims(workspace_root: &Path) -> Result<()> {
let step = whisker_build::ui::step("setup", "whisker-cli shims");
let mut cmd = std::process::Command::new("cargo");
cmd.args([
"build",
"-p",
"whisker-cli",
"--bin",
"whisker-rustc-shim",
"--bin",
"whisker-linker-shim",
])
.current_dir(workspace_root);
let status = step.pipe(&mut cmd).context("spawn cargo")?;
if !status.success() {
step.fail(format!("{status}"));
anyhow::bail!("cargo exited {status}");
}
step.done("");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exe_name_appends_dot_exe_on_windows_otherwise_passes_through() {
let name = exe_name("foo");
if cfg!(windows) {
assert_eq!(name, "foo.exe");
} else {
assert_eq!(name, "foo");
}
}
#[test]
fn expected_paths_default_to_workspace_target_debug() {
let p = expected_shim_paths(Path::new("/tmp/ws"));
let rustc_basename = p.rustc_shim.file_name().and_then(|n| n.to_str()).unwrap();
let linker_basename = p.linker_shim.file_name().and_then(|n| n.to_str()).unwrap();
assert_eq!(rustc_basename, exe_name("whisker-rustc-shim"));
assert_eq!(linker_basename, exe_name("whisker-linker-shim"));
assert!(
p.rustc_shim.parent().unwrap().ends_with("debug"),
"expected …/debug/, got {}",
p.rustc_shim.display(),
);
assert!(p.linker_shim.parent().unwrap().ends_with("debug"));
}
#[test]
fn resolve_returns_existing_paths_without_rebuilding() {
let dir = unique_tempdir();
let target = dir.join("target");
std::fs::create_dir_all(target.join("debug")).unwrap();
let rustc = target.join("debug").join(exe_name("whisker-rustc-shim"));
let linker = target.join("debug").join(exe_name("whisker-linker-shim"));
std::fs::write(&rustc, b"#!/bin/sh\nexit 0\n").unwrap();
std::fs::write(&linker, b"#!/bin/sh\nexit 0\n").unwrap();
let prev = std::env::var_os("CARGO_TARGET_DIR");
std::env::set_var("CARGO_TARGET_DIR", &target);
let result = resolve_shim_paths(&dir);
match prev {
Some(p) => std::env::set_var("CARGO_TARGET_DIR", p),
None => std::env::remove_var("CARGO_TARGET_DIR"),
}
let paths = result.expect("resolve");
assert_eq!(paths.rustc_shim, rustc);
assert_eq!(paths.linker_shim, linker);
let _ = std::fs::remove_dir_all(&dir);
}
fn unique_tempdir() -> PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let n = SEQ.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let p = std::env::temp_dir().join(format!("whisker-shim-paths-test-{pid}-{n}"));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
p
}
}