feat: pythonw support on gui scripts (#4409)

## Summary

Closes https://github.com/astral-sh/uv/issues/2956

This changes the bootstrap launcher script to use `pythonw.exe` instead
of `python.exe` on `gui_scripts` via a helper fn both in the shebang and
the python exe path encoded before `UVUV` magic, that way
uv-trampoline's `find_python_exe` can use the right pythonw executable.

## Test Plan

New unit tests for the helper was added.
Tested on example from #2956 on Windows to make sure it works as
expected.

## Questions

I noticed the docs in `fn windows_script_launcher` says ```The launcher
will look for `python[w].exe` adjacent to it in the same directory to
start the embedded script.``` but I didn't find such functionality when
I looked in uv-trampoline.
I only saw `clear_app_starting_state` getting called when `is_gui` is
set.

Was the intention to do this in uv-trampoline at some point instead?

---------

Co-authored-by: konstin <konstin@mailbox.org>
This commit is contained in:
samypr100 2024-06-23 05:48:47 -04:00 committed by GitHub
parent 3a63e1410d
commit 2288ff7bf4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 59 additions and 5 deletions

View file

@ -50,4 +50,6 @@ walkdir = { workspace = true }
zip = { workspace = true }
[dev-dependencies]
anyhow = { version = "1.0.80" }
assert_fs = { version = "1.1.1" }
indoc = { version = "2.0.4" }

View file

@ -152,8 +152,7 @@ fn format_shebang(executable: impl AsRef<Path>, os_name: &str) -> String {
}
/// A Windows script is a minimal .exe launcher binary with the python entrypoint script appended as
/// stored zip file. The launcher will look for `python[w].exe` adjacent to it in the same directory
/// to start the embedded script.
/// stored zip file.
///
/// <https://github.com/pypa/pip/blob/fd0ea6bc5e8cb95e518c23d901c26ca14db17f89/src/pip/_vendor/distlib/scripts.py#L248-L262>
#[allow(unused_variables)]
@ -233,6 +232,20 @@ pub(crate) fn windows_script_launcher(
Ok(launcher)
}
/// Returns a [`PathBuf`] to `python[w].exe` for script execution.
fn get_script_executable(python_executable: &Path, is_gui: bool) -> PathBuf {
// Only check for pythonw.exe on Windows
if cfg!(windows) && is_gui {
python_executable
.parent()
.map(|parent| parent.join("pythonw.exe"))
.filter(|path| path.is_file())
.unwrap_or_else(|| python_executable.to_path_buf())
} else {
python_executable.to_path_buf()
}
}
/// Create the wrapper scripts in the bin folder of the venv for launching console scripts.
pub(crate) fn write_script_entrypoints(
layout: &Layout,
@ -269,9 +282,10 @@ pub(crate) fn write_script_entrypoints(
})?;
// Generate the launcher script.
let launcher_executable = get_script_executable(&layout.sys_executable, is_gui);
let launcher_python_script = get_script_launcher(
entrypoint,
&format_shebang(&layout.sys_executable, &layout.os_name),
&format_shebang(&launcher_executable, &layout.os_name),
);
// If necessary, wrap the launcher script in a Windows launcher binary.
@ -279,7 +293,7 @@ pub(crate) fn write_script_entrypoints(
write_file_recorded(
site_packages,
&entrypoint_relative,
&windows_script_launcher(&launcher_python_script, is_gui, &layout.sys_executable)?,
&windows_script_launcher(&launcher_python_script, is_gui, &launcher_executable)?,
record,
)?;
} else {
@ -722,12 +736,16 @@ mod test {
use std::io::Cursor;
use std::path::Path;
use anyhow::Result;
use assert_fs::prelude::*;
use indoc::{formatdoc, indoc};
use crate::wheel::format_shebang;
use crate::Error;
use super::{parse_key_value_file, parse_wheel_file, read_record_file, Script};
use super::{
get_script_executable, parse_key_value_file, parse_wheel_file, read_record_file, Script,
};
#[test]
fn test_parse_key_value_file() {
@ -922,4 +940,36 @@ mod test {
super::LAUNCHER_AArch64_CONSOLE.len()
);
}
#[test]
fn test_script_executable() -> Result<()> {
// Test with adjacent pythonw.exe
let temp_dir = assert_fs::TempDir::new()?;
let python_exe = temp_dir.child("python.exe");
let pythonw_exe = temp_dir.child("pythonw.exe");
python_exe.write_str("")?;
pythonw_exe.write_str("")?;
let script_path = get_script_executable(&python_exe, true);
#[cfg(windows)]
assert_eq!(script_path, pythonw_exe.to_path_buf());
#[cfg(not(windows))]
assert_eq!(script_path, python_exe.to_path_buf());
let script_path = get_script_executable(&python_exe, false);
assert_eq!(script_path, python_exe.to_path_buf());
// Test without adjacent pythonw.exe
let temp_dir = assert_fs::TempDir::new()?;
let python_exe = temp_dir.child("python.exe");
python_exe.write_str("")?;
let script_path = get_script_executable(&python_exe, true);
assert_eq!(script_path, python_exe.to_path_buf());
let script_path = get_script_executable(&python_exe, false);
assert_eq!(script_path, python_exe.to_path_buf());
Ok(())
}
}