Skip to main content

uv_trampoline_builder/
lib.rs

1use std::io;
2use std::path::{Path, PathBuf};
3use std::str::Utf8Error;
4
5use fs_err::File;
6use thiserror::Error;
7
8use uv_fs::Simplified;
9
10#[cfg(all(windows, target_arch = "x86"))]
11const LAUNCHER_I686_GUI: &[u8] = include_bytes!("../trampolines/uv-trampoline-i686-gui.exe");
12
13#[cfg(all(windows, target_arch = "x86"))]
14const LAUNCHER_I686_CONSOLE: &[u8] =
15    include_bytes!("../trampolines/uv-trampoline-i686-console.exe");
16
17#[cfg(all(windows, target_arch = "x86_64"))]
18const LAUNCHER_X86_64_GUI: &[u8] = include_bytes!("../trampolines/uv-trampoline-x86_64-gui.exe");
19
20#[cfg(all(windows, target_arch = "x86_64"))]
21const LAUNCHER_X86_64_CONSOLE: &[u8] =
22    include_bytes!("../trampolines/uv-trampoline-x86_64-console.exe");
23
24#[cfg(all(windows, target_arch = "aarch64"))]
25const LAUNCHER_AARCH64_GUI: &[u8] = include_bytes!("../trampolines/uv-trampoline-aarch64-gui.exe");
26
27#[cfg(all(windows, target_arch = "aarch64"))]
28const LAUNCHER_AARCH64_CONSOLE: &[u8] =
29    include_bytes!("../trampolines/uv-trampoline-aarch64-console.exe");
30
31// https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types
32#[cfg(windows)]
33const RT_RCDATA: u16 = 10;
34
35// Resource IDs matching uv-trampoline
36#[cfg(windows)]
37const RESOURCE_TRAMPOLINE_KIND: windows::core::PCWSTR = windows::core::w!("UV_TRAMPOLINE_KIND");
38#[cfg(windows)]
39const RESOURCE_PYTHON_PATH: windows::core::PCWSTR = windows::core::w!("UV_PYTHON_PATH");
40// Note: This does not need to be looked up as a resource, as we rely on `zipimport`
41// to do the loading work. Still, keeping the content under a resource means that it
42// sits nicely under the PE format.
43#[cfg(windows)]
44const RESOURCE_SCRIPT_DATA: windows::core::PCWSTR = windows::core::w!("UV_SCRIPT_DATA");
45
46#[derive(Debug)]
47pub struct Launcher {
48    pub kind: LauncherKind,
49    pub python_path: PathBuf,
50    pub script_data: Option<Vec<u8>>,
51}
52
53impl Launcher {
54    /// Attempt to read [`Launcher`] metadata from a trampoline executable file.
55    ///
56    /// On Unix, this always returns [`None`]. Trampolines are a Windows-specific feature and cannot
57    /// be read on other platforms.
58    #[cfg(not(windows))]
59    pub fn try_from_path(_path: &Path) -> Result<Option<Self>, Error> {
60        Ok(None)
61    }
62
63    /// Read [`Launcher`] metadata from a trampoline executable file.
64    ///
65    /// Returns `Ok(None)` if the file is not a trampoline executable.
66    /// Returns `Err` if the file looks like a trampoline executable but is formatted incorrectly.
67    #[cfg(windows)]
68    pub fn try_from_path(path: &Path) -> Result<Option<Self>, Error> {
69        use std::os::windows::ffi::OsStrExt;
70        use windows::Win32::System::LibraryLoader::LOAD_LIBRARY_AS_DATAFILE;
71        use windows::Win32::System::LibraryLoader::LoadLibraryExW;
72
73        let path_str = path
74            .as_os_str()
75            .encode_wide()
76            .chain(std::iter::once(0))
77            .collect::<Vec<_>>();
78
79        // SAFETY: winapi call; null-terminated strings
80        #[allow(unsafe_code)]
81        let Some(module) = (unsafe {
82            LoadLibraryExW(
83                windows::core::PCWSTR(path_str.as_ptr()),
84                None,
85                LOAD_LIBRARY_AS_DATAFILE,
86            )
87            .ok()
88        }) else {
89            return Ok(None);
90        };
91
92        let result = (|| {
93            let Some(kind_data) = read_resource(module, RESOURCE_TRAMPOLINE_KIND) else {
94                return Ok(None);
95            };
96            let Some(kind) = LauncherKind::from_resource_value(kind_data[0]) else {
97                return Err(Error::UnprocessableMetadata);
98            };
99
100            let Some(path_data) = read_resource(module, RESOURCE_PYTHON_PATH) else {
101                return Ok(None);
102            };
103            let python_path = PathBuf::from(
104                String::from_utf8(path_data).map_err(|err| Error::InvalidPath(err.utf8_error()))?,
105            );
106
107            let script_data = read_resource(module, RESOURCE_SCRIPT_DATA);
108
109            Ok(Some(Self {
110                kind,
111                python_path,
112                script_data,
113            }))
114        })();
115
116        // SAFETY: winapi call; handle is known to be valid.
117        #[allow(unsafe_code)]
118        unsafe {
119            windows::Win32::Foundation::FreeLibrary(module)
120                .map_err(|err| Error::Io(io::Error::from_raw_os_error(err.code().0)))?;
121        };
122
123        result
124    }
125
126    /// Write this trampoline launcher to a file.
127    ///
128    /// On Unix, this always returns [`Error::NotWindows`]. Trampolines are a Windows-specific
129    /// feature and cannot be written on other platforms.
130    #[cfg(not(windows))]
131    pub fn write_to_file(self, _file: &mut File, _is_gui: bool) -> Result<(), Error> {
132        Err(Error::NotWindows)
133    }
134
135    /// Write this trampoline launcher to a file.
136    #[cfg(windows)]
137    pub fn write_to_file(self, file: &mut File, is_gui: bool) -> Result<(), Error> {
138        use std::io::Write;
139        use uv_fs::Simplified;
140
141        let python_path = self.python_path.simplified_display().to_string();
142
143        // Create temporary file for the base launcher
144        let temp_dir = tempfile::TempDir::new()?;
145        let temp_file = temp_dir
146            .path()
147            .join(format!("uv-trampoline-{}.exe", std::process::id()));
148
149        // Write the launcher binary
150        fs_err::write(&temp_file, get_launcher_bin(is_gui)?)?;
151
152        // Write resources
153        let resources = &[
154            (
155                RESOURCE_TRAMPOLINE_KIND,
156                &[self.kind.to_resource_value()][..],
157            ),
158            (RESOURCE_PYTHON_PATH, python_path.as_bytes()),
159        ];
160        if let Some(script_data) = self.script_data {
161            let mut all_resources = resources.to_vec();
162            all_resources.push((RESOURCE_SCRIPT_DATA, &script_data));
163            write_resources(&temp_file, &all_resources)?;
164        } else {
165            write_resources(&temp_file, resources)?;
166        }
167
168        // Read back the complete file
169        let launcher = fs_err::read(&temp_file)?;
170        fs_err::remove_file(&temp_file)?;
171
172        // Then write it to the handle
173        file.write_all(&launcher)?;
174
175        Ok(())
176    }
177
178    #[must_use]
179    pub fn with_python_path(self, path: PathBuf) -> Self {
180        Self {
181            kind: self.kind,
182            python_path: path,
183            script_data: self.script_data,
184        }
185    }
186}
187
188/// The window mode of a Windows trampoline launcher.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum WindowMode {
191    /// The launcher runs attached to a console.
192    Console,
193    /// The launcher runs without opening a console window.
194    Windowed,
195}
196
197/// The kind of trampoline launcher to create.
198///
199/// See [`uv-trampoline::bounce::TrampolineKind`].
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum LauncherKind {
202    /// The trampoline should execute itself, it's a zipped Python script.
203    Script,
204    /// The trampoline should just execute Python, it's a proxy Python executable.
205    Python,
206}
207
208impl LauncherKind {
209    #[cfg(windows)]
210    fn to_resource_value(self) -> u8 {
211        match self {
212            Self::Script => 1,
213            Self::Python => 2,
214        }
215    }
216
217    #[cfg(windows)]
218    fn from_resource_value(value: u8) -> Option<Self> {
219        match value {
220            1 => Some(Self::Script),
221            2 => Some(Self::Python),
222            _ => None,
223        }
224    }
225}
226
227/// Note: The caller is responsible for adding the path of the wheel we're installing.
228#[derive(Error, Debug)]
229pub enum Error {
230    #[error(transparent)]
231    Io(#[from] io::Error),
232    #[error("Failed to parse executable path")]
233    InvalidPath(#[source] Utf8Error),
234    #[error(
235        "Unable to create Windows launcher for: {0} (only x86_64, x86, and arm64 are supported)"
236    )]
237    UnsupportedWindowsArch(&'static str),
238    #[error("Unable to create Windows launcher on non-Windows platform")]
239    NotWindows,
240    #[error("Cannot process launcher metadata from resource")]
241    UnprocessableMetadata,
242    #[cfg(windows)]
243    #[error("Failed to write Windows launcher ZIP payload")]
244    AsyncZip(#[from] async_zip::error::ZipError),
245    #[error("Resources over 2^32 bytes are not supported")]
246    ResourceTooLarge,
247    #[error("Failed to update Windows PE resources: {}", path.user_display())]
248    WriteResources {
249        path: PathBuf,
250        #[source]
251        err: io::Error,
252    },
253}
254
255#[allow(clippy::unnecessary_wraps, unused_variables)]
256#[cfg(windows)]
257fn get_launcher_bin(gui: bool) -> Result<&'static [u8], Error> {
258    Ok(match std::env::consts::ARCH {
259        #[cfg(all(windows, target_arch = "x86"))]
260        "x86" => {
261            if gui {
262                LAUNCHER_I686_GUI
263            } else {
264                LAUNCHER_I686_CONSOLE
265            }
266        }
267        #[cfg(all(windows, target_arch = "x86_64"))]
268        "x86_64" => {
269            if gui {
270                LAUNCHER_X86_64_GUI
271            } else {
272                LAUNCHER_X86_64_CONSOLE
273            }
274        }
275        #[cfg(all(windows, target_arch = "aarch64"))]
276        "aarch64" => {
277            if gui {
278                LAUNCHER_AARCH64_GUI
279            } else {
280                LAUNCHER_AARCH64_CONSOLE
281            }
282        }
283        #[cfg(windows)]
284        arch => {
285            return Err(Error::UnsupportedWindowsArch(arch));
286        }
287    })
288}
289
290/// Helper to write Windows PE resources
291#[cfg(windows)]
292fn write_resources(path: &Path, resources: &[(windows::core::PCWSTR, &[u8])]) -> Result<(), Error> {
293    // SAFETY: winapi calls; null-terminated strings
294    #[allow(unsafe_code)]
295    unsafe {
296        use std::os::windows::ffi::OsStrExt;
297        use windows::Win32::System::LibraryLoader::{
298            BeginUpdateResourceW, EndUpdateResourceW, UpdateResourceW,
299        };
300
301        let map_err = |err: windows::core::Error| Error::WriteResources {
302            path: path.to_path_buf(),
303            err: io::Error::from_raw_os_error(err.code().0),
304        };
305
306        let path_str = path
307            .as_os_str()
308            .encode_wide()
309            .chain(std::iter::once(0))
310            .collect::<Vec<_>>();
311        let handle = BeginUpdateResourceW(windows::core::PCWSTR(path_str.as_ptr()), false)
312            .map_err(map_err)?;
313
314        for (name, data) in resources {
315            UpdateResourceW(
316                handle,
317                windows::core::PCWSTR(RT_RCDATA as *const _),
318                *name,
319                0,
320                Some(data.as_ptr().cast()),
321                u32::try_from(data.len()).map_err(|_| Error::ResourceTooLarge)?,
322            )
323            .map_err(&map_err)?;
324        }
325
326        EndUpdateResourceW(handle, false).map_err(map_err)?;
327    }
328
329    Ok(())
330}
331
332/// Safely reads a resource from a PE file
333#[cfg(windows)]
334fn read_resource(
335    handle: windows::Win32::Foundation::HMODULE,
336    name: windows::core::PCWSTR,
337) -> Option<Vec<u8>> {
338    // SAFETY: winapi calls; null-terminated strings; all pointers are checked.
339    #[allow(unsafe_code)]
340    unsafe {
341        use windows::Win32::System::LibraryLoader::{
342            FindResourceW, LoadResource, LockResource, SizeofResource,
343        };
344        // Find the resource
345        let resource = FindResourceW(
346            Some(handle),
347            name,
348            windows::core::PCWSTR(RT_RCDATA as *const _),
349        );
350        if resource.is_invalid() {
351            return None;
352        }
353
354        // Get resource size and data
355        let size = SizeofResource(Some(handle), resource);
356        if size == 0 {
357            return None;
358        }
359        let data = LoadResource(Some(handle), resource).ok()?;
360        let ptr = LockResource(data) as *const u8;
361        if ptr.is_null() {
362            return None;
363        }
364
365        // Copy the resource data into a Vec
366        Some(std::slice::from_raw_parts(ptr, size as usize).to_vec())
367    }
368}
369
370/// Construct a Windows script launcher.
371///
372/// On Unix, this always returns [`Error::NotWindows`]. Trampolines are a Windows-specific feature
373/// and cannot be created on other platforms.
374#[cfg(not(windows))]
375pub fn windows_script_launcher(
376    _launcher_python_script: &str,
377    _is_gui: bool,
378    _python_executable: impl AsRef<Path>,
379) -> Result<Vec<u8>, Error> {
380    Err(Error::NotWindows)
381}
382
383/// Construct a Windows script launcher.
384///
385/// A Windows script is a minimal .exe launcher binary with the python entrypoint script appended as
386/// stored zip file.
387///
388/// <https://github.com/pypa/pip/blob/fd0ea6bc5e8cb95e518c23d901c26ca14db17f89/src/pip/_vendor/distlib/scripts.py#L248-L262>
389#[cfg(windows)]
390pub fn windows_script_launcher(
391    launcher_python_script: &str,
392    is_gui: bool,
393    python_executable: impl AsRef<Path>,
394) -> Result<Vec<u8>, Error> {
395    use async_zip::base::write::ZipFileWriter;
396    use async_zip::{Compression, ZipEntryBuilder};
397    use futures_lite::future::block_on;
398    use futures_lite::io::Cursor;
399
400    use uv_fs::Simplified;
401
402    let launcher_bin: &[u8] = get_launcher_bin(is_gui)?;
403
404    // Store the launcher script without compression.
405    // https://github.com/njsmith/posy/blob/04927e657ca97a5e35bb2252d168125de9a3a025/src/trampolines/mod.rs#L75-L82
406    // https://github.com/pypa/distlib/blob/8ed03aab48add854f377ce392efffb79bb4d6091/PC/launcher.c#L259-L271
407    let mut archive = ZipFileWriter::new(Cursor::new(Vec::new()));
408    let entry = ZipEntryBuilder::new("__main__.py".to_string().into(), Compression::Stored);
409    block_on(archive.write_entry_whole(entry, launcher_python_script.as_bytes()))?;
410    let payload = block_on(archive.close())?.into_inner();
411
412    let python = python_executable.as_ref();
413    let python_path = python.simplified_display().to_string();
414
415    // Start with base launcher binary
416    // Create temporary file for the launcher
417    let temp_dir = tempfile::TempDir::new()?;
418    let temp_file = temp_dir
419        .path()
420        .join(format!("uv-trampoline-{}.exe", std::process::id()));
421    fs_err::write(&temp_file, launcher_bin)?;
422
423    // Write resources
424    let resources = &[
425        (
426            RESOURCE_TRAMPOLINE_KIND,
427            &[LauncherKind::Script.to_resource_value()][..],
428        ),
429        (RESOURCE_PYTHON_PATH, python_path.as_bytes()),
430        (RESOURCE_SCRIPT_DATA, &payload),
431    ];
432    write_resources(&temp_file, resources)?;
433
434    // Read back the complete file
435    // TODO(zanieb): It's weird that we write/read from a temporary file here because in the main
436    // usage at `write_script_entrypoints` we do the same thing again. We should refactor these
437    // to avoid repeated work.
438    let launcher = fs_err::read(&temp_file)?;
439    fs_err::remove_file(temp_file)?;
440
441    Ok(launcher)
442}
443
444/// Construct a Windows Python launcher.
445///
446/// On Unix, this always returns [`Error::NotWindows`]. Trampolines are a Windows-specific feature
447/// and cannot be created on other platforms.
448#[cfg(not(windows))]
449pub fn windows_python_launcher(
450    _python_executable: impl AsRef<Path>,
451    _window_mode: WindowMode,
452) -> Result<Vec<u8>, Error> {
453    Err(Error::NotWindows)
454}
455
456/// Construct a Windows Python launcher.
457///
458/// A minimal .exe launcher binary for Python.
459///
460/// Sort of equivalent to a `python` symlink on Unix.
461#[cfg(windows)]
462pub fn windows_python_launcher(
463    python_executable: impl AsRef<Path>,
464    window_mode: WindowMode,
465) -> Result<Vec<u8>, Error> {
466    use uv_fs::Simplified;
467
468    let launcher_bin: &[u8] = get_launcher_bin(matches!(window_mode, WindowMode::Windowed))?;
469
470    let python = python_executable.as_ref();
471    let python_path = python.simplified_display().to_string();
472
473    // Create temporary file for the launcher
474    let temp_dir = tempfile::TempDir::new()?;
475    let temp_file = temp_dir
476        .path()
477        .join(format!("uv-trampoline-{}.exe", std::process::id()));
478    fs_err::write(&temp_file, launcher_bin)?;
479
480    // Write resources
481    let resources = &[
482        (
483            RESOURCE_TRAMPOLINE_KIND,
484            &[LauncherKind::Python.to_resource_value()][..],
485        ),
486        (RESOURCE_PYTHON_PATH, python_path.as_bytes()),
487    ];
488    write_resources(&temp_file, resources)?;
489
490    // Read back the complete file
491    let launcher = fs_err::read(&temp_file)?;
492    fs_err::remove_file(temp_file)?;
493
494    Ok(launcher)
495}
496
497#[cfg(all(test, windows))]
498#[expect(clippy::print_stdout)]
499mod test {
500    use std::io::Write;
501    use std::path::Path;
502    use std::path::PathBuf;
503    use std::process::Command;
504
505    use anyhow::Result;
506    use assert_cmd::prelude::OutputAssertExt;
507    use assert_fs::prelude::PathChild;
508    use fs_err::File;
509
510    use which::which;
511
512    use super::{
513        Launcher, LauncherKind, WindowMode, windows_python_launcher, windows_script_launcher,
514    };
515
516    #[test]
517    #[cfg(all(windows, target_arch = "x86", feature = "production"))]
518    fn test_launchers_are_small() {
519        // At time of writing, they are ~40kb.
520        assert!(
521            super::LAUNCHER_I686_GUI.len() < 50 * 1024,
522            "GUI launcher: {}",
523            super::LAUNCHER_I686_GUI.len()
524        );
525        assert!(
526            super::LAUNCHER_I686_CONSOLE.len() < 50 * 1024,
527            "CLI launcher: {}",
528            super::LAUNCHER_I686_CONSOLE.len()
529        );
530    }
531
532    #[test]
533    #[cfg(all(windows, target_arch = "x86_64", feature = "production"))]
534    fn test_launchers_are_small() {
535        // At time of writing, they are ~45kb.
536        assert!(
537            super::LAUNCHER_X86_64_GUI.len() < 50 * 1024,
538            "GUI launcher: {}",
539            super::LAUNCHER_X86_64_GUI.len()
540        );
541        assert!(
542            super::LAUNCHER_X86_64_CONSOLE.len() < 50 * 1024,
543            "CLI launcher: {}",
544            super::LAUNCHER_X86_64_CONSOLE.len()
545        );
546    }
547
548    #[test]
549    #[cfg(all(windows, target_arch = "aarch64", feature = "production"))]
550    fn test_launchers_are_small() {
551        // At time of writing, they are ~45kb.
552        assert!(
553            super::LAUNCHER_AARCH64_GUI.len() < 50 * 1024,
554            "GUI launcher: {}",
555            super::LAUNCHER_AARCH64_GUI.len()
556        );
557        assert!(
558            super::LAUNCHER_AARCH64_CONSOLE.len() < 50 * 1024,
559            "CLI launcher: {}",
560            super::LAUNCHER_AARCH64_CONSOLE.len()
561        );
562    }
563
564    /// Utility script for the test.
565    fn get_script_launcher(shebang: &str, is_gui: bool) -> String {
566        if is_gui {
567            format!(
568                r#"{shebang}
569# -*- coding: utf-8 -*-
570import re
571import sys
572
573def make_gui() -> None:
574    from tkinter import Tk, ttk
575    root = Tk()
576    root.title("uv Test App")
577    frm = ttk.Frame(root, padding=10)
578    frm.grid()
579    ttk.Label(frm, text="Hello from uv-trampoline-gui.exe").grid(column=0, row=0)
580    root.mainloop()
581
582if __name__ == "__main__":
583    sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
584    sys.exit(make_gui())
585"#
586            )
587        } else {
588            format!(
589                r#"{shebang}
590# -*- coding: utf-8 -*-
591import re
592import sys
593
594def main_console() -> None:
595    print("Hello from uv-trampoline-console.exe", file=sys.stdout)
596    print("Hello from uv-trampoline-console.exe", file=sys.stderr)
597    for arg in sys.argv[1:]:
598        print(arg, file=sys.stderr)
599
600if __name__ == "__main__":
601    sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
602    sys.exit(main_console())
603"#
604            )
605        }
606    }
607
608    /// See [`uv-install-wheel::wheel::format_shebang`].
609    fn format_shebang(executable: impl AsRef<Path>) -> String {
610        // Convert the executable to a simplified path.
611        let executable = executable.as_ref().display().to_string();
612        format!("#!{executable}")
613    }
614
615    /// Creates a self-signed certificate and returns its path.
616    fn create_temp_certificate(temp_dir: &tempfile::TempDir) -> Result<(PathBuf, PathBuf)> {
617        use rcgen::{
618            CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair, KeyUsagePurpose, SanType,
619        };
620
621        let mut params = CertificateParams::default();
622        params.key_usages.push(KeyUsagePurpose::DigitalSignature);
623        params
624            .extended_key_usages
625            .push(ExtendedKeyUsagePurpose::CodeSigning);
626        params
627            .distinguished_name
628            .push(DnType::OrganizationName, "Astral Software Inc.");
629        params
630            .distinguished_name
631            .push(DnType::CommonName, "uv-test-signer");
632        params
633            .subject_alt_names
634            .push(SanType::DnsName("uv-test-signer".try_into()?));
635
636        let private_key = KeyPair::generate()?;
637        let public_cert = params.self_signed(&private_key)?;
638
639        let public_cert_path = temp_dir.path().join("uv-trampoline-test.crt");
640        let private_key_path = temp_dir.path().join("uv-trampoline-test.key");
641        fs_err::write(public_cert_path.as_path(), public_cert.pem())?;
642        fs_err::write(private_key_path.as_path(), private_key.serialize_pem())?;
643
644        Ok((public_cert_path, private_key_path))
645    }
646
647    /// Signs the given binary using `PowerShell`'s `Set-AuthenticodeSignature` with a temporary certificate.
648    fn sign_authenticode(bin_path: impl AsRef<Path>) {
649        let temp_dir = tempfile::TempDir::new().expect("Failed to create temporary directory");
650        let (public_cert, private_key) =
651            create_temp_certificate(&temp_dir).expect("Failed to create self-signed certificate");
652
653        // Instead of powershell, we rely on pwsh which supports CreateFromPemFile.
654        Command::new("pwsh")
655            .args([
656                "-NoProfile",
657                "-NonInteractive",
658                "-Command",
659                &format!(
660                    r"
661                    $ErrorActionPreference = 'Stop'
662                    Import-Module Microsoft.PowerShell.Security
663                    $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromPemFile('{}', '{}')
664                    Set-AuthenticodeSignature -FilePath '{}' -Certificate $cert;
665                    ",
666                    public_cert.display().to_string().replace('\'', "''"),
667                    private_key.display().to_string().replace('\'', "''"),
668                    bin_path.as_ref().display().to_string().replace('\'', "''"),
669                ),
670            ])
671            .env_remove("PSModulePath")
672            .assert()
673            .success();
674
675        println!("Signed binary: {}", bin_path.as_ref().display());
676    }
677
678    #[test]
679    fn console_script_launcher() -> Result<()> {
680        // Create Temp Dirs
681        let temp_dir = assert_fs::TempDir::new()?;
682        let console_bin_path = temp_dir.child("launcher.console.exe");
683
684        // Locate an arbitrary python installation from PATH
685        let python_executable_path = which("python")?;
686
687        // Generate Launcher Script
688        let launcher_console_script =
689            get_script_launcher(&format_shebang(&python_executable_path), false);
690
691        // Generate Launcher Payload
692        let console_launcher =
693            windows_script_launcher(&launcher_console_script, false, &python_executable_path)?;
694
695        // Create Launcher
696        File::create(console_bin_path.path())?.write_all(console_launcher.as_ref())?;
697
698        println!(
699            "Wrote Console Launcher in {}",
700            console_bin_path.path().display()
701        );
702
703        let stdout_predicate = "Hello from uv-trampoline-console.exe\r\n";
704        let stderr_predicate = "Hello from uv-trampoline-console.exe\r\n";
705
706        // Test Console Launcher
707        #[cfg(windows)]
708        Command::new(console_bin_path.path())
709            .assert()
710            .success()
711            .stdout(stdout_predicate)
712            .stderr(stderr_predicate);
713
714        let args_to_test = vec!["foo", "bar", "foo bar", "foo \"bar\"", "foo 'bar'"];
715        let stderr_predicate = format!("{}{}\r\n", stderr_predicate, args_to_test.join("\r\n"));
716
717        // Test Console Launcher (with args)
718        Command::new(console_bin_path.path())
719            .args(args_to_test)
720            .assert()
721            .success()
722            .stdout(stdout_predicate)
723            .stderr(stderr_predicate);
724
725        let launcher = Launcher::try_from_path(console_bin_path.path())
726            .expect("We should succeed at reading the launcher")
727            .expect("The launcher should be valid");
728
729        assert_eq!(launcher.kind, LauncherKind::Script);
730        assert_eq!(launcher.python_path, python_executable_path);
731
732        // Now code-sign the launcher and verify that it still works.
733        sign_authenticode(console_bin_path.path());
734
735        let stdout_predicate = "Hello from uv-trampoline-console.exe\r\n";
736        let stderr_predicate = "Hello from uv-trampoline-console.exe\r\n";
737        Command::new(console_bin_path.path())
738            .assert()
739            .success()
740            .stdout(stdout_predicate)
741            .stderr(stderr_predicate);
742
743        Ok(())
744    }
745
746    #[test]
747    fn console_python_launcher() -> Result<()> {
748        // Create Temp Dirs
749        let temp_dir = assert_fs::TempDir::new()?;
750        let console_bin_path = temp_dir.child("launcher.console.exe");
751
752        // Locate an arbitrary python installation from PATH
753        let python_executable_path = which("python")?;
754
755        // Generate Launcher Payload
756        let console_launcher =
757            windows_python_launcher(&python_executable_path, WindowMode::Console)?;
758
759        // Create Launcher
760        {
761            File::create(console_bin_path.path())?.write_all(console_launcher.as_ref())?;
762        }
763
764        println!(
765            "Wrote Python Launcher in {}",
766            console_bin_path.path().display()
767        );
768
769        // Test Console Launcher
770        Command::new(console_bin_path.path())
771            .arg("-c")
772            .arg("print('Hello from Python Launcher')")
773            .assert()
774            .success()
775            .stdout("Hello from Python Launcher\r\n");
776
777        let launcher = Launcher::try_from_path(console_bin_path.path())
778            .expect("We should succeed at reading the launcher")
779            .expect("The launcher should be valid");
780
781        assert_eq!(launcher.kind, LauncherKind::Python);
782        assert_eq!(launcher.python_path, python_executable_path);
783
784        // Now code-sign the launcher and verify that it still works.
785        sign_authenticode(console_bin_path.path());
786        Command::new(console_bin_path.path())
787            .arg("-c")
788            .arg("print('Hello from Python Launcher')")
789            .assert()
790            .success()
791            .stdout("Hello from Python Launcher\r\n");
792
793        Ok(())
794    }
795
796    #[test]
797    #[ignore = "This test will spawn a GUI and wait until you close the window."]
798    fn gui_launcher() -> Result<()> {
799        // Create Temp Dirs
800        let temp_dir = assert_fs::TempDir::new()?;
801        let gui_bin_path = temp_dir.child("launcher.gui.exe");
802
803        // Locate an arbitrary pythonw installation from PATH
804        let pythonw_executable_path = which("pythonw")?;
805
806        // Generate Launcher Script
807        let launcher_gui_script =
808            get_script_launcher(&format_shebang(&pythonw_executable_path), true);
809
810        // Generate Launcher Payload
811        let gui_launcher =
812            windows_script_launcher(&launcher_gui_script, true, &pythonw_executable_path)?;
813
814        // Create Launcher
815        {
816            File::create(gui_bin_path.path())?.write_all(gui_launcher.as_ref())?;
817        }
818
819        println!("Wrote GUI Launcher in {}", gui_bin_path.path().display());
820
821        // Test GUI Launcher
822        // NOTICE: This will spawn a GUI and will wait until you close the window.
823        Command::new(gui_bin_path.path()).assert().success();
824
825        Ok(())
826    }
827}