Avoid UV_PRERELEASE=allow

It had some downsides:
- the lockfile was discarded when switching between beta/non-beta
- it could result in beta versions of transitory dependencies

By adding 'anki' and 'aqt' as first-party packages with explicit
version numbers (validated by the version list we get from PyPi),
we can allow them to be installed without breaking other deps.

https://forums.ankiweb.net/t/bundling-numpy-in-an-add-on/62669/15
This commit is contained in:
Damien Elmes 2025-07-08 15:32:54 +07:00
parent 1098d9ac2a
commit 1ad82ea8b5

View file

@ -56,6 +56,12 @@ pub enum VersionKind {
Uv(String), Uv(String),
} }
#[derive(Debug)]
pub struct Releases {
pub latest: Vec<String>,
pub all: Vec<String>,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum MainMenuChoice { pub enum MainMenuChoice {
Latest, Latest,
@ -230,13 +236,7 @@ fn check_versions(state: &mut State) {
} }
fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Result<()> { fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Result<()> {
update_pyproject_for_version( update_pyproject_for_version(choice.clone(), state)?;
choice.clone(),
state.dist_pyproject_path.clone(),
state.user_pyproject_path.clone(),
state.dist_python_version_path.clone(),
state.user_python_version_path.clone(),
)?;
// 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 = extract_aqt_version(&state.uv_path, &state.uv_install_root); let previous_version_to_save = extract_aqt_version(&state.uv_path, &state.uv_install_root);
@ -284,11 +284,6 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re
command.args(["--python", version]); command.args(["--python", version]);
} }
// Set UV_PRERELEASE=allow if beta mode is enabled
if state.prerelease_marker.exists() {
command.env("UV_PRERELEASE", "allow");
}
if state.no_cache_marker.exists() { if state.no_cache_marker.exists() {
command.env("UV_NO_CACHE", "1"); command.env("UV_NO_CACHE", "1");
} }
@ -387,7 +382,7 @@ fn write_sync_marker(sync_complete_marker: &std::path::Path) -> Result<()> {
fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> { fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
loop { loop {
println!("1) Latest Anki (just press Enter)"); println!("1) Latest Anki (press Enter)");
println!("2) Choose a version"); println!("2) Choose a version");
if let Some(current_version) = &state.current_version { if let Some(current_version) = &state.current_version {
let normalized_current = normalize_version(current_version); let normalized_current = normalize_version(current_version);
@ -465,13 +460,9 @@ fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
fn get_version_kind(state: &State) -> Result<Option<VersionKind>> { fn get_version_kind(state: &State) -> Result<Option<VersionKind>> {
println!("Please wait..."); println!("Please wait...");
let include_prereleases = state.prerelease_marker.exists(); let releases = get_releases(state)?;
let all_versions = fetch_versions(state)?; let releases_str = releases
let all_versions = filter_and_normalize_versions(all_versions, include_prereleases); .latest
let latest_patches = with_only_latest_patch(&all_versions);
let latest_releases: Vec<&String> = latest_patches.iter().take(5).collect();
let releases_str = latest_releases
.iter() .iter()
.map(|v| v.as_str()) .map(|v| v.as_str())
.collect::<Vec<_>>() .collect::<Vec<_>>()
@ -494,7 +485,7 @@ fn get_version_kind(state: &State) -> Result<Option<VersionKind>> {
let normalized_input = normalize_version(input); let normalized_input = normalize_version(input);
// Check if the version exists in the available versions // Check if the version exists in the available versions
let version_exists = all_versions.iter().any(|v| v == &normalized_input); let version_exists = releases.all.iter().any(|v| v == &normalized_input);
match (parse_version_kind(input), version_exists) { match (parse_version_kind(input), version_exists) {
(Some(version_kind), true) => { (Some(version_kind), true) => {
@ -502,7 +493,7 @@ fn get_version_kind(state: &State) -> Result<Option<VersionKind>> {
Ok(Some(version_kind)) Ok(Some(version_kind))
} }
(None, true) => { (None, true) => {
println!("Versions before 2.1.50 can't be installedn"); println!("Versions before 2.1.50 can't be installed.");
Ok(None) Ok(None)
} }
_ => { _ => {
@ -635,37 +626,23 @@ fn fetch_versions(state: &State) -> Result<Vec<String>> {
Ok(versions) Ok(versions)
} }
fn update_pyproject_for_version( fn get_releases(state: &State) -> Result<Releases> {
menu_choice: MainMenuChoice, let include_prereleases = state.prerelease_marker.exists();
dist_pyproject_path: std::path::PathBuf, let all_versions = fetch_versions(state)?;
user_pyproject_path: std::path::PathBuf, let all_versions = filter_and_normalize_versions(all_versions, include_prereleases);
dist_python_version_path: std::path::PathBuf,
user_python_version_path: std::path::PathBuf, let latest_patches = with_only_latest_patch(&all_versions);
) -> Result<()> { let latest_releases: Vec<String> = latest_patches.into_iter().take(5).collect();
match menu_choice { Ok(Releases {
MainMenuChoice::Latest => { latest: latest_releases,
let content = read_file(&dist_pyproject_path)?; all: all_versions,
write_file(&user_pyproject_path, &content)?; })
let python_version_content = read_file(&dist_python_version_path)?; }
write_file(&user_python_version_path, &python_version_content)?;
} fn apply_version_kind(version_kind: &VersionKind, state: &State) -> Result<()> {
MainMenuChoice::KeepExisting => { let content = read_file(&state.dist_pyproject_path)?;
// Do nothing - keep existing pyproject.toml and .python-version let content_str = String::from_utf8(content).context("Invalid UTF-8 in pyproject.toml")?;
} let updated_content = match version_kind {
MainMenuChoice::ToggleBetas => {
unreachable!();
}
MainMenuChoice::ToggleCache => {
unreachable!();
}
MainMenuChoice::Uninstall => {
unreachable!();
}
MainMenuChoice::Version(version_kind) => {
let content = read_file(&dist_pyproject_path)?;
let content_str =
String::from_utf8(content).context("Invalid UTF-8 in pyproject.toml")?;
let updated_content = match &version_kind {
VersionKind::PyOxidizer(version) => { VersionKind::PyOxidizer(version) => {
// Replace package name and add PyQt6 dependencies // Replace package name and add PyQt6 dependencies
content_str.replace( content_str.replace(
@ -684,21 +661,50 @@ fn update_pyproject_for_version(
), ),
) )
} }
VersionKind::Uv(version) => { VersionKind::Uv(version) => content_str.replace(
content_str.replace("anki-release", &format!("anki-release=={version}")) "anki-release",
} &format!("anki-release=={version}\",\n \"anki=={version}\",\n \"aqt=={version}"),
),
}; };
write_file(&user_pyproject_path, &updated_content)?; write_file(&state.user_pyproject_path, &updated_content)?;
// Update .python-version based on version kind // Update .python-version based on version kind
match &version_kind { match version_kind {
VersionKind::PyOxidizer(_) => { VersionKind::PyOxidizer(_) => {
write_file(&user_python_version_path, "3.9")?; write_file(&state.user_python_version_path, "3.9")?;
} }
VersionKind::Uv(_) => { VersionKind::Uv(_) => {
copy_file(&dist_python_version_path, &user_python_version_path)?; copy_file(
&state.dist_python_version_path,
&state.user_python_version_path,
)?;
} }
} }
Ok(())
}
fn update_pyproject_for_version(menu_choice: MainMenuChoice, state: &State) -> Result<()> {
match menu_choice {
MainMenuChoice::Latest => {
// Get the latest release version and create a VersionKind for it
let releases = get_releases(state)?;
let latest_version = releases.latest.first().context("No latest version found")?;
apply_version_kind(&VersionKind::Uv(latest_version.clone()), state)?;
}
MainMenuChoice::KeepExisting => {
// Do nothing - keep existing pyproject.toml and .python-version
}
MainMenuChoice::ToggleBetas => {
unreachable!();
}
MainMenuChoice::ToggleCache => {
unreachable!();
}
MainMenuChoice::Uninstall => {
unreachable!();
}
MainMenuChoice::Version(version_kind) => {
apply_version_kind(&version_kind, state)?;
} }
MainMenuChoice::Quit => { MainMenuChoice::Quit => {
std::process::exit(0); std::process::exit(0);
@ -875,11 +881,6 @@ fn build_python_command(state: &State, args: &[String]) -> Result<Command> {
cmd.env("ANKI_LAUNCHER_UV", state.uv_path.utf8()?.as_str()); cmd.env("ANKI_LAUNCHER_UV", state.uv_path.utf8()?.as_str());
cmd.env("UV_PROJECT", state.uv_install_root.utf8()?.as_str()); cmd.env("UV_PROJECT", state.uv_install_root.utf8()?.as_str());
// Set UV_PRERELEASE=allow if beta mode is enabled
if state.prerelease_marker.exists() {
cmd.env("UV_PRERELEASE", "allow");
}
Ok(cmd) Ok(cmd)
} }