feat: add desktop app hot update channel
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
// afterPack 钩子:未购买 Apple 证书时,electron-builder(24) 会「跳过签名」,
|
||||
// 导致 .app 的 bundle 封印(seal)残缺(codesign --verify 报 "code has no resources
|
||||
// but signature indicates they must be present")。这里在打 DMG 前,用 ad-hoc 身份
|
||||
// 对整个 app(含内置 sing-box)做一次完整深度重新签名,得到有效封印——
|
||||
// 用户去掉隔离标记后即可稳定运行。
|
||||
const { execFileSync } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
exports.default = async function afterPack(context) {
|
||||
if (context.electronPlatformName !== 'darwin') return;
|
||||
|
||||
const appName = `${context.packager.appInfo.productFilename}.app`;
|
||||
const appPath = path.join(context.appOutDir, appName);
|
||||
|
||||
console.log(` • ad-hoc 深度重签名 app=${appPath}`);
|
||||
// --force 覆盖已有签名,--deep 递归签所有嵌套二进制(sing-box 等),-s - 表示 ad-hoc
|
||||
execFileSync('codesign', ['--force', '--deep', '--sign', '-', appPath], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
// 校验封印是否完整(失败则中断构建,避免发出坏包)
|
||||
execFileSync('codesign', ['--verify', '--deep', '--strict', '--verbose=2', appPath], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
console.log(' • ad-hoc 签名校验通过 ✔');
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# 下载全局代理功能所需的 sing-box 引擎二进制(macOS arm64/x64 + Windows x64)。
|
||||
# 这些二进制不进 git(太大),打包前跑一次即可。用法: bash scripts/fetch-singbox.sh
|
||||
set -euo pipefail
|
||||
VER="${SINGBOX_VERSION:-1.13.14}"
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DEST="$ROOT/resources/singbox"
|
||||
mkdir -p "$DEST"
|
||||
cd "$DEST"
|
||||
|
||||
fetch() { # $1=平台目录 $2=资源名(不含扩展) $3=压缩类型 $4=包内二进制名
|
||||
local dir="$1" asset="$2" kind="$3" bin="$4"
|
||||
echo "▶ $dir …"
|
||||
if [ -f "$dir/$bin" ]; then echo " 已存在,跳过"; return; fi
|
||||
mkdir -p "$dir"
|
||||
local url="https://github.com/SagerNet/sing-box/releases/download/v${VER}/${asset}.${kind}"
|
||||
if [ "$kind" = "tar.gz" ]; then
|
||||
curl -fsSL "$url" -o t.tgz && tar xzf t.tgz && mv "${asset}/${bin}" "$dir/" && rm -rf t.tgz "$asset"
|
||||
else
|
||||
curl -fsSL "$url" -o t.zip && unzip -qo t.zip && mv "${asset}/${bin}" "$dir/" && rm -rf t.zip "$asset"
|
||||
fi
|
||||
chmod +x "$dir/$bin" 2>/dev/null || true
|
||||
echo " ✓ $dir/$bin"
|
||||
}
|
||||
|
||||
fetch "darwin-arm64" "sing-box-${VER}-darwin-arm64" "tar.gz" "sing-box"
|
||||
fetch "darwin-x64" "sing-box-${VER}-darwin-amd64" "tar.gz" "sing-box"
|
||||
fetch "windows-x64" "sing-box-${VER}-windows-amd64" "zip" "sing-box.exe"
|
||||
echo "全部就位 ✅ (sing-box v${VER})"
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env node
|
||||
// publish-hot.js —— 发布热更新包到自建 gitea。
|
||||
//
|
||||
// 用法:
|
||||
// GIT_TOKEN=xxx node scripts/publish-hot.js --version 1.5.1 [--min 1.5.0] \
|
||||
// [--notes "更新说明"] [--mandatory] [--requires-full]
|
||||
// token 也可放在 ~/.tokendun-git-token 文件里(优先用环境变量 GIT_TOKEN)。
|
||||
//
|
||||
// 流程:打包 electron/+renderer/ 成 zip → sha256(+可选签名) → 写 hotupdate.json
|
||||
// → gitea 的 tag=hot release:删旧 bundle/manifest、传新的。
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const { execFileSync } = require("child_process");
|
||||
|
||||
const ROOT = path.join(__dirname, "..");
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8"));
|
||||
|
||||
// —— 参数 ——
|
||||
const argv = process.argv.slice(2);
|
||||
function arg(name, def) {
|
||||
const i = argv.indexOf(name);
|
||||
return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[i + 1] : def;
|
||||
}
|
||||
const has = (name) => argv.includes(name);
|
||||
const version = arg("--version", pkg.hotContentVersion || pkg.version);
|
||||
const minAppVersion = arg("--min", pkg.version);
|
||||
const notes = arg("--notes", "");
|
||||
const mandatory = has("--mandatory");
|
||||
const requiresFull = has("--requires-full");
|
||||
|
||||
// —— token ——
|
||||
let token = process.env.GIT_TOKEN || "";
|
||||
if (!token) {
|
||||
try { token = fs.readFileSync(path.join(os.homedir(), ".tokendun-git-token"), "utf8").trim(); } catch {}
|
||||
}
|
||||
if (!token) { console.error("缺少 gitea token:设置 GIT_TOKEN 或写入 ~/.tokendun-git-token"); process.exit(1); }
|
||||
|
||||
const OWNER = "backup-bot", REPO = "backup", TAG = "hot";
|
||||
const API = `https://up.tokendun.cc/api/v1/repos/${OWNER}/${REPO}`;
|
||||
const DL = `https://up.tokendun.cc/${OWNER}/${REPO}/releases/download/${TAG}`;
|
||||
|
||||
const OUT = path.join(ROOT, "dist-hot");
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
const bundleName = `bundle-${version}.zip`;
|
||||
const bundlePath = path.join(OUT, bundleName);
|
||||
|
||||
// —— 1) 打包(zip,deflate,可被 lib/zip.js 解压)——
|
||||
fs.rmSync(bundlePath, { force: true });
|
||||
console.log(`打包 electron/ + renderer/ → ${bundleName} …`);
|
||||
execFileSync("zip", ["-r", "-X", "-q", bundlePath, "electron", "renderer",
|
||||
"-x", "*.map", "-x", "*/node_modules/*"], { cwd: ROOT, stdio: "inherit" });
|
||||
|
||||
// —— 2) sha256(+ 可选 ed25519 签名)——
|
||||
const buf = fs.readFileSync(bundlePath);
|
||||
const sha256 = crypto.createHash("sha256").update(buf).digest("hex");
|
||||
let signature = "";
|
||||
try {
|
||||
const keyPath = path.join(os.homedir(), ".codex-hot-key.pem"); // 私钥(不入库)
|
||||
if (fs.existsSync(keyPath)) {
|
||||
const key = fs.readFileSync(keyPath);
|
||||
signature = crypto.sign(null, Buffer.from(`${sha256}:${version}`), key).toString("base64");
|
||||
console.log("已用本地私钥签名");
|
||||
}
|
||||
} catch (e) { console.warn("签名跳过:" + e.message); }
|
||||
|
||||
// —— 3) manifest ——
|
||||
const manifest = {
|
||||
contentVersion: version,
|
||||
minAppVersion,
|
||||
requiresFull,
|
||||
bundleUrl: `${DL}/${bundleName}`,
|
||||
size: buf.length,
|
||||
sha256,
|
||||
signature,
|
||||
notes,
|
||||
mandatory,
|
||||
};
|
||||
const manifestPath = path.join(OUT, "hotupdate.json");
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
console.log(`sha256=${sha256} size=${(buf.length / 1048576).toFixed(2)}MB`);
|
||||
|
||||
// —— 4) gitea API ——
|
||||
function api(method, urlPath, body) {
|
||||
const args = ["-s", "-X", method, "-H", `Authorization: token ${token}`, `${API}${urlPath}`];
|
||||
if (body !== undefined) { args.push("-H", "Content-Type: application/json", "-d", JSON.stringify(body)); }
|
||||
return execFileSync("curl", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
||||
}
|
||||
function apiJson(method, urlPath, body) {
|
||||
const out = api(method, urlPath, body);
|
||||
try { return JSON.parse(out); } catch { return null; }
|
||||
}
|
||||
|
||||
// 确保 hot release 存在
|
||||
let rel = apiJson("GET", `/releases/tags/${TAG}`);
|
||||
if (!rel || !rel.id) {
|
||||
console.log("创建 hot release …");
|
||||
rel = apiJson("POST", `/releases`, { tag_name: TAG, target_commitish: "main", name: "热更新通道", body: "asar 热更新包(自动发布)", draft: false, prerelease: false });
|
||||
}
|
||||
if (!rel || !rel.id) { console.error("无法创建/获取 hot release"); process.exit(1); }
|
||||
|
||||
// 删除同名旧 asset
|
||||
for (const a of rel.assets || []) {
|
||||
if (a.name === bundleName || a.name === "hotupdate.json" || /^bundle-.*\.zip$/.test(a.name)) {
|
||||
api("DELETE", `/releases/${rel.id}/assets/${a.id}`);
|
||||
console.log("删除旧 asset:", a.name);
|
||||
}
|
||||
}
|
||||
// 上传 bundle + manifest
|
||||
function upload(file, name) {
|
||||
const code = execFileSync("curl", ["-s", "-o", "/dev/null", "-w", "%{http_code}",
|
||||
"-X", "POST", "-H", `Authorization: token ${token}`,
|
||||
"-F", `attachment=@${file};type=application/octet-stream`,
|
||||
`${API}/releases/${rel.id}/assets?name=${encodeURIComponent(name)}`], { encoding: "utf8" });
|
||||
console.log(`上传 ${name} → HTTP ${code}`);
|
||||
}
|
||||
upload(bundlePath, bundleName);
|
||||
upload(manifestPath, "hotupdate.json");
|
||||
|
||||
console.log(`\n✅ 热更 v${version} 已发布。`);
|
||||
console.log(` minAppVersion=${minAppVersion} mandatory=${mandatory} requiresFull=${requiresFull}`);
|
||||
console.log(` 客户端将从 ${API}/releases/tags/${TAG} 检测到。`);
|
||||
Reference in New Issue
Block a user