mirror of
https://github.com/ankitects/anki.git
synced 2025-11-07 13:17:12 -05:00
convert uv methods back to free fns
This commit is contained in:
parent
36a0e0dcc7
commit
73ac0a1cfc
4 changed files with 184 additions and 237 deletions
|
|
@ -26,7 +26,7 @@ pub fn init() -> Option<State> {
|
|||
State::Normal(ref mut state) => state.check_versions(),
|
||||
State::LaunchAnki(ref paths) => {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let cmd = paths.build_python_command(&args).unwrap();
|
||||
let cmd = uv::build_python_command(paths, &args).unwrap();
|
||||
uv::launch_anki_normally(cmd).unwrap();
|
||||
return None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ pub async fn choose_version<R: Runtime>(
|
|||
)?;
|
||||
}
|
||||
|
||||
let warming_up = paths.post_install()?;
|
||||
let warming_up = uv::post_install(&paths)?;
|
||||
|
||||
Ok(ChooseVersionResponse {
|
||||
version,
|
||||
|
|
@ -210,7 +210,7 @@ pub async fn choose_version<R: Runtime>(
|
|||
}
|
||||
|
||||
pub async fn launch_anki<R: Runtime>(app: AppHandle<R>, _window: WebviewWindow<R>) -> Result<()> {
|
||||
app.state::<State>().paths()?.launch_anki()
|
||||
app.state::<State>().paths().and_then(uv::launch_anki)
|
||||
}
|
||||
|
||||
pub async fn exit<R: Runtime>(app: AppHandle<R>, window: WebviewWindow<R>) -> Result<()> {
|
||||
|
|
|
|||
|
|
@ -102,14 +102,14 @@ impl NormalState {
|
|||
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 res = uv::get_releases(&paths);
|
||||
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 res = uv::check_versions(&paths);
|
||||
let _ = cv_tx.send(Some(res));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ use crate::platform::get_uv_binary_name;
|
|||
pub use crate::platform::launch_anki_normally;
|
||||
use crate::platform::respawn_launcher;
|
||||
use crate::state::ExistingVersions;
|
||||
use crate::state::State;
|
||||
use crate::state::Version;
|
||||
use crate::state::Versions;
|
||||
|
||||
|
|
@ -60,6 +61,92 @@ pub enum VersionKind {
|
|||
Uv(String),
|
||||
}
|
||||
|
||||
fn extract_aqt_version(state: &Paths) -> 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: &Paths) -> Result<ExistingVersions> {
|
||||
let mut res = ExistingVersions {
|
||||
pyproject_modified_by_user: state.pyproject_modified_by_user,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// If sync_complete_marker is missing, do nothing
|
||||
if !state.sync_complete_marker.exists() {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
// Determine current version by invoking uv pip show aqt
|
||||
match extract_aqt_version(state) {
|
||||
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 = 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() {
|
||||
res.previous = Some(normalize_version(&version));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn set_allow_betas(state: &Paths, allow_betas: bool) -> Result<()> {
|
||||
if allow_betas {
|
||||
write_file(&state.prerelease_marker, "")?;
|
||||
} else {
|
||||
let _ = remove_file(&state.prerelease_marker);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_cache_enabled(state: &Paths, cache_enabled: bool) -> Result<()> {
|
||||
if cache_enabled {
|
||||
let _ = remove_file(&state.no_cache_marker);
|
||||
} else {
|
||||
write_file(&state.no_cache_marker, "")?;
|
||||
// Delete the cache directory and everything in it
|
||||
if state.uv_cache_dir.exists() {
|
||||
let _ = anki_io::remove_dir_all(&state.uv_cache_dir);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_version_install_or_update<F>(
|
||||
state: &Paths,
|
||||
version: &str,
|
||||
|
|
@ -239,25 +326,12 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_allow_betas(state: &Paths, allow_betas: bool) -> Result<()> {
|
||||
if allow_betas {
|
||||
write_file(&state.prerelease_marker, "")?;
|
||||
} else {
|
||||
let _ = remove_file(&state.prerelease_marker);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_cache_enabled(state: &Paths, cache_enabled: bool) -> Result<()> {
|
||||
if cache_enabled {
|
||||
let _ = remove_file(&state.no_cache_marker);
|
||||
} else {
|
||||
write_file(&state.no_cache_marker, "")?;
|
||||
// Delete the cache directory and everything in it
|
||||
if state.uv_cache_dir.exists() {
|
||||
let _ = anki_io::remove_dir_all(&state.uv_cache_dir);
|
||||
}
|
||||
}
|
||||
fn write_sync_marker(state: &Paths) -> 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(())
|
||||
}
|
||||
|
||||
|
|
@ -376,6 +450,46 @@ fn filter_and_normalize_versions1(all_versions: Vec<String>) -> Vec<Version> {
|
|||
valid_versions
|
||||
}
|
||||
|
||||
fn fetch_versions(state: &Paths) -> Result<Vec<String>> {
|
||||
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: &Paths) -> Result<Versions> {
|
||||
let all_versions = fetch_versions(state)?;
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply_version_kind(version_kind: &VersionKind, state: &Paths) -> Result<()> {
|
||||
let content = read_file(&state.dist_pyproject_path)?;
|
||||
let content_str = String::from_utf8(content).context("Invalid UTF-8 in pyproject.toml")?;
|
||||
|
|
@ -666,8 +780,53 @@ pub fn set_mirror(state: &Paths, enabled: bool) -> Result<()> {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn post_install(state: &Paths) -> Result<bool> {
|
||||
// Write marker file to indicate we've completed the sync process
|
||||
write_sync_marker(state)?;
|
||||
|
||||
impl crate::state::State {
|
||||
// whether or not anki needs to warm up
|
||||
Ok(cfg!(target_os = "macos"))
|
||||
}
|
||||
|
||||
pub fn launch_anki(_state: &Paths) -> Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let cmd = self.build_python_command(&[])?;
|
||||
platform::mac::prepare_for_launch_after_update(cmd, &_state.uv_install_root)?;
|
||||
}
|
||||
|
||||
// respawn the launcher as a disconnected subprocess for normal startup
|
||||
respawn_launcher()
|
||||
}
|
||||
|
||||
pub fn build_python_command(state: &Paths, 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
impl 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)
|
||||
|
|
@ -742,218 +901,6 @@ impl crate::state::State {
|
|||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
Loading…
Reference in a new issue