Skip to main content

uv_shell/
runnable.rs

1//! Utilities for running executables and scripts. Particularly in Windows.
2
3use std::env::consts::EXE_EXTENSION;
4use std::ffi::OsStr;
5use std::path::Path;
6use std::process::Command;
7
8pub struct WindowsRunnable;
9
10#[derive(Debug)]
11enum WindowsRunnableKind {
12    /// Windows PE (.exe)
13    Executable,
14    /// `PowerShell` script (.ps1)
15    PowerShell,
16    /// Command Prompt NT script (.cmd)
17    Command,
18    /// Command Prompt script (.bat)
19    Batch,
20}
21
22impl WindowsRunnableKind {
23    /// Returns a list of all supported Windows runnable types.
24    fn all() -> &'static [Self] {
25        &[
26            Self::Executable,
27            Self::PowerShell,
28            Self::Command,
29            Self::Batch,
30        ]
31    }
32
33    /// Returns the extension for a given Windows runnable type.
34    fn to_extension(&self) -> &'static str {
35        match self {
36            Self::Executable => EXE_EXTENSION,
37            Self::PowerShell => "ps1",
38            Self::Command => "cmd",
39            Self::Batch => "bat",
40        }
41    }
42
43    /// Determines the runnable type from a given Windows file extension.
44    fn from_extension(ext: &str) -> Option<Self> {
45        match ext {
46            EXE_EXTENSION => Some(Self::Executable),
47            "ps1" => Some(Self::PowerShell),
48            "cmd" => Some(Self::Command),
49            "bat" => Some(Self::Batch),
50            _ => None,
51        }
52    }
53
54    /// Returns a [`Command`] to run the given type under the appropriate Windows runtime.
55    fn as_command(&self, runnable_path: &Path) -> Command {
56        match self {
57            Self::Executable => Command::new(runnable_path),
58            Self::PowerShell => {
59                let mut cmd = Command::new("powershell");
60                cmd.arg("-NoLogo").arg("-File").arg(runnable_path);
61                cmd
62            }
63            Self::Command | Self::Batch => {
64                let mut cmd = Command::new("cmd");
65                cmd.arg("/q").arg("/c").arg(runnable_path);
66                cmd
67            }
68        }
69    }
70}
71
72impl WindowsRunnable {
73    /// Handle console and legacy setuptools scripts for Windows.
74    ///
75    /// Returns [`Command`] that can be used to invoke a supported runnable on Windows
76    /// under the scripts path of an interpreter environment.
77    pub fn from_script_path(script_path: &Path, runnable_name: &OsStr) -> Command {
78        let script_path = script_path.join(runnable_name);
79
80        // Honor explicit extension if provided and recognized.
81        if let Some(script_type) = script_path
82            .extension()
83            .and_then(OsStr::to_str)
84            .and_then(WindowsRunnableKind::from_extension)
85            .filter(|_| script_path.is_file())
86        {
87            return script_type.as_command(&script_path);
88        }
89
90        // Guess the extension when an explicit one is not provided.
91        // We also add the extension when missing since for some types (e.g. PowerShell) it must be explicit.
92        WindowsRunnableKind::all()
93            .iter()
94            .map(|script_type| {
95                (
96                    script_type,
97                    script_path.with_added_extension(script_type.to_extension()),
98                )
99            })
100            .find(|(_, script_path)| script_path.is_file())
101            .map(|(script_type, script_path)| script_type.as_command(&script_path))
102            .unwrap_or_else(|| Command::new(runnable_name))
103    }
104}
105
106#[cfg(test)]
107mod tests {
108
109    #[cfg(target_os = "windows")]
110    use super::WindowsRunnable;
111    #[cfg(target_os = "windows")]
112    use fs_err as fs;
113    #[cfg(target_os = "windows")]
114    use std::ffi::OsStr;
115    #[cfg(target_os = "windows")]
116    use std::io;
117
118    /// Helper function to create a temporary directory with test files
119    #[cfg(target_os = "windows")]
120    fn create_test_environment() -> io::Result<tempfile::TempDir> {
121        let temp_dir = tempfile::tempdir()?;
122        let scripts_dir = temp_dir.path().join("Scripts");
123        fs::create_dir_all(&scripts_dir)?;
124
125        // Create test executable files
126        fs::write(scripts_dir.join("python.exe"), "")?;
127        fs::write(scripts_dir.join("awslabs.cdk-mcp-server.exe"), "")?;
128        fs::write(scripts_dir.join("org.example.tool.exe"), "")?;
129        fs::write(scripts_dir.join("multi.dot.package.name.exe"), "")?;
130        fs::write(scripts_dir.join("script.ps1"), "")?;
131        fs::write(scripts_dir.join("batch.bat"), "")?;
132        fs::write(scripts_dir.join("command.cmd"), "")?;
133        fs::write(scripts_dir.join("explicit.ps1"), "")?;
134
135        Ok(temp_dir)
136    }
137
138    #[cfg(target_os = "windows")]
139    #[test]
140    fn test_from_script_path_single_dot_package() {
141        let temp_dir = create_test_environment().expect("Failed to create test environment");
142        let scripts_dir = temp_dir.path().join("Scripts");
143
144        // Test package name with single dot (awslabs.cdk-mcp-server)
145        let command =
146            WindowsRunnable::from_script_path(&scripts_dir, OsStr::new("awslabs.cdk-mcp-server"));
147
148        // The command should be constructed with the correct executable path
149        let expected_path = scripts_dir.join("awslabs.cdk-mcp-server.exe");
150        assert_eq!(command.get_program(), expected_path.as_os_str());
151    }
152
153    #[cfg(target_os = "windows")]
154    #[test]
155    fn test_from_script_path_multiple_dots_package() {
156        let temp_dir = create_test_environment().expect("Failed to create test environment");
157        let scripts_dir = temp_dir.path().join("Scripts");
158
159        // Test package name with multiple dots (org.example.tool)
160        let command =
161            WindowsRunnable::from_script_path(&scripts_dir, OsStr::new("org.example.tool"));
162
163        let expected_path = scripts_dir.join("org.example.tool.exe");
164        assert_eq!(command.get_program(), expected_path.as_os_str());
165
166        // Test another multi-dot package
167        let command =
168            WindowsRunnable::from_script_path(&scripts_dir, OsStr::new("multi.dot.package.name"));
169
170        let expected_path = scripts_dir.join("multi.dot.package.name.exe");
171        assert_eq!(command.get_program(), expected_path.as_os_str());
172    }
173
174    #[cfg(target_os = "windows")]
175    #[test]
176    fn test_from_script_path_simple_package_name() {
177        let temp_dir = create_test_environment().expect("Failed to create test environment");
178        let scripts_dir = temp_dir.path().join("Scripts");
179
180        // Test simple package name without dots
181        let command = WindowsRunnable::from_script_path(&scripts_dir, OsStr::new("python"));
182
183        let expected_path = scripts_dir.join("python.exe");
184        assert_eq!(command.get_program(), expected_path.as_os_str());
185    }
186
187    #[cfg(target_os = "windows")]
188    #[test]
189    fn test_from_script_path_explicit_extensions() {
190        let temp_dir = create_test_environment().expect("Failed to create test environment");
191        let scripts_dir = temp_dir.path().join("Scripts");
192
193        // Test explicit .ps1 extension
194        let command = WindowsRunnable::from_script_path(&scripts_dir, OsStr::new("explicit.ps1"));
195
196        let expected_path = scripts_dir.join("explicit.ps1");
197        assert_eq!(command.get_program(), "powershell");
198
199        // Verify the arguments contain the script path
200        let args: Vec<&OsStr> = command.get_args().collect();
201        assert!(args.contains(&OsStr::new("-File")));
202        assert!(args.contains(&expected_path.as_os_str()));
203
204        // Test explicit .bat extension
205        let command = WindowsRunnable::from_script_path(&scripts_dir, OsStr::new("batch.bat"));
206        assert_eq!(command.get_program(), "cmd");
207
208        // Test explicit .cmd extension
209        let command = WindowsRunnable::from_script_path(&scripts_dir, OsStr::new("command.cmd"));
210        assert_eq!(command.get_program(), "cmd");
211    }
212}