Anki/qt/launcher/src/platform/mod.rs
2025-11-02 17:56:39 +08:00

303 lines
9.2 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
#[cfg(all(unix, not(target_os = "macos")))]
pub mod unix;
#[cfg(target_os = "macos")]
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;
pub fn get_exe_and_resources_dirs() -> Result<(PathBuf, PathBuf)> {
let exe_dir = std::env::current_exe()
.context("Failed to get current executable path")?
.parent()
.context("Failed to get executable directory")?
.to_owned();
let resources_dir = if cfg!(target_os = "macos") {
// On macOS, resources are in ../Resources relative to the executable
exe_dir
.parent()
.context("Failed to get parent directory")?
.join("Resources")
} else {
// On other platforms, resources are in the same directory as executable
exe_dir.clone()
};
Ok((exe_dir, resources_dir))
}
pub fn get_uv_binary_name() -> &'static str {
if cfg!(target_os = "windows") {
"uv.exe"
} else if cfg!(target_os = "macos") {
"uv"
} else if cfg!(target_arch = "x86_64") {
"uv.amd64"
} else {
"uv.arm64"
}
}
pub fn respawn_launcher() -> Result<()> {
use std::process::Stdio;
let mut launcher_cmd = if cfg!(target_os = "macos") {
// On macOS, we need to launch the .app bundle, not the executable directly
let current_exe =
std::env::current_exe().context("Failed to get current executable path")?;
// Navigate from Contents/MacOS/launcher to the .app bundle
let app_bundle = current_exe
.parent() // MacOS
.and_then(|p| p.parent()) // Contents
.and_then(|p| p.parent()) // .app
.context("Failed to find .app bundle")?;
let mut cmd = std::process::Command::new("open");
cmd.arg(app_bundle);
cmd
} else {
let current_exe =
std::env::current_exe().context("Failed to get current executable path")?;
std::process::Command::new(current_exe)
};
launcher_cmd
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
const DETACHED_PROCESS: u32 = 0x00000008;
launcher_cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS);
}
#[cfg(all(unix, not(target_os = "macos")))]
{
use std::os::unix::process::CommandExt;
launcher_cmd.process_group(0);
}
let child = launcher_cmd.ensure_spawn()?;
std::mem::forget(child);
Ok(())
}
pub fn launch_anki_normally(mut cmd: std::process::Command) -> Result<()> {
#[cfg(windows)]
{
crate::platform::windows::prepare_to_launch_normally();
cmd.ensure_success()?;
}
#[cfg(unix)]
cmd.ensure_exec()?;
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;
#[cfg(unix)]
pub fn ensure_terminal_shown() -> Result<()> {
use std::io::IsTerminal;
let want_terminal = std::env::var("ANKI_LAUNCHER_WANT_TERMINAL").is_ok();
let stdout_is_terminal = IsTerminal::is_terminal(&std::io::stdout());
if want_terminal || !stdout_is_terminal {
#[cfg(target_os = "macos")]
mac::relaunch_in_terminal()?;
#[cfg(not(target_os = "macos"))]
unix::relaunch_in_terminal()?;
}
// Set terminal title to "Anki Launcher"
print!("\x1b]2;Anki Launcher\x07");
Ok(())
}
pub fn ensure_os_supported() -> Result<()> {
#[cfg(all(unix, not(target_os = "macos")))]
unix::ensure_glibc_supported()?;
#[cfg(target_os = "windows")]
windows::ensure_windows_version_supported()?;
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(())
}
}