Merge pull request #9974 from daniliammo/fix/launcher-prune-nightlies

🤖 Agent: code-owner approval given; pruning only touches marker-carrying Nightlies, manual/legacy ones are outside the quota (tests cover all four cases).
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-07 09:47:56 +01:00
commit 0876de55cc
3 files changed
+165 -16

No files matched your search

+131 -11
View File
@@ -2988,17 +2988,47 @@ pub fn remove_version(tag: &str) -> Result<(), String> {
Ok(())
}
/// How many of the newest release versions stay installed when the player has not said.
/// How many of the newest managed versions of each kind stay installed by default.
const KEEP_NEWEST: usize = 2;
/// The release versions to delete: all but the `keep` newest of `installed` (newest first), and
/// never one of `protected`. Nightly builds are left out: `nightly::after_update` removes the
/// older ones.
fn stale_versions(installed: &[String], keep: usize, protected: &[String]) -> Vec<String> {
/// Only Nightlies installed by an update may be removed automatically. Absence of this marker
/// means a manual install or a version from an older launcher whose origin is unknown.
const AUTO_INSTALLED: &str = ".auto-installed";
pub(crate) fn auto_installed(tag: &str) -> bool {
version_dir(tag).join(AUTO_INSTALLED).is_file()
}
pub fn mark_auto_installed(tag: &str) -> Result<(), String> {
let dir = version_dir(tag);
if !dir.join(COMPLETE).is_file() {
return Err(format!("{tag} is not installed."));
}
fs::write(dir.join(AUTO_INSTALLED), "").map_err(|e| e.to_string())
}
/// Keep the newest releases and automatically installed Nightlies separately. Manual and legacy
/// Nightlies are outside the quota; selected and instance-pinned versions are always protected.
fn stale_versions(
installed: &[String],
keep: usize,
protected: &[String],
auto: impl Fn(&str) -> bool,
) -> Vec<String> {
let (mut releases, mut nightlies) = (0, 0);
installed
.iter()
.filter(|t| is_game_tag(t))
.skip(keep)
.filter(|t| {
if is_game_tag(t) {
releases += 1;
releases > keep
} else if crate::nightly::date_of(t).is_some() && auto(t) {
nightlies += 1;
nightlies > keep
} else {
false
}
})
.filter(|t| !protected.contains(t))
.cloned()
.collect()
@@ -3043,7 +3073,12 @@ pub fn prune_old_versions() -> Vec<String> {
.filter_map(|i| i.version),
);
let mut gone = Vec::new();
for tag in stale_versions(&installed(), settings.keep_newest(), &protected) {
for tag in stale_versions(
&installed(),
settings.keep_newest(),
&protected,
auto_installed,
) {
if in_use(&tag) {
eprintln!("Keeping old version {tag}: the game is running from it.");
continue;
@@ -3371,6 +3406,11 @@ pub fn install_tag(tag: &str, as_update: bool, progress: impl Fn(f32)) -> Result
eprintln!("{warning}");
}
install(release, progress)?;
if as_update && release.nightly.is_some() {
if let Err(e) = mark_auto_installed(&release.tag) {
eprintln!("Could not mark updated Nightly {}: {e}", release.tag);
}
}
let mut settings = Settings::load();
// An older Nightly is a deliberate rollback: installed, but an update never moves the selection back.
let select = !as_update
@@ -6915,15 +6955,95 @@ pub fn {f}("
"v0.2.91", "v0.2.90", "v0.2.88", "v0.2.85", "v0.2.80", "nightly",
]);
assert_eq!(
stale_versions(&installed, 2, &tags(&["v0.2.85"])),
stale_versions(&installed, 2, &tags(&["v0.2.85"]), |_| false),
tags(&["v0.2.88", "v0.2.80"])
);
// A protected version among the newest still leaves the newest `keep` in place.
assert_eq!(
stale_versions(&installed, 2, &tags(&["v0.2.91"])),
stale_versions(&installed, 2, &tags(&["v0.2.91"]), |_| false),
tags(&["v0.2.88", "v0.2.85", "v0.2.80"])
);
assert!(stale_versions(&installed, 9, &[]).is_empty());
assert!(stale_versions(&installed, 9, &[], |_| false).is_empty());
}
#[test]
fn pruning_removes_unused_nightlies_when_the_instance_follows_updates() {
let installed = [
"nightly-2026-11-05-f319701",
"nightly-2026-11-04-6475f62",
"nightly-2026-11-03-f42ef42",
"nightly-2026-10-31-d91cab6",
]
.map(String::from);
// The default instance follows the selected version, so it adds no pin.
let protected = [installed[0].clone()];
assert_eq!(
stale_versions(&installed, 2, &protected, |_| true),
["nightly-2026-11-03-f42ef42", "nightly-2026-10-31-d91cab6"]
);
}
#[test]
fn pruning_keeps_selected_and_instance_pinned_nightlies() {
let installed = [
"nightly-2026-11-05-f319701",
"nightly-2026-11-04-6475f62",
"nightly-2026-11-03-f42ef42",
"nightly-2026-11-01-c9d474d",
"nightly-2026-10-31-d91cab6",
]
.map(String::from);
// A manual selection and a different instance's pin both survive the keep limit.
let protected = [installed[2].clone(), installed[4].clone()];
assert_eq!(
stale_versions(&installed, 1, &protected, |_| true),
["nightly-2026-11-04-6475f62", "nightly-2026-11-01-c9d474d"]
);
assert!(stale_versions(&installed, 5, &protected, |_| true).is_empty());
}
#[test]
fn pruning_keeps_releases_separate_from_nightlies_and_local_builds() {
let installed = [
"native-local-build",
"imported-2026-11-05-abcdef0",
"nightly-2026-11-05-f319701",
"nightly-2026-11-04-6475f62",
"v0.2.91",
"v0.2.90",
]
.map(String::from);
assert!(stale_versions(&installed, 2, &[], |_| true).is_empty());
assert_eq!(
stale_versions(&installed, 1, &[], |_| true),
["nightly-2026-11-04-6475f62", "v0.2.90"]
);
}
#[test]
fn pruning_preserves_manual_and_legacy_nightlies() {
let root =
std::env::temp_dir().join(format!("horizon-prune-origin-{}", std::process::id()));
let _ = fs::remove_dir_all(&root);
let installed = [
"nightly-2026-11-05-aaaaaaa", // automatically updated, selected
"nightly-2026-11-04-bbbbbbb", // installed manually
"nightly-2026-11-03-ccccccc", // automatically updated
"nightly-2026-11-02-ddddddd", // from an older launcher; origin unknown
"nightly-2026-11-01-eeeeeee", // automatically updated
]
.map(String::from);
for tag in [&installed[0], &installed[2], &installed[4]] {
let dir = root.join(tag);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join(AUTO_INSTALLED), "").unwrap();
}
let auto = |tag: &str| root.join(tag).join(AUTO_INSTALLED).is_file();
assert_eq!(
stale_versions(&installed, 2, &[installed[0].clone()], auto),
[installed[4].clone()]
);
let _ = fs::remove_dir_all(&root);
}
#[test]
+7
View File
@@ -1322,6 +1322,13 @@ impl Launcher {
}
match result {
Ok(()) => {
if as_update.is_some()
&& horizon_launcher::nightly::date_of(&tag).is_some()
{
if let Err(e) = backend::mark_auto_installed(&tag) {
eprintln!("Could not mark updated Nightly {tag}: {e}");
}
}
if self.updates.horizon.as_ref().is_some_and(|r| r.tag == tag) {
self.updates.horizon = None;
}
+27 -5
View File
@@ -74,22 +74,31 @@ pub fn after_update(version: &str) {
.into_iter()
.filter_map(|i| i.version)
.collect();
for old in stale(&backend::installed(), version, selected.as_deref(), &pinned) {
for old in stale(
&backend::installed(),
version,
selected.as_deref(),
&pinned,
backend::auto_installed,
) {
let _ = backend::remove(&old);
}
}
/// The installed Nightlies to remove once `current` is installed: every other one that is neither
/// selected nor pinned by an instance.
/// The automatically installed Nightlies to remove once `current` is installed: every other one
/// that is neither selected nor pinned by an instance. Manual and legacy installs stay.
fn stale(
installed: &[String],
current: &str,
selected: Option<&str>,
pinned: &[String],
auto: impl Fn(&str) -> bool,
) -> Vec<String> {
installed
.iter()
.filter(|v| date_of(v).is_some() && *v != current && Some(v.as_str()) != selected)
.filter(|v| {
date_of(v).is_some() && auto(v) && *v != current && Some(v.as_str()) != selected
})
.filter(|v| !pinned.contains(v))
.cloned()
.collect()
@@ -150,9 +159,22 @@ mod tests {
&installed,
"nightly-2026-10-29-aaaaaaa",
Some("nightly-2026-10-28-ccccccc"),
&pinned
&pinned,
|tag| tag != "nightly-2026-10-28-ccccccc"
),
["nightly-2026-10-28-bbbbbbb"]
);
}
#[test]
fn cli_update_keeps_unselected_manual_nightly() {
let current = "nightly-2026-11-05-aaaaaaa";
let manual = "nightly-2026-11-03-bbbbbbb";
let previous_auto = "nightly-2026-11-02-ccccccc";
let installed = [current, manual, previous_auto].map(String::from);
assert_eq!(
stale(&installed, current, None, &[], |tag| tag != manual),
[previous_auto]
);
}
}