Updates: give packaged Linux installs the command that installs them (#456)

The updater downloads the new package and opens its folder. On Bazzite and
other rpm-ostree systems that left people to work out `rpm-ostree uninstall`
plus `rpm-ostree install` by hand. A packaged Linux copy now gets the one
command to run, with a Copy button: `rpm-ostree uninstall <pkg> --install
<rpm> && systemctl reboot` on rpm-ostree (the package name read from the rpm
database), `sudo dnf install` for rpm, `sudo apt install` for deb. The README's
Bazzite row says the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EU1Z2mdhAxBrH1yb48pxik
This commit is contained in:
phantomptrandClaude Opus 5.5 committed 2026-10-14 20:35:14 -07:00
1 parent ccdd5b1e28
commit f534ab020f
25 files changed
+129 -5

No files matched your search

+1 -1
View File
@@ -64,7 +64,7 @@ Pre-built downloads land on the
| macOS (Apple Silicon / Intel) | `PS5Upload-<ver>-mac-{arm64,x64}.dmg` | Open the `.dmg`, drag PS5Upload into Applications. See **First launch on macOS** below — Gatekeeper blocks downloaded apps the first time. |
| Windows (x64 / ARM64) | `PS5Upload-<ver>-win-{x64,arm64}.zip` | Unzip, double-click `PS5Upload.exe` — portable, no installer. See **First launch on Windows** — SmartScreen warns on first run. |
| Linux — Debian / Ubuntu (x64 / ARM64) | `PS5Upload-<ver>-linux-{x64,arm64}.deb` | `sudo apt install ./PS5Upload-<ver>-linux-<arch>.deb` — installs a normal app with a menu entry; pulls in the WebKitGTK deps automatically. |
| Linux — Fedora / RHEL / Bazzite (x64 / ARM64) | `PS5Upload-<ver>-linux-{x64,arm64}.rpm` | `sudo dnf install ./PS5Upload-<ver>-linux-<arch>.rpm` (Bazzite/Silverblue: `rpm-ostree install`) — menu entry + auto deps. |
| Linux — Fedora / RHEL / Bazzite (x64 / ARM64) | `PS5Upload-<ver>-linux-{x64,arm64}.rpm` | `sudo dnf install ./PS5Upload-<ver>-linux-<arch>.rpm` (Bazzite/Silverblue: `rpm-ostree install`; to update, `rpm-ostree uninstall ps5-upload --install ./<new>.rpm` and reboot, which Settings → Updates shows ready to copy) — menu entry + auto deps. |
| Linux — any distro (x64 / ARM64) | `PS5Upload-<ver>-linux-{x64,arm64}.zip` | Universal fallback (no install). Unzip, then `chmod +x PS5Upload.sh PS5Upload.AppImage` and run **`./PS5Upload.sh`** (the wrapper — handles the FUSE-less and WebKit white-screen cases for you). Running `./PS5Upload.AppImage` directly also works if your system has libfuse2 and a happy WebKitGTK. |
| Linux — NixOS (x64 / ARM64) | no release artifact | Packaged in [NUR](https://github.com/GriefNorth/nur-packages) as `ps5upload` — see **NixOS** below. |
| Android | `PS5Upload-<ver>-android.apk` | Enable "install unknown apps" for your browser/file manager, then open the `.apk`. Same interface, mobile-friendly; connects to and manages your PS5 over Wi-Fi. On Android 17, allow local network access ("nearby devices") when asked. |
+85 -3
View File
@@ -289,9 +289,28 @@ fn is_newer(current: &str, latest: &str) -> bool {
#[cfg(test)]
mod is_newer_tests {
use super::{
is_newer, is_safe_update_url, UpdateChannel, DEFAULT_MANIFEST_URL, PRERELEASE_LIST_URL,
install_command_for, is_newer, is_safe_update_url, UpdateChannel, DEFAULT_MANIFEST_URL,
PRERELEASE_LIST_URL,
};
#[test]
fn a_packaged_linux_install_gets_its_own_command() {
let p = "/home/me/Downloads/PS5Upload-7.0.23-linux-x64.rpm";
assert_eq!(
install_command_for(p, Some("rpm"), true, Some("ps5-upload")).as_deref(),
Some("rpm-ostree uninstall ps5-upload --install '/home/me/Downloads/PS5Upload-7.0.23-linux-x64.rpm' && systemctl reboot")
);
assert_eq!(
install_command_for(p, Some("rpm"), false, None).as_deref(),
Some("sudo dnf install '/home/me/Downloads/PS5Upload-7.0.23-linux-x64.rpm'")
);
assert_eq!(
install_command_for("/tmp/it's.deb", Some("deb"), false, None).as_deref(),
Some("sudo apt install '/tmp/it'\\''s.deb'")
);
assert_eq!(install_command_for(p, None, true, None), None);
}
#[test]
fn higher_numeric_is_newer() {
assert!(is_newer("2.2.0", "2.2.1"));
@@ -537,6 +556,65 @@ pub struct UpdateDownload {
pub path: String,
/// Total bytes written to disk.
pub bytes: u64,
/// For a packaged Linux install: the one command that installs the download, to copy into
/// a terminal (#456: on Bazzite, `rpm-ostree uninstall ps5-upload --install <rpm>`).
#[serde(skip_serializing_if = "Option::is_none")]
pub install_command: Option<String>,
}
/// `path` quoted for a POSIX shell.
fn shell_quote(path: &str) -> String {
format!("'{}'", path.replace('\'', "'\\''"))
}
/// The command that installs `path` over this packaged Linux copy, or None (portable copy,
/// another OS). An rpm-ostree system (Bazzite, Silverblue, Kinoite) cannot `dnf install`; its
/// layered package is swapped in one transaction that applies on the next boot.
fn install_command_for(
path: &str,
kind: Option<&str>,
ostree: bool,
package: Option<&str>,
) -> Option<String> {
let q = shell_quote(path);
match kind? {
"rpm" if ostree => Some(format!(
"rpm-ostree uninstall {} --install {q} && systemctl reboot",
package.unwrap_or("ps5-upload")
)),
"rpm" => Some(format!("sudo dnf install {q}")),
"deb" => Some(format!("sudo apt install {q}")),
_ => None,
}
}
#[cfg(target_os = "linux")]
fn install_command(path: &str) -> Option<String> {
let kind = linux_package_kind();
let ostree = std::path::Path::new("/run/ostree-booted").exists();
let package = (kind == Some("rpm"))
.then(|| {
let exe = std::env::current_exe().ok()?;
let exe = std::fs::canonicalize(&exe).unwrap_or(exe);
let out = std::process::Command::new("rpm")
.args(["-qf", "--qf", "%{NAME}"])
.arg(&exe)
.output()
.ok()?;
let name = String::from_utf8(out.stdout).ok()?;
let name = name.trim();
(out.status.success() && !name.is_empty() && !name.contains(char::is_whitespace))
.then(|| name.to_string())
})
.flatten();
install_command_for(path, kind, ostree, package.as_deref())
}
#[cfg(not(target_os = "linux"))]
fn install_command(path: &str) -> Option<String> {
let _ = install_command_for;
let _ = path;
None
}
/// Download the platform-specific asset from the URL returned by
@@ -649,8 +727,10 @@ pub async fn update_download(
if let Err(e) = reveal(&app, &target).await {
eprintln!("[updates] cached bundle at {target:?} but reveal failed: {e}");
}
let path = target.to_string_lossy().into_owned();
return Ok(UpdateDownload {
path: target.to_string_lossy().into_owned(),
install_command: install_command(&path),
path,
bytes: meta.len(),
});
}
@@ -712,8 +792,10 @@ pub async fn update_download(
eprintln!("[updates] download saved to {target:?} but reveal failed: {e}");
}
let path = target.to_string_lossy().into_owned();
Ok(UpdateDownload {
path: target.to_string_lossy().into_owned(),
install_command: install_command(&path),
path,
bytes: total,
})
}
+2
View File
@@ -4628,6 +4628,8 @@ export interface UpdateDownload {
path: string;
/** Total bytes written. Handy for "Downloaded N MB" status lines. */
bytes: number;
/** A packaged Linux install: the command that installs the download (#456). */
install_command?: string;
}
/** Ask GitHub for the latest-release manifest and compare to our
+1
View File
@@ -195,6 +195,7 @@ update_downloaded_status: "تم تنزيل v{version} — تم فتح المثب
update_download_error: "فشل التنزيل: {message}",
update_downloaded_title: "تم التنزيل. أكمل التثبيت:",
update_step_quit: "اخرج من PS5Upload.",
update_step_run_command: "شغّل هذا في الطرفية:",
update_step_launch: "ابدأ تشغيل الإصدار الجديد.",
update_dismiss: "إغلاق",
install_hint_dmg: "انقر نقرًا مزدوجًا فوق .dmg، واسحب PS5Upload إلى Applications، واستبدله عند المطالبة.",
+1
View File
@@ -200,6 +200,7 @@ update_downloaded_status: "v{version} ডাউনলোড হয়েছে
update_download_error: "ডাউনলোড ব্যর্থ: {message}",
update_downloaded_title: "ডাউনলোড হয়েছে। ইনস্টলেশন সম্পূর্ণ করুন:",
update_step_quit: "PS5Upload থেকে প্রস্থান করুন।",
update_step_run_command: "এটি একটি টার্মিনালে চালান:",
update_step_launch: "নতুন সংস্করণ চালু করুন।",
update_dismiss: "বন্ধ করুন",
install_hint_dmg: ".dmg-তে ডাবল-ক্লিক করুন, PS5Upload-কে Applications-এ টানুন, জিজ্ঞাসা করা হলে প্রতিস্থাপন করুন।",
+1
View File
@@ -201,6 +201,7 @@ update_downloaded_status: "v{version} heruntergeladen — Installer geöffnet.",
update_download_error: "Download fehlgeschlagen: {message}",
update_downloaded_title: "Heruntergeladen. Installation abschließen:",
update_step_quit: "PS5Upload beenden.",
update_step_run_command: "Führe das in einem Terminal aus:",
update_step_launch: "Neue Version starten.",
update_dismiss: "Schließen",
install_hint_dmg: ".dmg doppelklicken, PS5Upload in Programme ziehen, beim Nachfragen ersetzen.",
+1
View File
@@ -401,6 +401,7 @@ update_autocheck_hint:
update_channel_prerelease_label: "Get pre-release versions",
update_channel_prerelease_hint: "Offers new releases as soon as they are published, before they have been checked on real hardware. Leave this off to be offered only releases that have been verified.",
update_step_quit: "Quit PS5Upload.",
update_step_run_command: "Run this in a terminal:",
update_step_launch: "Launch the new version.",
update_dismiss: "Dismiss",
install_hint_dmg: "Double-click the .dmg, drag PS5Upload into Applications, replace when asked.",
+1
View File
@@ -195,6 +195,7 @@ update_downloaded_status: "v{version} descargada — instalador abierto.",
update_download_error: "Descarga fallida: {message}",
update_downloaded_title: "Descargado. Termina la instalación:",
update_step_quit: "Sal de PS5Upload.",
update_step_run_command: "Ejecuta esto en una terminal:",
update_step_launch: "Lanza la nueva versión.",
update_dismiss: "Cerrar",
install_hint_dmg: "Haz doble clic en el .dmg, arrastra PS5Upload a Aplicaciones, reemplaza cuando se pregunte.",
+1
View File
@@ -373,6 +373,7 @@ update_autocheck_hint:
update_channel_prerelease_label: "دریافت نسخه‌های پیش‌انتشار",
update_channel_prerelease_hint: "نسخه‌های جدید را به‌محض انتشار ارائه می‌دهد، پیش از آزمایش روی سخت‌افزار واقعی. خاموش بگذارید تا تنها نسخه‌های تأییدشده ارائه شوند.",
update_step_quit: "PS5Upload را ببندید.",
update_step_run_command: "این را در ترمینال اجرا کنید:",
update_step_launch: "نسخه‌ی جدید را اجرا کنید.",
update_dismiss: "نادیده گرفتن",
install_hint_dmg: "روی .dmg دوبار کلیک کنید، PS5Upload را به Applications بکشید، در صورت درخواست جایگزین کنید.",
+1
View File
@@ -195,6 +195,7 @@ update_downloaded_status: "v{version} téléchargée — installateur ouvert.",
update_download_error: "Échec du téléchargement : {message}",
update_downloaded_title: "Téléchargé. Terminez l'installation :",
update_step_quit: "Quittez PS5Upload.",
update_step_run_command: "Exécutez ceci dans un terminal :",
update_step_launch: "Lancez la nouvelle version.",
update_dismiss: "Ignorer",
install_hint_dmg: "Double-cliquez sur le .dmg, glissez PS5Upload dans Applications, remplacez si demandé.",
+1
View File
@@ -200,6 +200,7 @@ update_downloaded_status: "v{version} डाउनलोड किया गय
update_download_error: "डाउनलोड विफल: {message}",
update_downloaded_title: "डाउनलोड किया गया। इंस्टॉलेशन पूरा करें:",
update_step_quit: "PS5Upload से बाहर निकलें।",
update_step_run_command: "इसे टर्मिनल में चलाएँ:",
update_step_launch: "नया संस्करण लॉन्च करें।",
update_dismiss: "बंद करें",
install_hint_dmg: ".dmg पर डबल-क्लिक करें, PS5Upload को Applications पर खींचें, संकेत मिलने पर बदलें।",
+1
View File
@@ -376,6 +376,7 @@ update_autocheck_hint:
update_channel_prerelease_label: "Pre-release verziók kérése",
update_channel_prerelease_hint: "Az új kiadásokat azonnal felajánlja, ahogy megjelennek, mielőtt valódi hardveren ellenőrizték volna. Hagyd kikapcsolva, hogy csak ellenőrzött kiadásokat kapj.",
update_step_quit: "Zárd be a PS5Uploadot.",
update_step_run_command: "Futtasd ezt egy terminálban:",
update_step_launch: "Indítsd el az új verziót.",
update_dismiss: "Elvetés",
install_hint_dmg: "Dupla kattintás a .dmg-re, húzd a PS5Uploadot az Alkalmazásokba, cseréld le, ha kérdezi.",
+1
View File
@@ -200,6 +200,7 @@ update_downloaded_status: "v{version} diunduh — pemasang dibuka.",
update_download_error: "Unduhan gagal: {message}",
update_downloaded_title: "Diunduh. Selesaikan pemasangan:",
update_step_quit: "Keluar dari PS5Upload.",
update_step_run_command: "Jalankan ini di terminal:",
update_step_launch: "Luncurkan versi baru.",
update_dismiss: "Tutup",
install_hint_dmg: "Klik dua kali .dmg, seret PS5Upload ke Applications, ganti saat diminta.",
+1
View File
@@ -200,6 +200,7 @@ update_downloaded_status: "v{version} scaricata — installer aperto.",
update_download_error: "Download fallito: {message}",
update_downloaded_title: "Scaricato. Termina l'installazione:",
update_step_quit: "Esci da PS5Upload.",
update_step_run_command: "Esegui questo in un terminale:",
update_step_launch: "Avvia la nuova versione.",
update_dismiss: "Chiudi",
install_hint_dmg: "Doppio clic sul .dmg, trascina PS5Upload in Applicazioni, sostituisci se richiesto.",
+1
View File
@@ -201,6 +201,7 @@ update_downloaded_status: "v{version} ダウンロード完了 — インスト
update_download_error: "ダウンロード失敗: {message}",
update_downloaded_title: "ダウンロード完了。インストールを完了:",
update_step_quit: "PS5Upload を終了。",
update_step_run_command: "ターミナルでこれを実行してください:",
update_step_launch: "新バージョンを起動。",
update_dismiss: "閉じる",
install_hint_dmg: ".dmg をダブルクリックし、PS5Upload を Applications にドラッグ、確認時に置き換え。",
+1
View File
@@ -201,6 +201,7 @@ update_downloaded_status: "v{version} 다운로드 완료 — 설치 프로그
update_download_error: "다운로드 실패: {message}",
update_downloaded_title: "다운로드 완료. 설치 마무리:",
update_step_quit: "PS5Upload 종료.",
update_step_run_command: "터미널에서 다음을 실행하세요:",
update_step_launch: "새 버전 실행.",
update_dismiss: "닫기",
install_hint_dmg: ".dmg 더블클릭, PS5Upload를 Applications로 드래그, 묻는 메시지에서 교체.",
+1
View File
@@ -359,6 +359,7 @@ update_autocheck_label: "Sprawdzaj aktualizacje automatycznie",
update_autocheck_hint:
"Sprawdza nowe wydania przy uruchomieniu (co najwyżej raz dziennie) i informuje cię, jeśli jest dostępne.",
update_step_quit: "Wyjdź z PS5Upload.",
update_step_run_command: "Uruchom to w terminalu:",
update_step_launch: "Uruchom nową wersję.",
update_dismiss: "Odrzuć",
install_hint_dmg: "Kliknij dwa razy na .dmg, przeciągnij PS5Upload do Aplikacji, podmień jeśli zostaniesz zapytany/a.",
+1
View File
@@ -200,6 +200,7 @@ update_downloaded_status: "v{version} baixada — instalador aberto.",
update_download_error: "Falha no download: {message}",
update_downloaded_title: "Baixado. Conclua a instalação:",
update_step_quit: "Saia do PS5Upload.",
update_step_run_command: "Execute isto em um terminal:",
update_step_launch: "Inicie a nova versão.",
update_dismiss: "Fechar",
install_hint_dmg: "Clique duas vezes no .dmg, arraste o PS5Upload para Aplicativos, substitua quando solicitado.",
+1
View File
@@ -200,6 +200,7 @@ update_downloaded_status: "v{version} загружена — установщи
update_download_error: "Сбой загрузки: {message}",
update_downloaded_title: "Загружено. Завершите установку:",
update_step_quit: "Закройте PS5Upload.",
update_step_run_command: "Выполните это в терминале:",
update_step_launch: "Запустите новую версию.",
update_dismiss: "Закрыть",
install_hint_dmg: "Дважды щёлкните .dmg, перетащите PS5Upload в Программы, замените при запросе.",
+1
View File
@@ -200,6 +200,7 @@ update_downloaded_status: "ดาวน์โหลด v{version} แล้ว
update_download_error: "ดาวน์โหลดล้มเหลว: {message}",
update_downloaded_title: "ดาวน์โหลดแล้ว ดำเนินการติดตั้งให้เสร็จ:",
update_step_quit: "ออกจาก PS5Upload",
update_step_run_command: "รันคำสั่งนี้ในเทอร์มินัล:",
update_step_launch: "เปิดเวอร์ชันใหม่",
update_dismiss: "ปิด",
install_hint_dmg: "ดับเบิลคลิก .dmg ลาก PS5Upload ไปยัง Applications แทนที่เมื่อถูกถาม",
+1
View File
@@ -200,6 +200,7 @@ update_downloaded_status: "v{version} indirildi — yükleyici açıldı.",
update_download_error: "İndirme başarısız: {message}",
update_downloaded_title: "İndirildi. Yüklemeyi tamamlayın:",
update_step_quit: "PS5Upload'dan çıkın.",
update_step_run_command: "Bunu bir terminalde çalıştırın:",
update_step_launch: "Yeni sürümü başlatın.",
update_dismiss: "Kapat",
install_hint_dmg: ".dmg'ye çift tıklayın, PS5Upload'ı Applications klasörüne sürükleyin, sorulduğunda değiştirin.",
+1
View File
@@ -201,6 +201,7 @@ update_downloaded_status: "v{version} đã tải — trình cài đặt đã m
update_download_error: "Tải về thất bại: {message}",
update_downloaded_title: "Đã tải xong. Hoàn tất cài đặt:",
update_step_quit: "Thoát PS5Upload.",
update_step_run_command: "Chạy lệnh này trong terminal:",
update_step_launch: "Khởi chạy phiên bản mới.",
update_dismiss: "Bỏ qua",
install_hint_dmg: "Nhấp đúp .dmg, kéo PS5Upload vào Applications, thay thế khi được hỏi.",
+1
View File
@@ -202,6 +202,7 @@ update_downloaded_status: "v{version} 已下载 — 安装器已打开。",
update_download_error: "下载失败: {message}",
update_downloaded_title: "已下载。完成安装:",
update_step_quit: "退出 PS5Upload。",
update_step_run_command: "在终端中运行:",
update_step_launch: "启动新版本。",
update_dismiss: "关闭",
install_hint_dmg: "双击 .dmg,将 PS5Upload 拖入应用程序,询问时替换。",
+1
View File
@@ -196,6 +196,7 @@ update_downloaded_status: "v{version} 已下載 — 安裝程式已開啟。",
update_download_error: "下載失敗: {message}",
update_downloaded_title: "已下載。完成安裝:",
update_step_quit: "結束 PS5Upload。",
update_step_run_command: "在終端機中執行:",
update_step_launch: "啟動新版本。",
update_dismiss: "關閉",
install_hint_dmg: "按兩下 .dmg,將 PS5Upload 拖入應用程式,詢問時替換。",
+20 -1
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { writeClipboard } from "../../lib/clipboard";
import { EMPTY_CORPUS, deleteFakelibSet, loadFakelibCorpus, type FakelibCorpus } from "../../state/fakelibCorpus";
import { LibrarySourcePicker } from "../InstalledApps/LibrarySourcePicker";
import {
@@ -1801,7 +1802,25 @@ function UpdatesPanel() {
</div>
<ol className="list-inside list-decimal space-y-0.5 text-[var(--color-muted)]">
<li>{tr("update_step_quit", undefined, "Quit PS5Upload.")}</li>
<li>{pickInstallHint(phase.download.path, tr)}</li>
{phase.download.install_command ? (
<li>
{tr("update_step_run_command", undefined, "Run this in a terminal:")}
<span className="mt-1 flex items-start gap-2">
<code className="min-w-0 flex-1 break-all rounded bg-[var(--color-surface-3)] px-2 py-1 font-mono text-[var(--color-text)]">
{phase.download.install_command}
</code>
<button
type="button"
className="shrink-0 underline-offset-2 hover:underline"
onClick={() => void writeClipboard(phase.download.install_command ?? "")}
>
{tr("copy", undefined, "Copy")}
</button>
</span>
</li>
) : (
<li>{pickInstallHint(phase.download.path, tr)}</li>
)}
<li>
{tr("update_step_launch", undefined, "Launch the new version.")}
</li>