From d943ff6074a76459cba3e4b53e8482023d8d142f Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:09:44 +0800 Subject: [PATCH 01/22] add Win32_System_LibraryLoader feature for LoadLibraryExW --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fe7f5acd5..21fab4301 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,7 +142,7 @@ walkdir = "2.5.0" which = "8.0.0" widestring = "1.1.0" winapi = { version = "0.3", features = ["wincon", "winreg"] } -windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams", "Win32_System_Console", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_Foundation", "Win32_UI_Shell", "Wdk_System_SystemServices"] } +windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams", "Win32_System_Console", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_Foundation", "Win32_UI_Shell", "Wdk_System_SystemServices", "Win32_System_LibraryLoader"] } wiremock = "0.6.3" xz2 = "0.1.7" zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] } From 92339ce29042f5c5a3f14cd85743adee0ba552cf Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:11:35 +0800 Subject: [PATCH 02/22] add libc crate for macos as well (for dlopen) --- qt/launcher/Cargo.toml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qt/launcher/Cargo.toml b/qt/launcher/Cargo.toml index 5fd1c9900..37c78fe14 100644 --- a/qt/launcher/Cargo.toml +++ b/qt/launcher/Cargo.toml @@ -14,16 +14,13 @@ anki_process.workspace = true anyhow.workspace = true camino.workspace = true dirs.workspace = true +libc.workspace = true locale_config.workspace = true serde_json.workspace = true -[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] -libc.workspace = true - [target.'cfg(windows)'.dependencies] windows.workspace = true widestring.workspace = true -libc.workspace = true libc-stdhandle.workspace = true [[bin]] From d9f94badf6f16d2afe4a3ef20acdd3047d79e74a Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:12:37 +0800 Subject: [PATCH 03/22] add helper scripts --- qt/launcher/src/libpython_nix.py | 10 ++++++++++ qt/launcher/src/libpython_win.py | 10 ++++++++++ 2 files changed, 20 insertions(+) create mode 100644 qt/launcher/src/libpython_nix.py create mode 100644 qt/launcher/src/libpython_win.py diff --git a/qt/launcher/src/libpython_nix.py b/qt/launcher/src/libpython_nix.py new file mode 100644 index 000000000..99a50e72a --- /dev/null +++ b/qt/launcher/src/libpython_nix.py @@ -0,0 +1,10 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import os +import sysconfig + +cfg = sysconfig.get_config_var +base = cfg("installed_base") or cfg("installed_platbase") +lib = cfg("LDLIBRARY") or cfg("INSTSONAME") +print(os.path.join(base, "lib", lib)) diff --git a/qt/launcher/src/libpython_win.py b/qt/launcher/src/libpython_win.py new file mode 100644 index 000000000..97a2d6131 --- /dev/null +++ b/qt/launcher/src/libpython_win.py @@ -0,0 +1,10 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import os +import sysconfig + +cfg = sysconfig.get_config_var +base = cfg("installed_base") or cfg("installed_platbase") +lib = "python" + cfg("py_version_nodot") + ".dll" +print(os.path.join(base, lib)) From 9d0ae31780a64ea1a626c0e99978e934c71d0467 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:14:51 +0800 Subject: [PATCH 04/22] add PyFfi --- qt/launcher/src/platform/mod.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index eec7634f1..3fe322ea0 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -139,3 +139,17 @@ pub fn ensure_os_supported() -> Result<()> { Ok(()) } + +pub type PyInitializeEx = extern "C" fn(initsigs: std::ffi::c_int); +pub type PyIsInitialized = extern "C" fn() -> std::ffi::c_int; +pub type PyRunSimpleString = extern "C" fn(command: *const std::ffi::c_char) -> std::ffi::c_int; +pub type PyFinalizeEx = extern "C" fn() -> std::ffi::c_int; + +#[allow(non_snake_case)] +struct PyFfi { + lib: *mut std::ffi::c_void, + Py_InitializeEx: PyInitializeEx, + Py_IsInitialized: PyIsInitialized, + PyRun_SimpleString: PyRunSimpleString, + Py_FinalizeEx: PyFinalizeEx, +} From f2c2d6d93a68f601d2c9c205937eee6da61ef2bd Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:19:15 +0800 Subject: [PATCH 05/22] add libpython_path --- qt/launcher/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index dab9435ea..9c1c4abb2 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -53,6 +53,7 @@ struct State { previous_version: Option, resources_dir: std::path::PathBuf, venv_folder: std::path::PathBuf, + libpython_path: std::path::PathBuf, /// system Python + PyQt6 library mode system_qt: bool, } @@ -132,6 +133,7 @@ fn run() -> Result<()> { && resources_dir.join("system_qt").exists(), resources_dir, venv_folder: uv_install_root.join(".venv"), + libpython_path: uv_install_root.join("libpath"), }; // Check for uninstall request from Windows uninstaller From 69e0fc3aae72ef2f7304a7ba847422a9b670f800 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:20:50 +0800 Subject: [PATCH 06/22] add get_libpython_path --- qt/launcher/src/main.rs | 61 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 9c1c4abb2..d8657da37 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -1033,7 +1033,7 @@ fn uv_command(state: &State) -> Result { Ok(command) } -fn build_python_command(state: &State, args: &[String]) -> Result { +fn _build_python_command(state: &State) -> Result { let python_exe = if cfg!(target_os = "windows") { let show_console = std::env::var("ANKI_CONSOLE").is_ok(); if show_console { @@ -1046,8 +1046,6 @@ fn build_python_command(state: &State, args: &[String]) -> Result { }; let mut cmd = Command::new(&python_exe); - cmd.args(["-c", "import aqt, sys; sys.argv[0] = 'Anki'; aqt.run()"]); - cmd.args(args); // tell the Python code it was invoked by the launcher, and updating is // available cmd.env("ANKI_LAUNCHER", std::env::current_exe()?.utf8()?.as_str()); @@ -1060,6 +1058,63 @@ fn build_python_command(state: &State, args: &[String]) -> Result { Ok(cmd) } +fn build_python_command(state: &State, args: &[String]) -> Result { + let mut cmd = _build_python_command(state)?; + cmd.args(["-c", "import aqt, sys; sys.argv[0] = 'Anki'; aqt.run()"]); + cmd.args(args); + Ok(cmd) +} + +fn get_libpython_path(state: &State) -> Result { + // we can cache this, as it can only change after syncing the project + // as it stands, we already expect there to be a trusted python exe in + // a particular place so this doesn't seem too concerning security-wise + // TODO: let-chains... + if let Ok(path) = read_file(&state.libpython_path) { + if let Ok(rel_path) = String::from_utf8(path).map(std::path::PathBuf::from) { + if let Ok(lib_path) = state.uv_install_root.join(rel_path).canonicalize() { + // make sure we're still within AnkiProgramFiles + if lib_path.strip_prefix(&state.uv_install_root).is_ok() { + return Ok(lib_path); + } + } + } + } + + let mut cmd = _build_python_command(state)?; + // NOTE: + // we can check which sysconfig vars are available + // with `sysconfig.get_config_vars()`. very limited on + // windows pre-3.13 (probably because no ./configure) + // from what i've found, `installed_base` seems to be + // available on 3.9/3.13 on both windows and linux. + // `LIBDIR` and `LDLIBRARY` aren't present on 3.9/win. + // on win, we can't use python3.dll, only python3XX.dll + let script = if cfg!(windows) { + include_str!("libpython_win.py") + } else { + include_str!("libpython_nix.py") + } + .trim(); + + cmd.args(["-c", script]); + + let output = cmd.utf8_output()?; + let lib_path_str = output.stdout.trim(); + let lib_path = std::path::PathBuf::from(lib_path_str); + if !lib_path.exists() { + anyhow::bail!("library path doesn't exist: {lib_path:?}"); + } + + let cached_path = lib_path + .strip_prefix(&state.uv_install_root)? + .to_str() + .ok_or_else(|| anyhow::anyhow!("failed to make relative path"))?; + let _ = write_file(&state.libpython_path, cached_path); + + Ok(lib_path) +} + fn is_mirror_enabled(state: &State) -> bool { state.mirror_path.exists() } From a882a878002af0e5c12b0716532890afd55a1553 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:21:32 +0800 Subject: [PATCH 07/22] clear lib path cache on sync --- qt/launcher/src/main.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index d8657da37..442a1fc64 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -276,6 +276,9 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re // Remove sync marker before attempting sync let _ = remove_file(&state.sync_complete_marker); + // clear possibly invalidated libpython path cache + let _ = remove_file(&state.libpython_path); + println!("{}\n", state.tr.launcher_updating_anki()); let python_version_trimmed = if state.user_python_version_path.exists() { From ec75658a0a8d90cc5bd735c80c8f75199022e9df Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:23:35 +0800 Subject: [PATCH 08/22] add PyFfi impl --- qt/launcher/src/platform/mod.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index 3fe322ea0..ee4501e1e 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -13,6 +13,7 @@ pub mod windows; use std::path::PathBuf; use anki_process::CommandExt; +use anyhow::ensure; use anyhow::Context; use anyhow::Result; @@ -153,3 +154,21 @@ struct PyFfi { PyRun_SimpleString: PyRunSimpleString, Py_FinalizeEx: PyFinalizeEx, } + +impl PyFfi { + fn run(self, preamble: impl AsRef) -> Result<()> { + (self.Py_InitializeEx)(1); + + let res = (self.Py_IsInitialized)(); + ensure!(res != 0, "failed to initialise"); + let res = (self.PyRun_SimpleString)(preamble.as_ref().as_ptr()); + ensure!(res == 0, "failed to run preamble"); + // test importing aqt first before falling back to usual launch + let res = (self.PyRun_SimpleString)(c"import aqt".as_ptr()); + ensure!(res == 0, "failed to import aqt"); + // from here on, don't fallback if we fail + let _ = (self.PyRun_SimpleString)(c"aqt.run()".as_ptr()); + + Ok(()) + } +} From 6e5b0c96efb9d5c60a0116978c56b955cbcab210 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:41:55 +0800 Subject: [PATCH 09/22] add PyFfi impl for nixes --- qt/launcher/src/platform/mod.rs | 3 + qt/launcher/src/platform/nix.rs | 104 ++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 qt/launcher/src/platform/nix.rs diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index ee4501e1e..8ef786dca 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -10,6 +10,9 @@ pub mod mac; #[cfg(target_os = "windows")] pub mod windows; +#[cfg(unix)] +pub mod nix; + use std::path::PathBuf; use anki_process::CommandExt; diff --git a/qt/launcher/src/platform/nix.rs b/qt/launcher/src/platform/nix.rs new file mode 100644 index 000000000..bac8c45c3 --- /dev/null +++ b/qt/launcher/src/platform/nix.rs @@ -0,0 +1,104 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::ffi::CStr; +use std::ffi::CString; +use std::os::unix::prelude::OsStrExt; + +use anki_io::ToUtf8Path; +use anyhow::anyhow; +use anyhow::Result; + +use crate::get_libpython_path; +use crate::platform::PyFfi; +use crate::State; + +impl Drop for PyFfi { + fn drop(&mut self) { + unsafe { + (self.Py_FinalizeEx)(); + libc::dlclose(self.lib) + }; + } +} + +macro_rules! load_sym { + ($lib:expr, $name:literal) => {{ + libc::dlerror(); + let sym = libc::dlsym($lib, $name.as_ptr()); + if sym.is_null() { + let dlerror_str = CStr::from_ptr(libc::dlerror()).to_str()?; + anyhow::bail!("failed to load {}: {dlerror_str}", $name.to_string_lossy()); + } + std::mem::transmute(sym) + }}; +} + +impl PyFfi { + #[allow(non_snake_case)] + pub fn load(path: impl AsRef) -> Result { + unsafe { + libc::dlerror(); + let lib = libc::dlopen( + CString::new(path.as_ref().as_os_str().as_bytes())?.as_ptr(), + libc::RTLD_LAZY | libc::RTLD_GLOBAL, + ); + if lib.is_null() { + let dlerror_str = CStr::from_ptr(libc::dlerror()).to_str()?; + anyhow::bail!("failed to load library: {dlerror_str}"); + } + + #[allow(clippy::missing_transmute_annotations)] // they're not missing + Ok(PyFfi { + Py_InitializeEx: load_sym!(lib, c"Py_InitializeEx"), + Py_IsInitialized: load_sym!(lib, c"Py_IsInitialized"), + PyRun_SimpleString: load_sym!(lib, c"PyRun_SimpleString"), + Py_FinalizeEx: load_sym!(lib, c"Py_FinalizeEx"), + lib, + }) + } + } +} + +pub fn run(state: &State) -> Result<()> { + let lib_path = get_libpython_path(state)?; + + // NOTE: activate venv before loading lib + let path = std::env::var("PATH")?; + let paths = std::env::split_paths(&path); + let path = std::env::join_paths(std::iter::once(state.venv_folder.join("bin")).chain(paths))?; + std::env::set_var("PATH", path); + std::env::set_var("VIRTUAL_ENV", &state.venv_folder); + std::env::set_var("PYTHONHOME", ""); + + std::env::set_var("ANKI_LAUNCHER", std::env::current_exe()?.utf8()?.as_str()); + std::env::set_var("ANKI_LAUNCHER_UV", state.uv_path.utf8()?.as_str()); + std::env::set_var("UV_PROJECT", state.uv_install_root.utf8()?.as_str()); + std::env::remove_var("SSLKEYLOGFILE"); + + let ffi = PyFfi::load(lib_path)?; + + // NOTE: sys.argv would normally be set via PyConfig, but we don't have it here + let args: String = std::env::args() + .skip(1) + .map(|s| format!(r#","{s}""#)) + .collect(); + + // NOTE: + // the venv activation script doesn't seem to be + // necessary for linux, only PATH and VIRTUAL_ENV + // but just call it anyway to have a standard setup + let venv_activate_path = state.venv_folder.join("bin/activate_this.py"); + let venv_activate_path = venv_activate_path + .as_os_str() + .to_str() + .ok_or_else(|| anyhow!("failed to get venv activation script path"))?; + + let preamble = std::ffi::CString::new(format!( + r#"import sys, runpy; sys.argv = ['Anki'{args}]; runpy.run_path("{venv_activate_path}")"#, + ))?; + + ffi.run(preamble)?; + + Ok(()) +} From de531e07ab3aa3a3577c333ffc0853910e97e27a Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:42:11 +0800 Subject: [PATCH 10/22] add PyFfi impl for windows --- qt/launcher/src/platform/windows.rs | 118 ++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/qt/launcher/src/platform/windows.rs b/qt/launcher/src/platform/windows.rs index d20c9a8b4..bae66f83e 100644 --- a/qt/launcher/src/platform/windows.rs +++ b/qt/launcher/src/platform/windows.rs @@ -1,17 +1,27 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use std::ffi::CString; use std::io::stdin; +use std::os::windows::ffi::OsStrExt; use std::process::Command; +use anki_io::ToUtf8Path; +use anyhow::anyhow; use anyhow::Context; use anyhow::Result; use widestring::u16cstr; +use windows::core::PCSTR; use windows::core::PCWSTR; use windows::Wdk::System::SystemServices::RtlGetVersion; +use windows::Win32::Foundation::FreeLibrary; +use windows::Win32::Foundation::HMODULE; use windows::Win32::System::Console::AttachConsole; use windows::Win32::System::Console::GetConsoleWindow; use windows::Win32::System::Console::ATTACH_PARENT_PROCESS; +use windows::Win32::System::LibraryLoader::GetProcAddress; +use windows::Win32::System::LibraryLoader::LoadLibraryExW; +use windows::Win32::System::LibraryLoader::LOAD_LIBRARY_FLAGS; use windows::Win32::System::Registry::RegCloseKey; use windows::Win32::System::Registry::RegOpenKeyExW; use windows::Win32::System::Registry::RegQueryValueExW; @@ -22,6 +32,10 @@ use windows::Win32::System::Registry::REG_SZ; use windows::Win32::System::SystemInformation::OSVERSIONINFOW; use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID; +use crate::get_libpython_path; +use crate::platform::PyFfi; +use crate::State; + /// Returns true if running on Windows 10 (not Windows 11) fn is_windows_10() -> bool { unsafe { @@ -261,3 +275,107 @@ pub fn prepare_to_launch_normally() { attach_to_parent_console(); } + +impl Drop for PyFfi { + fn drop(&mut self) { + unsafe { + (self.Py_FinalizeEx)(); + let _ = FreeLibrary(HMODULE(self.lib)); + }; + } +} + +macro_rules! load_sym { + ($lib:expr, $name:literal) => { + std::mem::transmute( + GetProcAddress($lib, PCSTR::from_raw($name.as_ptr().cast())) + .ok_or_else(|| anyhow!("failed to load {}", $name.to_string_lossy()))?, + ) + }; +} + +impl PyFfi { + #[allow(non_snake_case)] + pub fn load(path: impl AsRef) -> Result { + unsafe { + let wide_filename: Vec = path + .as_ref() + .as_os_str() + .encode_wide() + .chain(Some(0)) + .collect(); + + let lib = LoadLibraryExW( + PCWSTR::from_raw(wide_filename.as_ptr()), + None, + LOAD_LIBRARY_FLAGS::default(), + )?; + + #[allow(clippy::missing_transmute_annotations)] // they're not missing + Ok(PyFfi { + Py_InitializeEx: load_sym!(lib, c"Py_InitializeEx"), + Py_IsInitialized: load_sym!(lib, c"Py_IsInitialized"), + PyRun_SimpleString: load_sym!(lib, c"PyRun_SimpleString"), + Py_FinalizeEx: load_sym!(lib, c"Py_FinalizeEx"), + lib: lib.0, + }) + } + } +} + +pub fn run(state: &State, console: bool) -> Result<()> { + let lib_path = get_libpython_path(state)?; + + // NOTE: needed for 3.9 on windows (not for 3.13) + std::env::set_current_dir( + lib_path + .parent() + .ok_or_else(|| anyhow!("expected parent dir for lib_path: {lib_path:?}"))?, + )?; + + let path = std::env::var("PATH")?; + let paths = std::env::split_paths(&path); + let path = + std::env::join_paths(std::iter::once(state.venv_folder.join("Scripts")).chain(paths))?; + std::env::set_var("PATH", path); + std::env::set_var("VIRTUAL_ENV", &state.venv_folder); + std::env::set_var("PYTHONHOME", ""); + + std::env::set_var("ANKI_LAUNCHER", std::env::current_exe()?.utf8()?.as_str()); + std::env::set_var("ANKI_LAUNCHER_UV", state.uv_path.utf8()?.as_str()); + std::env::set_var("UV_PROJECT", state.uv_install_root.utf8()?.as_str()); + std::env::remove_var("SSLKEYLOGFILE"); + + let ffi = PyFfi::load(lib_path)?; + + let args: String = std::env::args() + .skip(1) + .map(|s| format!(r#","{s}""#)) + .collect::() + .replace('\\', "\\\\"); + + let venv_activate_path = state.venv_folder.join("Scripts\\activate_this.py"); + let venv_activate_path = venv_activate_path + .as_os_str() + .to_str() + .ok_or_else(|| anyhow!("failed to get venv activation script path"))? + .replace('\\', "\\\\"); + + // NOTE: without windows_subsystem=console or pythonw, + // we need to reconnect stdin/stdout/stderr within the interp + // reconnect_stdio_to_console doesn't make a difference here + let console_snippet = if console { + r#" sys.stdout = sys.stderr = open("CONOUT$", "w"); sys.stdin = open("CONIN$", "r");"# + } else { + "" + }; + + // NOTE: windows needs the venv activation script + let preamble = CString::new(format!( + r#"import sys, runpy; sys.argv = ['Anki'{args}]; runpy.run_path("{venv_activate_path}");{console_snippet}"#, + ))?; + + ffi.run(preamble)?; + + Ok(()) +} From e31c8b4586cbc29b88cb5115c78de51b7576ede0 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:42:29 +0800 Subject: [PATCH 11/22] add run_anki_normally --- qt/launcher/src/platform/mod.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index 8ef786dca..6bff124be 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -113,6 +113,30 @@ pub fn launch_anki_normally(mut cmd: std::process::Command) -> Result<()> { Ok(()) } +pub fn _run_anki_normally(state: &crate::State) -> Result<()> { + #[cfg(windows)] + { + let console = std::env::var("ANKI_CONSOLE").is_ok(); + if console { + // no pythonw.exe available for us to use + ensure_terminal_shown()?; + } + crate::platform::windows::prepare_to_launch_normally(); + windows::run(state, console)?; + } + #[cfg(unix)] + nix::run(state)?; + Ok(()) +} + +pub fn run_anki_normally(state: &crate::State) -> bool { + if let Err(e) = _run_anki_normally(state) { + eprintln!("failed to run as embedded: {e:?}"); + return false; + } + true +} + #[cfg(windows)] pub use windows::ensure_terminal_shown; From c22e38f4a8464941de801c48c07a3f0f4be2d92a Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:42:46 +0800 Subject: [PATCH 12/22] launch anki embeddedly(?), fallback to usual launch --- qt/launcher/src/main.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 442a1fc64..e2db8d223 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -28,6 +28,7 @@ use crate::platform::get_exe_and_resources_dirs; use crate::platform::get_uv_binary_name; use crate::platform::launch_anki_normally; use crate::platform::respawn_launcher; +use crate::platform::run_anki_normally; mod platform; @@ -158,9 +159,11 @@ fn run() -> Result<()> { if !launcher_requested && !pyproject_has_changed && !different_launcher { // If no launcher request and venv is already up to date, launch Anki normally - let args: Vec = std::env::args().skip(1).collect(); - let cmd = build_python_command(&state, &args)?; - launch_anki_normally(cmd)?; + if std::env::var("ANKI_LAUNCHER_NO_EMBED").is_ok() || !run_anki_normally(&state) { + let args: Vec = std::env::args().skip(1).collect(); + let cmd = build_python_command(&state, &args)?; + launch_anki_normally(cmd)?; + } return Ok(()); } From 36ca68a869c0bf1cdfaa397832ae19891cc247a9 Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 17:49:19 +0800 Subject: [PATCH 13/22] add bindgen'd py39/313 PyConfig definitions --- qt/launcher/src/platform/mod.rs | 3 + qt/launcher/src/platform/py313.rs | 93 +++++++++++++++++++++++++++++++ qt/launcher/src/platform/py39.rs | 75 +++++++++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 qt/launcher/src/platform/py313.rs create mode 100644 qt/launcher/src/platform/py39.rs diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index 6bff124be..5b37ae3f1 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -13,6 +13,9 @@ pub mod windows; #[cfg(unix)] pub mod nix; +mod py313; +mod py39; + use std::path::PathBuf; use anki_process::CommandExt; diff --git a/qt/launcher/src/platform/py313.rs b/qt/launcher/src/platform/py313.rs new file mode 100644 index 000000000..1f9ddd241 --- /dev/null +++ b/qt/launcher/src/platform/py313.rs @@ -0,0 +1,93 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use libc::wchar_t; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct PyStatus { + pub _type: ::std::os::raw::c_uint, + pub func: *const ::std::os::raw::c_char, + pub err_msg: *const ::std::os::raw::c_char, + pub exitcode: ::std::os::raw::c_int, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct PyWideStringList { + pub length: isize, + pub items: *mut *mut wchar_t, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct PyConfig { + pub _config_init: ::std::os::raw::c_int, + pub isolated: ::std::os::raw::c_int, + pub use_environment: ::std::os::raw::c_int, + pub dev_mode: ::std::os::raw::c_int, + pub install_signal_handlers: ::std::os::raw::c_int, + pub use_hash_seed: ::std::os::raw::c_int, + pub hash_seed: ::std::os::raw::c_ulong, + pub faulthandler: ::std::os::raw::c_int, + pub tracemalloc: ::std::os::raw::c_int, + pub perf_profiling: ::std::os::raw::c_int, + pub import_time: ::std::os::raw::c_int, + pub code_debug_ranges: ::std::os::raw::c_int, + pub show_ref_count: ::std::os::raw::c_int, + pub dump_refs: ::std::os::raw::c_int, + pub dump_refs_file: *mut wchar_t, + pub malloc_stats: ::std::os::raw::c_int, + pub filesystem_encoding: *mut wchar_t, + pub filesystem_errors: *mut wchar_t, + pub pycache_prefix: *mut wchar_t, + pub parse_argv: ::std::os::raw::c_int, + pub orig_argv: PyWideStringList, + pub argv: PyWideStringList, + pub xoptions: PyWideStringList, + pub warnoptions: PyWideStringList, + pub site_import: ::std::os::raw::c_int, + pub bytes_warning: ::std::os::raw::c_int, + pub warn_default_encoding: ::std::os::raw::c_int, + pub inspect: ::std::os::raw::c_int, + pub interactive: ::std::os::raw::c_int, + pub optimization_level: ::std::os::raw::c_int, + pub parser_debug: ::std::os::raw::c_int, + pub write_bytecode: ::std::os::raw::c_int, + pub verbose: ::std::os::raw::c_int, + pub quiet: ::std::os::raw::c_int, + pub user_site_directory: ::std::os::raw::c_int, + pub configure_c_stdio: ::std::os::raw::c_int, + pub buffered_stdio: ::std::os::raw::c_int, + pub stdio_encoding: *mut wchar_t, + pub stdio_errors: *mut wchar_t, + #[cfg(windows)] + pub legacy_windows_stdio: ::std::os::raw::c_int, + pub check_hash_pycs_mode: *mut wchar_t, + pub use_frozen_modules: ::std::os::raw::c_int, + pub safe_path: ::std::os::raw::c_int, + pub int_max_str_digits: ::std::os::raw::c_int, + pub cpu_count: ::std::os::raw::c_int, + pub pathconfig_warnings: ::std::os::raw::c_int, + pub program_name: *mut wchar_t, + pub pythonpath_env: *mut wchar_t, + pub home: *mut wchar_t, + pub platlibdir: *mut wchar_t, + pub module_search_paths_set: ::std::os::raw::c_int, + pub module_search_paths: PyWideStringList, + pub stdlib_dir: *mut wchar_t, + pub executable: *mut wchar_t, + pub base_executable: *mut wchar_t, + pub prefix: *mut wchar_t, + pub base_prefix: *mut wchar_t, + pub exec_prefix: *mut wchar_t, + pub base_exec_prefix: *mut wchar_t, + pub skip_source_first_line: ::std::os::raw::c_int, + pub run_command: *mut wchar_t, + pub run_module: *mut wchar_t, + pub run_filename: *mut wchar_t, + pub sys_path_0: *mut wchar_t, + pub _install_importlib: ::std::os::raw::c_int, + pub _init_main: ::std::os::raw::c_int, + pub _is_python_build: ::std::os::raw::c_int, +} diff --git a/qt/launcher/src/platform/py39.rs b/qt/launcher/src/platform/py39.rs new file mode 100644 index 000000000..ebb42b0a6 --- /dev/null +++ b/qt/launcher/src/platform/py39.rs @@ -0,0 +1,75 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use libc::wchar_t; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct PyWideStringList { + pub length: ::std::os::raw::c_longlong, + pub items: *mut *mut wchar_t, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct PyConfig { + pub _config_init: ::std::os::raw::c_int, + pub isolated: ::std::os::raw::c_int, + pub use_environment: ::std::os::raw::c_int, + pub dev_mode: ::std::os::raw::c_int, + pub install_signal_handlers: ::std::os::raw::c_int, + pub use_hash_seed: ::std::os::raw::c_int, + pub hash_seed: ::std::os::raw::c_ulong, + pub faulthandler: ::std::os::raw::c_int, + pub _use_peg_parser: ::std::os::raw::c_int, + pub tracemalloc: ::std::os::raw::c_int, + pub import_time: ::std::os::raw::c_int, + pub show_ref_count: ::std::os::raw::c_int, + pub dump_refs: ::std::os::raw::c_int, + pub malloc_stats: ::std::os::raw::c_int, + pub filesystem_encoding: *mut wchar_t, + pub filesystem_errors: *mut wchar_t, + pub pycache_prefix: *mut wchar_t, + pub parse_argv: ::std::os::raw::c_int, + pub argv: PyWideStringList, + pub program_name: *mut wchar_t, + pub xoptions: PyWideStringList, + pub warnoptions: PyWideStringList, + pub site_import: ::std::os::raw::c_int, + pub bytes_warning: ::std::os::raw::c_int, + pub inspect: ::std::os::raw::c_int, + pub interactive: ::std::os::raw::c_int, + pub optimization_level: ::std::os::raw::c_int, + pub parser_debug: ::std::os::raw::c_int, + pub write_bytecode: ::std::os::raw::c_int, + pub verbose: ::std::os::raw::c_int, + pub quiet: ::std::os::raw::c_int, + pub user_site_directory: ::std::os::raw::c_int, + pub configure_c_stdio: ::std::os::raw::c_int, + pub buffered_stdio: ::std::os::raw::c_int, + pub stdio_encoding: *mut wchar_t, + pub stdio_errors: *mut wchar_t, + #[cfg(windows)] + pub legacy_windows_stdio: ::std::os::raw::c_int, + pub check_hash_pycs_mode: *mut wchar_t, + pub pathconfig_warnings: ::std::os::raw::c_int, + pub pythonpath_env: *mut wchar_t, + pub home: *mut wchar_t, + pub module_search_paths_set: ::std::os::raw::c_int, + pub module_search_paths: PyWideStringList, + pub executable: *mut wchar_t, + pub base_executable: *mut wchar_t, + pub prefix: *mut wchar_t, + pub base_prefix: *mut wchar_t, + pub exec_prefix: *mut wchar_t, + pub base_exec_prefix: *mut wchar_t, + pub platlibdir: *mut wchar_t, + pub skip_source_first_line: ::std::os::raw::c_int, + pub run_command: *mut wchar_t, + pub run_module: *mut wchar_t, + pub run_filename: *mut wchar_t, + pub _install_importlib: ::std::os::raw::c_int, + pub _init_main: ::std::os::raw::c_int, + pub _isolated_interpreter: ::std::os::raw::c_int, + pub _orig_argv: PyWideStringList, +} From 84619fd099f7e55ae679d5826b26bdcbaf37940a Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 17:50:42 +0800 Subject: [PATCH 14/22] add ffi types --- qt/launcher/src/platform/mod.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index 5b37ae3f1..62d3107ed 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -171,10 +171,24 @@ pub fn ensure_os_supported() -> Result<()> { Ok(()) } -pub type PyInitializeEx = extern "C" fn(initsigs: std::ffi::c_int); pub type PyIsInitialized = extern "C" fn() -> std::ffi::c_int; pub type PyRunSimpleString = extern "C" fn(command: *const std::ffi::c_char) -> std::ffi::c_int; pub type PyFinalizeEx = extern "C" fn() -> std::ffi::c_int; +pub type PyConfigInitPythonConfig = extern "C" fn(*mut std::ffi::c_void); +// WARN: py39 and py313's PyStatus are identical +// check if this remains true in future versions +pub type PyConfigSetBytesString = extern "C" fn( + config: *mut std::ffi::c_void, + config_str: *mut *mut libc::wchar_t, + str_: *const std::os::raw::c_char, +) -> py313::PyStatus; +pub type PyConfigSetBytesArgv = extern "C" fn( + config: *mut std::ffi::c_void, + argc: isize, + argv: *const *mut std::os::raw::c_char, +) -> py313::PyStatus; +pub type PyInitializeFromConfig = extern "C" fn(*const std::ffi::c_void) -> py313::PyStatus; +pub type PyStatusException = extern "C" fn(err: py313::PyStatus) -> std::os::raw::c_int; #[allow(non_snake_case)] struct PyFfi { From a64aad5e485f7c3a90079e86ce452795a87162ed Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 17:53:09 +0800 Subject: [PATCH 15/22] refactor get_libpython_path into get_python_env_info instead of just libpython's path we now get and cache the nodot version and venv bin path --- qt/launcher/src/libpython_nix.py | 5 +- qt/launcher/src/libpython_win.py | 7 ++- qt/launcher/src/main.rs | 105 +++++++++++++++++++++++++------ 3 files changed, 95 insertions(+), 22 deletions(-) diff --git a/qt/launcher/src/libpython_nix.py b/qt/launcher/src/libpython_nix.py index 99a50e72a..3f5c7fb3e 100644 --- a/qt/launcher/src/libpython_nix.py +++ b/qt/launcher/src/libpython_nix.py @@ -1,10 +1,13 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import json import os +import sys import sysconfig cfg = sysconfig.get_config_var base = cfg("installed_base") or cfg("installed_platbase") lib = cfg("LDLIBRARY") or cfg("INSTSONAME") -print(os.path.join(base, "lib", lib)) +version = cfg("py_version_nodot") +print(json.dumps([version, os.path.join(base, "lib", lib), sys.executable])) diff --git a/qt/launcher/src/libpython_win.py b/qt/launcher/src/libpython_win.py index 97a2d6131..c4699e57e 100644 --- a/qt/launcher/src/libpython_win.py +++ b/qt/launcher/src/libpython_win.py @@ -1,10 +1,13 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import json import os +import sys import sysconfig cfg = sysconfig.get_config_var base = cfg("installed_base") or cfg("installed_platbase") -lib = "python" + cfg("py_version_nodot") + ".dll" -print(os.path.join(base, lib)) +version = cfg("py_version_nodot") +lib = "python" + version + ".dll" +print(json.dumps([version, os.path.join(base, lib), sys.executable])) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index e2db8d223..3e27da5bc 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -3,9 +3,13 @@ #![windows_subsystem = "windows"] +use std::ffi::CString; use std::io::stdin; use std::io::stdout; use std::io::Write; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; use std::process::Command; use std::time::SystemTime; use std::time::UNIX_EPOCH; @@ -13,6 +17,7 @@ use std::time::UNIX_EPOCH; use anki_i18n::I18n; use anki_io::copy_file; use anki_io::create_dir_all; +use anki_io::create_file; use anki_io::modified_time; use anki_io::read_file; use anki_io::remove_file; @@ -54,7 +59,7 @@ struct State { previous_version: Option, resources_dir: std::path::PathBuf, venv_folder: std::path::PathBuf, - libpython_path: std::path::PathBuf, + libpython_info: std::path::PathBuf, /// system Python + PyQt6 library mode system_qt: bool, } @@ -134,7 +139,7 @@ fn run() -> Result<()> { && resources_dir.join("system_qt").exists(), resources_dir, venv_folder: uv_install_root.join(".venv"), - libpython_path: uv_install_root.join("libpath"), + libpython_info: uv_install_root.join(".cached-info"), }; // Check for uninstall request from Windows uninstaller @@ -279,8 +284,8 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re // Remove sync marker before attempting sync let _ = remove_file(&state.sync_complete_marker); - // clear possibly invalidated libpython path cache - let _ = remove_file(&state.libpython_path); + // clear possibly invalidated ibpython info cache + let _ = remove_file(&state.libpython_info); println!("{}\n", state.tr.launcher_updating_anki()); @@ -1071,17 +1076,66 @@ fn build_python_command(state: &State, args: &[String]) -> Result { Ok(cmd) } -fn get_libpython_path(state: &State) -> Result { +/// Normalize a path, removing things like `.` and `..` w/o following symlinks +/// NOTE: lifted from https://github.com/rust-lang/cargo/blob/28b79ea2b7b6d922d3ee85a935e63deed42db9c1/crates/cargo-util/src/paths.rs#L84 +pub fn normalize_path(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(Component::RootDir); + } + Component::CurDir => {} + Component::ParentDir => { + if ret.ends_with(Component::ParentDir) { + ret.push(Component::ParentDir); + } else { + let popped = ret.pop(); + if !popped && !ret.has_root() { + ret.push(Component::ParentDir); + } + } + } + Component::Normal(c) => { + ret.push(c); + } + } + } + ret +} + +fn get_python_env_info(state: &State) -> Result<(String, std::path::PathBuf, CString)> { // we can cache this, as it can only change after syncing the project // as it stands, we already expect there to be a trusted python exe in // a particular place so this doesn't seem too concerning security-wise + type Cache = (String, PathBuf, PathBuf); // TODO: let-chains... - if let Ok(path) = read_file(&state.libpython_path) { - if let Ok(rel_path) = String::from_utf8(path).map(std::path::PathBuf::from) { - if let Ok(lib_path) = state.uv_install_root.join(rel_path).canonicalize() { - // make sure we're still within AnkiProgramFiles - if lib_path.strip_prefix(&state.uv_install_root).is_ok() { - return Ok(lib_path); + if let Ok(cached) = read_file(&state.libpython_info) { + if let Ok(cached) = String::from_utf8(cached) { + if let Ok((version, lib_path, exec_path)) = serde_json::from_str::(&cached) { + if let Ok(lib_path) = state.uv_install_root.join(lib_path).canonicalize() { + // can't use canonicalise here as it follows symlinks, + // we need bin to be in the venv for it to know where + // to find the pyvenv.cfg that's in the parent dir + let exec_path = normalize_path(&state.uv_install_root.join(exec_path)); + // make sure we're still within AnkiProgramFiles... + if lib_path.strip_prefix(&state.uv_install_root).is_ok() + && exec_path.strip_prefix(&state.uv_install_root).is_ok() + { + return Ok(( + version, + lib_path, + CString::new(exec_path.as_os_str().as_encoded_bytes())?, + )); + } } } } @@ -1106,19 +1160,32 @@ fn get_libpython_path(state: &State) -> Result { cmd.args(["-c", script]); let output = cmd.utf8_output()?; - let lib_path_str = output.stdout.trim(); - let lib_path = std::path::PathBuf::from(lib_path_str); + let output = output.stdout.trim(); + + let (version, lib_path, exec_path): Cache = serde_json::from_str(output)?; + if !lib_path.exists() { anyhow::bail!("library path doesn't exist: {lib_path:?}"); } - let cached_path = lib_path - .strip_prefix(&state.uv_install_root)? - .to_str() - .ok_or_else(|| anyhow::anyhow!("failed to make relative path"))?; - let _ = write_file(&state.libpython_path, cached_path); + if !exec_path.exists() { + anyhow::bail!("exec path doesn't exist: {exec_path:?}"); + } - Ok(lib_path) + if let Ok(file) = create_file(&state.libpython_info) { + let cached = ( + version.clone(), + lib_path.strip_prefix(&state.uv_install_root)?, + exec_path.strip_prefix(&state.uv_install_root)?, + ); + let _ = serde_json::to_writer(file, &cached); + } + + Ok(( + version.to_owned(), + lib_path, + CString::new(exec_path.as_os_str().as_encoded_bytes())?, + )) } fn is_mirror_enabled(state: &State) -> bool { From 328179643540798cfc8977f757b3ff44987d93ed Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 17:54:53 +0800 Subject: [PATCH 16/22] add new ffi methods we can't rely on the very high level layer alone anymore --- qt/launcher/src/platform/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index 62d3107ed..3bb46dfee 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -16,6 +16,7 @@ pub mod nix; mod py313; mod py39; +use std::ffi::CString; use std::path::PathBuf; use anki_process::CommandExt; @@ -192,11 +193,16 @@ pub type PyStatusException = extern "C" fn(err: py313::PyStatus) -> std::os::raw #[allow(non_snake_case)] struct PyFfi { + exec: CString, lib: *mut std::ffi::c_void, - Py_InitializeEx: PyInitializeEx, Py_IsInitialized: PyIsInitialized, PyRun_SimpleString: PyRunSimpleString, Py_FinalizeEx: PyFinalizeEx, + PyConfig_InitPythonConfig: PyConfigInitPythonConfig, + PyConfig_SetBytesString: PyConfigSetBytesString, + Py_InitializeFromConfig: PyInitializeFromConfig, + PyConfig_SetBytesArgv: PyConfigSetBytesArgv, + PyStatus_Exception: PyStatusException, } impl PyFfi { From cd156d85240fc4e914526a417f265a6fc8de9d1c Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 17:56:39 +0800 Subject: [PATCH 17/22] add PyConfigExt and refactor PyFfi impl --- qt/launcher/src/platform/mod.rs | 93 ++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index 3bb46dfee..cc16b2364 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -16,10 +16,12 @@ pub mod nix; mod py313; mod py39; +use std::ffi::CStr; use std::ffi::CString; use std::path::PathBuf; use anki_process::CommandExt; +use anyhow::anyhow; use anyhow::ensure; use anyhow::Context; use anyhow::Result; @@ -205,17 +207,94 @@ struct PyFfi { PyStatus_Exception: PyStatusException, } -impl PyFfi { - fn run(self, preamble: impl AsRef) -> Result<()> { - (self.Py_InitializeEx)(1); +trait PyConfigExt { + fn init(ffi: &PyFfi) -> Self + where + Self: Sized; + fn set_exec(&mut self, ffi: &PyFfi) -> Result<&mut Self>; + fn set_argv(&mut self, ffi: &PyFfi) -> Result<&mut Self>; +} + +macro_rules! impl_pyconfig { + ($($pyconfig:path),* $(,)*) => { + $(impl PyConfigExt for $pyconfig { + fn init(ffi: &PyFfi) -> Self + where + Self: Sized, + { + let mut config: Self = unsafe { std::mem::zeroed() }; + (ffi.PyConfig_InitPythonConfig)(&mut config as *const _ as *mut _); + config.parse_argv = 0; + config.install_signal_handlers = 1; + config + } + + fn set_exec(&mut self, ffi: &PyFfi) -> Result<&mut Self> { + let status = (ffi.PyConfig_SetBytesString)( + self as *const _ as *mut _, + &mut self.executable, + ffi.exec.as_ptr(), + ); + ensure!( + (ffi.PyStatus_Exception)(status) == 0, + "failed to set config" + ); + Ok(self) + } + + fn set_argv(&mut self, ffi: &PyFfi) -> Result<&mut Self> { + let argv = std::env::args_os() + .map(|x| CString::new(x.as_encoded_bytes())) + .collect::, _>>() + .map_err(|_| anyhow!("unable to construct argv"))?; + let argvp = argv + .iter() + .map(|x| x.as_ptr() as *mut i8) + .collect::>(); + let status = (ffi.PyConfig_SetBytesArgv)( + self as *mut _ as *mut _, + argvp.len() as isize, + argvp.as_ptr() as *mut _, + ); + ensure!((ffi.PyStatus_Exception)(status) == 0, "failed to set argv"); + Ok(self) + } + })* + }; +} + +impl_pyconfig![py39::PyConfig, py313::PyConfig]; + +impl PyFfi { + fn run(self, version: &str, preamble: Option<&CStr>) -> Result<()> { + match version { + "39" => { + let mut config = py39::PyConfig::init(&self); + config.set_exec(&self)?.set_argv(&self)?; + (self.Py_InitializeFromConfig)(&config as *const _ as *const _); + } + "313" => { + let mut config = py313::PyConfig::init(&self); + config.set_exec(&self)?.set_argv(&self)?; + (self.Py_InitializeFromConfig)(&config as *const _ as *const _); + } + _ => Err(anyhow!("unsupported python version: {version}"))?, + }; + + ensure!( + (self.Py_IsInitialized)() == 1, + "interpreter was not initialised!" + ); + + if let Some(preamble) = preamble { + let res = (self.PyRun_SimpleString)(preamble.as_ptr()); + ensure!(res == 0, "failed to run preamble"); + } - let res = (self.Py_IsInitialized)(); - ensure!(res != 0, "failed to initialise"); - let res = (self.PyRun_SimpleString)(preamble.as_ref().as_ptr()); - ensure!(res == 0, "failed to run preamble"); // test importing aqt first before falling back to usual launch let res = (self.PyRun_SimpleString)(c"import aqt".as_ptr()); ensure!(res == 0, "failed to import aqt"); + // from here on, don't fallback if we fail let _ = (self.PyRun_SimpleString)(c"aqt.run()".as_ptr()); From cea6263688660083dfa9ec9cd430d1f6d3c4993c Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 17:57:59 +0800 Subject: [PATCH 18/22] update ffi load impls --- qt/launcher/src/platform/nix.rs | 29 ++++++++++++++++++++--------- qt/launcher/src/platform/windows.rs | 29 ++++++++++++++++++++--------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/qt/launcher/src/platform/nix.rs b/qt/launcher/src/platform/nix.rs index bac8c45c3..83a587493 100644 --- a/qt/launcher/src/platform/nix.rs +++ b/qt/launcher/src/platform/nix.rs @@ -23,7 +23,7 @@ impl Drop for PyFfi { } macro_rules! load_sym { - ($lib:expr, $name:literal) => {{ + ($lib:expr, $name:expr) => {{ libc::dlerror(); let sym = libc::dlsym($lib, $name.as_ptr()); if sym.is_null() { @@ -34,9 +34,16 @@ macro_rules! load_sym { }}; } +macro_rules! ffi { + ($lib:expr, $exec:expr, $($field:ident),* $(,)?) => { + #[allow(clippy::missing_transmute_annotations)] // they're not missing + PyFfi { exec: $exec, $($field: load_sym!($lib, ::std::ffi::CString::new(stringify!($field)).unwrap()),)* lib: $lib, } + }; +} + impl PyFfi { #[allow(non_snake_case)] - pub fn load(path: impl AsRef) -> Result { + pub fn load(path: impl AsRef, exec: CString) -> Result { unsafe { libc::dlerror(); let lib = libc::dlopen( @@ -48,14 +55,18 @@ impl PyFfi { anyhow::bail!("failed to load library: {dlerror_str}"); } - #[allow(clippy::missing_transmute_annotations)] // they're not missing - Ok(PyFfi { - Py_InitializeEx: load_sym!(lib, c"Py_InitializeEx"), - Py_IsInitialized: load_sym!(lib, c"Py_IsInitialized"), - PyRun_SimpleString: load_sym!(lib, c"PyRun_SimpleString"), - Py_FinalizeEx: load_sym!(lib, c"Py_FinalizeEx"), + Ok(ffi!( lib, - }) + exec, + Py_IsInitialized, + PyRun_SimpleString, + Py_FinalizeEx, + PyConfig_InitPythonConfig, + PyConfig_SetBytesString, + Py_InitializeFromConfig, + PyConfig_SetBytesArgv, + PyStatus_Exception + )) } } } diff --git a/qt/launcher/src/platform/windows.rs b/qt/launcher/src/platform/windows.rs index bae66f83e..807aaa3e0 100644 --- a/qt/launcher/src/platform/windows.rs +++ b/qt/launcher/src/platform/windows.rs @@ -286,7 +286,7 @@ impl Drop for PyFfi { } macro_rules! load_sym { - ($lib:expr, $name:literal) => { + ($lib:expr, $name:expr) => { std::mem::transmute( GetProcAddress($lib, PCSTR::from_raw($name.as_ptr().cast())) .ok_or_else(|| anyhow!("failed to load {}", $name.to_string_lossy()))?, @@ -294,9 +294,16 @@ macro_rules! load_sym { }; } +macro_rules! ffi { + ($lib:expr, $exec:expr, $($field:ident),* $(,)?) => { + #[allow(clippy::missing_transmute_annotations)] // they're not missing + PyFfi { exec: $exec, $($field: load_sym!($lib, ::std::ffi::CString::new(stringify!($field)).unwrap()),)* lib: $lib.0, } + }; +} + impl PyFfi { #[allow(non_snake_case)] - pub fn load(path: impl AsRef) -> Result { + pub fn load(path: impl AsRef, exec: CString) -> Result { unsafe { let wide_filename: Vec = path .as_ref() @@ -311,13 +318,17 @@ impl PyFfi { LOAD_LIBRARY_FLAGS::default(), )?; - #[allow(clippy::missing_transmute_annotations)] // they're not missing - Ok(PyFfi { - Py_InitializeEx: load_sym!(lib, c"Py_InitializeEx"), - Py_IsInitialized: load_sym!(lib, c"Py_IsInitialized"), - PyRun_SimpleString: load_sym!(lib, c"PyRun_SimpleString"), - Py_FinalizeEx: load_sym!(lib, c"Py_FinalizeEx"), - lib: lib.0, + Ok(ffi! { + lib, + exec, + Py_IsInitialized, + PyRun_SimpleString, + Py_FinalizeEx, + PyConfig_InitPythonConfig, + PyConfig_SetBytesString, + Py_InitializeFromConfig, + PyConfig_SetBytesArgv, + PyStatus_Exception, }) } } From 2c018574e107a5a1c9201508c2276489defe6d87 Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 17:58:48 +0800 Subject: [PATCH 19/22] update ffl run impls --- qt/launcher/src/platform/nix.rs | 39 ++-------------------- qt/launcher/src/platform/windows.rs | 51 ++++------------------------- 2 files changed, 9 insertions(+), 81 deletions(-) diff --git a/qt/launcher/src/platform/nix.rs b/qt/launcher/src/platform/nix.rs index 83a587493..55f6f5f6c 100644 --- a/qt/launcher/src/platform/nix.rs +++ b/qt/launcher/src/platform/nix.rs @@ -6,10 +6,9 @@ use std::ffi::CString; use std::os::unix::prelude::OsStrExt; use anki_io::ToUtf8Path; -use anyhow::anyhow; use anyhow::Result; -use crate::get_libpython_path; +use crate::get_python_env_info; use crate::platform::PyFfi; use crate::State; @@ -72,44 +71,12 @@ impl PyFfi { } pub fn run(state: &State) -> Result<()> { - let lib_path = get_libpython_path(state)?; - - // NOTE: activate venv before loading lib - let path = std::env::var("PATH")?; - let paths = std::env::split_paths(&path); - let path = std::env::join_paths(std::iter::once(state.venv_folder.join("bin")).chain(paths))?; - std::env::set_var("PATH", path); - std::env::set_var("VIRTUAL_ENV", &state.venv_folder); - std::env::set_var("PYTHONHOME", ""); + let (version, lib_path, exec) = get_python_env_info(state)?; std::env::set_var("ANKI_LAUNCHER", std::env::current_exe()?.utf8()?.as_str()); std::env::set_var("ANKI_LAUNCHER_UV", state.uv_path.utf8()?.as_str()); std::env::set_var("UV_PROJECT", state.uv_install_root.utf8()?.as_str()); std::env::remove_var("SSLKEYLOGFILE"); - let ffi = PyFfi::load(lib_path)?; - - // NOTE: sys.argv would normally be set via PyConfig, but we don't have it here - let args: String = std::env::args() - .skip(1) - .map(|s| format!(r#","{s}""#)) - .collect(); - - // NOTE: - // the venv activation script doesn't seem to be - // necessary for linux, only PATH and VIRTUAL_ENV - // but just call it anyway to have a standard setup - let venv_activate_path = state.venv_folder.join("bin/activate_this.py"); - let venv_activate_path = venv_activate_path - .as_os_str() - .to_str() - .ok_or_else(|| anyhow!("failed to get venv activation script path"))?; - - let preamble = std::ffi::CString::new(format!( - r#"import sys, runpy; sys.argv = ['Anki'{args}]; runpy.run_path("{venv_activate_path}")"#, - ))?; - - ffi.run(preamble)?; - - Ok(()) + PyFfi::load(lib_path, exec)?.run(&version, None) } diff --git a/qt/launcher/src/platform/windows.rs b/qt/launcher/src/platform/windows.rs index 807aaa3e0..9cdf247b7 100644 --- a/qt/launcher/src/platform/windows.rs +++ b/qt/launcher/src/platform/windows.rs @@ -32,7 +32,7 @@ use windows::Win32::System::Registry::REG_SZ; use windows::Win32::System::SystemInformation::OSVERSIONINFOW; use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID; -use crate::get_libpython_path; +use crate::get_python_env_info; use crate::platform::PyFfi; use crate::State; @@ -335,58 +335,19 @@ impl PyFfi { } pub fn run(state: &State, console: bool) -> Result<()> { - let lib_path = get_libpython_path(state)?; - - // NOTE: needed for 3.9 on windows (not for 3.13) - std::env::set_current_dir( - lib_path - .parent() - .ok_or_else(|| anyhow!("expected parent dir for lib_path: {lib_path:?}"))?, - )?; - - let path = std::env::var("PATH")?; - let paths = std::env::split_paths(&path); - let path = - std::env::join_paths(std::iter::once(state.venv_folder.join("Scripts")).chain(paths))?; - std::env::set_var("PATH", path); - std::env::set_var("VIRTUAL_ENV", &state.venv_folder); - std::env::set_var("PYTHONHOME", ""); + let (version, lib_path, exec) = get_python_env_info(state)?; std::env::set_var("ANKI_LAUNCHER", std::env::current_exe()?.utf8()?.as_str()); std::env::set_var("ANKI_LAUNCHER_UV", state.uv_path.utf8()?.as_str()); std::env::set_var("UV_PROJECT", state.uv_install_root.utf8()?.as_str()); std::env::remove_var("SSLKEYLOGFILE"); - let ffi = PyFfi::load(lib_path)?; - - let args: String = std::env::args() - .skip(1) - .map(|s| format!(r#","{s}""#)) - .collect::() - .replace('\\', "\\\\"); - - let venv_activate_path = state.venv_folder.join("Scripts\\activate_this.py"); - let venv_activate_path = venv_activate_path - .as_os_str() - .to_str() - .ok_or_else(|| anyhow!("failed to get venv activation script path"))? - .replace('\\', "\\\\"); - // NOTE: without windows_subsystem=console or pythonw, // we need to reconnect stdin/stdout/stderr within the interp // reconnect_stdio_to_console doesn't make a difference here - let console_snippet = if console { - r#" sys.stdout = sys.stderr = open("CONOUT$", "w"); sys.stdin = open("CONIN$", "r");"# - } else { - "" - }; + let preamble = console.then_some( + cr#"import sys; sys.stdout = sys.stderr = open("CONOUT$", "w"); sys.stdin = open("CONIN$", "r");"#, + ); - // NOTE: windows needs the venv activation script - let preamble = CString::new(format!( - r#"import sys, runpy; sys.argv = ['Anki'{args}]; runpy.run_path("{venv_activate_path}");{console_snippet}"#, - ))?; - - ffi.run(preamble)?; - - Ok(()) + PyFfi::load(lib_path, exec)?.run(&version, preamble) } From 8ac763cebab03fa32b919a8d4e7454a70d389250 Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 18:31:01 +0800 Subject: [PATCH 20/22] remove CString unwraps no idea why windows needs an explicit decl, possible compiler bug? --- qt/launcher/src/platform/nix.rs | 2 +- qt/launcher/src/platform/windows.rs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/qt/launcher/src/platform/nix.rs b/qt/launcher/src/platform/nix.rs index 55f6f5f6c..8dd848999 100644 --- a/qt/launcher/src/platform/nix.rs +++ b/qt/launcher/src/platform/nix.rs @@ -36,7 +36,7 @@ macro_rules! load_sym { macro_rules! ffi { ($lib:expr, $exec:expr, $($field:ident),* $(,)?) => { #[allow(clippy::missing_transmute_annotations)] // they're not missing - PyFfi { exec: $exec, $($field: load_sym!($lib, ::std::ffi::CString::new(stringify!($field)).unwrap()),)* lib: $lib, } + PyFfi { exec: $exec, $($field: load_sym!($lib, ::std::ffi::CString::new(stringify!($field)).map_err(|_| anyhow::anyhow!("failed to construct symbol CString"))?),)* lib: $lib, } }; } diff --git a/qt/launcher/src/platform/windows.rs b/qt/launcher/src/platform/windows.rs index 9cdf247b7..ca93b29f3 100644 --- a/qt/launcher/src/platform/windows.rs +++ b/qt/launcher/src/platform/windows.rs @@ -297,7 +297,10 @@ macro_rules! load_sym { macro_rules! ffi { ($lib:expr, $exec:expr, $($field:ident),* $(,)?) => { #[allow(clippy::missing_transmute_annotations)] // they're not missing - PyFfi { exec: $exec, $($field: load_sym!($lib, ::std::ffi::CString::new(stringify!($field)).unwrap()),)* lib: $lib.0, } + PyFfi { exec: $exec, $($field: { + let sym = ::std::ffi::CString::new(stringify!($field)).map_err(|_| anyhow::anyhow!("failed to construct symbol CString"))?; + load_sym!($lib, sym) + },)* lib: $lib.0, } }; } From 88752aeeb7f39f3b517fe9b0020f7e94229b84a4 Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 19:24:02 +0800 Subject: [PATCH 21/22] =?UTF-8?q?sys.executable=20is=20the=20bin=20we're?= =?UTF-8?q?=20getting=20sys.executable=20from=20=F0=9F=A4=A6=E2=80=8D?= =?UTF-8?q?=E2=99=82=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- qt/launcher/src/libpython_nix.py | 5 +- qt/launcher/src/libpython_win.py | 5 +- qt/launcher/src/main.rs | 94 +++++++++----------------------- 3 files changed, 29 insertions(+), 75 deletions(-) diff --git a/qt/launcher/src/libpython_nix.py b/qt/launcher/src/libpython_nix.py index 3f5c7fb3e..25fbc90ba 100644 --- a/qt/launcher/src/libpython_nix.py +++ b/qt/launcher/src/libpython_nix.py @@ -1,13 +1,12 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import json import os -import sys import sysconfig cfg = sysconfig.get_config_var base = cfg("installed_base") or cfg("installed_platbase") lib = cfg("LDLIBRARY") or cfg("INSTSONAME") version = cfg("py_version_nodot") -print(json.dumps([version, os.path.join(base, "lib", lib), sys.executable])) +print(version) +print(os.path.join(base, "lib", lib)) diff --git a/qt/launcher/src/libpython_win.py b/qt/launcher/src/libpython_win.py index c4699e57e..63257c87c 100644 --- a/qt/launcher/src/libpython_win.py +++ b/qt/launcher/src/libpython_win.py @@ -1,13 +1,12 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import json import os -import sys import sysconfig cfg = sysconfig.get_config_var base = cfg("installed_base") or cfg("installed_platbase") version = cfg("py_version_nodot") lib = "python" + version + ".dll" -print(json.dumps([version, os.path.join(base, lib), sys.executable])) +print(version) +print(os.path.join(base, lib)) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 3e27da5bc..19d0ccd67 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -7,9 +7,6 @@ use std::ffi::CString; use std::io::stdin; use std::io::stdout; use std::io::Write; -use std::path::Component; -use std::path::Path; -use std::path::PathBuf; use std::process::Command; use std::time::SystemTime; use std::time::UNIX_EPOCH; @@ -17,13 +14,13 @@ use std::time::UNIX_EPOCH; use anki_i18n::I18n; use anki_io::copy_file; use anki_io::create_dir_all; -use anki_io::create_file; use anki_io::modified_time; use anki_io::read_file; use anki_io::remove_file; use anki_io::write_file; use anki_io::ToUtf8Path; use anki_process::CommandExt as AnkiCommandExt; +use anyhow::anyhow; use anyhow::Context; use anyhow::Result; @@ -1044,8 +1041,8 @@ fn uv_command(state: &State) -> Result { Ok(command) } -fn _build_python_command(state: &State) -> Result { - let python_exe = if cfg!(target_os = "windows") { +fn get_venv_bin_path(state: &State) -> std::path::PathBuf { + if cfg!(target_os = "windows") { let show_console = std::env::var("ANKI_CONSOLE").is_ok(); if show_console { state.venv_folder.join("Scripts/python.exe") @@ -1054,9 +1051,11 @@ fn _build_python_command(state: &State) -> Result { } } else { state.venv_folder.join("bin/python") - }; + } +} - let mut cmd = Command::new(&python_exe); +fn _build_python_command(state: &State, python_exe: &std::path::Path) -> Result { + let mut cmd = Command::new(python_exe); // tell the Python code it was invoked by the launcher, and updating is // available cmd.env("ANKI_LAUNCHER", std::env::current_exe()?.utf8()?.as_str()); @@ -1070,78 +1069,38 @@ fn _build_python_command(state: &State) -> Result { } fn build_python_command(state: &State, args: &[String]) -> Result { - let mut cmd = _build_python_command(state)?; + let python_exe = get_venv_bin_path(state); + let mut cmd = _build_python_command(state, &python_exe)?; cmd.args(["-c", "import aqt, sys; sys.argv[0] = 'Anki'; aqt.run()"]); cmd.args(args); Ok(cmd) } -/// Normalize a path, removing things like `.` and `..` w/o following symlinks -/// NOTE: lifted from https://github.com/rust-lang/cargo/blob/28b79ea2b7b6d922d3ee85a935e63deed42db9c1/crates/cargo-util/src/paths.rs#L84 -pub fn normalize_path(path: &Path) -> PathBuf { - let mut components = path.components().peekable(); - let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { - components.next(); - PathBuf::from(c.as_os_str()) - } else { - PathBuf::new() - }; - - for component in components { - match component { - Component::Prefix(..) => unreachable!(), - Component::RootDir => { - ret.push(Component::RootDir); - } - Component::CurDir => {} - Component::ParentDir => { - if ret.ends_with(Component::ParentDir) { - ret.push(Component::ParentDir); - } else { - let popped = ret.pop(); - if !popped && !ret.has_root() { - ret.push(Component::ParentDir); - } - } - } - Component::Normal(c) => { - ret.push(c); - } - } - } - ret -} - fn get_python_env_info(state: &State) -> Result<(String, std::path::PathBuf, CString)> { + let python_exe = get_venv_bin_path(state); // we can cache this, as it can only change after syncing the project // as it stands, we already expect there to be a trusted python exe in // a particular place so this doesn't seem too concerning security-wise - type Cache = (String, PathBuf, PathBuf); // TODO: let-chains... if let Ok(cached) = read_file(&state.libpython_info) { if let Ok(cached) = String::from_utf8(cached) { - if let Ok((version, lib_path, exec_path)) = serde_json::from_str::(&cached) { + if let Some((version, lib_path)) = cached.split_once('\n') { if let Ok(lib_path) = state.uv_install_root.join(lib_path).canonicalize() { - // can't use canonicalise here as it follows symlinks, - // we need bin to be in the venv for it to know where - // to find the pyvenv.cfg that's in the parent dir - let exec_path = normalize_path(&state.uv_install_root.join(exec_path)); // make sure we're still within AnkiProgramFiles... - if lib_path.strip_prefix(&state.uv_install_root).is_ok() - && exec_path.strip_prefix(&state.uv_install_root).is_ok() - { + if lib_path.strip_prefix(&state.uv_install_root).is_ok() { return Ok(( - version, + version.to_string(), lib_path, - CString::new(exec_path.as_os_str().as_encoded_bytes())?, + CString::new(python_exe.as_os_str().as_encoded_bytes())?, )); } } } } + let _ = remove_file(&state.libpython_info); } - let mut cmd = _build_python_command(state)?; + let mut cmd = _build_python_command(state, &python_exe)?; // NOTE: // we can check which sysconfig vars are available // with `sysconfig.get_config_vars()`. very limited on @@ -1162,29 +1121,26 @@ fn get_python_env_info(state: &State) -> Result<(String, std::path::PathBuf, CSt let output = cmd.utf8_output()?; let output = output.stdout.trim(); - let (version, lib_path, exec_path): Cache = serde_json::from_str(output)?; + let (version, lib_path) = output + .split_once('\n') + .ok_or_else(|| anyhow!("invalid libpython info"))?; + let lib_path = std::path::PathBuf::from(lib_path); if !lib_path.exists() { anyhow::bail!("library path doesn't exist: {lib_path:?}"); } - if !exec_path.exists() { - anyhow::bail!("exec path doesn't exist: {exec_path:?}"); - } - - if let Ok(file) = create_file(&state.libpython_info) { - let cached = ( - version.clone(), - lib_path.strip_prefix(&state.uv_install_root)?, - exec_path.strip_prefix(&state.uv_install_root)?, + if let Ok(lib_path) = lib_path.strip_prefix(&state.uv_install_root) { + let _ = write_file( + &state.libpython_info, + format!("{version}\n{}", lib_path.display()), ); - let _ = serde_json::to_writer(file, &cached); } Ok(( version.to_owned(), lib_path, - CString::new(exec_path.as_os_str().as_encoded_bytes())?, + CString::new(python_exe.as_os_str().as_encoded_bytes())?, )) } From fd1cbd38b028d7213659f8cb0f44fcc37e61e22d Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 19:35:33 +0800 Subject: [PATCH 22/22] trim after splitting --- qt/launcher/src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 19d0ccd67..bd2b02c17 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -1085,11 +1085,11 @@ fn get_python_env_info(state: &State) -> Result<(String, std::path::PathBuf, CSt if let Ok(cached) = read_file(&state.libpython_info) { if let Ok(cached) = String::from_utf8(cached) { if let Some((version, lib_path)) = cached.split_once('\n') { - if let Ok(lib_path) = state.uv_install_root.join(lib_path).canonicalize() { + if let Ok(lib_path) = state.uv_install_root.join(lib_path.trim()).canonicalize() { // make sure we're still within AnkiProgramFiles... if lib_path.strip_prefix(&state.uv_install_root).is_ok() { return Ok(( - version.to_string(), + version.trim().to_string(), lib_path, CString::new(python_exe.as_os_str().as_encoded_bytes())?, )); @@ -1124,7 +1124,7 @@ fn get_python_env_info(state: &State) -> Result<(String, std::path::PathBuf, CSt let (version, lib_path) = output .split_once('\n') .ok_or_else(|| anyhow!("invalid libpython info"))?; - let lib_path = std::path::PathBuf::from(lib_path); + let lib_path = std::path::PathBuf::from(lib_path.trim()); if !lib_path.exists() { anyhow::bail!("library path doesn't exist: {lib_path:?}"); @@ -1138,7 +1138,7 @@ fn get_python_env_info(state: &State) -> Result<(String, std::path::PathBuf, CSt } Ok(( - version.to_owned(), + version.trim().to_owned(), lib_path, CString::new(python_exe.as_os_str().as_encoded_bytes())?, ))