Skip to main content

dejavu/exec/
shim.rs

1//! Shim generation. Each shim is a minimal `/bin/sh` script that re-invokes
2//! `dejavu run`. Writes are idempotent (temp + atomic rename) and only touch
3//! files whose content or exec bit differs.
4
5pub 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    /// Absolute path to the `dejavu` binary, baked in as the fallback when
15    /// `DEJAVU_BIN` is unset.
16    pub dejavu_bin: PathBuf,
17    /// Builtin + user `[intercept] extra` names to generate shims for.
18    pub enabled: Vec<String>,
19}
20
21/// Generate/refresh shims for the enabled set; remove shims for anything else
22/// (disabled builtins AND `extra` entries removed from config) so `which` no
23/// longer finds them. Returns the number of enabled shims.
24pub 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    // Content-based sweep: a generated command shim is self-identifying (it
29    // contains the DEJAVU_BIN marker), so we can safely remove stale ones
30    // without keeping a registry of past names. The `dejavu` self-shim and
31    // any foreign file are left untouched.
32    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    // Self-shim: the reduced envelope tells the agent to run `dejavu show <id>`;
55    // that must work even when the binary itself is not otherwise on PATH.
56    // A plain exec (no `run --shim-name`), and deliberately no DEJAVU_BIN
57    // marker so `is_dejavu_shim` never mistakes it for a command shim.
58    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
71/// Write the shim only if content/perms differ. Uses temp + rename so a
72/// concurrent `dejavu start` never sees a torn file.
73fn 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        // Re-run with pnpm disabled -> its shim is removed.
121        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}