1pub use crate::config::SHIM_NAMES;
6
7use std::collections::HashSet;
8use std::io::Write;
9use std::os::unix::fs::PermissionsExt;
10use std::path::{Path, PathBuf};
11
12pub struct ShimContext {
13 pub shim_dir: PathBuf,
14 pub dejavu_bin: PathBuf,
17 pub enabled: Vec<String>,
19}
20
21pub fn generate_shims(ctx: &ShimContext) -> std::io::Result<usize> {
25 std::fs::create_dir_all(&ctx.shim_dir)?;
26 let enabled: HashSet<&str> = ctx.enabled.iter().map(String::as_str).collect();
27
28 if let Ok(entries) = std::fs::read_dir(&ctx.shim_dir) {
33 for entry in entries.flatten() {
34 let name = entry.file_name();
35 let Some(name) = name.to_str() else { continue };
36 if name == "dejavu" || enabled.contains(name) {
37 continue;
38 }
39 let path = entry.path();
40 if std::fs::read_to_string(&path)
41 .is_ok_and(|body| body.starts_with("#!/bin/sh") && body.contains("DEJAVU_BIN"))
42 {
43 let _ = std::fs::remove_file(&path);
44 }
45 }
46 }
47
48 for name in &ctx.enabled {
49 let path = ctx.shim_dir.join(name);
50 let body = shim_script(name, &ctx.dejavu_bin);
51 write_if_changed(&path, &body)?;
52 }
53
54 let self_body = format!("#!/bin/sh\nexec \"{}\" \"$@\"\n", ctx.dejavu_bin.display());
59 write_if_changed(&ctx.shim_dir.join("dejavu"), &self_body)?;
60
61 Ok(ctx.enabled.len())
62}
63
64fn shim_script(name: &str, dejavu_bin: &Path) -> String {
65 format!(
66 "#!/bin/sh\nexec \"${{DEJAVU_BIN:-{bin}}}\" run --shim-name {name} -- \"$@\"\n",
67 bin = dejavu_bin.display(),
68 )
69}
70
71fn write_if_changed(path: &Path, body: &str) -> std::io::Result<bool> {
74 if let Ok(existing) = std::fs::read_to_string(path) {
75 if existing == body {
76 if let Ok(meta) = std::fs::metadata(path) {
77 if meta.permissions().mode() & 0o111 != 0 {
78 return Ok(false);
79 }
80 }
81 }
82 }
83 let tmp = path.with_extension("dejavu-tmp");
84 {
85 let mut file = std::fs::File::create(&tmp)?;
86 file.write_all(body.as_bytes())?;
87 let mut perms = file.metadata()?.permissions();
88 perms.set_mode(0o755);
89 file.set_permissions(perms)?;
90 file.sync_all()?;
91 }
92 std::fs::rename(&tmp, path)?;
93 Ok(true)
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn generates_executable_shims_and_removes_disabled() {
102 let tmp = tempfile::tempdir().unwrap();
103 let shim_dir = tmp.path().join("shims/bin");
104 let ctx = ShimContext {
105 shim_dir: shim_dir.clone(),
106 dejavu_bin: PathBuf::from("/opt/dejavu"),
107 enabled: vec!["pnpm".to_string(), "git".to_string()],
108 };
109 let n = generate_shims(&ctx).unwrap();
110 assert_eq!(n, 2);
111
112 let pnpm = shim_dir.join("pnpm");
113 assert!(pnpm.exists());
114 let mode = std::fs::metadata(&pnpm).unwrap().permissions().mode();
115 assert!(mode & 0o111 != 0);
116 let body = std::fs::read_to_string(&pnpm).unwrap();
117 assert!(body.contains("run --shim-name pnpm --"));
118 assert!(body.contains("${DEJAVU_BIN:-/opt/dejavu}"));
119
120 let ctx2 = ShimContext {
122 shim_dir: shim_dir.clone(),
123 dejavu_bin: PathBuf::from("/opt/dejavu"),
124 enabled: vec!["git".to_string()],
125 };
126 generate_shims(&ctx2).unwrap();
127 assert!(!pnpm.exists());
128 assert!(shim_dir.join("git").exists());
129 }
130}