From 2e5e1b8143932b96d6c1a51c21b722876f9a8e13 Mon Sep 17 00:00:00 2001 From: JoeAllen <2494136784@qq.com> Date: Tue, 29 Sep 2026 17:29:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20OTA=20=E9=99=84=E4=BB=B6=E6=94=B9?= =?UTF-8?q?=E7=94=A8=207z=20=E5=8E=8B=E7=BC=A9=E5=8C=85=EF=BC=88=E4=BD=93?= =?UTF-8?q?=E7=A7=AF=20-73%=EF=BC=89=EF=BC=8C=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E7=BA=AF=20Rust=20=E8=A7=A3=E5=8E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 发布脚本:附件打包为 .7z(-mx=9)+ .7z.sha256 上传; 同时保留裸二进制附件,v1.0.3 及更早客户端(不认识 7z)仍可升级 - update.rs:pick_assets 优先选 7z、无则回退裸二进制; 下载 → 大小对账 + sha256 校验 → sevenz-rust 解压出二进制暂存 (压缩包随即删除);暂存目录清理排除 .7z - UpdateInfo 增加 asset_name 字段区分压缩包/裸文件 - 实测 21.3MB exe → 5.8MB(7-Zip -mx=9) --- Cargo.lock | 88 +++++++++++++++++++-- gpui-app/Cargo.toml | 2 + gpui-app/src/update.rs | 157 +++++++++++++++++++++++++++----------- scripts/publish-gitea.ps1 | 35 +++++++++ scripts/publish-gitea.sh | 31 ++++++++ 5 files changed, 265 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7449dd..a0222f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -632,15 +632,30 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "bit-set" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" +dependencies = [ + "bit-vec 0.7.0", +] + [[package]] name = "bit-set" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" dependencies = [ - "bit-vec", + "bit-vec 0.9.1", ] +[[package]] +name = "bit-vec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" + [[package]] name = "bit-vec" version = "0.9.1" @@ -1308,6 +1323,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.5.2" @@ -1827,6 +1857,17 @@ dependencies = [ "libc", ] +[[package]] +name = "filetime_creation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c25b5d475550e559de5b0c0084761c65325444e3b6c9e298af9cefe7a9ef3a5f" +dependencies = [ + "cfg-if", + "filetime", + "windows-sys 0.52.0", +] + [[package]] name = "find-msvc-tools" version = "0.1.14" @@ -3926,6 +3967,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "lzma-rust" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baab2bbbd7d75a144d671e9ff79270e903957d92fb7386fd39034c709bd2661" +dependencies = [ + "byteorder", +] + [[package]] name = "mac" version = "0.1.1" @@ -4153,6 +4203,7 @@ dependencies = [ "sentry", "serde", "serde_json", + "sevenz-rust", "sha1", "sha2 0.10.9", "smol", @@ -4175,7 +4226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" dependencies = [ "arrayvec", - "bit-set", + "bit-set 0.9.1", "bitflags 2.13.2", "cfg-if", "cfg_aliases", @@ -4312,6 +4363,16 @@ dependencies = [ "instant", ] +[[package]] +name = "nt-time" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de419e64947cd8830e66beb584acc3fb42ed411d103e3c794dda355d1b374b5" +dependencies = [ + "chrono", + "time", +] + [[package]] name = "ntapi" version = "0.4.3" @@ -6577,6 +6638,23 @@ dependencies = [ "serde", ] +[[package]] +name = "sevenz-rust" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26482cf1ecce4540dc782fc70019eba89ffc4d87b3717eb5ec524b5db6fdefef" +dependencies = [ + "bit-set 0.6.0", + "byteorder", + "crc", + "filetime_creation", + "js-sys", + "lzma-rust", + "nt-time", + "sha2 0.10.9", + "wasm-bindgen", +] + [[package]] name = "sha1" version = "0.10.7" @@ -8250,8 +8328,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" dependencies = [ "arrayvec", - "bit-set", - "bit-vec", + "bit-set 0.9.1", + "bit-vec 0.9.1", "bitflags 2.13.2", "bytemuck", "cfg_aliases", @@ -8322,7 +8400,7 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set", + "bit-set 0.9.1", "bitflags 2.13.2", "block2 0.6.2", "bytemuck", diff --git a/gpui-app/Cargo.toml b/gpui-app/Cargo.toml index 7455e64..6552fd8 100644 --- a/gpui-app/Cargo.toml +++ b/gpui-app/Cargo.toml @@ -43,6 +43,8 @@ rfd = "0.17" # OTA 自更新:Gitea Release 检查/下载(ureq 用 rustls,与 rumqttc 依赖同源) ureq = { version = "2", features = ["json"] } sha2 = "0.10" +# Release 附件为 7z 压缩包,纯 Rust 解压(客户端免装 7-Zip) +sevenz-rust = "0.6" # 错误上报(自建 Sentry):panic 捕获 + 持久化错误;rustls 传输避免 openssl 依赖 sentry = { version = "0.49", default-features = false, features = [ diff --git a/gpui-app/src/update.rs b/gpui-app/src/update.rs index 98031cb..859f27b 100644 --- a/gpui-app/src/update.rs +++ b/gpui-app/src/update.rs @@ -23,6 +23,8 @@ pub struct UpdateInfo { pub version: String, /// Release 说明(Markdown 原文) pub notes: String, + /// 附件名(判断 7z 压缩包 / 裸二进制) + pub asset_name: String, /// 平台对应产物下载地址 pub asset_url: String, /// sha256 校验文件地址(若 Release 提供了 `.sha256` 侧车) @@ -52,12 +54,17 @@ pub fn platform_triple() -> &'static str { } } -/// 某版本的产物文件名(与 CI 的命名约定 `mqttx---mqttx[.exe]` 一致)。 -pub fn asset_name_for(version: &str) -> String { +/// 某版本解压后二进制的暂存名(与 CI 的命名约定 `mqttx-v--mqttx[.exe]` 一致)。 +pub fn binary_name_for(version: &str) -> String { let ext = if cfg!(windows) { ".exe" } else { "" }; format!("mqttx-v{}-{}-mqttx{}", version, platform_triple(), ext) } +/// 某版本的 7z 压缩附件名(打包端与此约定一致;内部为 [`binary_name_for`])。 +pub fn asset_name_for(version: &str) -> String { + format!("{}.7z", binary_name_for(version)) +} + fn agent() -> ureq::Agent { ureq::AgentBuilder::new() // 整体上限:防止慢速滴流的下载无限占用阻塞线程 @@ -108,32 +115,46 @@ pub fn is_newer(a: &str, b: &str) -> bool { } /// 从 Release JSON 里挑选当前平台的产物与 sha256 侧车。 +/// 优先 7z 压缩包(体积小、下载快);Release 未提供 7z 时回退裸二进制 +/// (兼容旧发布方式)。 pub fn pick_assets(rel: &serde_json::Value) -> Option<(String, String, u64, Option)> { let assets = rel.get("assets")?.as_array()?; let want_ext = if cfg!(windows) { ".exe" } else { "" }; let triple = platform_triple(); - for a in assets { - let Some(name) = a.get("name").and_then(|n| n.as_str()) else { - continue; - }; - if !name.contains(triple) || !name.ends_with(want_ext) || name.ends_with(".sha256") { - continue; + + // (后缀过滤器, 说明):第一轮 7z,第二轮裸二进制 + for suffix in [".7z", want_ext] { + for a in assets { + let Some(name) = a.get("name").and_then(|n| n.as_str()) else { + continue; + }; + if !name.contains(triple) + || !name.ends_with(suffix) + || name.ends_with(".sha256") + { + continue; + } + // 7z 轮要排除裸二进制(windows 裸 exe 不以 .7z 结尾,天然互斥) + if suffix == ".7z" && !name.ends_with(&format!("{want_ext}.7z")) && !want_ext.is_empty() + { + continue; + } + let Some(url) = a.get("browser_download_url").and_then(|u| u.as_str()) else { + continue; + }; + let url = url.to_string(); + let size = a.get("size").and_then(|s| s.as_u64()).unwrap_or(0); + // 侧车命名:.sha256 + let sha = assets + .iter() + .find(|b| { + b.get("name").and_then(|n| n.as_str()) == Some(&format!("{name}.sha256")) + }) + .and_then(|b| b.get("browser_download_url")) + .and_then(|u| u.as_str()) + .map(|s| s.to_string()); + return Some((name.to_string(), url, size, sha)); } - let Some(url) = a.get("browser_download_url").and_then(|u| u.as_str()) else { - continue; - }; - let url = url.to_string(); - let size = a.get("size").and_then(|s| s.as_u64()).unwrap_or(0); - // 侧车命名:.sha256 - let sha = assets - .iter() - .find(|b| { - b.get("name").and_then(|n| n.as_str()) == Some(&format!("{name}.sha256")) - }) - .and_then(|b| b.get("browser_download_url")) - .and_then(|u| u.as_str()) - .map(|s| s.to_string()); - return Some((name.to_string(), url, size, sha)); } None } @@ -164,7 +185,7 @@ pub fn check_latest() -> Result, String> { if !is_newer(tag, current_version()) { return Ok(None); } - let Some((_, url, size, sha)) = pick_assets(&rel) else { + let Some((name, url, size, sha)) = pick_assets(&rel) else { return Err(format!("Release {tag} 没有当前平台({})的产物", platform_triple())); }; Ok(Some(UpdateInfo { @@ -174,6 +195,7 @@ pub fn check_latest() -> Result, String> { .and_then(|b| b.as_str()) .unwrap_or_default() .to_string(), + asset_name: name, asset_url: url, sha256_url: sha, size, @@ -214,13 +236,21 @@ pub fn check_and_download() -> Result, String> { /// 下载并校验到暂存区,返回待安装的更新。 /// +/// 附件为 7z 压缩包:下载 → 校验 → 解压出二进制暂存(压缩包随即删除)。 +/// 回退路径(裸二进制附件)直接校验暂存。 /// `on_progress(downloaded, total)` 在阻塞线程上回调,total 为 0 表示未知。 pub fn download_and_stage( info: &UpdateInfo, on_progress: &dyn Fn(u64, u64), ) -> Result { let dir = stage_dir()?; - let final_name = asset_name_for(&info.version); + let binary_name = binary_name_for(&info.version); + let is_7z = info.asset_name.ends_with(".7z"); + let final_name = if is_7z { + asset_name_for(&info.version) + } else { + binary_name.clone() + }; // tmp 名加入毫秒时间戳:同 pid 并发下载不共用文件 let tmp = dir.join(format!( "{}.{}.{}.tmp", @@ -240,11 +270,31 @@ pub fn download_and_stage( } // 清掉其他版本的残留暂存文件,避免堆积与误装旧版 - remove_other_staged(&dir, &dst); - std::fs::rename(&tmp, &dst).map_err(|e| format!("暂存失败: {e}"))?; + let staged_path = if is_7z { + // 解压到暂存目录,取出二进制后删掉压缩包 + if let Err(e) = sevenz_rust::decompress_file(&tmp, &dir) { + let _ = std::fs::remove_file(&tmp); + return Err(format!("解压更新包失败: {e}")); + } + let _ = std::fs::remove_file(&tmp); + let extracted = dir.join(&binary_name); + if !extracted.exists() { + return Err(format!( + "更新包内缺少 {},压缩包内容异常", + binary_name + )); + } + // 确认解压产物后再清理其他版本,最后返回二进制路径 + remove_other_staged(&dir, &extracted); + extracted + } else { + remove_other_staged(&dir, &dst); + std::fs::rename(&tmp, &dst).map_err(|e| format!("暂存失败: {e}"))?; + dst + }; Ok(StagedUpdate { version: info.version.clone(), - path: dst, + path: staged_path, }) } @@ -307,6 +357,7 @@ fn remove_other_staged(dir: &std::path::Path, keep: &std::path::Path) { && name.contains(platform_triple()) && !name.ends_with(".tmp") && !name.ends_with(".sha256") + && !name.ends_with(".7z") { let _ = std::fs::remove_file(&p); } @@ -538,36 +589,56 @@ mod tests { #[test] fn asset_naming_matches_ci_convention() { + let ext = if cfg!(windows) { ".exe" } else { "" }; + // 附件为 7z 压缩包;解压出的二进制保留原命名 assert_eq!( asset_name_for("1.0.1"), - format!("mqttx-v1.0.1-{}-mqttx{}", platform_triple(), if cfg!(windows) { ".exe" } else { "" }) + format!("mqttx-v1.0.1-{triple}-mqttx{ext}.7z", triple = platform_triple()) + ); + assert_eq!( + binary_name_for("1.0.1"), + format!("mqttx-v1.0.1-{triple}-mqttx{ext}", triple = platform_triple()) ); } #[test] - fn pick_assets_selects_platform_and_sidecar() { + fn pick_assets_prefers_7z_over_raw_binary() { + let rel: serde_json::Value = serde_json::json!({ + "tag_name": "v1.0.1", + "assets": [ + {"name": "mqttx-v1.0.1-x86_64-pc-windows-msvc-mqttx.exe", "size": 21000000, + "browser_download_url": "https://x/win.exe"}, + {"name": "mqttx-v1.0.1-x86_64-pc-windows-msvc-mqttx.exe.sha256", "size": 65, + "browser_download_url": "https://x/win.exe.sha256"}, + {"name": "mqttx-v1.0.1-x86_64-pc-windows-msvc-mqttx.exe.7z", "size": 9000000, + "browser_download_url": "https://x/win.7z"}, + {"name": "mqttx-v1.0.1-x86_64-pc-windows-msvc-mqttx.exe.7z.sha256", "size": 65, + "browser_download_url": "https://x/win.7z.sha256"} + ] + }); + let (name, url, size, sha) = pick_assets(&rel).expect("应选中 7z"); + assert!(name.ends_with(".7z"), "应优先 7z 压缩包,实际 {name}"); + assert_eq!(url, "https://x/win.7z"); + assert_eq!(size, 9000000); + assert_eq!(sha.as_deref(), Some("https://x/win.7z.sha256")); + } + + #[test] + fn pick_assets_falls_back_to_raw_binary() { + // 旧发布方式只有裸二进制:仍可被选中 let rel: serde_json::Value = serde_json::json!({ "tag_name": "v1.0.1", "assets": [ - {"name": "mqttx-v1.0.1-x86_64-unknown-linux-gnu-mqttx", "size": 1, - "browser_download_url": "https://x/linux"}, {"name": "mqttx-v1.0.1-x86_64-pc-windows-msvc-mqttx.exe", "size": 19132928, "browser_download_url": "https://x/win"}, {"name": "mqttx-v1.0.1-x86_64-pc-windows-msvc-mqttx.exe.sha256", "size": 65, "browser_download_url": "https://x/win.sha256"} ] }); - let (name, url, size, sha) = pick_assets(&rel).expect("应选中平台产物"); - if cfg!(windows) { - assert_eq!(name, "mqttx-v1.0.1-x86_64-pc-windows-msvc-mqttx.exe"); - assert_eq!(url, "https://x/win"); - assert_eq!(size, 19132928); - assert_eq!(sha.as_deref(), Some("https://x/win.sha256")); - } else { - assert_eq!(name, "mqttx-v1.0.1-x86_64-unknown-linux-gnu-mqttx"); - assert_eq!(url, "https://x/linux"); - assert_eq!(sha, None); - } + let (name, url, _size, sha) = pick_assets(&rel).expect("应回退裸二进制"); + assert!(name.ends_with(".exe")); + assert_eq!(url, "https://x/win"); + assert_eq!(sha.as_deref(), Some("https://x/win.sha256")); } #[test] diff --git a/scripts/publish-gitea.ps1 b/scripts/publish-gitea.ps1 index fac4a9e..22dfae2 100644 --- a/scripts/publish-gitea.ps1 +++ b/scripts/publish-gitea.ps1 @@ -237,6 +237,41 @@ foreach ($f in $Files) { if ($name -match "^mqttx-v.*-mqttx(\.exe)?$") { $asset = $name } elseif ($name.EndsWith(".exe")) { $asset = "mqttx-v$VerNum-$Triple-mqttx.exe" } else { $asset = "mqttx-v$VerNum-$Triple-mqttx" } + + # ── 7z 压缩附件(OTA 主通道:体积小、下载快)── + # 内部文件名 = $asset(应用解压后按此名暂存安装) + $SevenZip = @("$env:ProgramFiles\7-Zip\7z.exe", "${env:ProgramFiles(x86)}\7-Zip\7z.exe") | + Where-Object { Test-Path $_ } | Select-Object -First 1 + if (-not $SevenZip) { $SevenZip = (Get-Command 7z -ErrorAction SilentlyContinue).Source } + if ($SevenZip) { + $packDir = Join-Path $env:TEMP ("mqttx-pack-" + [guid]::NewGuid().ToString("N")) + New-Item -ItemType Directory -Path $packDir | Out-Null + Copy-Item $f (Join-Path $packDir $asset) + $archive = Join-Path $env:TEMP "$asset.7z" + & $SevenZip a -y -mx=9 $archive (Join-Path $packDir $asset) | Out-Null + if ($LASTEXITCODE -eq 0 -and (Test-Path $archive)) { + $asum = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLower() + "$asum $asset.7z" | Set-Content -NoNewline -Encoding ascii "$archive.sha256" + foreach ($a in @("$asset.7z", "$asset.7z.sha256")) { + $code = Upload-ReleaseAsset "$Api/releases/$releaseId/assets?name=$a" (Join-Path $env:TEMP $a) + if ($code -eq 201) { + Write-Host " ✓ Release 附件 $a" -ForegroundColor Green + } + else { + Write-Host " ! Release 附件 $a 上传失败 (HTTP $code)" -ForegroundColor Yellow + } + } + } + else { + Write-Host " ! 7z 打包失败(exit=$LASTEXITCODE),跳过压缩附件" -ForegroundColor Yellow + } + Remove-Item $packDir, $archive, "$archive.sha256" -Recurse -Force -ErrorAction SilentlyContinue + } + else { + Write-Host " ! 未找到 7-Zip,跳过 7z 附件(仅上传裸二进制)" -ForegroundColor Yellow + } + + # ── 裸二进制附件:v1.0.3 及更早客户端不认识 7z,需要它升级 ── $tmp = Join-Path $env:TEMP $asset Copy-Item $f $tmp -Force $sum = (Get-FileHash -Algorithm SHA256 $f).Hash.ToLower() diff --git a/scripts/publish-gitea.sh b/scripts/publish-gitea.sh index ea363e8..bee3530 100644 --- a/scripts/publish-gitea.sh +++ b/scripts/publish-gitea.sh @@ -193,6 +193,37 @@ for f in "${FILES[@]}"; do *.exe) asset="mqttx-v${VER_NUM}-${TRIPLE}-mqttx.exe" ;; *) asset="mqttx-v${VER_NUM}-${TRIPLE}-mqttx" ;; esac + + # ── 7z 压缩附件(OTA 主通道):内部文件名 = $asset(应用解压后按此名安装)── + SEVENZIP="$(command -v 7z 7za 2>/dev/null | head -1)" + [ -z "$SEVENZIP" ] && [ -x "/c/Program Files/7-Zip/7z.exe" ] && SEVENZIP="/c/Program Files/7-Zip/7z.exe" + if [ -n "$SEVENZIP" ]; then + "$SEVENZIP" a -y -mx=9 "$STAGE/$asset.7z" "$f" >/dev/null + # 归档内的文件名需为 $asset:7z 存的是源文件名,先复制改名再压 + rm -f "$STAGE/$asset.7z" + cp -f "$f" "$STAGE/$asset" + "$SEVENZIP" a -y -mx=9 "$STAGE/$asset.7z" "$STAGE/$asset" >/dev/null + if [ -f "$STAGE/$asset.7z" ]; then + printf '%s %s\n' "$(sha256 "$STAGE/$asset.7z")" "$asset.7z" > "$STAGE/$asset.7z.sha256" + for a in "$asset.7z" "$asset.7z.sha256"; do + code="$(curl -sS -o "$STAGE/up.json" -w '%{http_code}' \ + -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -F "attachment=@${STAGE}/${a}" \ + "${API}/releases/${RELEASE_ID}/assets?name=${a}")" + case "$code" in + 201) echo " ✓ Release 附件 $a" ;; + *) echo " ! Release 附件 $a 上传失败 (HTTP $code)" >&2; cat "$STAGE/up.json" >&2 || true ;; + esac + done + else + echo " ! 7z 打包失败,跳过压缩附件" >&2 + fi + else + echo " ! 未找到 7z/7za,跳过 7z 附件(仅上传裸二进制)" >&2 + fi + + # ── 裸二进制附件:v1.0.3 及更早客户端不认识 7z,需要它升级 ── cp -f "$f" "$STAGE/$asset" printf '%s %s\n' "$(sha256 "$f")" "$asset" > "$STAGE/$asset.sha256" for a in "$asset" "$asset.sha256"; do