Launcher tweaks

- Handle beta/rc tags in .version when launching Anki
- Update pyproject.toml/.python_version if distributed version newer
- Support prerelease marker to opt in to betas
- Check for updates when using uv sync
- Avoid system Python by default, as it can cause breakages
(e.g. ARM Python installed on Windows)
This commit is contained in:
Damien Elmes 2025-06-20 01:20:13 +07:00
parent 4abc0eb8b8
commit cd71931506
6 changed files with 67 additions and 38 deletions

View file

@ -309,12 +309,17 @@ def int_version() -> int:
"""Anki's version as an integer in the form YYMMPP, e.g. 230900.
(year, month, patch).
In 2.1.x releases, this was just the last number."""
import re
from anki.buildinfo import version
# Strip non-numeric characters (handles beta/rc suffixes like '25.02b1' or 'rc3')
numeric_version = re.sub(r"[^0-9.]", "", version)
try:
[year, month, patch] = version.split(".")
[year, month, patch] = numeric_version.split(".")
except ValueError:
[year, month] = version.split(".")
[year, month] = numeric_version.split(".")
patch = "0"
year_num = int(year)

View file

@ -36,10 +36,10 @@ class CustomBuildHook(BuildHookInterface):
def _set_anki_dependency(self, version: str, build_data: Dict[str, Any]) -> None:
# Get current dependencies and replace 'anki' with exact version
dependencies = build_data.setdefault("dependencies", [])
# Remove any existing anki dependency
dependencies[:] = [dep for dep in dependencies if not dep.startswith("anki")]
# Handle version detection
actual_version = version
if version == "standard":
@ -48,7 +48,7 @@ class CustomBuildHook(BuildHookInterface):
version_file = project_root / ".version"
if version_file.exists():
actual_version = version_file.read_text().strip()
# Only add exact version for real releases, not editable installs
if actual_version != "editable":
dependencies.append(f"anki=={actual_version}")

View file

@ -8,6 +8,7 @@ APP_LAUNCHER="$OUTPUT_DIR/Anki.app"
rm -rf "$APP_LAUNCHER"
# Build binaries for both architectures
rustup target add aarch64-apple-darwin x86_64-apple-darwin
cargo build -p launcher --release --target aarch64-apple-darwin
cargo build -p launcher --release --target x86_64-apple-darwin
(cd ../../.. && ./ninja launcher:uv_universal)

View file

@ -5,18 +5,4 @@ description = "UV-based launcher for Anki."
requires-python = ">=3.9"
dependencies = [
"anki-release",
# so we can use testpypi
"anki",
"aqt",
]
[tool.uv.sources]
anki-release = { index = "testpypi" }
anki = { index = "testpypi" }
aqt = { index = "testpypi" }
[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

View file

@ -6,9 +6,9 @@
use std::io::stdin;
use std::process::Command;
use anki_io::copy_file;
use anki_io::copy_if_newer;
use anki_io::create_dir_all;
use anki_io::metadata;
use anki_io::modified_time;
use anki_io::remove_file;
use anki_io::write_file;
use anki_process::CommandExt;
@ -51,6 +51,7 @@ fn run() -> Result<()> {
.join("AnkiProgramFiles");
let sync_complete_marker = uv_install_root.join(".sync_complete");
let prerelease_marker = uv_install_root.join("prerelease");
let (exe_dir, resources_dir) = get_exe_and_resources_dirs()?;
let dist_pyproject_path = resources_dir.join("pyproject.toml");
let user_pyproject_path = uv_install_root.join("pyproject.toml");
@ -59,14 +60,15 @@ fn run() -> Result<()> {
let uv_lock_path = uv_install_root.join("uv.lock");
let uv_path: std::path::PathBuf = exe_dir.join(get_uv_binary_name());
// Create install directory and copy project files in
create_dir_all(&uv_install_root)?;
copy_if_newer(&dist_pyproject_path, &user_pyproject_path)?;
copy_if_newer(&dist_python_version_path, &user_python_version_path)?;
let pyproject_has_changed =
!user_pyproject_path.exists() || !sync_complete_marker.exists() || {
let pyproject_toml_time = metadata(&user_pyproject_path)?
.modified()
.context("Failed to get pyproject.toml modified time")?;
let sync_complete_time = metadata(&sync_complete_marker)?
.modified()
.context("Failed to get sync marker modified time")?;
let pyproject_toml_time = modified_time(&user_pyproject_path)?;
let sync_complete_time = modified_time(&sync_complete_marker)?;
Ok::<bool, anyhow::Error>(pyproject_toml_time > sync_complete_time)
}
.unwrap_or(true);
@ -81,22 +83,21 @@ fn run() -> Result<()> {
// we'll need to launch uv; reinvoke ourselves in a terminal so the user can see
handle_terminal_launch()?;
// Create install directory and copy project files in
create_dir_all(&uv_install_root)?;
if !user_pyproject_path.exists() {
copy_file(&dist_pyproject_path, &user_pyproject_path)?;
copy_file(&dist_python_version_path, &user_python_version_path)?;
}
// Remove sync marker before attempting sync
let _ = remove_file(&sync_complete_marker);
// Sync the venv
if let Err(e) = Command::new(&uv_path)
let mut command = Command::new(&uv_path);
command
.current_dir(&uv_install_root)
.args(["sync", "--refresh"])
.ensure_success()
{
.args(["sync", "--upgrade", "--managed-python"]);
// Set UV_PRERELEASE=allow if prerelease file exists
if prerelease_marker.exists() {
command.env("UV_PRERELEASE", "allow");
}
if let Err(e) = command.ensure_success() {
// If sync fails due to things like a missing wheel on pypi,
// we need to remove the lockfile or uv will cache the bad result.
let _ = remove_file(&uv_lock_path);

View file

@ -152,6 +152,34 @@ pub fn copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<u64> {
})
}
/// Copy a file from src to dst if dst doesn't exist or if src is newer than
/// dst. Preserves the modification time from the source file.
pub fn copy_if_newer(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<bool> {
let src = src.as_ref();
let dst = dst.as_ref();
let should_copy = if !dst.exists() {
true
} else {
let src_time = modified_time(src)?;
let dst_time = modified_time(dst)?;
src_time > dst_time
};
if should_copy {
copy_file(src, dst)?;
// Preserve the modification time from the source file
let src_mtime = modified_time(src)?;
let times = FileTimes::new().set_modified(src_mtime);
set_file_times(dst, times)?;
Ok(true)
} else {
Ok(false)
}
}
/// Like [read_file], but skips the section that is potentially locked by
/// SQLite.
pub fn read_locked_db_file(path: impl AsRef<Path>) -> Result<Vec<u8>> {
@ -188,6 +216,14 @@ pub fn metadata(path: impl AsRef<Path>) -> Result<std::fs::Metadata> {
})
}
/// Get the modification time of a file.
pub fn modified_time(path: impl AsRef<Path>) -> Result<std::time::SystemTime> {
metadata(&path)?.modified().context(FileIoSnafu {
path: path.as_ref(),
op: FileOp::Metadata,
})
}
pub fn new_tempfile() -> Result<NamedTempFile> {
NamedTempFile::new().context(FileIoSnafu {
path: std::env::temp_dir(),