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"] } 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]] diff --git a/qt/launcher/src/libpython_nix.py b/qt/launcher/src/libpython_nix.py new file mode 100644 index 000000000..25fbc90ba --- /dev/null +++ b/qt/launcher/src/libpython_nix.py @@ -0,0 +1,12 @@ +# 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") +version = cfg("py_version_nodot") +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 new file mode 100644 index 000000000..63257c87c --- /dev/null +++ b/qt/launcher/src/libpython_win.py @@ -0,0 +1,12 @@ +# 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") +version = cfg("py_version_nodot") +lib = "python" + version + ".dll" +print(version) +print(os.path.join(base, lib)) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index dab9435ea..bd2b02c17 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -3,6 +3,7 @@ #![windows_subsystem = "windows"] +use std::ffi::CString; use std::io::stdin; use std::io::stdout; use std::io::Write; @@ -19,6 +20,7 @@ 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; @@ -28,6 +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; mod platform; @@ -53,6 +56,7 @@ struct State { previous_version: Option, resources_dir: std::path::PathBuf, venv_folder: std::path::PathBuf, + libpython_info: std::path::PathBuf, /// system Python + PyQt6 library mode system_qt: bool, } @@ -132,6 +136,7 @@ fn run() -> Result<()> { && resources_dir.join("system_qt").exists(), resources_dir, venv_folder: uv_install_root.join(".venv"), + libpython_info: uv_install_root.join(".cached-info"), }; // Check for uninstall request from Windows uninstaller @@ -156,9 +161,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(()); } @@ -274,6 +281,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 ibpython info cache + let _ = remove_file(&state.libpython_info); + println!("{}\n", state.tr.launcher_updating_anki()); let python_version_trimmed = if state.user_python_version_path.exists() { @@ -1031,8 +1041,8 @@ fn uv_command(state: &State) -> Result { Ok(command) } -fn build_python_command(state: &State, args: &[String]) -> 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") @@ -1041,11 +1051,11 @@ fn build_python_command(state: &State, args: &[String]) -> Result { } } else { state.venv_folder.join("bin/python") - }; + } +} - let mut cmd = Command::new(&python_exe); - cmd.args(["-c", "import aqt, sys; sys.argv[0] = 'Anki'; aqt.run()"]); - cmd.args(args); +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()); @@ -1058,6 +1068,82 @@ fn build_python_command(state: &State, args: &[String]) -> Result { Ok(cmd) } +fn build_python_command(state: &State, args: &[String]) -> Result { + 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) +} + +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 + // TODO: let-chains... + 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.trim()).canonicalize() { + // make sure we're still within AnkiProgramFiles... + if lib_path.strip_prefix(&state.uv_install_root).is_ok() { + return Ok(( + version.trim().to_string(), + lib_path, + CString::new(python_exe.as_os_str().as_encoded_bytes())?, + )); + } + } + } + } + let _ = remove_file(&state.libpython_info); + } + + 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 + // 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 output = output.stdout.trim(); + + let (version, lib_path) = output + .split_once('\n') + .ok_or_else(|| anyhow!("invalid libpython info"))?; + let lib_path = std::path::PathBuf::from(lib_path.trim()); + + if !lib_path.exists() { + anyhow::bail!("library path doesn't exist: {lib_path:?}"); + } + + 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()), + ); + } + + Ok(( + version.trim().to_owned(), + lib_path, + CString::new(python_exe.as_os_str().as_encoded_bytes())?, + )) +} + fn is_mirror_enabled(state: &State) -> bool { state.mirror_path.exists() } diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index eec7634f1..cc16b2364 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -10,9 +10,19 @@ pub mod mac; #[cfg(target_os = "windows")] pub mod windows; +#[cfg(unix)] +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; @@ -109,6 +119,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; @@ -139,3 +173,131 @@ pub fn ensure_os_supported() -> Result<()> { Ok(()) } + +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 { + exec: CString, + lib: *mut std::ffi::c_void, + Py_IsInitialized: PyIsInitialized, + PyRun_SimpleString: PyRunSimpleString, + Py_FinalizeEx: PyFinalizeEx, + PyConfig_InitPythonConfig: PyConfigInitPythonConfig, + PyConfig_SetBytesString: PyConfigSetBytesString, + Py_InitializeFromConfig: PyInitializeFromConfig, + PyConfig_SetBytesArgv: PyConfigSetBytesArgv, + PyStatus_Exception: PyStatusException, +} + +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"); + } + + // 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(()) + } +} diff --git a/qt/launcher/src/platform/nix.rs b/qt/launcher/src/platform/nix.rs new file mode 100644 index 000000000..8dd848999 --- /dev/null +++ b/qt/launcher/src/platform/nix.rs @@ -0,0 +1,82 @@ +// 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::Result; + +use crate::get_python_env_info; +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: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, } + }; +} + +impl PyFfi { + #[allow(non_snake_case)] + pub fn load(path: impl AsRef, exec: CString) -> 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}"); + } + + Ok(ffi!( + lib, + exec, + Py_IsInitialized, + PyRun_SimpleString, + Py_FinalizeEx, + PyConfig_InitPythonConfig, + PyConfig_SetBytesString, + Py_InitializeFromConfig, + PyConfig_SetBytesArgv, + PyStatus_Exception + )) + } + } +} + +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/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, +} diff --git a/qt/launcher/src/platform/windows.rs b/qt/launcher/src/platform/windows.rs index d20c9a8b4..ca93b29f3 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_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 { unsafe { @@ -261,3 +275,82 @@ 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: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, } + }; +} + +impl PyFfi { + #[allow(non_snake_case)] + pub fn load(path: impl AsRef, exec: CString) -> 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(), + )?; + + Ok(ffi! { + lib, + exec, + Py_IsInitialized, + PyRun_SimpleString, + Py_FinalizeEx, + PyConfig_InitPythonConfig, + PyConfig_SetBytesString, + Py_InitializeFromConfig, + PyConfig_SetBytesArgv, + PyStatus_Exception, + }) + } + } +} + +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) +}