Skip to content

Commit 8fe2c0f

Browse files
committed
fix: fall back to older bundled distrobox when latest version not downloaded
When DISTROBOX_VERSION is bumped but not yet downloaded, the app now: 1. Falls back to the most recent bundled version on disk, or system distrobox 2. Shows an 'Update Distrobox' button in the sidebar when an update is available 3. Cleans up old bundled versions after a successful download Added resolve_bundled_distrobox_path() for smart path resolution, is_bundled_update_available() for UI state, and bundled_update_available property on RootStore for reactive sidebar button visibility.
1 parent 8a31eb8 commit 8fe2c0f

3 files changed

Lines changed: 140 additions & 9 deletions

File tree

src/distrobox_downloader.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,81 @@ pub fn get_bundled_distrobox_dir() -> PathBuf {
2525
user_data_dir.join("distroshelf")
2626
}
2727

28+
/// Resolves the best available bundled distrobox path.
29+
/// Returns the current version's path if it exists, otherwise finds the most recent older version.
30+
/// Returns None if no bundled version is found at all.
31+
pub fn resolve_bundled_distrobox_path() -> Option<PathBuf> {
32+
let current = get_bundled_distrobox_path();
33+
if current.exists() {
34+
return Some(current);
35+
}
36+
find_latest_bundled_version()
37+
}
38+
39+
/// Returns true if the user has a bundled distrobox but it's not the latest (DISTROBOX_VERSION).
40+
/// This means an update is available to download.
41+
pub fn is_bundled_update_available() -> bool {
42+
let current = get_bundled_distrobox_path();
43+
if current.exists() {
44+
return false; // Already have the latest
45+
}
46+
// An update is available if there's an older version on disk but not the current one
47+
find_latest_bundled_version().is_some()
48+
}
49+
50+
/// Scans the bundled distrobox directory for version subdirectories and returns the path
51+
/// to the distrobox binary in the most recent version found.
52+
fn find_latest_bundled_version() -> Option<PathBuf> {
53+
let parent = get_bundled_distrobox_dir();
54+
let entries = std::fs::read_dir(&parent).ok()?;
55+
56+
let mut versions: Vec<(Vec<u32>, PathBuf)> = entries
57+
.filter_map(|entry| {
58+
let entry = entry.ok()?;
59+
let name = entry.file_name();
60+
let name_str = name.to_str()?;
61+
let version_str = name_str.strip_prefix("distrobox-")?;
62+
let binary_path = entry.path().join("distrobox");
63+
if !binary_path.exists() {
64+
return None;
65+
}
66+
let parts: Option<Vec<u32>> = version_str.split('.').map(|p| p.parse().ok()).collect();
67+
Some((parts?, binary_path))
68+
})
69+
.collect();
70+
71+
versions.sort_by(|a, b| a.0.cmp(&b.0));
72+
versions.last().map(|(_, path)| path.clone())
73+
}
74+
75+
/// Removes all bundled distrobox version directories except the current one (DISTROBOX_VERSION).
76+
pub fn cleanup_old_bundled_versions() {
77+
let parent = get_bundled_distrobox_dir();
78+
let current_dir_name = format!("distrobox-{}", DISTROBOX_VERSION);
79+
80+
let entries = match std::fs::read_dir(&parent) {
81+
Ok(e) => e,
82+
Err(_) => return,
83+
};
84+
85+
for entry in entries.flatten() {
86+
let name = entry.file_name();
87+
let name_str = match name.to_str() {
88+
Some(s) => s.to_string(),
89+
None => continue,
90+
};
91+
if name_str.starts_with("distrobox-") && name_str != current_dir_name {
92+
if entry.path().is_dir() {
93+
if let Err(e) = std::fs::remove_dir_all(entry.path()) {
94+
tracing::warn!("Failed to remove old bundled version {:?}: {}", entry.path(), e);
95+
} else {
96+
tracing::info!("Removed old bundled version: {}", name_str);
97+
}
98+
}
99+
}
100+
}
101+
}
102+
28103
fn log(task: &DistroboxTask, msg: &str) {
29104
task.append_output(msg);
30105
task.append_output("\n");
@@ -130,8 +205,13 @@ pub fn download_distrobox(root_store: &RootStore) -> DistroboxTask {
130205

131206
log(&task, "Distrobox installed successfully.");
132207

208+
// Clean up old bundled versions
209+
log(&task, "Cleaning up old bundled versions...");
210+
cleanup_old_bundled_versions();
211+
133212
if let Some(root_store) = root_store_weak.upgrade() {
134213
root_store.distrobox_version().refetch();
214+
root_store.update_bundled_update_available();
135215
root_store.set_current_dialog(crate::models::DialogType::None);
136216
}
137217

src/models/root_store.rs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ mod imp {
7474
#[property(get, set, builder(DialogType::default()))]
7575
current_dialog: RefCell<DialogType>,
7676

77+
#[property(get, set)]
78+
bundled_update_available: std::cell::Cell<bool>,
79+
7780
/// Parameters for the current dialog (not a GObject property)
7881
pub dialog_params: RefCell<DialogParams>,
7982
}
@@ -104,6 +107,7 @@ mod imp {
104107
containers_query: Query::new("containers".into(), || async { Ok(vec![]) }),
105108
tasks: TypedListStore::new(),
106109
selected_task: Default::default(),
110+
bundled_update_available: std::cell::Cell::new(false),
107111
settings: gio::Settings::new("com.ranfdev.DistroShelf"),
108112
}
109113
}
@@ -125,17 +129,17 @@ mod imp {
125129
move |settings, _key| {
126130
let val = settings.string("distrobox-executable");
127131
if val == "bundled" {
128-
// Check if bundled version exists
129-
let path = crate::distrobox_downloader::get_bundled_distrobox_path();
130-
if !path.exists() {
132+
if crate::distrobox_downloader::resolve_bundled_distrobox_path()
133+
.is_none()
134+
{
131135
obj.download_distrobox();
132136
} else {
133-
// Just refetch version to update UI
134137
obj.distrobox_version().refetch();
135138
}
136139
} else {
137140
obj.distrobox_version().refetch();
138141
}
142+
obj.update_bundled_update_available();
139143
}
140144
),
141145
);
@@ -172,9 +176,9 @@ impl RootStore {
172176
let cmd_factory: crate::backends::distrobox::command::CmdFactory = Box::new(move || {
173177
let distrobox_executable_val = this_clone.settings().string("distrobox-executable");
174178
let selected_program: String = if distrobox_executable_val == "bundled" {
175-
crate::distrobox_downloader::get_bundled_distrobox_path()
176-
.to_string_lossy()
177-
.into_owned()
179+
crate::distrobox_downloader::resolve_bundled_distrobox_path()
180+
.map(|p| p.to_string_lossy().into_owned())
181+
.unwrap_or_else(|| "distrobox".into())
178182
} else {
179183
"distrobox".into()
180184
};
@@ -206,6 +210,11 @@ impl RootStore {
206210
let this_clone = this.clone();
207211
this.distrobox_version().connect_error(move |_error| {
208212
this_clone.set_current_view(ViewType::Welcome);
213+
this_clone.update_bundled_update_available();
214+
});
215+
let this_clone = this.clone();
216+
this.distrobox_version().connect_success(move |_version| {
217+
this_clone.update_bundled_update_available();
209218
});
210219
this.distrobox_version().refetch();
211220

@@ -345,6 +354,16 @@ impl RootStore {
345354
self.containers_query().refetch();
346355
}
347356

357+
/// Recalculates and sets the `bundled_update_available` property.
358+
/// Should be called after distrobox_version query completes, after a download, or when
359+
/// the distrobox-executable setting changes.
360+
pub fn update_bundled_update_available(&self) {
361+
let settings_val = self.settings().string("distrobox-executable");
362+
let available =
363+
settings_val == "bundled" && crate::distrobox_downloader::is_bundled_update_available();
364+
self.set_bundled_update_available(available);
365+
}
366+
348367
pub fn download_distrobox(&self) -> DistroboxTask {
349368
// Guard: if a download task is already in progress, return it instead of creating a duplicate
350369
for task in self.tasks().iter() {

src/widgets/window.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,12 +325,44 @@ impl DistroShelfWindow {
325325
.set_visible_child_name(visible_child_name);
326326
});
327327

328-
// Add tasks button to the bottom of the sidebar
328+
// Add tasks button and update button to the bottom of the sidebar
329+
let sidebar_bottom_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
330+
331+
// "Update Distrobox" button — visible only when a bundled update is available
332+
let update_button = gtk::Button::builder()
333+
.label(&gettext("Update Distrobox"))
334+
.build();
335+
update_button.add_css_class("suggested-action");
336+
update_button.add_css_class("pill");
337+
update_button.set_margin_start(12);
338+
update_button.set_margin_end(12);
339+
update_button.set_margin_top(12);
340+
update_button.set_visible(self.root_store().bundled_update_available());
341+
update_button.connect_clicked(clone!(
342+
#[weak(rename_to = this)]
343+
self,
344+
move |_| {
345+
this.root_store().download_distrobox();
346+
this.root_store()
347+
.set_current_dialog(DialogType::TaskManager);
348+
}
349+
));
350+
self.root_store().connect_bundled_update_available_notify(clone!(
351+
#[weak]
352+
update_button,
353+
move |root_store| {
354+
update_button.set_visible(root_store.bundled_update_available());
355+
}
356+
));
357+
sidebar_bottom_box.append(&update_button);
358+
329359
let tasks_button = TasksButton::new(&self.root_store());
330360
tasks_button.add_css_class("flat");
361+
sidebar_bottom_box.append(&tasks_button);
362+
331363
self.imp()
332364
.sidebar_bottom_slot
333-
.set_child(Some(&tasks_button));
365+
.set_child(Some(&sidebar_bottom_box));
334366
}
335367

336368
pub fn add_toast(&self, toast: adw::Toast) {

0 commit comments

Comments
 (0)