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#[cfg(windows)]
33const RT_RCDATA: u16 = 10;
34
35#[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#[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 #[cfg(not(windows))]
59 pub fn try_from_path(_path: &Path) -> Result<Option<Self>, Error> {
60 Ok(None)
61 }
62
63 #[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 #[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 #[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 #[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 #[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 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 fs_err::write(&temp_file, get_launcher_bin(is_gui)?)?;
151
152 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 let launcher = fs_err::read(&temp_file)?;
170 fs_err::remove_file(&temp_file)?;
171
172 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum WindowMode {
191 Console,
193 Windowed,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum LauncherKind {
202 Script,
204 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#[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#[cfg(windows)]
292fn write_resources(path: &Path, resources: &[(windows::core::PCWSTR, &[u8])]) -> Result<(), Error> {
293 #[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#[cfg(windows)]
334fn read_resource(
335 handle: windows::Win32::Foundation::HMODULE,
336 name: windows::core::PCWSTR,
337) -> Option<Vec<u8>> {
338 #[allow(unsafe_code)]
340 unsafe {
341 use windows::Win32::System::LibraryLoader::{
342 FindResourceW, LoadResource, LockResource, SizeofResource,
343 };
344 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 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 Some(std::slice::from_raw_parts(ptr, size as usize).to_vec())
367 }
368}
369
370#[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#[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 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 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 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 let launcher = fs_err::read(&temp_file)?;
439 fs_err::remove_file(temp_file)?;
440
441 Ok(launcher)
442}
443
444#[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#[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 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 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 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 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 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 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 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 fn format_shebang(executable: impl AsRef<Path>) -> String {
610 let executable = executable.as_ref().display().to_string();
612 format!("#!{executable}")
613 }
614
615 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 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 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 let temp_dir = assert_fs::TempDir::new()?;
682 let console_bin_path = temp_dir.child("launcher.console.exe");
683
684 let python_executable_path = which("python")?;
686
687 let launcher_console_script =
689 get_script_launcher(&format_shebang(&python_executable_path), false);
690
691 let console_launcher =
693 windows_script_launcher(&launcher_console_script, false, &python_executable_path)?;
694
695 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 #[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 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 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 let temp_dir = assert_fs::TempDir::new()?;
750 let console_bin_path = temp_dir.child("launcher.console.exe");
751
752 let python_executable_path = which("python")?;
754
755 let console_launcher =
757 windows_python_launcher(&python_executable_path, WindowMode::Console)?;
758
759 {
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 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 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 let temp_dir = assert_fs::TempDir::new()?;
801 let gui_bin_path = temp_dir.child("launcher.gui.exe");
802
803 let pythonw_executable_path = which("pythonw")?;
805
806 let launcher_gui_script =
808 get_script_launcher(&format_shebang(&pythonw_executable_path), true);
809
810 let gui_launcher =
812 windows_script_launcher(&launcher_gui_script, true, &pythonw_executable_path)?;
813
814 {
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 Command::new(gui_bin_path.path()).assert().success();
824
825 Ok(())
826 }
827}