Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/uv-shell/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod runnable;
mod shlex;
pub mod windows;

Expand Down
100 changes: 100 additions & 0 deletions crates/uv-shell/src/runnable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! Utilities for running executables and scripts. Particularly in Windows.

use std::env::consts::EXE_EXTENSION;
use std::ffi::OsStr;
use std::path::Path;
use std::process::Command;

#[derive(Debug)]
pub enum WindowsRunnable {
/// Windows PE (.exe)
Executable,
/// `PowerShell` script (.ps1)
PowerShell,
/// Command Prompt NT script (.cmd)
Command,
/// Command Prompt script (.bat)
Batch,
}

impl WindowsRunnable {
/// Returns a list of all supported Windows runnable types.
fn all() -> &'static [Self] {
&[
Self::Executable,
Self::PowerShell,
Self::Command,
Self::Batch,
]
}

/// Returns the extension for a given Windows runnable type.
fn to_extension(&self) -> &'static str {
match self {
Self::Executable => EXE_EXTENSION,
Self::PowerShell => "ps1",
Self::Command => "cmd",
Self::Batch => "bat",
}
}

/// Determines the runnable type from a given Windows file extension.
fn from_extension(ext: &str) -> Option<Self> {
match ext {
EXE_EXTENSION => Some(Self::Executable),
"ps1" => Some(Self::PowerShell),
"cmd" => Some(Self::Command),
"bat" => Some(Self::Batch),
_ => None,
}
}

/// Returns a [`Command`] to run the given type under the appropriate Windows runtime.
fn as_command(&self, runnable_path: &Path) -> Command {
match self {
Self::Executable => Command::new(runnable_path),
Self::PowerShell => {
let mut cmd = Command::new("powershell");
cmd.arg("-NoLogo").arg("-File").arg(runnable_path);
cmd
}
Self::Command | Self::Batch => {
let mut cmd = Command::new("cmd");
cmd.arg("/q").arg("/c").arg(runnable_path);
cmd
}
}
}

/// Handle console and legacy setuptools scripts for Windows.
///
/// Returns [`Command`] that can be used to invoke a supported runnable on Windows
/// under the scripts path of an interpreter environment.
pub fn from_script_path(script_path: &Path, runnable_name: &OsStr) -> Command {
let script_path = script_path.join(runnable_name);

// Honor explicit extension if provided and recognized.
if let Some(script_type) = script_path
.extension()
.and_then(OsStr::to_str)
.and_then(Self::from_extension)
.filter(|_| script_path.is_file())
{
return script_type.as_command(&script_path);
}

// Guess the extension when an explicit one is not provided.
// We also add the extension when missing since for some types (e.g. PowerShell) it must be explicit.
Self::all()
.iter()
.map(|script_type| {
(
script_type,
script_path.with_extension(script_type.to_extension()),
)
})
.find(|(_, script_path)| script_path.is_file())
.map(|(script_type, script_path)| script_type.as_command(&script_path))
.unwrap_or_else(|| Command::new(runnable_name))
}
}
88 changes: 3 additions & 85 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::borrow::Cow;
use std::env::VarError;
use std::ffi::{OsStr, OsString};
use std::ffi::OsString;
use std::fmt::Write;
use std::io::Read;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -33,6 +33,7 @@ use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::Lock;
use uv_scripts::Pep723Item;
use uv_settings::PythonInstallMirrors;
use uv_shell::runnable::WindowsRunnable;
use uv_static::EnvVars;
use uv_warnings::warn_user;
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceCache, WorkspaceError};
Expand Down Expand Up @@ -1126,58 +1127,6 @@ fn can_skip_ephemeral(
}
}

#[derive(Debug)]
enum WindowsScript {
/// `PowerShell` script (.ps1)
PowerShell,
/// Command Prompt NT script (.cmd)
Command,
/// Command Prompt script (.bat)
Batch,
}

