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