Update now: download, verify and install a release in one click (#1118)
Where SolidCraft can replace itself safely, the Update window gets Update now: - AppImage: the new AppImage is downloaded next to the old one, verified and renamed over it; Restart now starts it as SolidCraft exits. - Windows MSI: the verified .msi starts (msiexec /i … /passive) once SolidCraft has closed, after the usual questions about unsaved designs. - Windows portable zip / standalone exe: the running solidcraft.exe is renamed to .old, the new one takes its name (solidcraft-cli.exe too), Restart now; .old goes at the next start. - macOS: the verified .dmg is opened (the app is ad-hoc signed and not notarized, so it isn't replaced behind the user's back). - Flatpak, Snap, deb, rpm, tar.gz: never replaced; the window shows the command. Every package's SHA-256 must match the release's SHA256SUMS.txt (HTTPS, same release); a mismatch deletes the download and changes nothing. With PUBLIC_KEY set in update/install.rs, SHA256SUMS.txt.minisig must also verify (minisign-verify, MIT) with a trusted comment naming the release. release.yml signs the list when the UPDATE_SIGNING_KEY secret exists and fails when the app expects signatures but the secret is missing; docs/releasing.md has the steps. Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a8258eb63a
commit
d4a7b3abf3
16 files changed
+966
-12
No files matched your search
@@ -63,6 +63,12 @@ impl eframe::App for App {
|
||||
fn on_exit(&mut self) {
|
||||
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
|
||||
solidcraft_engine::crash::end_session();
|
||||
// An update asked to restart into the new version or to run its installer.
|
||||
if let Some((program, args)) = solidcraft_ui_egui::update::take_launch(&mut self.solve)
|
||||
&& let Err(e) = std::process::Command::new(&program).args(&args).spawn()
|
||||
{
|
||||
log::error!("update: starting {}: {e}", program.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +120,7 @@ fn services() -> Services {
|
||||
})),
|
||||
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
|
||||
http_get: Some(std::sync::Arc::new(http_get)),
|
||||
http_download: Some(std::sync::Arc::new(http_download)),
|
||||
show_in_folder: Some(Box::new(|path| {
|
||||
let folder = std::path::Path::new(path).parent().unwrap_or_else(|| std::path::Path::new("."));
|
||||
let mut command = std::process::Command::new(if cfg!(target_os = "windows") {
|
||||
@@ -138,6 +145,40 @@ fn services() -> Services {
|
||||
/// 15 s limit, at most 1 MB, and no headers beyond what GitHub requires (a User-Agent without
|
||||
/// the version or anything about this machine, and the API's media type).
|
||||
fn http_get(url: &str) -> Result<Vec<u8>, solidcraft_ui_egui::update::FetchError> {
|
||||
use solidcraft_ui_egui::update::FetchError;
|
||||
let mut response = http_call(url, std::time::Duration::from_secs(15))?;
|
||||
response.body_mut().with_config().limit(1 << 20).read_to_vec().map_err(|e| FetchError::Failed(e.to_string()))
|
||||
}
|
||||
|
||||
/// HTTPS download of an update package to `to` (docs/updates.md), reporting progress; stops when
|
||||
/// `progress` returns false. The caller verifies the file.
|
||||
fn http_download(url: &str, to: &std::path::Path, progress: &dyn Fn(u64, Option<u64>) -> bool) -> Result<(), solidcraft_ui_egui::update::FetchError> {
|
||||
use solidcraft_ui_egui::update::FetchError;
|
||||
use std::io::{Read, Write};
|
||||
let fail = |e: String| FetchError::Failed(e);
|
||||
let mut response = http_call(url, std::time::Duration::from_secs(30 * 60))?;
|
||||
let total = response.headers().get("content-length").and_then(|v| v.to_str().ok()).and_then(|v| v.parse::<u64>().ok());
|
||||
let mut body = response.body_mut().with_config().limit(solidcraft_ui_egui::update::install::MAX_PACKAGE).reader();
|
||||
let mut file = std::fs::File::create(to).map_err(|e| fail(format!("{}: {e}", to.display())))?;
|
||||
let mut buf = vec![0u8; 1 << 16];
|
||||
let mut done = 0u64;
|
||||
loop {
|
||||
let n = body.read(&mut buf).map_err(|e| fail(e.to_string()))?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
file.write_all(buf.get(..n).unwrap_or_default()).map_err(|e| fail(format!("{}: {e}", to.display())))?;
|
||||
done += n as u64;
|
||||
if !progress(done, total) {
|
||||
return Err(fail("cancelled".into()));
|
||||
}
|
||||
}
|
||||
file.sync_all().map_err(|e| fail(format!("{}: {e}", to.display())))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One GET to GitHub: a 200 answer, or why not (rate limits included).
|
||||
fn http_call(url: &str, timeout: std::time::Duration) -> Result<ureq::http::Response<ureq::Body>, solidcraft_ui_egui::update::FetchError> {
|
||||
use solidcraft_ui_egui::update::FetchError;
|
||||
use std::time::Duration;
|
||||
let tls = ureq::tls::TlsConfig::builder()
|
||||
@@ -148,13 +189,13 @@ fn http_get(url: &str) -> Result<Vec<u8>, solidcraft_ui_egui::update::FetchError
|
||||
.tls_config(tls)
|
||||
.https_only(true)
|
||||
.http_status_as_error(false)
|
||||
.timeout_global(Some(Duration::from_secs(15)))
|
||||
.timeout_global(Some(timeout))
|
||||
.timeout_connect(Some(Duration::from_secs(5)))
|
||||
.user_agent("SolidCraft-update-check")
|
||||
.max_redirects(3)
|
||||
.build()
|
||||
.into();
|
||||
let mut response = agent
|
||||
let response = agent
|
||||
.get(url)
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.header("X-GitHub-Api-Version", "2022-11-28")
|
||||
@@ -170,7 +211,7 @@ fn http_get(url: &str) -> Result<Vec<u8>, solidcraft_ui_egui::update::FetchError
|
||||
if status != 200 {
|
||||
return Err(FetchError::Failed(format!("GitHub answered HTTP {status}")));
|
||||
}
|
||||
response.body_mut().with_config().limit(1 << 20).read_to_vec().map_err(|e| FetchError::Failed(e.to_string()))
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// The per-user settings folder: eframe's storage (`app.ron`) and the anti-aliasing preference.
|
||||
|
||||
Reference in new issue
Block a user