From 9507bb9ae841c9877382d538ff1f0c934668c384 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:09:44 +0800 Subject: [PATCH 01/27] 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 4b7dadeccfc68d768a0d8b00728b4154bde99f17 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:11:35 +0800 Subject: [PATCH 02/27] 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 5f5849c996a0327b68efdc90611d68c5409d5c85 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:12:37 +0800 Subject: [PATCH 03/27] 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 085e5cd75056e1d00f6be27deb716926d458f5a6 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:14:51 +0800 Subject: [PATCH 04/27] 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 310a841b14c2e85258542c2bdbc4368cb8281254 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:19:15 +0800 Subject: [PATCH 05/27] 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 18759f4aa1616fe88836083a0194759c96a5c851 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:20:50 +0800 Subject: [PATCH 06/27] 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 7d581a42e473aa81dc5ca53b45a91e289d61ffc5 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:21:32 +0800 Subject: [PATCH 07/27] 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 97f6e556c6d9219d94a75dc4edfd592fcf9f92ab Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:23:35 +0800 Subject: [PATCH 08/27] 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 a9aed2928c52ac995a6a93f1f836c7ca129f1d11 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:41:55 +0800 Subject: [PATCH 09/27] 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 66f95b11e86de47c0168da5abd682ceaf4a44c6e Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:42:11 +0800 Subject: [PATCH 10/27] 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 90f969f0732c3e6422a78807ccd49e7fdc1266e8 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:42:29 +0800 Subject: [PATCH 11/27] 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 f1cfca27e5f81b5b40f005d0e7b7030e564540f9 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 1 Nov 2025 22:42:46 +0800 Subject: [PATCH 12/27] 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 2e11d86a3f694441a1faf83e510eb69d279cbf36 Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 17:49:19 +0800 Subject: [PATCH 13/27] 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 866d7adbd220730947efc06eff513f31369de32f Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 17:50:42 +0800 Subject: [PATCH 14/27] 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 e8bccf13c96617d0da4371e9d7ef52de129f74d1 Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 17:53:09 +0800 Subject: [PATCH 15/27] 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 75ada381fc716a4f5872a25bf908dfab7404d2cb Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 17:54:53 +0800 Subject: [PATCH 16/27] 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 546fd5db9718fdbda01afe6bc5a930fd377c2f71 Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 17:56:39 +0800 Subject: [PATCH 17/27] 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 41997be58669c24fb65fc2352a7a17856c4fda89 Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 17:57:59 +0800 Subject: [PATCH 18/27] 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 5e16989ccc4804f4cc659f367ce16fd587918c2c Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 17:58:48 +0800 Subject: [PATCH 19/27] 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 1ba84226729790d0918f902219015e7b36ef7150 Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 18:31:01 +0800 Subject: [PATCH 20/27] 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 09766c1b4a8be9d5b5a614f95a80d1bdf9071c83 Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 19:24:02 +0800 Subject: [PATCH 21/27] =?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 d690a0aa6c9cc290d48b68186927e9857dc54c85 Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 2 Nov 2025 19:35:33 +0800 Subject: [PATCH 22/27] 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())?, )) From f037e260c0ba652f7f8caf4a89a9e318837ff107 Mon Sep 17 00:00:00 2001 From: llama Date: Wed, 12 Nov 2025 21:14:50 +0800 Subject: [PATCH 23/27] refactor, fix: dlerror may return null --- qt/launcher/src/platform/nix.rs | 36 ++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/qt/launcher/src/platform/nix.rs b/qt/launcher/src/platform/nix.rs index 8dd848999..823c968c3 100644 --- a/qt/launcher/src/platform/nix.rs +++ b/qt/launcher/src/platform/nix.rs @@ -21,22 +21,30 @@ impl Drop for PyFfi { } } -macro_rules! load_sym { - ($lib:expr, $name:expr) => {{ - 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) - }}; -} - 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)).map_err(|_| anyhow::anyhow!("failed to construct symbol CString"))?),)* lib: $lib, } + #[allow(clippy::missing_transmute_annotations)] + $crate::platform::PyFfi { + exec: $exec, + $($field: { + ::libc::dlerror(); + let name = ::std::ffi::CString::new(stringify!($field)).map_err(|_| ::anyhow::anyhow!("failed to construct sym"))?; + let sym = ::libc::dlsym($lib, name.as_ptr()); + if sym.is_null() { + let dlerror_ptr = ::libc::dlerror(); + let dlerror_str = if !dlerror_ptr.is_null() { + ::std::ffi::CStr::from_ptr(dlerror_ptr) + .to_str() + .unwrap_or_default() + } else { + "" + }; + ::anyhow::bail!("failed to load {}: {dlerror_str}", stringify!($field)); + } + ::std::mem::transmute(sym) + },)* + lib: $lib, + } }; } From 2aa9108046a12dec1d86dac7d8ac7e191ed1fc6e Mon Sep 17 00:00:00 2001 From: llama Date: Wed, 12 Nov 2025 21:16:20 +0800 Subject: [PATCH 24/27] refactor windows ffi macro --- qt/launcher/src/platform/windows.rs | 30 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/qt/launcher/src/platform/windows.rs b/qt/launcher/src/platform/windows.rs index ca93b29f3..2b1836ead 100644 --- a/qt/launcher/src/platform/windows.rs +++ b/qt/launcher/src/platform/windows.rs @@ -11,7 +11,6 @@ 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; @@ -19,7 +18,6 @@ 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; @@ -285,22 +283,22 @@ impl Drop for PyFfi { } } -macro_rules! load_sym { - ($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()))?, - ) - }; -} - macro_rules! ffi { ($lib:expr, $exec:expr, $($field:ident),* $(,)?) => { - #[allow(clippy::missing_transmute_annotations)] // they're not missing - 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, } + #[allow(clippy::missing_transmute_annotations)] + $crate::platform::PyFfi { + exec: $exec, + $($field: { + let sym = ::std::ffi::CString::new(stringify!($field)).map_err(|_| ::anyhow::anyhow!("failed to construct sym"))?; + ::std::mem::transmute( + ::windows::Win32::System::LibraryLoader::GetProcAddress( + $lib, + ::windows::core::PCSTR::from_raw(sym.as_ptr().cast()), + ) + .ok_or_else(|| anyhow!("failed to load {}", stringify!($field)))?, + ) + },)* + lib: $lib.0, } }; } From dbc7adca2dd9a736e31aa57f9092768bdb0cddbb Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 22 Nov 2025 18:23:09 +0800 Subject: [PATCH 25/27] refactor pointer casts --- qt/launcher/src/platform/mod.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index cc16b2364..e6c7efd37 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -223,7 +223,7 @@ macro_rules! impl_pyconfig { Self: Sized, { let mut config: Self = unsafe { std::mem::zeroed() }; - (ffi.PyConfig_InitPythonConfig)(&mut config as *const _ as *mut _); + (ffi.PyConfig_InitPythonConfig)(&raw mut config as _); config.parse_argv = 0; config.install_signal_handlers = 1; config @@ -231,7 +231,7 @@ macro_rules! impl_pyconfig { fn set_exec(&mut self, ffi: &PyFfi) -> Result<&mut Self> { let status = (ffi.PyConfig_SetBytesString)( - self as *const _ as *mut _, + self as *mut _ as _, &mut self.executable, ffi.exec.as_ptr(), ); @@ -252,9 +252,9 @@ macro_rules! impl_pyconfig { .map(|x| x.as_ptr() as *mut i8) .collect::>(); let status = (ffi.PyConfig_SetBytesArgv)( - self as *mut _ as *mut _, + self as *mut _ as _, argvp.len() as isize, - argvp.as_ptr() as *mut _, + argvp.as_ptr().cast(), ); ensure!((ffi.PyStatus_Exception)(status) == 0, "failed to set argv"); Ok(self) @@ -271,12 +271,12 @@ impl PyFfi { "39" => { let mut config = py39::PyConfig::init(&self); config.set_exec(&self)?.set_argv(&self)?; - (self.Py_InitializeFromConfig)(&config as *const _ as *const _); + (self.Py_InitializeFromConfig)(&raw const config as _); } "313" => { let mut config = py313::PyConfig::init(&self); config.set_exec(&self)?.set_argv(&self)?; - (self.Py_InitializeFromConfig)(&config as *const _ as *const _); + (self.Py_InitializeFromConfig)(&raw const config as _); } _ => Err(anyhow!("unsupported python version: {version}"))?, }; From 7c4a1e6c0083c3d4f75f24b206ae993e6a78bf86 Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 23 Nov 2025 16:31:18 +0800 Subject: [PATCH 26/27] dedup platform-specific run --- qt/launcher/src/main.rs | 4 ++-- qt/launcher/src/platform/mod.rs | 24 ++++++++++++++++++------ qt/launcher/src/platform/nix.rs | 14 -------------- qt/launcher/src/platform/windows.rs | 21 --------------------- 4 files changed, 20 insertions(+), 43 deletions(-) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index bd2b02c17..5cc06308c 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -30,7 +30,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; +use crate::platform::run_anki_embeddedly; mod platform; @@ -161,7 +161,7 @@ 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 - if std::env::var("ANKI_LAUNCHER_NO_EMBED").is_ok() || !run_anki_normally(&state) { + if std::env::var("ANKI_LAUNCHER_NO_EMBED").is_ok() || !run_anki_embeddedly(&state) { let args: Vec = std::env::args().skip(1).collect(); let cmd = build_python_command(&state, &args)?; launch_anki_normally(cmd)?; diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index e6c7efd37..ac959a075 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -20,6 +20,7 @@ use std::ffi::CStr; use std::ffi::CString; use std::path::PathBuf; +use anki_io::ToUtf8Path; use anki_process::CommandExt; use anyhow::anyhow; use anyhow::ensure; @@ -119,7 +120,16 @@ pub fn launch_anki_normally(mut cmd: std::process::Command) -> Result<()> { Ok(()) } -pub fn _run_anki_normally(state: &crate::State) -> Result<()> { +pub fn _run_anki_embeddedly(state: &crate::State) -> Result<()> { + let (version, lib_path, exec) = crate::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, exec)?; + #[cfg(windows)] { let console = std::env::var("ANKI_CONSOLE").is_ok(); @@ -128,15 +138,17 @@ pub fn _run_anki_normally(state: &crate::State) -> Result<()> { ensure_terminal_shown()?; } crate::platform::windows::prepare_to_launch_normally(); - windows::run(state, console)?; + // NOTE: without windows_subsystem=console or pythonw, + // we need to reconnect stdin/stdout/stderr within the interp + let preamble = console.then_some(cr#"import sys; sys.stdout = sys.stderr = open("CONOUT$", "w"); sys.stdin = open("CONIN$", "r");"#); + ffi.run(&version, preamble) } #[cfg(unix)] - nix::run(state)?; - Ok(()) + ffi.run(&version, None) } -pub fn run_anki_normally(state: &crate::State) -> bool { - if let Err(e) = _run_anki_normally(state) { +pub fn run_anki_embeddedly(state: &crate::State) -> bool { + if let Err(e) = _run_anki_embeddedly(state) { eprintln!("failed to run as embedded: {e:?}"); return false; } diff --git a/qt/launcher/src/platform/nix.rs b/qt/launcher/src/platform/nix.rs index 823c968c3..f1d769221 100644 --- a/qt/launcher/src/platform/nix.rs +++ b/qt/launcher/src/platform/nix.rs @@ -5,12 +5,9 @@ use std::ffi::CStr; use std::ffi::CString; use std::os::unix::prelude::OsStrExt; -use anki_io::ToUtf8Path; use anyhow::Result; -use crate::get_python_env_info; use crate::platform::PyFfi; -use crate::State; impl Drop for PyFfi { fn drop(&mut self) { @@ -77,14 +74,3 @@ impl PyFfi { } } } - -pub fn run(state: &State) -> Result<()> { - 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"); - - 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 2b1836ead..dca8b483e 100644 --- a/qt/launcher/src/platform/windows.rs +++ b/qt/launcher/src/platform/windows.rs @@ -6,7 +6,6 @@ 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; @@ -30,9 +29,7 @@ use windows::Win32::System::Registry::REG_SZ; use windows::Win32::System::SystemInformation::OSVERSIONINFOW; use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID; -use crate::get_python_env_info; use crate::platform::PyFfi; -use crate::State; /// Returns true if running on Windows 10 (not Windows 11) fn is_windows_10() -> bool { @@ -334,21 +331,3 @@ impl PyFfi { } } } - -pub fn run(state: &State, console: bool) -> Result<()> { - 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"); - - // 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 preamble = console.then_some( - cr#"import sys; sys.stdout = sys.stderr = open("CONOUT$", "w"); sys.stdin = open("CONIN$", "r");"#, - ); - - PyFfi::load(lib_path, exec)?.run(&version, preamble) -} From 3e0e5914ebf9208dba65cd4e24b93b15fe5592d1 Mon Sep 17 00:00:00 2001 From: llama Date: Sun, 23 Nov 2025 18:51:04 +0800 Subject: [PATCH 27/27] remove incorrect call to ensure_terminal_shown --- qt/launcher/src/platform/mod.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index ac959a075..e66860155 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -133,10 +133,6 @@ pub fn _run_anki_embeddedly(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(); // NOTE: without windows_subsystem=console or pythonw, // we need to reconnect stdin/stdout/stderr within the interp