mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
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:
3 files changed
+165
-16
No files matched your search
+131
-11
@@ -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]
|
||||
|
||||
@@ -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
@@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user