Skip to main content

leo_lang/cli/
plugin.rs

1// Copyright (C) 2019-2026 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use leo_errors::Result;
18
19use colored::Colorize;
20use std::{
21    convert::Infallible,
22    ffi::OsString,
23    path::{Path, PathBuf},
24    process,
25};
26
27const PLUGIN_PREFIX: &str = "leo-";
28
29/// Scan `PATH` for an executable named `name`.
30pub fn find_exe(name: &str) -> Option<PathBuf> {
31    let filename = format!("{name}{}", std::env::consts::EXE_SUFFIX);
32    let var = std::env::var_os("PATH")?;
33    std::env::split_paths(&var).find_map(|dir| {
34        let candidate = dir.join(&filename);
35        if is_executable(&candidate) { Some(candidate) } else { None }
36    })
37}
38
39/// Find and execute a plugin binary, forwarding `args` and optionally setting
40/// the working directory to `cwd`.
41///
42/// On Unix this replaces the current process via `exec`. On other platforms it
43/// spawns the plugin and propagates the exit code.
44pub fn exec(name: &str, args: &[OsString], cwd: Option<&Path>) -> Result<Infallible> {
45    let path = find_exe(name).ok_or_else(|| -> leo_errors::LeoError {
46        crate::errors::custom(format!(
47            "'{name}' not found. Install the plugin and ensure it is available on your PATH."
48        ))
49        .into()
50    })?;
51
52    let mut cmd = process::Command::new(&path);
53    cmd.args(args);
54    if let Some(dir) = cwd {
55        cmd.current_dir(dir);
56    }
57
58    #[cfg(unix)]
59    {
60        use std::os::unix::process::CommandExt;
61        // Does not return on success.
62        let err = cmd.exec();
63        Err(crate::errors::custom(format!("failed to exec '{name}': {err}")).into())
64    }
65
66    #[cfg(not(unix))]
67    {
68        let status = cmd
69            .stdin(process::Stdio::inherit())
70            .stdout(process::Stdio::inherit())
71            .stderr(process::Stdio::inherit())
72            .status()
73            .map_err(|err| -> leo_errors::LeoError {
74                crate::errors::custom(format!("failed to spawn '{name}': {err}")).into()
75            })?;
76        process::exit(status.code().unwrap_or(1));
77    }
78}
79
80/// Iterate over all `leo-*` plugin executables found on `PATH`.
81///
82/// Yields `(subcommand_name, path)` pairs, e.g. `("fmt", "/usr/local/bin/leo-fmt")`.
83/// Each plugin appears at most once (first match on PATH wins).
84pub fn all() -> Vec<(String, PathBuf)> {
85    let Some(var) = std::env::var_os("PATH") else {
86        return Vec::new();
87    };
88    let mut seen = std::collections::HashSet::new();
89    let mut plugins = Vec::new();
90    for dir in std::env::split_paths(&var) {
91        let Ok(entries) = std::fs::read_dir(&dir) else {
92            continue;
93        };
94        for entry in entries.filter_map(|e| e.ok()) {
95            let path = entry.path();
96            let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
97                continue;
98            };
99            let stem = name.strip_suffix(std::env::consts::EXE_SUFFIX).unwrap_or(name);
100            if let Some(subcmd) = stem.strip_prefix(PLUGIN_PREFIX)
101                && !subcmd.is_empty()
102                && is_executable(&path)
103                && seen.insert(subcmd.to_string())
104            {
105                plugins.push((subcmd.to_string(), path));
106            }
107        }
108    }
109    plugins.sort_by(|(a, _), (b, _)| a.cmp(b));
110    plugins
111}
112
113/// Print all installed `leo-*` plugins to stdout.
114pub fn print_all() {
115    let plugins = all();
116    if plugins.is_empty() {
117        println!("No leo plugins detected on PATH.");
118        return;
119    }
120    println!("Installed plugins:\n");
121    let name_col_w = plugins.iter().map(|(cmd, _)| cmd.len()).max().unwrap_or(0);
122    for (cmd, path) in &plugins {
123        println!("  {:<name_col_w$}  {}", cmd.bold(), path.display());
124    }
125}
126
127#[cfg(unix)]
128fn is_executable(path: &Path) -> bool {
129    use std::os::unix::fs::PermissionsExt;
130    path.is_file() && std::fs::metadata(path).map(|m| m.permissions().mode() & 0o111 != 0).unwrap_or(false)
131}
132
133#[cfg(not(unix))]
134fn is_executable(path: &Path) -> bool {
135    path.is_file()
136}