mirror of
https://github.com/ankitects/anki.git
synced 2025-11-07 05:07:10 -05:00
refactor state and uv code
This commit is contained in:
parent
920fbdba7a
commit
6d6df1c91d
4 changed files with 524 additions and 334 deletions
|
|
@ -1,35 +1,14 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
use std::io;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anki_process::CommandExt as AnkiCommandExt;
|
use anki_process::CommandExt as AnkiCommandExt;
|
||||||
use anyhow::Context;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
pub fn prepare_for_launch_after_update(mut cmd: Command, root: &Path) -> Result<()> {
|
pub fn prepare_for_launch_after_update(mut cmd: Command, root: &Path) -> Result<()> {
|
||||||
// Pre-validate by running --version to trigger any Gatekeeper checks
|
// Pre-validate by running --version to trigger any Gatekeeper checks
|
||||||
print!("\n\x1B[1mThis may take a few minutes. Please wait\x1B[0m");
|
|
||||||
io::stdout().flush().unwrap();
|
|
||||||
|
|
||||||
// Start progress indicator
|
|
||||||
let running = Arc::new(AtomicBool::new(true));
|
|
||||||
let running_clone = running.clone();
|
|
||||||
let progress_thread = thread::spawn(move || {
|
|
||||||
while running_clone.load(Ordering::Relaxed) {
|
|
||||||
print!(".");
|
|
||||||
io::stdout().flush().unwrap();
|
|
||||||
thread::sleep(Duration::from_secs(1));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let _ = cmd
|
let _ = cmd
|
||||||
.env("ANKI_FIRST_RUN", "1")
|
.env("ANKI_FIRST_RUN", "1")
|
||||||
|
|
@ -52,23 +31,9 @@ pub fn prepare_for_launch_after_update(mut cmd: Command, root: &Path) -> Result<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop progress indicator
|
|
||||||
running.store(false, Ordering::Relaxed);
|
|
||||||
progress_thread.join().unwrap();
|
|
||||||
println!(); // New line after dots
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn relaunch_in_terminal() -> Result<()> {
|
|
||||||
let current_exe = std::env::current_exe().context("Failed to get current executable path")?;
|
|
||||||
Command::new("open")
|
|
||||||
.args(["-na", "Terminal"])
|
|
||||||
.arg(current_exe)
|
|
||||||
.env_remove("ANKI_LAUNCHER_WANT_TERMINAL")
|
|
||||||
.ensure_spawn()?;
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn finalize_uninstall() {
|
pub fn finalize_uninstall() {
|
||||||
if let Ok(exe_path) = std::env::current_exe() {
|
if let Ok(exe_path) = std::env::current_exe() {
|
||||||
// Find the .app bundle by walking up the directory tree
|
// Find the .app bundle by walking up the directory tree
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
#[cfg(all(unix, not(target_os = "macos")))]
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
pub mod unix;
|
pub mod unix;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
// #[cfg(target_os = "macos")]
|
||||||
pub mod mac;
|
pub mod mac;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
|
@ -49,6 +49,58 @@ pub fn get_uv_binary_name() -> &'static str {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
// TODO: remove
|
||||||
|
launcher_cmd.env("ANKI_LAUNCHER_SKIP", "1");
|
||||||
|
|
||||||
|
#[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<()> {
|
pub fn launch_anki_normally(mut cmd: std::process::Command) -> Result<()> {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
|
|
@ -56,7 +108,7 @@ pub fn launch_anki_normally(mut cmd: std::process::Command) -> Result<()> {
|
||||||
cmd.ensure_spawn()?;
|
cmd.ensure_spawn()?;
|
||||||
}
|
}
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
cmd.ensure_spawn()?;
|
cmd.ensure_exec()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
122
qt/launcher-gui/src-tauri/src/state.rs
Normal file
122
qt/launcher-gui/src-tauri/src/state.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub use anki_proto::launcher::ExistingVersions;
|
||||||
|
use anki_proto::launcher::Mirror;
|
||||||
|
use anki_proto::launcher::Options as OptionsProto;
|
||||||
|
pub use anki_proto::launcher::Version;
|
||||||
|
pub use anki_proto::launcher::Versions;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use anyhow::Result;
|
||||||
|
use tokio::sync::watch;
|
||||||
|
|
||||||
|
use crate::uv;
|
||||||
|
|
||||||
|
pub struct Options {
|
||||||
|
allow_betas: bool,
|
||||||
|
download_caching: bool,
|
||||||
|
mirror: Mirror,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Options> for OptionsProto {
|
||||||
|
fn from(o: &Options) -> Self {
|
||||||
|
Self {
|
||||||
|
allow_betas: o.allow_betas,
|
||||||
|
download_caching: o.download_caching,
|
||||||
|
mirror: o.mirror.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&uv::Paths> for Options {
|
||||||
|
fn from(state: &uv::Paths) -> Self {
|
||||||
|
let allow_betas = state.prerelease_marker.exists();
|
||||||
|
let download_caching = !state.no_cache_marker.exists();
|
||||||
|
let mirror = if state.mirror_path.exists() {
|
||||||
|
Mirror::China
|
||||||
|
} else {
|
||||||
|
Mirror::Disabled
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
allow_betas,
|
||||||
|
download_caching,
|
||||||
|
mirror,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NormalState {
|
||||||
|
pub paths: Arc<uv::Paths>,
|
||||||
|
pub initial_options: Options,
|
||||||
|
pub current_versions: Option<watch::Receiver<Option<Result<ExistingVersions>>>>,
|
||||||
|
pub available_versions: Option<watch::Receiver<Option<Result<Versions>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<uv::Paths> for NormalState {
|
||||||
|
fn from(paths: uv::Paths) -> Self {
|
||||||
|
Self {
|
||||||
|
initial_options: Options::from(&paths),
|
||||||
|
current_versions: None,
|
||||||
|
available_versions: None,
|
||||||
|
paths: paths.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NormalState> for State {
|
||||||
|
fn from(state: NormalState) -> Self {
|
||||||
|
Self::Normal(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum State {
|
||||||
|
LaunchAnki(Arc<uv::Paths>),
|
||||||
|
Error(anyhow::Error),
|
||||||
|
Uninstall(Arc<uv::Paths>),
|
||||||
|
Normal(NormalState),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub fn normal(&self) -> Result<&NormalState> {
|
||||||
|
match self {
|
||||||
|
State::Normal(state) => Ok(state),
|
||||||
|
_ => Err(anyhow!("unexpected state")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paths(&self) -> Result<&uv::Paths> {
|
||||||
|
match self {
|
||||||
|
State::LaunchAnki(paths) => Ok(paths),
|
||||||
|
State::Uninstall(paths) => Ok(paths),
|
||||||
|
State::Normal(state) => Ok(&state.paths),
|
||||||
|
_ => Err(anyhow!("unexpected state")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn should_launch_anki(&self) -> bool {
|
||||||
|
matches!(self, State::LaunchAnki(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NormalState {
|
||||||
|
pub fn check_versions(&mut self) {
|
||||||
|
let (av_tx, av_rx) = tokio::sync::watch::channel(None);
|
||||||
|
let paths = self.paths.clone();
|
||||||
|
tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
let res = paths.get_releases();
|
||||||
|
let _ = av_tx.send(Some(res));
|
||||||
|
});
|
||||||
|
|
||||||
|
let (cv_tx, cv_rx) = tokio::sync::watch::channel(None);
|
||||||
|
let paths = self.paths.clone();
|
||||||
|
tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
let res = paths.check_versions();
|
||||||
|
let _ = cv_tx.send(Some(res));
|
||||||
|
});
|
||||||
|
|
||||||
|
self.current_versions = Some(cv_rx);
|
||||||
|
self.available_versions = Some(av_rx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,19 +19,19 @@ use anki_process::CommandExt as AnkiCommandExt;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use tauri::AppHandle;
|
|
||||||
use tauri::Emitter;
|
|
||||||
use tauri::Runtime;
|
|
||||||
|
|
||||||
use crate::platform;
|
use crate::platform;
|
||||||
use crate::platform::ensure_os_supported;
|
use crate::platform::ensure_os_supported;
|
||||||
use crate::platform::get_exe_and_resources_dirs;
|
use crate::platform::get_exe_and_resources_dirs;
|
||||||
use crate::platform::get_uv_binary_name;
|
use crate::platform::get_uv_binary_name;
|
||||||
pub use crate::platform::launch_anki_normally;
|
pub use crate::platform::launch_anki_normally;
|
||||||
|
use crate::platform::respawn_launcher;
|
||||||
|
use crate::state::ExistingVersions;
|
||||||
|
use crate::state::Version;
|
||||||
|
use crate::state::Versions;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct State {
|
pub struct Paths {
|
||||||
pub current_version: Option<String>,
|
|
||||||
pub prerelease_marker: std::path::PathBuf,
|
pub prerelease_marker: std::path::PathBuf,
|
||||||
uv_install_root: std::path::PathBuf,
|
uv_install_root: std::path::PathBuf,
|
||||||
uv_cache_dir: std::path::PathBuf,
|
uv_cache_dir: std::path::PathBuf,
|
||||||
|
|
@ -48,7 +48,6 @@ pub struct State {
|
||||||
launcher_trigger_file: std::path::PathBuf,
|
launcher_trigger_file: std::path::PathBuf,
|
||||||
pub mirror_path: std::path::PathBuf,
|
pub mirror_path: std::path::PathBuf,
|
||||||
pub pyproject_modified_by_user: bool,
|
pub pyproject_modified_by_user: bool,
|
||||||
previous_version: Option<String>,
|
|
||||||
resources_dir: std::path::PathBuf,
|
resources_dir: std::path::PathBuf,
|
||||||
venv_folder: std::path::PathBuf,
|
venv_folder: std::path::PathBuf,
|
||||||
/// system Python + PyQt6 library mode
|
/// system Python + PyQt6 library mode
|
||||||
|
|
@ -61,186 +60,25 @@ pub enum VersionKind {
|
||||||
Uv(String),
|
Uv(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub fn handle_version_install_or_update<F>(
|
||||||
pub struct Releases {
|
state: &Paths,
|
||||||
pub latest: Vec<String>,
|
|
||||||
pub all: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_state() -> Result<Option<State>> {
|
|
||||||
let uv_install_root = if let Ok(custom_root) = std::env::var("ANKI_LAUNCHER_VENV_ROOT") {
|
|
||||||
std::path::PathBuf::from(custom_root)
|
|
||||||
} else {
|
|
||||||
dirs::data_local_dir()
|
|
||||||
.context("Unable to determine data_dir")?
|
|
||||||
.join("AnkiProgramFiles")
|
|
||||||
};
|
|
||||||
|
|
||||||
let (exe_dir, resources_dir) = get_exe_and_resources_dirs()?;
|
|
||||||
|
|
||||||
let mut state = State {
|
|
||||||
// TODO: return error instead of relying on member field here if os unsupported
|
|
||||||
current_version: None,
|
|
||||||
prerelease_marker: uv_install_root.join("prerelease"),
|
|
||||||
uv_install_root: uv_install_root.clone(),
|
|
||||||
uv_cache_dir: uv_install_root.join("cache"),
|
|
||||||
no_cache_marker: uv_install_root.join("nocache"),
|
|
||||||
anki_base_folder: get_anki_base_path()?,
|
|
||||||
uv_path: exe_dir.join(get_uv_binary_name()),
|
|
||||||
uv_python_install_dir: uv_install_root.join("python"),
|
|
||||||
user_pyproject_path: uv_install_root.join("pyproject.toml"),
|
|
||||||
user_python_version_path: uv_install_root.join(".python-version"),
|
|
||||||
dist_pyproject_path: resources_dir.join("pyproject.toml"),
|
|
||||||
dist_python_version_path: resources_dir.join(".python-version"),
|
|
||||||
uv_lock_path: uv_install_root.join("uv.lock"),
|
|
||||||
sync_complete_marker: uv_install_root.join(".sync_complete"),
|
|
||||||
launcher_trigger_file: uv_install_root.join(".want-launcher"),
|
|
||||||
mirror_path: uv_install_root.join("mirror"),
|
|
||||||
pyproject_modified_by_user: false, // calculated later
|
|
||||||
previous_version: None,
|
|
||||||
system_qt: (cfg!(unix) && !cfg!(target_os = "macos"))
|
|
||||||
&& resources_dir.join("system_qt").exists(),
|
|
||||||
resources_dir,
|
|
||||||
venv_folder: uv_install_root.join(".venv"),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check for uninstall request from Windows uninstaller
|
|
||||||
if std::env::var("ANKI_LAUNCHER_UNINSTALL").is_ok() {
|
|
||||||
// handle_uninstall(&state)?;
|
|
||||||
println!("TODO: UNINSTALL");
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create install directory
|
|
||||||
create_dir_all(&state.uv_install_root)?;
|
|
||||||
|
|
||||||
let launcher_requested =
|
|
||||||
state.launcher_trigger_file.exists() || !state.user_pyproject_path.exists();
|
|
||||||
|
|
||||||
// Calculate whether user has custom edits that need syncing
|
|
||||||
let pyproject_time = file_timestamp_secs(&state.user_pyproject_path);
|
|
||||||
let sync_time = file_timestamp_secs(&state.sync_complete_marker);
|
|
||||||
state.pyproject_modified_by_user = pyproject_time > sync_time;
|
|
||||||
let pyproject_has_changed = state.pyproject_modified_by_user;
|
|
||||||
|
|
||||||
let debug = cfg!(debug_assertions);
|
|
||||||
|
|
||||||
if !launcher_requested && !pyproject_has_changed && !debug {
|
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
||||||
let cmd = build_python_command(&state, &args)?;
|
|
||||||
launch_anki_normally(cmd)?;
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
if launcher_requested {
|
|
||||||
// Remove the trigger file to make request ephemeral
|
|
||||||
let _ = remove_file(&state.launcher_trigger_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
let _ = ensure_os_supported();
|
|
||||||
|
|
||||||
// TODO: we should call this here instead of via getVersions
|
|
||||||
// check_versions(&mut state);
|
|
||||||
|
|
||||||
Ok(Some(state))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn post_install(state: &State) -> Result<()> {
|
|
||||||
// Write marker file to indicate we've completed the sync process
|
|
||||||
write_sync_marker(state)?;
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
let cmd = build_python_command(&state, &[])?;
|
|
||||||
platform::mac::prepare_for_launch_after_update(cmd, &uv_install_root)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// respawn the launcher as a disconnected subprocess for normal startup
|
|
||||||
// respawn_launcher()?;
|
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
||||||
let cmd = build_python_command(state, &args)?;
|
|
||||||
launch_anki_normally(cmd)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_aqt_version(state: &State) -> Option<String> {
|
|
||||||
// Check if .venv exists first
|
|
||||||
if !state.venv_folder.exists() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let output = uv_command(state)
|
|
||||||
.ok()?
|
|
||||||
.env("VIRTUAL_ENV", &state.venv_folder)
|
|
||||||
.args(["pip", "show", "aqt"])
|
|
||||||
.output();
|
|
||||||
|
|
||||||
let output = output.ok()?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let stdout = String::from_utf8(output.stdout).ok()?;
|
|
||||||
for line in stdout.lines() {
|
|
||||||
if let Some(version) = line.strip_prefix("Version: ") {
|
|
||||||
return Some(version.trim().to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn check_versions(state: &mut State) -> Result<(Option<String>, Option<String>)> {
|
|
||||||
// If sync_complete_marker is missing, do nothing
|
|
||||||
if !state.sync_complete_marker.exists() {
|
|
||||||
return Ok((None, None));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine current version by invoking uv pip show aqt
|
|
||||||
match extract_aqt_version(state) {
|
|
||||||
Some(version) => {
|
|
||||||
state.current_version = Some(normalize_version(&version));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"Warning: Could not determine current Anki version"
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read previous version from "previous-version" file
|
|
||||||
let previous_version_path = state.uv_install_root.join("previous-version");
|
|
||||||
if let Ok(content) = read_file(&previous_version_path) {
|
|
||||||
if let Ok(version_str) = String::from_utf8(content) {
|
|
||||||
let version = version_str.trim().to_string();
|
|
||||||
if !version.is_empty() {
|
|
||||||
state.previous_version = Some(normalize_version(&version));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
state.current_version.clone(),
|
|
||||||
state.previous_version.clone(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_version_install_or_update<R: Runtime>(
|
|
||||||
app: AppHandle<R>,
|
|
||||||
state: &State,
|
|
||||||
version: &str,
|
version: &str,
|
||||||
keep_existing: bool,
|
keep_existing: bool,
|
||||||
) -> Result<()> {
|
previous_version_to_save: Option<&str>,
|
||||||
let version_kind =
|
on_pty_data: F,
|
||||||
parse_version_kind(version).ok_or_else(|| anyhow!("{version} is not a valid version!"))?;
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
F: Fn(String) + Send + 'static,
|
||||||
|
{
|
||||||
|
let version_kind = parse_version_kind(version)
|
||||||
|
.ok_or_else(|| anyhow!(r#""{version}" is not a valid version!"#))?;
|
||||||
if !keep_existing {
|
if !keep_existing {
|
||||||
apply_version_kind(&version_kind, state)?;
|
apply_version_kind(&version_kind, state)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: support this
|
||||||
// Extract current version before syncing (but don't write to file yet)
|
// Extract current version before syncing (but don't write to file yet)
|
||||||
let previous_version_to_save = state.current_version.clone();
|
// let previous_version_to_save = state.current_version.clone();
|
||||||
|
|
||||||
// Remove sync marker before attempting sync
|
// Remove sync marker before attempting sync
|
||||||
let _ = remove_file(&state.sync_complete_marker);
|
let _ = remove_file(&state.sync_complete_marker);
|
||||||
|
|
@ -326,15 +164,15 @@ pub fn handle_version_install_or_update<R: Runtime>(
|
||||||
.openpty(portable_pty::PtySize {
|
.openpty(portable_pty::PtySize {
|
||||||
// NOTE: must be the same as xterm.js', otherwise text won't wrap
|
// NOTE: must be the same as xterm.js', otherwise text won't wrap
|
||||||
// TODO: maybe don't hardcode?
|
// TODO: maybe don't hardcode?
|
||||||
rows: 12,
|
rows: 10,
|
||||||
cols: 60,
|
cols: 50,
|
||||||
pixel_width: 0,
|
pixel_width: 0,
|
||||||
pixel_height: 0,
|
pixel_height: 0,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.with_context(|| "failed to open pty")?;
|
||||||
|
|
||||||
let mut reader = pair.master.try_clone_reader().unwrap();
|
let mut reader = pair.master.try_clone_reader()?;
|
||||||
let mut writer = pair.master.take_writer().unwrap();
|
let mut writer = pair.master.take_writer()?;
|
||||||
|
|
||||||
tauri::async_runtime::spawn_blocking(move || {
|
tauri::async_runtime::spawn_blocking(move || {
|
||||||
let mut buf = [0u8; 1024];
|
let mut buf = [0u8; 1024];
|
||||||
|
|
@ -345,13 +183,13 @@ pub fn handle_version_install_or_update<R: Runtime>(
|
||||||
Ok(0) => break,
|
Ok(0) => break,
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
let output = String::from_utf8_lossy(&buf[..n]).to_string();
|
let output = String::from_utf8_lossy(&buf[..n]).to_string();
|
||||||
// NOTE: windows requests curspr position before actually running child
|
// NOTE: windows requests cursor position before actually running child
|
||||||
if output == "\x1b[6n" {
|
if output == "\x1b[6n" {
|
||||||
writeln!(&mut writer, "\x1b[0;0R").unwrap();
|
writeln!(&mut writer, "\x1b[0;0R").unwrap();
|
||||||
}
|
}
|
||||||
// cheaper to base64ise a string than jsonify an [u8]
|
// cheaper to base64ise a string than jsonify an [u8]
|
||||||
let data = data_encoding::BASE64.encode(&buf[..n]);
|
let data = data_encoding::BASE64.encode(&buf[..n]);
|
||||||
let _ = app.emit("pty-data", data);
|
on_pty_data(data);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error reading from PTY: {}", e);
|
eprintln!("Error reading from PTY: {}", e);
|
||||||
|
|
@ -361,6 +199,8 @@ pub fn handle_version_install_or_update<R: Runtime>(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let cmdline = command.as_unix_command_line()?;
|
||||||
|
|
||||||
let mut child = pair.slave.spawn_command(command).unwrap();
|
let mut child = pair.slave.spawn_command(command).unwrap();
|
||||||
drop(pair.slave);
|
drop(pair.slave);
|
||||||
println!("waiting on uv...");
|
println!("waiting on uv...");
|
||||||
|
|
@ -368,8 +208,8 @@ pub fn handle_version_install_or_update<R: Runtime>(
|
||||||
println!("uv exited with status: {:?}", status);
|
println!("uv exited with status: {:?}", status);
|
||||||
|
|
||||||
match status {
|
match status {
|
||||||
Ok(_) => {
|
// Sync succeeded
|
||||||
// Sync succeeded
|
Ok(exit_status) if exit_status.success() => {
|
||||||
if !keep_existing && matches!(version_kind, VersionKind::PyOxidizer(_)) {
|
if !keep_existing && matches!(version_kind, VersionKind::PyOxidizer(_)) {
|
||||||
inject_helper_addon()?;
|
inject_helper_addon()?;
|
||||||
}
|
}
|
||||||
|
|
@ -377,7 +217,7 @@ pub fn handle_version_install_or_update<R: Runtime>(
|
||||||
// Now that sync succeeded, save the previous version
|
// Now that sync succeeded, save the previous version
|
||||||
if let Some(current_version) = previous_version_to_save {
|
if let Some(current_version) = previous_version_to_save {
|
||||||
let previous_version_path = state.uv_install_root.join("previous-version");
|
let previous_version_path = state.uv_install_root.join("previous-version");
|
||||||
if let Err(e) = write_file(&previous_version_path, ¤t_version) {
|
if let Err(e) = write_file(&previous_version_path, current_version) {
|
||||||
// TODO:
|
// TODO:
|
||||||
println!("Warning: Could not save previous version: {e}");
|
println!("Warning: Could not save previous version: {e}");
|
||||||
}
|
}
|
||||||
|
|
@ -385,19 +225,21 @@ pub fn handle_version_install_or_update<R: Runtime>(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
// If sync fails due to things like a missing wheel on pypi,
|
||||||
// TODO:
|
// we need to remove the lockfile or uv will cache the bad result.
|
||||||
// If sync fails due to things like a missing wheel on pypi,
|
Ok(exit_status) => {
|
||||||
// we need to remove the lockfile or uv will cache the bad result.
|
let _ = remove_file(&state.uv_lock_path);
|
||||||
|
let code = exit_status.exit_code();
|
||||||
|
Err(anyhow!("Failed to run ({code}): {cmdline}"))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
let _ = remove_file(&state.uv_lock_path);
|
let _ = remove_file(&state.uv_lock_path);
|
||||||
println!("Install failed: {e:#}");
|
|
||||||
println!();
|
|
||||||
Err(e.into())
|
Err(e.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_allow_betas(state: &State, allow_betas: bool) -> Result<()> {
|
pub fn set_allow_betas(state: &Paths, allow_betas: bool) -> Result<()> {
|
||||||
if allow_betas {
|
if allow_betas {
|
||||||
write_file(&state.prerelease_marker, "")?;
|
write_file(&state.prerelease_marker, "")?;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -406,7 +248,7 @@ pub fn set_allow_betas(state: &State, allow_betas: bool) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_cache_enabled(state: &State, cache_enabled: bool) -> Result<()> {
|
pub fn set_cache_enabled(state: &Paths, cache_enabled: bool) -> Result<()> {
|
||||||
if cache_enabled {
|
if cache_enabled {
|
||||||
let _ = remove_file(&state.no_cache_marker);
|
let _ = remove_file(&state.no_cache_marker);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -419,15 +261,6 @@ pub fn set_cache_enabled(state: &State, cache_enabled: bool) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_sync_marker(state: &State) -> Result<()> {
|
|
||||||
let timestamp = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.context("Failed to get system time")?
|
|
||||||
.as_secs();
|
|
||||||
write_file(&state.sync_complete_marker, timestamp.to_string())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get mtime of provided file, or 0 if unavailable
|
/// Get mtime of provided file, or 0 if unavailable
|
||||||
fn file_timestamp_secs(path: &std::path::Path) -> i64 {
|
fn file_timestamp_secs(path: &std::path::Path) -> i64 {
|
||||||
modified_time(path)
|
modified_time(path)
|
||||||
|
|
@ -435,13 +268,13 @@ fn file_timestamp_secs(path: &std::path::Path) -> i64 {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_only_latest_patch(versions: &[String]) -> Vec<String> {
|
fn with_only_latest_patch(versions: &[Version]) -> Vec<Version> {
|
||||||
// Only show the latest patch release for a given (major, minor)
|
// Only show the latest patch release for a given (major, minor)
|
||||||
let mut seen_major_minor = std::collections::HashSet::new();
|
let mut seen_major_minor = std::collections::HashSet::new();
|
||||||
versions
|
versions
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|v| {
|
.filter(|v| {
|
||||||
let (major, minor, _, _) = parse_version_for_filtering(v);
|
let (major, minor, _, _) = parse_version_for_filtering(&v.version);
|
||||||
if major == 2 {
|
if major == 2 {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -481,12 +314,15 @@ fn parse_version_for_filtering(version_str: &str) -> (u32, u32, u32, bool) {
|
||||||
(major, minor, patch, is_prerelease)
|
(major, minor, patch, is_prerelease)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_version(version: &str) -> String {
|
fn normalize_version(version: &str) -> Version {
|
||||||
let (major, minor, patch, _is_prerelease) = parse_version_for_filtering(version);
|
let (major, minor, patch, is_prerelease) = parse_version_for_filtering(version);
|
||||||
|
|
||||||
if major <= 2 {
|
if major <= 2 {
|
||||||
// Don't transform versions <= 2.x
|
// Don't transform versions <= 2.x
|
||||||
return version.to_string();
|
return Version {
|
||||||
|
version: version.to_string(),
|
||||||
|
is_prerelease,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// For versions > 2, pad the minor version with leading zero if < 10
|
// For versions > 2, pad the minor version with leading zero if < 10
|
||||||
|
|
@ -516,18 +352,20 @@ fn normalize_version(version: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconstruct the version
|
// Reconstruct the version
|
||||||
if version.matches('.').count() >= 2 {
|
let version = if version.matches('.').count() >= 2 {
|
||||||
format!("{major}.{normalized_minor}.{patch}{prerelease_suffix}")
|
format!("{major}.{normalized_minor}.{patch}{prerelease_suffix}")
|
||||||
} else {
|
} else {
|
||||||
format!("{major}.{normalized_minor}{prerelease_suffix}")
|
format!("{major}.{normalized_minor}{prerelease_suffix}")
|
||||||
|
};
|
||||||
|
|
||||||
|
Version {
|
||||||
|
version,
|
||||||
|
is_prerelease,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filter_and_normalize_versions(
|
fn filter_and_normalize_versions1(all_versions: Vec<String>) -> Vec<Version> {
|
||||||
all_versions: Vec<String>,
|
let mut valid_versions: Vec<Version> = all_versions
|
||||||
include_prereleases: bool,
|
|
||||||
) -> Vec<String> {
|
|
||||||
let mut valid_versions: Vec<String> = all_versions
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|v| normalize_version(&v))
|
.map(|v| normalize_version(&v))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -535,57 +373,10 @@ fn filter_and_normalize_versions(
|
||||||
// Reverse to get chronological order (newest first)
|
// Reverse to get chronological order (newest first)
|
||||||
valid_versions.reverse();
|
valid_versions.reverse();
|
||||||
|
|
||||||
if !include_prereleases {
|
|
||||||
valid_versions.retain(|v| {
|
|
||||||
let (_, _, _, is_prerelease) = parse_version_for_filtering(v);
|
|
||||||
!is_prerelease
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
valid_versions
|
valid_versions
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_versions(state: &State) -> Result<Vec<String>> {
|
pub fn apply_version_kind(version_kind: &VersionKind, state: &Paths) -> Result<()> {
|
||||||
let versions_script = state.resources_dir.join("versions.py");
|
|
||||||
|
|
||||||
let mut cmd = uv_command(state)?;
|
|
||||||
cmd.args(["run", "--no-project", "--no-config", "--managed-python"])
|
|
||||||
.args(["--with", "pip-system-certs,requests[socks]"]);
|
|
||||||
|
|
||||||
let python_version = read_file(&state.dist_python_version_path)?;
|
|
||||||
let python_version_str =
|
|
||||||
String::from_utf8(python_version).context("Invalid UTF-8 in .python-version")?;
|
|
||||||
let version_trimmed = python_version_str.trim();
|
|
||||||
if !version_trimmed.is_empty() {
|
|
||||||
cmd.args(["--python", version_trimmed]);
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.arg(&versions_script);
|
|
||||||
|
|
||||||
let output = match cmd.utf8_output() {
|
|
||||||
Ok(output) => output,
|
|
||||||
Err(e) => {
|
|
||||||
return Err(e.into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let versions = serde_json::from_str(&output.stdout).context("Failed to parse versions JSON")?;
|
|
||||||
Ok(versions)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_releases(state: &State) -> Result<Releases> {
|
|
||||||
let include_prereleases = state.prerelease_marker.exists();
|
|
||||||
let all_versions = fetch_versions(state)?;
|
|
||||||
let all_versions = filter_and_normalize_versions(all_versions, include_prereleases);
|
|
||||||
|
|
||||||
let latest_patches = with_only_latest_patch(&all_versions);
|
|
||||||
let latest_releases: Vec<String> = latest_patches.into_iter().take(5).collect();
|
|
||||||
Ok(Releases {
|
|
||||||
latest: latest_releases,
|
|
||||||
all: all_versions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_version_kind(version_kind: &VersionKind, state: &State) -> Result<()> {
|
|
||||||
let content = read_file(&state.dist_pyproject_path)?;
|
let content = read_file(&state.dist_pyproject_path)?;
|
||||||
let content_str = String::from_utf8(content).context("Invalid UTF-8 in pyproject.toml")?;
|
let content_str = String::from_utf8(content).context("Invalid UTF-8 in pyproject.toml")?;
|
||||||
let updated_content = match version_kind {
|
let updated_content = match version_kind {
|
||||||
|
|
@ -745,7 +536,7 @@ fn get_anki_addons21_path() -> Result<std::path::PathBuf> {
|
||||||
|
|
||||||
// TODO: revert
|
// TODO: revert
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
fn handle_uninstall(state: &State) -> Result<bool> {
|
fn handle_uninstall(state: &Paths) -> Result<bool> {
|
||||||
// println!("{}", state.tr.launcher_uninstall_confirm());
|
// println!("{}", state.tr.launcher_uninstall_confirm());
|
||||||
print!("> ");
|
print!("> ");
|
||||||
let _ = stdout().flush();
|
let _ = stdout().flush();
|
||||||
|
|
@ -795,7 +586,7 @@ fn handle_uninstall(state: &State) -> Result<bool> {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uv_command(state: &State) -> Result<Command> {
|
fn uv_command(state: &Paths) -> Result<Command> {
|
||||||
let mut command = Command::new(&state.uv_path);
|
let mut command = Command::new(&state.uv_path);
|
||||||
command.current_dir(&state.uv_install_root);
|
command.current_dir(&state.uv_install_root);
|
||||||
|
|
||||||
|
|
@ -825,7 +616,7 @@ fn uv_command(state: &State) -> Result<Command> {
|
||||||
Ok(command)
|
Ok(command)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uv_pty_command(state: &State) -> Result<portable_pty::CommandBuilder> {
|
fn uv_pty_command(state: &Paths) -> Result<portable_pty::CommandBuilder> {
|
||||||
let mut command = portable_pty::CommandBuilder::new(&state.uv_path);
|
let mut command = portable_pty::CommandBuilder::new(&state.uv_path);
|
||||||
command.cwd(&state.uv_install_root);
|
command.cwd(&state.uv_install_root);
|
||||||
|
|
||||||
|
|
@ -847,34 +638,7 @@ fn uv_pty_command(state: &State) -> Result<portable_pty::CommandBuilder> {
|
||||||
Ok(command)
|
Ok(command)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_python_command(state: &State, args: &[String]) -> Result<Command> {
|
fn get_mirror_urls(state: &Paths) -> Result<Option<(String, String)>> {
|
||||||
let python_exe = 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")
|
|
||||||
} else {
|
|
||||||
state.venv_folder.join("Scripts/pythonw.exe")
|
|
||||||
}
|
|
||||||
} 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);
|
|
||||||
// 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());
|
|
||||||
|
|
||||||
// Set UV and Python paths for the Python code
|
|
||||||
cmd.env("ANKI_LAUNCHER_UV", state.uv_path.utf8()?.as_str());
|
|
||||||
cmd.env("UV_PROJECT", state.uv_install_root.utf8()?.as_str());
|
|
||||||
cmd.env_remove("SSLKEYLOGFILE");
|
|
||||||
|
|
||||||
Ok(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_mirror_urls(state: &State) -> Result<Option<(String, String)>> {
|
|
||||||
if !state.mirror_path.exists() {
|
if !state.mirror_path.exists() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
@ -893,7 +657,7 @@ fn get_mirror_urls(state: &State) -> Result<Option<(String, String)>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_mirror(state: &State, enabled: bool) -> Result<()> {
|
pub fn set_mirror(state: &Paths, enabled: bool) -> Result<()> {
|
||||||
if enabled {
|
if enabled {
|
||||||
let china_mirrors = "https://registry.npmmirror.com/-/binary/python-build-standalone/\nhttps://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/";
|
let china_mirrors = "https://registry.npmmirror.com/-/binary/python-build-standalone/\nhttps://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/";
|
||||||
write_file(&state.mirror_path, china_mirrors)?;
|
write_file(&state.mirror_path, china_mirrors)?;
|
||||||
|
|
@ -903,12 +667,299 @@ pub fn set_mirror(state: &State, enabled: bool) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl crate::state::State {
|
||||||
|
pub fn init() -> Result<Self> {
|
||||||
|
let uv_install_root = if let Ok(custom_root) = std::env::var("ANKI_LAUNCHER_VENV_ROOT") {
|
||||||
|
std::path::PathBuf::from(custom_root)
|
||||||
|
} else {
|
||||||
|
dirs::data_local_dir()
|
||||||
|
.context("Unable to determine data_dir")?
|
||||||
|
.join("AnkiProgramFiles")
|
||||||
|
};
|
||||||
|
|
||||||
|
let (exe_dir, resources_dir) = get_exe_and_resources_dirs()?;
|
||||||
|
|
||||||
|
let mut paths = Paths {
|
||||||
|
prerelease_marker: uv_install_root.join("prerelease"),
|
||||||
|
uv_install_root: uv_install_root.clone(),
|
||||||
|
uv_cache_dir: uv_install_root.join("cache"),
|
||||||
|
no_cache_marker: uv_install_root.join("nocache"),
|
||||||
|
anki_base_folder: get_anki_base_path()?,
|
||||||
|
uv_path: exe_dir.join(get_uv_binary_name()),
|
||||||
|
uv_python_install_dir: uv_install_root.join("python"),
|
||||||
|
user_pyproject_path: uv_install_root.join("pyproject.toml"),
|
||||||
|
user_python_version_path: uv_install_root.join(".python-version"),
|
||||||
|
dist_pyproject_path: resources_dir.join("pyproject.toml"),
|
||||||
|
dist_python_version_path: resources_dir.join(".python-version"),
|
||||||
|
uv_lock_path: uv_install_root.join("uv.lock"),
|
||||||
|
sync_complete_marker: uv_install_root.join(".sync_complete"),
|
||||||
|
launcher_trigger_file: uv_install_root.join(".want-launcher"),
|
||||||
|
mirror_path: uv_install_root.join("mirror"),
|
||||||
|
pyproject_modified_by_user: false, // calculated later
|
||||||
|
system_qt: (cfg!(unix) && !cfg!(target_os = "macos"))
|
||||||
|
&& resources_dir.join("system_qt").exists(),
|
||||||
|
resources_dir,
|
||||||
|
venv_folder: uv_install_root.join(".venv"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for uninstall request from Windows uninstaller
|
||||||
|
if std::env::var("ANKI_LAUNCHER_UNINSTALL").is_ok() {
|
||||||
|
return Ok(Self::Uninstall(paths.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create install directory
|
||||||
|
create_dir_all(&paths.uv_install_root)?;
|
||||||
|
|
||||||
|
let launcher_requested =
|
||||||
|
paths.launcher_trigger_file.exists() || !paths.user_pyproject_path.exists();
|
||||||
|
|
||||||
|
// TODO: remove
|
||||||
|
let skip = std::env::var("ANKI_LAUNCHER_SKIP").is_ok();
|
||||||
|
|
||||||
|
// Calculate whether user has custom edits that need syncing
|
||||||
|
let pyproject_time = file_timestamp_secs(&paths.user_pyproject_path);
|
||||||
|
let sync_time = file_timestamp_secs(&paths.sync_complete_marker);
|
||||||
|
paths.pyproject_modified_by_user = pyproject_time > sync_time;
|
||||||
|
let pyproject_has_changed = paths.pyproject_modified_by_user;
|
||||||
|
|
||||||
|
#[allow(clippy::nonminimal_bool)]
|
||||||
|
let debug = true && cfg!(debug_assertions);
|
||||||
|
|
||||||
|
if !launcher_requested && !pyproject_has_changed && (!debug || skip) {
|
||||||
|
return Ok(Self::LaunchAnki(paths.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if launcher_requested {
|
||||||
|
// Remove the trigger file to make request ephemeral
|
||||||
|
let _ = remove_file(&paths.launcher_trigger_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_os_supported()?;
|
||||||
|
|
||||||
|
Ok(Self::Normal(paths.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paths {
|
||||||
|
fn get_mirror_urls(&self) -> Result<Option<(String, String)>> {
|
||||||
|
if !self.mirror_path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = read_file(&self.mirror_path)?;
|
||||||
|
let content_str = String::from_utf8(content).context("Invalid UTF-8 in mirror file")?;
|
||||||
|
|
||||||
|
let lines: Vec<&str> = content_str.lines().collect();
|
||||||
|
if lines.len() >= 2 {
|
||||||
|
Ok(Some((
|
||||||
|
lines[0].trim().to_string(),
|
||||||
|
lines[1].trim().to_string(),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uv_command(&self) -> Result<Command> {
|
||||||
|
let mut command = Command::new(&self.uv_path);
|
||||||
|
command.current_dir(&self.uv_install_root);
|
||||||
|
|
||||||
|
// remove UV_* environment variables to avoid interference
|
||||||
|
for (key, _) in std::env::vars() {
|
||||||
|
if key.starts_with("UV_") {
|
||||||
|
command.env_remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
command
|
||||||
|
.env_remove("VIRTUAL_ENV")
|
||||||
|
.env_remove("SSLKEYLOGFILE");
|
||||||
|
|
||||||
|
// Add mirror environment variable if enabled
|
||||||
|
if let Some((python_mirror, pypi_mirror)) = self.get_mirror_urls()? {
|
||||||
|
command
|
||||||
|
.env("UV_PYTHON_INSTALL_MIRROR", &python_mirror)
|
||||||
|
.env("UV_DEFAULT_INDEX", &pypi_mirror);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
|
command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
|
||||||
|
}
|
||||||
|
Ok(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_versions(&self) -> Result<Vec<String>> {
|
||||||
|
let versions_script = self.resources_dir.join("versions.py");
|
||||||
|
|
||||||
|
let mut cmd = self.uv_command()?;
|
||||||
|
cmd.args(["run", "--no-project", "--no-config", "--managed-python"])
|
||||||
|
.args(["--with", "pip-system-certs,requests[socks]"]);
|
||||||
|
|
||||||
|
let python_version = read_file(&self.dist_python_version_path)?;
|
||||||
|
let python_version_str =
|
||||||
|
String::from_utf8(python_version).context("Invalid UTF-8 in .python-version")?;
|
||||||
|
let version_trimmed = python_version_str.trim();
|
||||||
|
if !version_trimmed.is_empty() {
|
||||||
|
cmd.args(["--python", version_trimmed]);
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.arg(&versions_script);
|
||||||
|
|
||||||
|
let output = match cmd.utf8_output() {
|
||||||
|
Ok(output) => output,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let versions =
|
||||||
|
serde_json::from_str(&output.stdout).context("Failed to parse versions JSON")?;
|
||||||
|
Ok(versions)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_releases(&self) -> Result<Versions> {
|
||||||
|
let all_versions = self.fetch_versions()?;
|
||||||
|
let all_versions = filter_and_normalize_versions1(all_versions);
|
||||||
|
|
||||||
|
let latest_patches = with_only_latest_patch(&all_versions);
|
||||||
|
let latest_releases: Vec<Version> = latest_patches.into_iter().take(5).collect();
|
||||||
|
|
||||||
|
Ok(Versions {
|
||||||
|
latest: latest_releases,
|
||||||
|
all: all_versions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_aqt_version(&self) -> Option<String> {
|
||||||
|
// Check if .venv exists first
|
||||||
|
if !self.venv_folder.exists() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = self
|
||||||
|
.uv_command()
|
||||||
|
.ok()?
|
||||||
|
.env("VIRTUAL_ENV", &self.venv_folder)
|
||||||
|
.args(["pip", "show", "aqt"])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
let output = output.ok()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8(output.stdout).ok()?;
|
||||||
|
for line in stdout.lines() {
|
||||||
|
if let Some(version) = line.strip_prefix("Version: ") {
|
||||||
|
return Some(version.trim().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_versions(&self) -> Result<ExistingVersions> {
|
||||||
|
let mut res = ExistingVersions {
|
||||||
|
pyproject_modified_by_user: self.pyproject_modified_by_user,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// If sync_complete_marker is missing, do nothing
|
||||||
|
if !self.sync_complete_marker.exists() {
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine current version by invoking uv pip show aqt
|
||||||
|
match self.extract_aqt_version() {
|
||||||
|
Some(version) => {
|
||||||
|
res.current = Some(normalize_version(&version));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"Warning: Could not determine current Anki version"
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read previous version from "previous-version" file
|
||||||
|
let previous_version_path = self.uv_install_root.join("previous-version");
|
||||||
|
if let Ok(content) = read_file(&previous_version_path) {
|
||||||
|
if let Ok(version_str) = String::from_utf8(content) {
|
||||||
|
let version = version_str.trim().to_string();
|
||||||
|
if !version.is_empty() {
|
||||||
|
res.previous = Some(normalize_version(&version));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_sync_marker(&self) -> Result<()> {
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.context("Failed to get system time")?
|
||||||
|
.as_secs();
|
||||||
|
write_file(&self.sync_complete_marker, timestamp.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn post_install(&self) -> Result<bool> {
|
||||||
|
// Write marker file to indicate we've completed the sync process
|
||||||
|
self.write_sync_marker()?;
|
||||||
|
|
||||||
|
// whether or not anki needs to warm up
|
||||||
|
Ok(cfg!(target_os = "macos"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn launch_anki(&self) -> Result<()> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let cmd = self.build_python_command(&[])?;
|
||||||
|
platform::mac::prepare_for_launch_after_update(cmd, &self.uv_install_root)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// respawn the launcher as a disconnected subprocess for normal startup
|
||||||
|
respawn_launcher()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_python_command(&self, args: &[String]) -> Result<Command> {
|
||||||
|
let python_exe = if cfg!(target_os = "windows") {
|
||||||
|
let show_console = std::env::var("ANKI_CONSOLE").is_ok();
|
||||||
|
if show_console {
|
||||||
|
self.venv_folder.join("Scripts/python.exe")
|
||||||
|
} else {
|
||||||
|
self.venv_folder.join("Scripts/pythonw.exe")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.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);
|
||||||
|
// 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());
|
||||||
|
|
||||||
|
// Set UV and Python paths for the Python code
|
||||||
|
cmd.env("ANKI_LAUNCHER_UV", self.uv_path.utf8()?.as_str());
|
||||||
|
cmd.env("UV_PROJECT", self.uv_install_root.utf8()?.as_str());
|
||||||
|
cmd.env_remove("SSLKEYLOGFILE");
|
||||||
|
|
||||||
|
Ok(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_normalize_version() {
|
fn test_normalize_version() {
|
||||||
|
let normalize_version = |v| normalize_version(v).version;
|
||||||
|
|
||||||
// Test versions <= 2.x (should not be transformed)
|
// Test versions <= 2.x (should not be transformed)
|
||||||
assert_eq!(normalize_version("2.1.50"), "2.1.50");
|
assert_eq!(normalize_version("2.1.50"), "2.1.50");
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue