Add uvw as alias for uv without console window on Windows (#11786)

<!--
Thank you for contributing to uv! To help us out with reviewing, please
consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

Related to https://github.com/astral-sh/uv/issues/6801.

Currently on Windows, uv itself will always creates a console window,
even though the window could be empty if `uv run --gui-script` is used.
This is due to it using the [default `console` window
subsystem](https://rust-lang.github.io/rfcs/1665-windows-subsystem.html).

This PR introduces a wrapper `uvw` that, similar to the existing `uvx`,
invokes `uv` with the
[`CREATE_NO_WINDOW`](https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags#:~:text=CREATE_NO_WINDOW)
process creation flag on Windows, which creates child process without
console window.

Note that this PR does not alter any behaviors regarding `run --script`
and `run --gui-script`.

## Test Plan

Built and tested locally by doing something like `uvw run test.py`.
This commit is contained in:
Reci 2025-05-28 11:37:21 -07:00 committed by GitHub
parent f62836fa04
commit e5d002beb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 127 additions and 1 deletions

View file

@ -234,7 +234,7 @@ jobs:
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
with:
target: ${{ matrix.platform.target }}
args: --release --locked --out dist --features self-update
args: --release --locked --out dist --features self-update,windows-gui-bin
- name: "Test wheel"
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
shell: bash
@ -243,6 +243,7 @@ jobs:
${{ env.MODULE_NAME }} --help
python -m ${{ env.MODULE_NAME }} --help
uvx --help
uvw --help
- name: "Upload wheels"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
@ -254,6 +255,7 @@ jobs:
ARCHIVE_FILE=uv-${{ matrix.platform.target }}.zip
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/uv.exe
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/uvx.exe
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/uvw.exe
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2

View file

@ -368,3 +368,10 @@ aarch64-unknown-linux-gnu = "2.28"
"actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2
"actions/download-artifact" = "d3f86a106a0bac45b974a628896c90dbdf5c8093" # v4.3.0
"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3
[workspace.metadata.dist.binaries]
"*" = ["uv", "uvx"]
# Add "uvw" binary for Windows targets
aarch64-pc-windows-msvc = ["uv", "uvx", "uvw"]
i686-pc-windows-msvc = ["uv", "uvx", "uvw"]
x86_64-pc-windows-msvc = ["uv", "uvx", "uvw"]

View file

@ -178,6 +178,12 @@ python-managed = []
slow-tests = []
# Includes test cases that require ecosystem packages
test-ecosystem = []
# Build uvw binary on Windows
windows-gui-bin = []
[package.metadata.dist]
dist = true
[[bin]]
name = "uvw"
required-features = ["windows-gui-bin"]

111
crates/uv/src/bin/uvw.rs Normal file
View file

@ -0,0 +1,111 @@
#![cfg_attr(windows, windows_subsystem = "windows")]
use std::convert::Infallible;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode, ExitStatus};
/// Spawns a command exec style.
fn exec_spawn(cmd: &mut Command) -> std::io::Result<Infallible> {
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let err = cmd.exec();
Err(err)
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
cmd.stdin(std::process::Stdio::inherit());
let status = cmd.creation_flags(CREATE_NO_WINDOW).status()?;
#[allow(clippy::exit)]
std::process::exit(status.code().unwrap())
}
}
/// Assuming the binary is called something like `uvw@1.2.3(.exe)`, compute the `@1.2.3(.exe)` part
/// so that we can preferentially find `uv@1.2.3(.exe)`, for folks who like managing multiple
/// installs in this way.
fn get_uvw_suffix(current_exe: &Path) -> Option<&str> {
let os_file_name = current_exe.file_name()?;
let file_name_str = os_file_name.to_str()?;
file_name_str.strip_prefix("uvw")
}
/// Gets the path to `uv`, given info about `uvw`
fn get_uv_path(current_exe_parent: &Path, uvw_suffix: Option<&str>) -> std::io::Result<PathBuf> {
// First try to find a matching suffixed `uv`, e.g. `uv@1.2.3(.exe)`
let uv_with_suffix = uvw_suffix.map(|suffix| current_exe_parent.join(format!("uv{suffix}")));
if let Some(uv_with_suffix) = &uv_with_suffix {
#[allow(clippy::print_stderr, reason = "printing a very rare warning")]
match uv_with_suffix.try_exists() {
Ok(true) => return Ok(uv_with_suffix.to_owned()),
Ok(false) => { /* definitely not there, proceed to fallback */ }
Err(err) => {
// We don't know if `uv@1.2.3` exists, something errored when checking.
// We *could* blindly use `uv@1.2.3` in this case, as the code below does, however
// in this extremely narrow corner case it's *probably* better to default to `uv`,
// since we don't want to mess up existing users who weren't using suffixes?
eprintln!(
"warning: failed to determine if `{}` exists, trying `uv` instead: {err}",
uv_with_suffix.display()
);
}
}
}
// Then just look for good ol' `uv`
let uv = current_exe_parent.join(format!("uv{}", std::env::consts::EXE_SUFFIX));
// If we are sure the `uv` binary does not exist, display a clearer error message.
// If we're not certain if uv exists (try_exists == Err), keep going and hope it works.
if matches!(uv.try_exists(), Ok(false)) {
let message = if let Some(uv_with_suffix) = uv_with_suffix {
format!(
"Could not find the `uv` binary at either of:\n {}\n {}",
uv_with_suffix.display(),
uv.display(),
)
} else {
format!("Could not find the `uv` binary at: {}", uv.display())
};
Err(std::io::Error::new(std::io::ErrorKind::NotFound, message))
} else {
Ok(uv)
}
}
fn run() -> std::io::Result<ExitStatus> {
let current_exe = std::env::current_exe()?;
let Some(bin) = current_exe.parent() else {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Could not determine the location of the `uvw` binary",
));
};
let uvw_suffix = get_uvw_suffix(&current_exe);
let uv = get_uv_path(bin, uvw_suffix)?;
let args = std::env::args_os()
// Skip the `uvw` name
.skip(1)
.collect::<Vec<_>>();
let mut cmd = Command::new(uv);
cmd.args(&args);
match exec_spawn(&mut cmd)? {}
}
#[allow(clippy::print_stderr)]
fn main() -> ExitCode {
let result = run();
match result {
// Fail with 2 if the status cannot be cast to an exit code
Ok(status) => u8::try_from(status.code().unwrap_or(2)).unwrap_or(2).into(),
Err(err) => {
eprintln!("error: {err}");
ExitCode::from(2)
}
}
}