use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
pub const DIR_VAR: &str = "FGATE_SHIM_DIR";
fn file_name() -> String {
format!("cargo{}", std::env::consts::EXE_SUFFIX)
}
pub fn invoked_as_cargo() -> Option<PathBuf> {
let dir = std::env::var_os(DIR_VAR)?;
let me = std::env::current_exe().ok()?;
let stem = me.file_stem()?.to_string_lossy().to_lowercase();
if stem != "cargo" {
return None;
}
let found = real_cargo(Path::new(&dir), std::env::var_os("PATH").as_deref());
if found.is_none() {
eprintln!(
"fgate: no cargo found on the PATH after fgate's shim in {}",
dir.to_string_lossy()
);
}
found
}
fn real_cargo(shim: &Path, path: Option<&OsStr>) -> Option<PathBuf> {
std::env::split_paths(path?)
.filter(|d| !same_dir(d, shim))
.map(|d| d.join(file_name()))
.find(|p| p.is_file())
}
fn same_dir(a: &Path, b: &Path) -> bool {
match (a.canonicalize(), b.canonicalize()) {
(Ok(a), Ok(b)) => a == b,
_ => a == b,
}
}
pub fn install(dir: &Path) -> Option<PathBuf> {
let me = std::env::current_exe().ok()?;
crate::switch::copy_if_changed(&me, &dir.join(file_name())).ok()?;
Some(dir.to_path_buf())
}
pub fn dir_in(settings: &Path) -> PathBuf {
settings.join("shim")
}
pub fn path_with(dir: &Path) -> Option<OsString> {
let old = std::env::var_os("PATH").unwrap_or_default();
let rest: Vec<PathBuf> = std::env::split_paths(&old)
.filter(|d| !same_dir(d, dir))
.collect();
std::env::join_paths(std::iter::once(dir.to_path_buf()).chain(rest)).ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn temp(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("fgate-shim-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn the_real_cargo_is_the_first_after_the_shim() {
let root = temp("path");
let (shim, real, later) = (root.join("shim"), root.join("real"), root.join("later"));
for d in [&shim, &real, &later] {
std::fs::create_dir_all(d).unwrap();
std::fs::write(d.join(file_name()), "").unwrap();
}
let path = std::env::join_paths([&shim, &root.join("missing"), &real, &later]).unwrap();
assert_eq!(real_cargo(&shim, Some(&path)), Some(real.join(file_name())));
let only_shim = std::env::join_paths([&shim]).unwrap();
assert_eq!(real_cargo(&shim, Some(&only_shim)), None);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn the_shim_goes_first_on_the_path_once() {
let root = temp("first");
let path = path_with(&root).unwrap();
let dirs: Vec<PathBuf> = std::env::split_paths(&path).collect();
assert_eq!(dirs[0], root);
assert_eq!(dirs.iter().filter(|d| same_dir(d, &root)).count(), 1);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn the_shim_is_a_copy_of_fgate_named_cargo() {
let root = temp("install");
let dir = install(&root).unwrap();
let shim = dir.join(file_name());
let me = std::env::current_exe().unwrap();
assert_eq!(
std::fs::metadata(&shim).unwrap().len(),
std::fs::metadata(&me).unwrap().len()
);
let _ = std::fs::remove_dir_all(&root);
}
}