This commit is contained in:
llama 2025-11-02 11:37:11 +00:00 committed by GitHub
commit fe878d8bba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 626 additions and 14 deletions

View file

@ -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"] }

View file

@ -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]]

View file

@ -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))

View file

@ -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))

View file

@ -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<String>,
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<String> = 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<String> = 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<Command> {
Ok(command)
}
fn build_python_command(state: &State, args: &[String]) -> Result<Command> {
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<Command> {
}
} 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<Command> {
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<Command> {
Ok(cmd)
}
fn build_python_command(state: &State, args: &[String]) -> Result<Command> {
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()
}

View file

@ -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::<Result<Vec<_>, _>>()
.map_err(|_| anyhow!("unable to construct argv"))?;
let argvp = argv
.iter()
.map(|x| x.as_ptr() as *mut i8)
.collect::<Vec<_>>();
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(())
}
}

View file

@ -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<std::path::Path>, exec: CString) -> Result<Self> {
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)
}

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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<std::path::Path>, exec: CString) -> Result<Self> {
unsafe {
let wide_filename: Vec<u16> = 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)
}