mirror of
https://github.com/ankitects/anki.git
synced 2025-11-06 12:47:11 -05:00
303 lines
9.2 KiB
Rust
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(())
|
|
}
|
|
}
|