Win Trampoline: Use Python executable path encoded in binary (#1803)

This commit is contained in:
Micha Reiser 2024-02-22 16:10:02 +01:00 committed by GitHub
parent 4e011b305f
commit 12a96ad422
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 506 additions and 212 deletions

View file

@ -2,7 +2,7 @@ name: CI
on:
push:
branches: [main]
branches: [ main ]
pull_request:
workflow_dispatch:
@ -32,7 +32,7 @@ jobs:
name: "cargo clippy"
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
os: [ ubuntu-latest, windows-latest ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
@ -113,7 +113,7 @@ jobs:
workspaces: "crates/uv-trampoline"
- name: "Clippy"
working-directory: crates/uv-trampoline
run: cargo clippy --all-features --locked -- -D warnings
run: cargo clippy --all-features --locked --target x86_64-pc-windows-msvc -- -D warnings
- name: "Build"
working-directory: crates/uv-trampoline
run: cargo build --release -Z build-std=core,panic_abort,alloc -Z build-std-features=compiler-builtins-mem --target x86_64-pc-windows-msvc
run: cargo build --release --target x86_64-pc-windows-msvc

View file

@ -33,6 +33,8 @@ use crate::{find_dist_info, Error};
/// `#!/usr/bin/env python`
pub const SHEBANG_PYTHON: &str = "#!/usr/bin/env python";
const LAUNCHER_MAGIC_NUMBER: [u8; 4] = [b'U', b'V', b'U', b'V'];
#[cfg(all(windows, target_arch = "x86_64"))]
const LAUNCHER_X86_64_GUI: &[u8] =
include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-x86_64-gui.exe");
@ -281,19 +283,7 @@ fn unpack_wheel_files<R: Read + Seek>(
}
fn get_shebang(location: &InstallLocation<impl AsRef<Path>>) -> String {
let path = location.python().to_string_lossy().to_string();
let path = if cfg!(windows) {
// https://stackoverflow.com/a/50323079
const VERBATIM_PREFIX: &str = r"\\?\";
if let Some(stripped) = path.strip_prefix(VERBATIM_PREFIX) {
stripped.to_string()
} else {
path
}
} else {
path
};
format!("#!{path}")
format!("#!{}", location.python().normalized().display())
}
/// A Windows script is a minimal .exe launcher binary with the python entrypoint script appended as
@ -305,6 +295,7 @@ fn get_shebang(location: &InstallLocation<impl AsRef<Path>>) -> String {
pub(crate) fn windows_script_launcher(
launcher_python_script: &str,
is_gui: bool,
installation: &InstallLocation<impl AsRef<Path>>,
) -> Result<Vec<u8>, Error> {
// This method should only be called on Windows, but we avoid `#[cfg(windows)]` to retain
// compilation on all platforms.
@ -352,9 +343,20 @@ pub(crate) fn windows_script_launcher(
archive.finish().expect(error_msg);
}
let python = installation.python();
let python_path = python.normalized().to_string_lossy();
let mut launcher: Vec<u8> = Vec::with_capacity(launcher_bin.len() + payload.len());
launcher.extend_from_slice(launcher_bin);
launcher.extend_from_slice(&payload);
launcher.extend_from_slice(python_path.as_bytes());
launcher.extend_from_slice(
&u32::try_from(python_path.as_bytes().len())
.expect("File Path to be smaller than 4GB")
.to_le_bytes(),
);
launcher.extend_from_slice(&LAUNCHER_MAGIC_NUMBER);
Ok(launcher)
}
@ -393,7 +395,7 @@ pub(crate) fn write_script_entrypoints(
write_file_recorded(
site_packages,
&entrypoint_relative,
&windows_script_launcher(&launcher_python_script, is_gui)?,
&windows_script_launcher(&launcher_python_script, is_gui, location)?,
record,
)?;
} else {
@ -949,7 +951,7 @@ pub fn parse_key_value_file(
///
/// Wheel 1.0: <https://www.python.org/dev/peps/pep-0427/>
#[allow(clippy::too_many_arguments)]
#[instrument(skip_all, fields(name = %filename.name))]
#[instrument(skip_all, fields(name = % filename.name))]
pub fn install_wheel(
location: &InstallLocation<LockedDir>,
reader: impl Read + Seek,

View file

@ -0,0 +1,3 @@
[unstable]
build-std = ["core", "panic_abort", "alloc", "std"]
build-std-features = ["compiler-builtins-mem"]

View file

@ -17,16 +17,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "uv-trampoline"
version = "0.1.0"
dependencies = [
"embed-manifest",
"ufmt",
"ufmt-write",
"windows-sys",
]
[[package]]
name = "quote"
version = "1.0.35"
@ -80,6 +70,16 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "uv-trampoline"
version = "0.1.0"
dependencies = [
"embed-manifest",
"ufmt",
"ufmt-write",
"windows-sys",
]
[[package]]
name = "windows-sys"
version = "0.52.0"

View file

@ -43,15 +43,7 @@ windows-sys = { version = "0.52.0", features = [
"Win32_System_WindowsProgramming",
"Win32_UI_WindowsAndMessaging",
] }
# This provides implementations of memcpy, memset, etc., which the compiler assumes
# are available. But there's also a hidden copy of this crate inside `core`/`alloc`,
# and they may or may not conflict depending on how clever the linker is feeling.
# The issue is that the hidden copy doesn't have the "mem" feature enabled, and we
# need it. So two options:
# - Uncomment this, and cross fingers that it doesn't cause conflicts
# - Use -Zbuild-std=... -Zbuild-std-features=compiler-builtins-mem, which enables
# the mem feature on the built-in builtins.
#compiler_builtins = { version = "*", features = ["mem"]}
ufmt-write = "0.1.0"
ufmt = "0.2.0"

View file

@ -1,6 +1,7 @@
# Windows trampolines
This is a fork of [posy trampolines](https://github.com/njsmith/posy/tree/dda22e6f90f5fefa339b869dd2bbe107f5b48448/src/trampolines/windows-trampolines/posy-trampoline).
This is a fork
of [posy trampolines](https://github.com/njsmith/posy/tree/dda22e6f90f5fefa339b869dd2bbe107f5b48448/src/trampolines/windows-trampolines/posy-trampoline).
# What is this?
@ -13,27 +14,33 @@ That's what this does: it's a generic "trampoline" that lets us generate custom
`.exe`s for arbitrary Python scripts, and when invoked it bounces to invoking
`python <the script>` instead.
# How do you use it?
Basically, this looks up `python.exe` (for console programs) or
`pythonw.exe` (for GUI programs) in the adjacent directory, and invokes
`python[w].exe path\to\the\<the .exe>`.
The intended use is: take your Python script, name it `__main__.py`, and pack it
into a `.zip` file. Then concatenate that `.zip` file onto the end of one of our
prebuilt `.exe`s.
The intended use is:
* take your Python script, name it `__main__.py`, and pack it
into a `.zip` file. Then concatenate that `.zip` file onto the end of one of our
prebuilt `.exe`s.
* After the zip file content, write the path to the Python executable that the script uses to run
the Python script as UTF-8 encoded string, followed by the path's length as a 32-bit little-endian
integer.
* At the very end, write the magic number `UVUV` in bytes.
| `launcher.exe` |
|:---------------------------:|
| `<zipped python script>` |
| `<path to python.exe>` |
| `<len(path to python.exe)>` |
| `<b'U', b'V', b'U', b'V'>` |
Then when you run `python` on the `.exe`, it will see the `.zip` trailer at the
end of the `.exe`, and automagically look inside to find and execute
`__main__.py`. Easy-peasy.
(TODO: we should probably make the Python-finding logic slightly more flexible
at some point -- in particular to support more conventional venv-style
installation where you find `python` by looking in the directory next to the
trampoline `.exe` -- but this is good enough to get started.)
# Why does this exist?
I probably could have used Vinay's C++ implementation from `distlib`, but what's
@ -47,7 +54,6 @@ Python-finding logic we want. But mostly it was just an interesting challenge.
This does owe a *lot* to the `distlib` implementation though. The overall logic
is copied more-or-less directly.
# Anything I should know for hacking on this?
In order to minimize binary size, this uses `#![no_std]`, `panic="abort"`, and
@ -64,7 +70,7 @@ this:
Though uh, this does mean that literally all of our code is `unsafe`. Sorry!
- `runtime.rs` has the core glue to get panicking, heap allocation, and linking
working.
working.
- `diagnostics.rs` uses `ufmt` and some cute Windows tricks to get a convenient
version of `eprintln!` that works without `std`, and automatically prints to
@ -85,7 +91,6 @@ Miscellaneous tips:
`.unwrap_unchecked()` avoids this. Similar for `slice[idx]` vs
`slice.get_unchecked(idx)`.
# How do you build this stupid thing?
Building this can be frustrating, because the low-level compiler/runtime
@ -99,15 +104,8 @@ might not realize that, and still emit references to the unwinding helper
`__CxxFrameHandler3`. And then the linker blows up because that symbol doesn't
exist.
Two approaches that are reasonably likely to work:
- Uncomment `compiler-builtins` in `Cargo.toml`, and build normally: `cargo
build --profile release`.
- Leave `compiler-builtins` commented-out, and build like: `cargo build
--release -Z build-std=core,panic_abort,alloc -Z
build-std-features=compiler-builtins-mem --target x86_64-pc-windows-msvc`
`cargo build --release --target x86_64-pc-windows-msvc`
or `cargo build --release --target aarch64-pc-windows-msvc`
Hopefully in the future as `#![no_std]` develops, this will get smoother.
@ -125,6 +123,6 @@ rustup target add aarch64-pc-windows-msvc
```
```shell
cargo +nightly xwin build --release -Z build-std=core,panic_abort,alloc -Z build-std-features=compiler-builtins-mem --target x86_64-pc-windows-msvc
cargo +nightly xwin build --release -Z build-std=core,panic_abort,alloc -Z build-std-features=compiler-builtins-mem --target aarch64-pc-windows-msvc
cargo +nightly xwin build --release --target x86_64-pc-windows-msvc
cargo +nightly xwin build --release --target aarch64-pc-windows-msvc
```

View file

@ -1,10 +1,16 @@
use alloc::string::String;
use alloc::{ffi::CString, vec, vec::Vec};
use core::mem::MaybeUninit;
use core::{
ffi::CStr,
mem,
ptr::{addr_of, addr_of_mut, null, null_mut},
};
use windows_sys::Win32::Storage::FileSystem::{
CreateFileA, GetFileSizeEx, ReadFile, SetFilePointerEx, FILE_ATTRIBUTE_NORMAL, FILE_BEGIN,
FILE_SHARE_READ, OPEN_EXISTING,
};
use windows_sys::Win32::{
Foundation::*,
System::{
@ -18,7 +24,11 @@ use windows_sys::Win32::{
};
use crate::helpers::SizeOf;
use crate::{c, check, eprintln};
use crate::{c, eprintln, format};
const MAGIC_NUMBER: [u8; 4] = [b'U', b'V', b'U', b'V'];
const PATH_LEN_SIZE: usize = mem::size_of::<u32>();
const MAX_PATH_LEN: u32 = 32 * 1024;
fn getenv(name: &CStr) -> Option<CString> {
unsafe {
@ -38,9 +48,9 @@ fn getenv(name: &CStr) -> Option<CString> {
}
/// Transform `<command> <arguments>` to `python <command> <arguments>`.
fn make_child_cmdline(is_gui: bool) -> CString {
fn make_child_cmdline() -> CString {
let executable_name: CString = executable_filename();
let python_exe = find_python_exe(is_gui, &executable_name);
let python_exe = find_python_exe(&executable_name);
let mut child_cmdline = Vec::<u8>::new();
push_quoted_path(&python_exe, &mut child_cmdline);
@ -59,12 +69,14 @@ fn make_child_cmdline(is_gui: bool) -> CString {
// Helpful when debugging trampline issues
// eprintln!(
// "executable_name: '{}'\nnew_cmdline: {}",
// core::str::from_utf8(executable_name.to_bytes(),
// core::str::from_utf8(child_cmdline.as_slice())
// core::str::from_utf8(executable_name.to_bytes()).unwrap(),
// core::str::from_utf8(child_cmdline.as_slice()).unwrap()
// );
// SAFETY: We push the null termination byte at the end.
unsafe { CString::from_vec_with_nul_unchecked(child_cmdline) }
CString::from_vec_with_nul(child_cmdline).unwrap_or_else(|_| {
eprintln!("Child command line is not correctly null terminated.");
exit_with_status(1)
})
}
fn push_quoted_path(path: &CStr, command: &mut Vec<u8>) {
@ -94,18 +106,18 @@ fn executable_filename() -> CString {
// That's the error condition because len doesn't include the trailing null byte
if len as usize == buffer.len() {
unsafe {
let last_error = GetLastError();
match last_error {
ERROR_INSUFFICIENT_BUFFER => {
SetLastError(ERROR_SUCCESS);
// Try again with twice the size
buffer.resize(buffer.len() * 2, 0);
}
err => {
eprintln!("Failed to get executable name: code {}", err);
ExitProcess(1);
}
let last_error = unsafe { GetLastError() };
match last_error {
ERROR_INSUFFICIENT_BUFFER => {
unsafe { SetLastError(ERROR_SUCCESS) };
// Try again with twice the size
buffer.resize(buffer.len() * 2, 0);
}
err => {
print_last_error_and_exit(&format!(
"Failed to get executable name (code: {})",
err
));
}
}
} else {
@ -114,34 +126,156 @@ fn executable_filename() -> CString {
}
}
unsafe { CString::from_vec_with_nul_unchecked(buffer) }
CString::from_vec_with_nul(buffer).unwrap_or_else(|_| {
eprintln!("Executable name is not correctly null terminated.");
exit_with_status(1)
})
}
/// The scripts are in the same directory as the Python interpreter, so we can find Python by getting the locations of
/// the current .exe and replacing the filename with `python[w].exe`.
fn find_python_exe(is_gui: bool, executable_name: &CStr) -> CString {
// Replace the filename (the last segment of the path) with "python.exe"
// Assumption: We are not in an encoding where a backslash byte can be part of a larger character.
let bytes = executable_name.to_bytes();
let Some(last_backslash) = bytes.iter().rposition(|byte| *byte == b'\\') else {
eprintln!(
"Invalid current exe path (missing backslash): `{}`",
&*executable_name.to_string_lossy()
);
/// Reads the executable binary from the back to find the path to the Python executable that is written
/// after the ZIP file content.
///
/// The executable is expected to have the following format:
/// * The file must end with the magic number 'UVUV'.
/// * The last 4 bytes (little endian) are the length of the path to the Python executable.
/// * The path encoded as UTF-8 comes right before the length
///
/// # Panics
/// If there's any IO error, or the file does not conform to the specified format.
fn find_python_exe(executable_name: &CStr) -> CString {
let file_handle = expect_result(
unsafe {
ExitProcess(1);
CreateFileA(
executable_name.as_ptr() as _,
GENERIC_READ,
FILE_SHARE_READ,
null(),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
0,
)
},
INVALID_HANDLE_VALUE,
|| {
format!(
"Failed to open executable '{}'",
&*executable_name.to_string_lossy(),
)
},
);
let mut file_size: i64 = 0;
// `SetFilePointerEx` supports setting the file pointer from the back, but pointing it past the file's start
// results in an error. That's why we need to know the file size to avoid ever seeking past the start of the file.
expect_result(
unsafe { GetFileSizeEx(file_handle, &mut file_size) },
0,
|| {
format!(
"Failed to get the size of the executable '{}'",
&*executable_name.to_string_lossy(),
)
},
);
// Start with a size of 1024 bytes which should be enough for most paths but avoids reading the
// entire file.
let mut buffer: Vec<u8> = Vec::new();
let mut bytes_to_read = 1024.min(u32::try_from(file_size).unwrap_or(u32::MAX));
let path = loop {
// SAFETY: Casting to usize is safe because we only support 64bit systems where usize is guaranteed to be larger than u32.
buffer.resize(bytes_to_read as usize, 0);
expect_result(
unsafe {
SetFilePointerEx(
file_handle,
file_size - i64::from(bytes_to_read),
null_mut(),
FILE_BEGIN,
)
},
0,
|| String::from("Failed to set the file pointer to the end of the file."),
);
let mut read_bytes = 0u32;
expect_result(
unsafe {
ReadFile(
file_handle,
buffer.as_mut_ptr() as *mut _,
bytes_to_read,
&mut read_bytes,
null_mut(),
)
},
0,
|| String::from("Failed to read the executable file"),
);
// Truncate the buffer to the actual number of bytes read.
buffer.truncate(read_bytes as usize);
if !buffer.ends_with(&MAGIC_NUMBER) {
eprintln!("Magic number 'UVUV' not found at the end of the file. Did you append the magic number, the length and the path to the python executable at the end of the file?");
exit_with_status(1);
}
// Remove the magic number
buffer.truncate(buffer.len() - MAGIC_NUMBER.len());
let path_len = match buffer.get(buffer.len() - PATH_LEN_SIZE..) {
Some(path_len) => {
let path_len = u32::from_le_bytes(path_len.try_into().unwrap_or_else(|_| {
eprintln!("Slice length is not equal to 4 bytes");
exit_with_status(1)
}));
if path_len > MAX_PATH_LEN {
eprintln!("Only paths with a length up to 32KBs are supported but the python path has a length of {}.", path_len);
exit_with_status(1);
}
// SAFETY: path len is guaranteed to be less than 32KBs
path_len as usize
}
None => {
eprintln!("Python executable length missing. Did you write the length of the path to the Python executable before the Magic number?");
exit_with_status(1);
}
};
// Remove the path length
buffer.truncate(buffer.len() - PATH_LEN_SIZE);
if let Some(path_offset) = buffer.len().checked_sub(path_len) {
buffer.drain(..path_offset);
buffer.push(b'\0');
break CString::from_vec_with_nul(buffer).unwrap_or_else(|_| {
eprintln!("Python executable path is not correctly null terminated.");
exit_with_status(1)
});
} else {
// SAFETY: Casting to u32 is safe because `path_len` is guaranteed to be less than 32KBs,
// MAGIC_NUMBER is 4 bytes and PATH_LEN_SIZE is 4 bytes.
bytes_to_read = (path_len + MAGIC_NUMBER.len() + PATH_LEN_SIZE) as u32;
if i64::from(bytes_to_read) > file_size {
eprintln!("The length of the python executable path exceeds the file size. Verify that the path length is appended to the end of the launcher script as a u32 in little endian.");
exit_with_status(1);
}
}
};
let mut buffer = bytes[..last_backslash + 1].to_vec();
buffer.extend_from_slice(if is_gui {
b"pythonw.exe"
} else {
b"python.exe"
expect_result(unsafe { CloseHandle(file_handle) }, 0, || {
String::from("Failed to close file handle")
});
buffer.push(b'\0');
unsafe { CString::from_vec_with_nul_unchecked(buffer) }
path
}
fn push_arguments(output: &mut Vec<u8>) {
@ -192,22 +326,30 @@ fn make_job_object() -> HANDLE {
let job = CreateJobObjectW(null(), null());
let mut job_info = MaybeUninit::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>::uninit();
let mut retlen = 0u32;
check!(QueryInformationJobObject(
job,
JobObjectExtendedLimitInformation,
job_info.as_mut_ptr() as *mut _,
job_info.size_of(),
&mut retlen as *mut _,
));
expect_result(
QueryInformationJobObject(
job,
JobObjectExtendedLimitInformation,
job_info.as_mut_ptr() as *mut _,
job_info.size_of(),
&mut retlen as *mut _,
),
0,
|| String::from("Error from QueryInformationJobObject"),
);
let mut job_info = job_info.assume_init();
job_info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
job_info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
check!(SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
addr_of!(job_info) as *const _,
job_info.size_of(),
));
expect_result(
SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
addr_of!(job_info) as *const _,
job_info.size_of(),
),
0,
|| String::from("Error from SetInformationJobObject"),
);
job
}
}
@ -222,20 +364,24 @@ fn spawn_child(si: &STARTUPINFOA, child_cmdline: CString) -> HANDLE {
SetHandleInformation(si.hStdError, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
}
let mut child_process_info = MaybeUninit::<PROCESS_INFORMATION>::uninit();
check!(CreateProcessA(
null(),
// Why does this have to be mutable? Who knows. But it's not a mistake --
// MS explicitly documents that this buffer might be mutated by CreateProcess.
child_cmdline.into_bytes_with_nul().as_mut_ptr(),
null(),
null(),
1,
expect_result(
CreateProcessA(
null(),
// Why does this have to be mutable? Who knows. But it's not a mistake --
// MS explicitly documents that this buffer might be mutated by CreateProcess.
child_cmdline.as_ptr().cast_mut() as _,
null(),
null(),
1,
0,
null(),
null(),
addr_of!(*si),
child_process_info.as_mut_ptr(),
),
0,
null(),
null(),
addr_of!(*si),
child_process_info.as_mut_ptr(),
));
|| String::from("Failed to spawn the python child process"),
);
let child_process_info = child_process_info.assume_init();
CloseHandle(child_process_info.hThread);
child_process_info.hProcess
@ -311,7 +457,7 @@ fn clear_app_starting_state(child_handle: HANDLE) {
pub fn bounce(is_gui: bool) -> ! {
unsafe {
let child_cmdline = make_child_cmdline(is_gui);
let child_cmdline = make_child_cmdline();
let mut si = MaybeUninit::<STARTUPINFOA>::uninit();
GetStartupInfoA(si.as_mut_ptr());
@ -319,7 +465,9 @@ pub fn bounce(is_gui: bool) -> ! {
let child_handle = spawn_child(&si, child_cmdline);
let job = make_job_object();
check!(AssignProcessToJobObject(job, child_handle));
expect_result(AssignProcessToJobObject(job, child_handle), 0, || {
String::from("Error from AssignProcessToJobObject")
});
// (best effort) Close all the handles that we can
close_handles(&si);
@ -345,7 +493,86 @@ pub fn bounce(is_gui: bool) -> ! {
WaitForSingleObject(child_handle, INFINITE);
let mut exit_code = 0u32;
check!(GetExitCodeProcess(child_handle, addr_of_mut!(exit_code)));
ExitProcess(exit_code);
expect_result(
GetExitCodeProcess(child_handle, addr_of_mut!(exit_code)),
0,
|| String::from("Error from GetExitCodeProcess"),
);
exit_with_status(exit_code);
}
}
/// Unwraps the result of the C call by asserting that it doesn't match the `error_code`.
///
/// Prints the passed error message if the `actual_result` is equal to `error_code` and exits the process with status 1.
#[inline]
fn expect_result<T, F>(actual_result: T, error_code: T, error_message: F) -> T
where
T: Eq,
F: FnOnce() -> String,
{
if actual_result == error_code {
print_last_error_and_exit(&error_message());
}
actual_result
}
#[cold]
fn print_last_error_and_exit(message: &str) -> ! {
use windows_sys::Win32::{
Foundation::*,
System::Diagnostics::Debug::{
FormatMessageA, FORMAT_MESSAGE_ALLOCATE_BUFFER, FORMAT_MESSAGE_FROM_SYSTEM,
FORMAT_MESSAGE_IGNORE_INSERTS,
},
};
let err = unsafe { GetLastError() };
eprintln!("Received error code: {}", err);
let mut msg_ptr: *mut u8 = core::ptr::null_mut();
let size = unsafe {
FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER
| FORMAT_MESSAGE_FROM_SYSTEM
| FORMAT_MESSAGE_IGNORE_INSERTS,
null(),
err,
0,
// Weird calling convention: this argument is typed as *mut u16,
// but if you pass FORMAT_MESSAGE_ALLOCATE_BUFFER then you have to
// *actually* pass in a *mut *mut u16 and just lie about the type.
// Getting Rust to do this requires some convincing.
core::ptr::addr_of_mut!(msg_ptr) as *mut _ as _,
0,
core::ptr::null(),
)
};
if size == 0 {
eprintln!(
"{}: with code {} (failed to get error message)",
message, err
);
} else {
let reason = unsafe {
let reason = core::slice::from_raw_parts(msg_ptr, size as usize + 1);
CStr::from_bytes_with_nul_unchecked(reason)
};
eprintln!(
"(uv internal error) {}: {}",
message,
&*reason.to_string_lossy()
);
}
// Note: We don't need to free the buffer here because we're going to exit anyway.
exit_with_status(1);
}
#[cold]
fn exit_with_status(code: u32) -> ! {
unsafe {
ExitProcess(code);
}
}

View file

@ -13,55 +13,54 @@ use windows_sys::Win32::{
UI::WindowsAndMessaging::MessageBoxA,
};
pub struct DiagnosticBuffer {
buffer: String,
#[macro_export]
macro_rules! eprintln {
($($tt:tt)*) => {{
$crate::diagnostics::write_diagnostic(&$crate::format!($($tt)*));
}}
}
impl DiagnosticBuffer {
pub fn new() -> DiagnosticBuffer {
DiagnosticBuffer {
buffer: String::new(),
}
}
pub fn display(self) {
unsafe {
let handle = GetStdHandle(STD_ERROR_HANDLE);
let mut written: u32 = 0;
let mut remaining = self.buffer.as_str();
while !remaining.is_empty() {
let ok = WriteFile(
handle,
remaining.as_ptr(),
remaining.len() as u32,
addr_of_mut!(written),
null_mut(),
);
if ok == 0 {
let nul_terminated = CString::new(self.buffer.as_bytes()).unwrap_unchecked();
MessageBoxA(0, nul_terminated.as_ptr() as *const _, null(), 0);
return;
}
remaining = &remaining.get_unchecked(written as usize..);
}
}
}
#[macro_export]
macro_rules! format {
($($tt:tt)*) => {{
let mut buffer = $crate::diagnostics::StringBuffer::default();
_ = ufmt::uwriteln!(&mut buffer, $($tt)*);
buffer.0
}}
}
impl uWrite for DiagnosticBuffer {
#[derive(Default)]
pub(crate) struct StringBuffer(pub(crate) String);
impl uWrite for StringBuffer {
type Error = Infallible;
fn write_str(&mut self, s: &str) -> Result<(), Self::Error> {
self.buffer.push_str(s);
self.0.push_str(s);
Ok(())
}
}
#[macro_export]
macro_rules! eprintln {
($($tt:tt)*) => {{
let mut d = $crate::diagnostics::DiagnosticBuffer::new();
_ = ufmt::uwriteln!(&mut d, $($tt)*);
d.display();
}}
#[cold]
pub(crate) fn write_diagnostic(message: &str) {
unsafe {
let handle = GetStdHandle(STD_ERROR_HANDLE);
let mut written: u32 = 0;
let mut remaining = message;
while !remaining.is_empty() {
let ok = WriteFile(
handle,
remaining.as_ptr(),
remaining.len() as u32,
addr_of_mut!(written),
null_mut(),
);
if ok == 0 {
let nul_terminated = CString::new(message.as_bytes()).unwrap_unchecked();
MessageBoxA(0, nul_terminated.as_ptr() as *const _, null(), 0);
return;
}
remaining = &remaining.get_unchecked(written as usize..);
}
}
}

View file

@ -10,45 +10,6 @@ impl<T: Sized> SizeOf for T {
}
}
// Check result of win32 API call that returns BOOL
#[macro_export]
macro_rules! check {
($e:expr) => {
if $e == 0 {
use windows_sys::Win32::{
Foundation::*,
System::{
Diagnostics::Debug::{
FormatMessageA, FORMAT_MESSAGE_ALLOCATE_BUFFER, FORMAT_MESSAGE_FROM_SYSTEM,
FORMAT_MESSAGE_IGNORE_INSERTS,
},
}
};
let err = GetLastError();
let mut msg_ptr: *mut u8 = core::ptr::null_mut();
let size = FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER
| FORMAT_MESSAGE_FROM_SYSTEM
| FORMAT_MESSAGE_IGNORE_INSERTS,
null(),
err,
0,
// Weird calling convention: this argument is typed as *mut u16,
// but if you pass FORMAT_MESSAGE_ALLOCATE_BUFFER then you have to
// *actually* pass in a *mut *mut u16 and just lie about the type.
// Getting Rust to do this requires some convincing.
core::ptr::addr_of_mut!(msg_ptr) as *mut _ as _,
0,
core::ptr::null(),
);
let msg = core::slice::from_raw_parts(msg_ptr, size as usize);
let msg = core::str::from_utf8_unchecked(msg);
$crate::eprintln!("Error: {} (from {})", msg, stringify!($e));
ExitProcess(1);
}
}
}
// CStr literal: c!("...")
#[macro_export]
macro_rules! c {

View file

@ -319,7 +319,7 @@ pub fn run_and_format<'a>(
let output = command
.borrow_mut()
.output()
.unwrap_or_else(|_| panic!("Failed to spawn {program}"));
.unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"));
let mut snapshot = format!(
"success: {:?}\nexit_code: {}\n----- stdout -----\n{}\n----- stderr -----\n{}",

View file

@ -1486,3 +1486,115 @@ fn direct_url_zip_file_bunk_permissions() -> Result<()> {
Ok(())
}
#[test]
fn launcher() -> Result<()> {
let context = TestContext::new("3.12");
let project_root = fs_err::canonicalize(std::env::current_dir()?.join("../.."))?;
let filters = [
(r"(\d+m )?(\d+\.)?\d+(ms|s)", "[TIME]"),
(
r"simple-launcher==0\.1\.0 \(from .+\.whl\)",
"simple_launcher.whl",
),
];
uv_snapshot!(
filters,
command(&context)
.arg(format!("simple_launcher@{}", project_root.join("scripts/wheels/simple_launcher-0.1.0-py3-none-any.whl").display()))
.arg("--strict"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ simple_launcher.whl
"###
);
let bin_path = if cfg!(windows) { "Scripts" } else { "bin" };
uv_snapshot!(Command::new(
context.venv.join(bin_path).join("simple_launcher")
), @r###"
success: true
exit_code: 0
----- stdout -----
Hi from the simple launcher!
----- stderr -----
"###);
Ok(())
}
#[test]
fn launcher_with_symlink() -> Result<()> {
let context = TestContext::new("3.12");
let project_root = fs_err::canonicalize(std::env::current_dir()?.join("../.."))?;
let filters = [
(r"(\d+m )?(\d+\.)?\d+(ms|s)", "[TIME]"),
(
r"simple-launcher==0\.1\.0 \(from .+\.whl\)",
"simple_launcher.whl",
),
];
uv_snapshot!(filters,
command(&context)
.arg(format!("simple_launcher@{}", project_root.join("scripts/wheels/simple_launcher-0.1.0-py3-none-any.whl").display()))
.arg("--strict"),
@r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ simple_launcher.whl
"###
);
#[cfg(windows)]
if let Err(error) = std::os::windows::fs::symlink_file(
context.venv.join("Scripts\\simple_launcher.exe"),
context.temp_dir.join("simple_launcher.exe"),
) {
if error.kind() == std::io::ErrorKind::PermissionDenied {
// Not running as an administrator or developer mode isn't enabled.
// Ignore the test
return Ok(());
}
}
#[cfg(unix)]
std::os::unix::fs::symlink(
context.venv.join("bin/simple_launcher"),
context.temp_dir.join("simple_launcher"),
)?;
// Only support windows or linux
#[cfg(not(any(windows, unix)))]
return Ok(());
uv_snapshot!(Command::new(
context.temp_dir.join("simple_launcher")
), @r###"
success: true
exit_code: 0
----- stdout -----
Hi from the simple launcher!
----- stderr -----
"###);
Ok(())
}

Binary file not shown.