impl WindowsScript {
/// Returns a list of all supported Windows script types.
fn all() -> &'static [Self] {
&[Self::PowerShell, Self::Command, Self::Batch]
}

/// Returns the script extension for a given Windows script type.
fn to_extension(&self) -> &'static str {
match self {
Self::PowerShell => "ps1",
Self::Command => "cmd",
Self::Batch => "bat",
}
}

/// Determines the script type from a given Windows file extension.
fn from_extension(ext: &str) -> Option<Self> {
match ext {
"ps1" => Some(Self::PowerShell),
"cmd" => Some(Self::Command),
"bat" => Some(Self::Batch),
_ => None,
}
}

/// Returns a [`Command`] to run the given script under the appropriate Windows command.
fn as_command(&self, script: &Path) -> Command {
match self {
Self::PowerShell => {
let mut cmd = Command::new("powershell");
cmd.arg("-NoLogo").arg("-File").arg(script);
cmd
}
Self::Command | Self::Batch => {
let mut cmd = Command::new("cmd");
cmd.arg("/q").arg("/c").arg(script);
cmd
}
}
}
}

#[derive(Debug)]
pub(crate) enum RunCommand {
/// Execute `python`.
Expand Down Expand Up @@ -1239,37 +1188,6 @@ impl RunCommand {
}
}

/// Handle legacy setuptools scripts for Windows.
///
/// Returns [`Command`] that can be used to run `.ps1`, `.cmd`, or `.bat` scripts on Windows.
fn for_windows_script(interpreter: &Interpreter, executable: &OsStr) -> Command {
let script_path = interpreter.scripts().join(executable);

// Honor explicit extension if provided and recognized.
if let Some(script_type) = script_path
.extension()
.and_then(OsStr::to_str)
.and_then(WindowsScript::from_extension)
.filter(|_| script_path.is_file())
{
return script_type.as_command(&script_path);
}

// Guess the extension when an explicit one is not provided.
// We also add the extension when missing since for PowerShell it must be explicit.
WindowsScript::all()
.iter()
.map(|script_type| {
(
script_type,
script_path.with_extension(script_type.to_extension()),
)
})
.find(|(_, script_path)| script_path.is_file())
.map(|(script_type, script_path)| script_type.as_command(&script_path))
.unwrap_or_else(|| Command::new(executable))
}

/// Convert a [`RunCommand`] into a [`Command`].
fn as_command(&self, interpreter: &Interpreter) -> Command {
match self {
Expand Down Expand Up @@ -1386,7 +1304,7 @@ impl RunCommand {
}
Self::External(executable, args) => {
let mut process = if cfg!(windows) {
Self::for_windows_script(interpreter, executable)
WindowsRunnable::from_script_path(interpreter.scripts(), executable).into()
} else {
Command::new(executable)
};
Expand Down
8 changes: 7 additions & 1 deletion crates/uv/src/commands/tool/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use uv_python::{
};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_shell::runnable::WindowsRunnable;
use uv_static::EnvVars;
use uv_tool::{entrypoint_paths, InstalledTools};
use uv_warnings::warn_user;
Expand Down Expand Up @@ -260,7 +261,12 @@ pub(crate) async fn run(
let executable = from.executable();

// Construct the command
let mut process = Command::new(executable);
let mut process = if cfg!(windows) {
WindowsRunnable::from_script_path(environment.scripts(), executable.as_ref()).into()
} else {
Command::new(executable)
};

process.args(args);

// Construct the `PATH` environment variable.
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/tests/it/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4560,7 +4560,7 @@ fn run_windows_legacy_scripts() -> Result<()> {
Audited 1 package in [TIME]
"###);

// Test without explicit extension (.ps1 should be used)
// Test without explicit extension (.ps1 should be used) as there's no .exe available.
uv_snapshot!(context.filters(), context.run().arg("custom_pydoc"), @r###"
success: true
exit_code: 0
Expand Down
Loading
Loading