@yufengtadian/freedom-cli 1.13.2 → 1.14.0-preview
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +40 -0
- package/README.md +74 -21
- package/lib/agents.js +182 -52
- package/lib/build.js +119 -42
- package/lib/cli.js +106 -9
- package/lib/release.js +22 -6
- package/lib/security.js +525 -45
- package/lib/shell.js +288 -64
- package/lib/toolchain.js +339 -0
- package/lib/utils.js +51 -0
- package/lib/verify.js +85 -30
- package/lib/webkit.js +45 -0
- package/package.json +4 -3
- package/shell/darwin-arm64/freedom-shell +0 -0
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/skill/freedom/SKILL.md +3 -3
- package/templates/desktop/backend/desktop.mjs +16 -6
- package/templates/desktop/freedom.config.js +4 -1
- package/templates/go/go.mod +3 -0
- package/templates/go/pkg/freedom/anti_debug_linux.go +69 -0
- package/templates/go/pkg/freedom/anti_debug_other.go +5 -3
- package/templates/go/pkg/freedom/anti_debug_windows.go +136 -12
- package/templates/go/pkg/freedom/assets_embed.go +1 -0
- package/templates/go/pkg/freedom/backend_proc.go +5 -5
- package/templates/go/pkg/freedom/events_windows.go +5 -5
- package/templates/go/pkg/freedom/freedom.go +17 -3
- package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
- package/templates/go/pkg/freedom/resources.go +59 -1
- package/templates/go/pkg/freedom/security.go +475 -41
- package/templates/go/pkg/freedom/shutdown.go +55 -0
- package/templates/go/pkg/freedom/shutdown_windows.go +47 -0
- package/templates/go/pkg/freedom/store.go +3 -3
- package/templates/go/pkg/freedom/syscap_windows.go +33 -33
- package/templates/go/pkg/freedom/sysint_windows.go +2 -2
- package/templates/go/pkg/freedom/tray_windows.go +18 -18
- package/templates/go/pkg/freedom/updater.go +3 -3
- package/templates/go/third_party/webview_go/CHANGELOG.md +15 -0
- package/templates/go/third_party/webview_go/FREEDOM-PATCH.md +32 -0
- package/templates/go/third_party/webview_go/LICENSE +22 -0
- package/templates/go/third_party/webview_go/README.md +50 -0
- package/templates/go/third_party/webview_go/glue.c +36 -0
- package/templates/go/third_party/webview_go/go.mod +3 -0
- package/templates/go/third_party/webview_go/go.sum +0 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/LICENSE +27 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/version.txt +1 -0
- package/templates/go/third_party/webview_go/libs/webview/LICENSE +22 -0
- package/templates/go/third_party/webview_go/libs/webview/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/include/webview.h +3598 -0
- package/templates/go/third_party/webview_go/libs/webview/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/version.txt +1 -0
- package/templates/go/third_party/webview_go/webkit2_40.go +21 -0
- package/templates/go/third_party/webview_go/webkit2_41.go +8 -0
- package/templates/go/third_party/webview_go/webview.cc +1 -0
- package/templates/go/third_party/webview_go/webview.go +332 -0
package/lib/shell.js
CHANGED
|
@@ -23,7 +23,9 @@ const {
|
|
|
23
23
|
SHELL_EXE_NAME,
|
|
24
24
|
localShellPath,
|
|
25
25
|
nativePlatform,
|
|
26
|
+
normalizePlatform,
|
|
26
27
|
} = require('./utils');
|
|
28
|
+
const { applyWebkitTags } = require('./webkit');
|
|
27
29
|
|
|
28
30
|
// GitHub Releases 下载源(可用环境变量覆盖)。
|
|
29
31
|
// 资产命名约定:freedom-shell-<plat>(单文件二进制,不压缩)。
|
|
@@ -31,6 +33,17 @@ function releaseRepo() {
|
|
|
31
33
|
return process.env.FREEDOM_SHELL_REPO || 'YUfeng12TA/freedom';
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
// 直连下载域与 API 域(测试可指向本地桩服务;默认走 GitHub)。
|
|
37
|
+
function releaseBase() {
|
|
38
|
+
return (process.env.FREEDOM_SHELL_BASE || 'https://github.com').replace(/\/+$/, '');
|
|
39
|
+
}
|
|
40
|
+
function apiBase() {
|
|
41
|
+
return (process.env.FREEDOM_GITHUB_API || 'https://api.github.com').replace(/\/+$/, '');
|
|
42
|
+
}
|
|
43
|
+
function shellAssetName(plat) {
|
|
44
|
+
return `freedom-shell-${plat}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
// 远程壳下载的默认版本:与当前包版本保持一致。
|
|
35
48
|
// 通过环境变量 FREEDOM_SHELL_TAG 可强制覆盖;默认动态读取 package.json 的 version,
|
|
36
49
|
// 避免与包版本漂移(历史曾硬编码 v1.1.10 导致下载到旧壳)。
|
|
@@ -46,7 +59,7 @@ function releaseTag() {
|
|
|
46
59
|
return process.env.FREEDOM_SHELL_TAG || `v${pkgVersion()}`;
|
|
47
60
|
}
|
|
48
61
|
function releaseUrl(plat) {
|
|
49
|
-
return
|
|
62
|
+
return `${releaseBase()}/${releaseRepo()}/releases/download/${releaseTag()}/${shellAssetName(plat)}`;
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
// ---- 壳二进制平台格式校验 ----
|
|
@@ -163,6 +176,78 @@ function hasShell(plat) {
|
|
|
163
176
|
return fs.existsSync(localShellPath(plat));
|
|
164
177
|
}
|
|
165
178
|
|
|
179
|
+
// ---- 发布代际预检(npm publish 前置门)----
|
|
180
|
+
// 随包壳是三平台预编译二进制、不随 git 入库;改了框架源却忘了重新同步壳,就会发出
|
|
181
|
+
// 「新 CLI + 旧壳」的混合代 tarball——FRDM3 断代下新 CLI 产的产物被旧壳直接拒跑
|
|
182
|
+
// (1.14.0-preview 发布前实测:三只壳的生成时间全部早于当时 security.go 的改动时间)。
|
|
183
|
+
// 判据取 mtime:壳文件的时间是「最后一次 download/build 同步」的证据,框架源比它新即可疑。
|
|
184
|
+
// 盲区如实说明:全新克隆里壳刚下载完必然最新,此时本门只证明「同步之后没再改过源」。
|
|
185
|
+
const PUBLISH_PLATFORMS = ['win-x64', 'darwin-arm64', 'linux-x64'];
|
|
186
|
+
|
|
187
|
+
// newestSourceMtime 遍历将编进壳的框架源(*.go / go.mod / go.sum,排除 _test.go)。
|
|
188
|
+
function newestSourceMtime(dir) {
|
|
189
|
+
let newest = 0;
|
|
190
|
+
let file = null;
|
|
191
|
+
const stack = [dir];
|
|
192
|
+
while (stack.length) {
|
|
193
|
+
const d = stack.pop();
|
|
194
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
195
|
+
const p = path.join(d, e.name);
|
|
196
|
+
if (e.isDirectory()) {
|
|
197
|
+
stack.push(p);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!/\.(go|mod|sum)$/.test(e.name) || e.name.endsWith('_test.go')) continue;
|
|
201
|
+
const t = fs.statSync(p).mtimeMs;
|
|
202
|
+
if (t > newest) {
|
|
203
|
+
newest = t;
|
|
204
|
+
file = p;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { newest, file };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function checkBundledShells(opts = {}) {
|
|
212
|
+
const plats = opts.platforms || PUBLISH_PLATFORMS;
|
|
213
|
+
const tplDir = opts.goTemplateDir || goTemplateDir();
|
|
214
|
+
const shellsDir = opts.shellsDir || shellDir();
|
|
215
|
+
const iso = (ms) => new Date(ms).toISOString();
|
|
216
|
+
const rel = (p, base) => path.relative(base, p).split(path.sep).join('/');
|
|
217
|
+
const problems = [];
|
|
218
|
+
const src = newestSourceMtime(tplDir);
|
|
219
|
+
if (!src.newest) problems.push(`预检无法判定代际:${tplDir} 下找不到 *.go 框架源`);
|
|
220
|
+
for (const plat of plats) {
|
|
221
|
+
const exe = path.join(shellsDir, plat, SHELL_EXE_NAME[plat] || 'freedom-shell');
|
|
222
|
+
if (!fs.existsSync(exe)) {
|
|
223
|
+
problems.push(`${plat}: 缺随包壳 ${rel(exe, shellsDir)}`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const t = fs.statSync(exe).mtimeMs;
|
|
227
|
+
if (src.newest && t < src.newest) {
|
|
228
|
+
problems.push(`${plat}: 壳生成于 ${iso(t)},早于框架源最新改动 ${iso(src.newest)}(${rel(src.file, tplDir)})`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return problems;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function preflightBundledShells(opts = {}) {
|
|
235
|
+
const problems = checkBundledShells(opts);
|
|
236
|
+
if (!problems.length) {
|
|
237
|
+
process.stdout.write('[freedom] 发布预检通过:随包壳不早于框架源\n');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// 走 stderr + 退出码而非抛错:本函数由 npm 的 prepublishOnly 直接调用,抛错会带一串
|
|
241
|
+
// 对用户无用的堆栈,把「该重编哪几只壳」这句关键信息挤到屏幕外。
|
|
242
|
+
process.stderr.write(
|
|
243
|
+
'freedom: 随包壳与框架源代际不一致,禁止发布(发出即成混合代 tarball,新 CLI 的产物会被旧壳拒跑):\n - ' +
|
|
244
|
+
problems.join('\n - ') +
|
|
245
|
+
'\n处置:先推 tag 让 CI 产壳,再 freedom shell download <plat> 覆盖 shell/<plat>' +
|
|
246
|
+
'(本机有工具链时 freedom shell build <plat>),然后重跑发布。\n'
|
|
247
|
+
);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
|
|
166
251
|
// ---- 代理支持(修复:本地无 linux/mac 壳时自动下载必失败的历史根因) ----
|
|
167
252
|
// 背景:GitHub Releases 资产重定向到 S3,Node 内置 fetch 直连在受限网络下
|
|
168
253
|
// body 下载会卡死(可拿到 200 响应头但 arrayBuffer 永远不结束)。本机若跑
|
|
@@ -193,13 +278,14 @@ function hasCurl() {
|
|
|
193
278
|
}
|
|
194
279
|
|
|
195
280
|
// 用 curl 下载到 dest;成功返回 null,失败返回错误信息
|
|
196
|
-
function curlDownload(url, dest, proxy) {
|
|
281
|
+
function curlDownload(url, dest, proxy, extraHeaders) {
|
|
197
282
|
const args = [
|
|
198
283
|
'-L', '--fail', '--silent', '--show-error',
|
|
199
284
|
'--connect-timeout', '20', '--max-time', '180', '--retry', '2',
|
|
200
285
|
'--output', dest,
|
|
201
286
|
];
|
|
202
287
|
if (proxy) args.push('--proxy', proxy);
|
|
288
|
+
for (const h of extraHeaders || []) args.push('-H', h);
|
|
203
289
|
if (process.platform === 'win32' && /^https:/i.test(url)) {
|
|
204
290
|
args.push('--ssl-no-revoke'); // 规避 Windows schannel CRYPT_E_REVOCATION_OFFLINE
|
|
205
291
|
}
|
|
@@ -212,18 +298,124 @@ function curlDownload(url, dest, proxy) {
|
|
|
212
298
|
return null;
|
|
213
299
|
}
|
|
214
300
|
|
|
301
|
+
// 平台参数收口:win / mac / linux 等别名归一到平台 key,识别不了给可读错误。
|
|
302
|
+
function requirePlatform(plat) {
|
|
303
|
+
const key = normalizePlatform(plat);
|
|
304
|
+
if (!key) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
`未知平台:${plat}。可选:${ALL_PLATFORMS.join(' / ')}` +
|
|
307
|
+
`(也接受 win / mac / linux 别名,可带架构后缀,如 mac-arm64)`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return key;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 单次 HTTP GET 到 Buffer:配了代理就走 curl(原生支持 socks5),否则用内置 fetch。
|
|
314
|
+
// API 端点需要 Accept 头,故 headers 显式传入。
|
|
315
|
+
async function httpGetBuffer(url, headers) {
|
|
316
|
+
const proxy = resolveProxy();
|
|
317
|
+
if (proxy) {
|
|
318
|
+
if (!hasCurl()) throw new Error(`已配置代理 ${proxy} 但本机无 curl`);
|
|
319
|
+
const tmp = path.join(os.tmpdir(), `freedom-dl-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
320
|
+
const err = curlDownload(url, tmp, proxy, Object.entries(headers || {}).map(([k, v]) => `${k}: ${v}`));
|
|
321
|
+
try {
|
|
322
|
+
if (err) throw new Error(err);
|
|
323
|
+
return fs.readFileSync(tmp);
|
|
324
|
+
} finally {
|
|
325
|
+
try { fs.unlinkSync(tmp); } catch (e) { /* 清理失败忽略 */ }
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const res = await fetch(url, {
|
|
329
|
+
headers: Object.assign({ 'User-Agent': 'freedom-cli' }, headers),
|
|
330
|
+
redirect: 'follow',
|
|
331
|
+
signal: AbortSignal.timeout(60000),
|
|
332
|
+
});
|
|
333
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
334
|
+
return Buffer.from(await res.arrayBuffer());
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 从 release 资产列表里挑出本平台的壳资产(需带 API url 字段)。
|
|
338
|
+
function selectReleaseAsset(assets, plat) {
|
|
339
|
+
const name = shellAssetName(plat);
|
|
340
|
+
return (Array.isArray(assets) ? assets : []).find(
|
|
341
|
+
(a) => a && a.name === name && typeof a.url === 'string'
|
|
342
|
+
) || null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 经 GitHub Release API 取壳:先按 tag 查 release 拿资产 id,再以 octet-stream 拉字节。
|
|
346
|
+
// 背景(实测):releases/download 会 302 到 S3 签名域,受限网络下响应头拿得到、
|
|
347
|
+
// body 永远不结束;api.github.com 的资产端点由 GitHub 自己代理字节流,可稳定取回。
|
|
348
|
+
async function downloadShellViaApi(plat) {
|
|
349
|
+
const token = process.env.FREEDOM_GITHUB_TOKEN || process.env.GITHUB_TOKEN;
|
|
350
|
+
const auth = token ? { Authorization: `Bearer ${token}` } : {};
|
|
351
|
+
const metaUrl = `${apiBase()}/repos/${releaseRepo()}/releases/tags/${releaseTag()}`;
|
|
352
|
+
let meta;
|
|
353
|
+
try {
|
|
354
|
+
meta = JSON.parse((await httpGetBuffer(metaUrl, Object.assign({ Accept: 'application/json' }, auth))).toString('utf8'));
|
|
355
|
+
} catch (e) {
|
|
356
|
+
throw new Error(`查询 release 元数据失败(${metaUrl}):${e.message}`);
|
|
357
|
+
}
|
|
358
|
+
const asset = selectReleaseAsset(meta && meta.assets, plat);
|
|
359
|
+
if (!asset) {
|
|
360
|
+
throw new Error(`${releaseTag()} 中没有资产 ${shellAssetName(plat)}`);
|
|
361
|
+
}
|
|
362
|
+
return await httpGetBuffer(asset.url, Object.assign({ Accept: 'application/octet-stream' }, auth));
|
|
363
|
+
}
|
|
364
|
+
|
|
215
365
|
// 下载指定平台壳到包内 shell/<plat>/
|
|
216
366
|
// 返回下载后的绝对路径;失败抛错。
|
|
367
|
+
// 两条路径:直连 releases/download(有代理时经 curl)→ 失败回退 Release API 资产端点。
|
|
217
368
|
async function downloadShell(plat) {
|
|
218
|
-
|
|
219
|
-
throw new Error(`未知平台:${plat}。可选:${ALL_PLATFORMS.join(' / ')}`);
|
|
220
|
-
}
|
|
369
|
+
plat = requirePlatform(plat);
|
|
221
370
|
const url = releaseUrl(plat);
|
|
222
371
|
const dest = localShellPath(plat);
|
|
223
372
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
224
373
|
|
|
374
|
+
let buf = null;
|
|
375
|
+
let primaryErr = null;
|
|
376
|
+
try {
|
|
377
|
+
buf = await fetchShellDirect(plat, url);
|
|
378
|
+
} catch (e) {
|
|
379
|
+
primaryErr = e;
|
|
380
|
+
process.stdout.write(
|
|
381
|
+
`[freedom] 直连下载壳 ${plat} 未完成(${String(e.message).split('\n')[0]}),回退 GitHub API 资产端点…\n`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
if (!buf) {
|
|
385
|
+
try {
|
|
386
|
+
buf = await downloadShellViaApi(plat);
|
|
387
|
+
} catch (apiErr) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
`下载壳 ${plat} 失败:直连与 Release API 两条路径都不通。\n` +
|
|
390
|
+
` 直连 ${url}:${primaryErr ? primaryErr.message : '未返回数据'}\n` +
|
|
391
|
+
` API ${apiBase()}/repos/${releaseRepo()}:${apiErr.message}\n` +
|
|
392
|
+
`可设置代理重试(FREEDOM_SHELL_PROXY=socks5h://127.0.0.1:10808),` +
|
|
393
|
+
`或手动将壳二进制放入 ${dest}。`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// B13:下载后、落盘前校验格式/架构,防止代理劫持返回错误页或假壳被静默分发。
|
|
399
|
+
const fmt = detectShellFormat(buf);
|
|
400
|
+
const want = expectedFormat(plat);
|
|
401
|
+
if (fmt === 'unknown' || formatFamily(fmt) !== formatFamily(want)) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
`下载的壳格式异常:期望 ${want}(${plat}),实际 detect=${fmt}。` +
|
|
404
|
+
`下载地址 ${url} 可能返回了错误页或非本平台假壳,请检查 GitHub Release 资产 ` +
|
|
405
|
+
`${releaseRepo()} 的 ${releaseTag()}。已放弃本次写入。`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
fs.writeFileSync(dest, buf);
|
|
409
|
+
if (process.platform !== 'win32') {
|
|
410
|
+
fs.chmodSync(dest, 0o755);
|
|
411
|
+
}
|
|
412
|
+
return dest;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// 直连路径:代理在位时用 curl(原生支持 socks5/http 代理),否则用内置 fetch。
|
|
416
|
+
// 返回内容 Buffer;网络失败、超时、非 2xx 一律抛错(由 downloadShell 决定回退)。
|
|
417
|
+
async function fetchShellDirect(plat, url, dest) {
|
|
225
418
|
const proxy = resolveProxy();
|
|
226
|
-
let buf;
|
|
227
419
|
if (proxy) {
|
|
228
420
|
process.stdout.write(`[freedom] 下载壳 ${plat}(经代理 ${proxy})<- ${url}\n`);
|
|
229
421
|
if (!hasCurl()) {
|
|
@@ -238,74 +430,49 @@ async function downloadShell(plat) {
|
|
|
238
430
|
try { fs.unlinkSync(tmp); } catch (e) { /* 清理失败忽略 */ }
|
|
239
431
|
throw new Error(
|
|
240
432
|
`${curlErr}\n下载地址 ${url} 不可达。` +
|
|
241
|
-
`请确认代理 ${proxy} 可用(可执行 curl --proxy ${proxy} ${url} -I
|
|
242
|
-
`或手动将壳二进制放入 ${localShellPath(plat)}。`
|
|
433
|
+
`请确认代理 ${proxy} 可用(可执行 curl --proxy ${proxy} ${url} -I 自测)。`
|
|
243
434
|
);
|
|
244
435
|
}
|
|
245
436
|
try {
|
|
246
|
-
|
|
437
|
+
return fs.readFileSync(tmp);
|
|
247
438
|
} finally {
|
|
248
439
|
try { fs.unlinkSync(tmp); } catch (e) { /* 清理失败忽略 */ }
|
|
249
440
|
}
|
|
250
|
-
} else {
|
|
251
|
-
process.stdout.write(`[freedom] 下载壳 ${plat} <- ${url}\n`);
|
|
252
|
-
let res;
|
|
253
|
-
try {
|
|
254
|
-
// 30s 超时:网络挂起时明确报错,避免构建进程无限阻塞
|
|
255
|
-
res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(30000) });
|
|
256
|
-
} catch (e) {
|
|
257
|
-
if (e.name === 'AbortError' || e.name === 'TimeoutError') {
|
|
258
|
-
throw new Error(
|
|
259
|
-
`下载壳 ${plat} 超时(30s)。请检查网络后重试,` +
|
|
260
|
-
`或设置代理变量 FREEDOM_SHELL_PROXY=socks5h://127.0.0.1:10808 重试,` +
|
|
261
|
-
`或手动将壳二进制放入 ${localShellPath(plat)}。`
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
throw new Error(
|
|
265
|
-
`下载壳 ${plat} 失败:${e.message}。请检查网络(必要时设置 FREEDOM_SHELL_PROXY 走代理),` +
|
|
266
|
-
`或手动将壳二进制放入 ${localShellPath(plat)}。`
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
if (!res.ok) {
|
|
270
|
-
throw new Error(
|
|
271
|
-
`下载壳失败:HTTP ${res.status}。请确认 GitHub 仓库 ${releaseRepo()} 已发布 ` +
|
|
272
|
-
`${releaseTag()} 的资产 freedom-shell-${plat},或手动将壳二进制放入 ${localShellPath(plat)}。`
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
buf = Buffer.from(await res.arrayBuffer());
|
|
276
441
|
}
|
|
277
442
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
fs.writeFileSync(dest, buf);
|
|
289
|
-
if (process.platform !== 'win32') {
|
|
290
|
-
fs.chmodSync(dest, 0o755);
|
|
443
|
+
process.stdout.write(`[freedom] 下载壳 ${plat} <- ${url}\n`);
|
|
444
|
+
let res;
|
|
445
|
+
try {
|
|
446
|
+
// 30s 超时:网络挂起时明确报错,避免构建进程无限阻塞
|
|
447
|
+
res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(30000) });
|
|
448
|
+
} catch (e) {
|
|
449
|
+
if (e.name === 'AbortError' || e.name === 'TimeoutError') {
|
|
450
|
+
throw new Error('下载超时(30s)');
|
|
451
|
+
}
|
|
452
|
+
throw new Error(`请求失败:${e.message}`);
|
|
291
453
|
}
|
|
292
|
-
|
|
454
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
455
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
456
|
+
if (!buf.length) throw new Error('响应体为空');
|
|
457
|
+
return buf;
|
|
293
458
|
}
|
|
294
459
|
|
|
295
460
|
// 本地用 Go 编译指定平台壳(需要 Go + 该平台编译环境)。
|
|
296
461
|
// Windows 产物以 GUI 子系统编译(-H windowsgui),运行时无 cmd 黑窗。
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
462
|
+
//
|
|
463
|
+
// opts.dest:产物落点(默认为包内通用壳位置)。high 模式传临时路径,编出的是
|
|
464
|
+
// 本应用专属壳(Tier B),不得覆盖通用壳缓存。
|
|
465
|
+
// opts.inject:{-X 符号路径: 值} 映射,用于把信任锚编进壳(见 lib/security.js shellInject)。
|
|
466
|
+
// 值必须是命令行安全字符集——这里拼进单个 -ldflags 字符串,含空格即串味。
|
|
467
|
+
// opts.stageGoFiles:{文件名: Go 源码} —— 编壳时**本次构建专属**的 Go 源文件(Tier B 的
|
|
468
|
+
// keyslot 装配码),经 `go build -overlay` 虚拟进 pkg/freedom。刻意不落进 templates/go:
|
|
469
|
+
// 那是随 npm 包分发的公共树,写进去一次就等于把某个产物的主密钥编进之后所有壳(含通用壳),
|
|
470
|
+
// 还会撞上 CI 的 Template mirror 门。overlay 让它只活在这次构建的这一个进程里。
|
|
471
|
+
function buildShell(plat, opts = {}) {
|
|
472
|
+
plat = requirePlatform(plat);
|
|
308
473
|
// 本机只能编译本机平台(webview_go 依赖系统 WebView 框架,无法交叉编译)。
|
|
474
|
+
// 这道判断刻意前置于 Go 探测:平台不匹配是与工具有关的硬事实,先报工具有关
|
|
475
|
+
// 的错误会把「装个 Go 也没用」的场景误导成装 Go 能解决。
|
|
309
476
|
// 统一走 utils.nativePlatform:Intel Mac 会明确抛"已不支持",避免两套映射语义不一(B44)。
|
|
310
477
|
const native = nativePlatform();
|
|
311
478
|
if (plat !== native) {
|
|
@@ -314,23 +481,78 @@ function buildShell(plat) {
|
|
|
314
481
|
`请在目标平台执行 freedom shell build ${plat},或用 freedom shell download ${plat} 拉取 CI 预编译产物。`
|
|
315
482
|
);
|
|
316
483
|
}
|
|
484
|
+
const res = spawnSync('go', ['version'], { encoding: 'utf8' });
|
|
485
|
+
if (res.error || res.status !== 0) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
'未检测到 Go 工具链。执行 freedom toolchain install go 自动安装(默认只打印命令,加 --apply 才执行),' +
|
|
488
|
+
'或手动获取 https://go.dev/dl/ ,或改用 freedom shell download 拉取预编译壳。'
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
const dest = opts.dest ? path.resolve(opts.dest) : localShellPath(plat);
|
|
492
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
317
493
|
|
|
318
494
|
const buildDir = goTemplateDir();
|
|
319
495
|
// 剥离符号与调试信息(-s -w)+ 抹掉构建期绝对路径(-trimpath):
|
|
320
496
|
// 通用壳里编译进了 high 模式的密钥派生逻辑,函数名/DWARF 是给逆向者的地图。
|
|
321
497
|
const ldflags = ['-s', '-w'];
|
|
498
|
+
for (const [sym, val] of Object.entries(opts.inject || {})) {
|
|
499
|
+
if (!/^[A-Za-z0-9_./:-]+$/.test(val)) {
|
|
500
|
+
throw new Error(`-X 注入值含非法字符(仅允许字母数字与 _./:-):${sym}`);
|
|
501
|
+
}
|
|
502
|
+
ldflags.push('-X', `${sym}=${val}`);
|
|
503
|
+
}
|
|
322
504
|
if (plat.startsWith('win')) ldflags.push('-H', 'windowsgui');
|
|
323
|
-
|
|
505
|
+
// Linux:新发行版只有 webkit2gtk-4.1,需要 -tags webkit2_41(探测与注入见 lib/webkit.js)。
|
|
506
|
+
const webkit = applyWebkitTags(process.env);
|
|
507
|
+
if (webkit.tags.length) {
|
|
508
|
+
process.stdout.write(`[freedom] 本机 WebKitGTK 仅有 4.1,构建加 -tags ${webkit.tags.join(',')}\n`);
|
|
509
|
+
}
|
|
510
|
+
// 本次构建专属的 Go 源文件经 overlay 虚拟进壳的包目录(见函数注释)。
|
|
511
|
+
const staged = stageOverlayFiles(buildDir, opts.stageGoFiles);
|
|
512
|
+
const args = ['build', '-trimpath'];
|
|
513
|
+
if (staged.overlayPath) args.push(`-overlay=${staged.overlayPath}`);
|
|
514
|
+
args.push('-ldflags', ldflags.join(' '), '-o', dest, '.');
|
|
515
|
+
const build = spawnSync('go', args, {
|
|
324
516
|
cwd: buildDir,
|
|
325
517
|
encoding: 'utf8',
|
|
326
|
-
env: {
|
|
518
|
+
env: Object.assign({ CGO_ENABLED: '1' }, webkit.env),
|
|
327
519
|
});
|
|
328
|
-
|
|
329
|
-
|
|
520
|
+
try {
|
|
521
|
+
if (build.error || build.status !== 0) {
|
|
522
|
+
const hint = plat.startsWith('linux') && /webkit2gtk-4\.0/.test(`${build.stdout}${build.stderr}`)
|
|
523
|
+
? `\n提示:本机未装 webkit2gtk-4.0/4.1 开发库。Ubuntu/Debian 执行 ` +
|
|
524
|
+
`sudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev(老发行版用 4.0)后重试。`
|
|
525
|
+
: '';
|
|
526
|
+
throw new Error(`Go 编译失败:\n${build.stdout}\n${build.stderr}${hint}`);
|
|
527
|
+
}
|
|
528
|
+
} finally {
|
|
529
|
+
staged.cleanup();
|
|
330
530
|
}
|
|
331
531
|
return dest;
|
|
332
532
|
}
|
|
333
533
|
|
|
534
|
+
// stageOverlayFiles 把 {文件名: 源码} 写成临时文件并产出一份 go command 认的 overlay 清单
|
|
535
|
+
// (Replace: 虚拟路径 → 真实路径)。返回 { overlayPath, cleanup };无文件时 overlayPath 为 null。
|
|
536
|
+
// 虚拟路径必须落在目标包目录下,否则不会被视为该包的源文件。
|
|
537
|
+
function stageOverlayFiles(buildDir, files) {
|
|
538
|
+
const entries = Object.entries(files || {});
|
|
539
|
+
if (!entries.length) return { overlayPath: null, cleanup() {} };
|
|
540
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'freedom-stage-'));
|
|
541
|
+
const replace = {};
|
|
542
|
+
for (const [name, src] of entries) {
|
|
543
|
+
if (!/^keyslot_[0-9a-f]{6}\.go$/.test(name)) {
|
|
544
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
545
|
+
throw new Error(`internal: 不接受的壳源文件名 ${name}(只允许 keyslot_<tag>.go)`);
|
|
546
|
+
}
|
|
547
|
+
const real = path.join(dir, name);
|
|
548
|
+
fs.writeFileSync(real, src, 'utf8');
|
|
549
|
+
replace[path.join(buildDir, 'pkg', 'freedom', name)] = real;
|
|
550
|
+
}
|
|
551
|
+
const overlayPath = path.join(dir, 'overlay.json');
|
|
552
|
+
fs.writeFileSync(overlayPath, JSON.stringify({ Replace: replace }), 'utf8');
|
|
553
|
+
return { overlayPath, cleanup: () => fs.rmSync(dir, { recursive: true, force: true }) };
|
|
554
|
+
}
|
|
555
|
+
|
|
334
556
|
function nativePlatformKey() {
|
|
335
557
|
// 已废弃:统一使用 utils.nativePlatform(B44),本函数保留仅作内部兜底,勿再调用。
|
|
336
558
|
return nativePlatform();
|
|
@@ -346,4 +568,6 @@ module.exports = {
|
|
|
346
568
|
detectShellFormat,
|
|
347
569
|
expectedFormat,
|
|
348
570
|
validateLocalShell,
|
|
571
|
+
checkBundledShells,
|
|
572
|
+
preflightBundledShells,
|
|
349
573
|
};
|