Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9507bb9
add Win32_System_LibraryLoader feature for LoadLibraryExW
iamllama Nov 1, 2025
4b7dade
add libc crate for macos as well (for dlopen)
iamllama Nov 1, 2025
5f5849c
add helper scripts
iamllama Nov 1, 2025
085e5cd
add PyFfi
iamllama Nov 1, 2025
310a841
add libpython_path
iamllama Nov 1, 2025
18759f4
add get_libpython_path
iamllama Nov 1, 2025
7d581a4
clear lib path cache on sync
iamllama Nov 1, 2025
97f6e55
add PyFfi impl
iamllama Nov 1, 2025
a9aed29
add PyFfi impl for nixes
iamllama Nov 1, 2025
66f95b1
add PyFfi impl for windows
iamllama Nov 1, 2025
90f969f
add run_anki_normally
iamllama Nov 1, 2025
f1cfca2
launch anki embeddedly(?), fallback to usual launch
iamllama Nov 1, 2025
2e11d86
add bindgen'd py39/313 PyConfig definitions
iamllama Nov 2, 2025
866d7ad
add ffi types
iamllama Nov 2, 2025
e8bccf1
refactor get_libpython_path into get_python_env_info
iamllama Nov 2, 2025
75ada38
add new ffi methods
iamllama Nov 2, 2025
546fd5d
add PyConfigExt and refactor PyFfi impl
iamllama Nov 2, 2025
41997be
update ffi load impls
iamllama Nov 2, 2025
5e16989
update ffl run impls
iamllama Nov 2, 2025
1ba8422
remove CString unwraps
iamllama Nov 2, 2025
09766c1
sys.executable is the bin we're getting sys.executable from 🤦‍♂️
iamllama Nov 2, 2025
d690a0a
trim after splitting
iamllama Nov 2, 2025
f037e26
refactor, fix: dlerror may return null
iamllama Nov 12, 2025
2aa9108
refactor windows ffi macro
iamllama Nov 12, 2025
dbc7adc
refactor pointer casts
iamllama Nov 22, 2025
7c4a1e6
dedup platform-specific run
iamllama Nov 23, 2025
3e0e591
remove incorrect call to ensure_terminal_shown
iamllama Nov 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
5 changes: 1 addition & 4 deletions qt/launcher/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down
12 changes: 12 additions & 0 deletions qt/launcher/src/libpython_nix.py
Original file line number Diff line number Diff line change
@@ -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))
12 changes: 12 additions & 0 deletions qt/launcher/src/libpython_win.py
Original file line number Diff line number Diff line change
@@ -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))
104 changes: 95 additions & 9 deletions qt/launcher/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#![windows_subsystem = "windows"]

use std::ffi::CString;
use std::io::stdin;
use std::io::stdout;
use std::io::Write;
Expand All @@ -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;

Expand All @@ -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_embeddedly;

mod platform;

Expand All @@ -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,
}
Expand Down Expand Up @@ -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
Expand All @@ -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_embeddedly(&state) {
let args: Vec<String> = std::env::args().skip(1).collect();
let cmd = build_python_command(&state, &args)?;
launch_anki_normally(cmd)?;
}
return Ok(());
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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")
Expand All @@ -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());
Expand All @@ -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()
}
Expand Down
Loading