@yufengtadian/freedom-cli 1.13.2 → 1.13.3
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 +44 -14
- package/lib/agents.js +182 -52
- package/lib/build.js +22 -7
- package/lib/cli.js +100 -7
- package/lib/shell.js +165 -61
- package/lib/toolchain.js +339 -0
- package/lib/utils.js +51 -0
- package/lib/verify.js +5 -4
- package/lib/webkit.js +45 -0
- package/package.json +2 -2
- 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 +13 -2
- package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
- package/templates/go/pkg/freedom/resources.go +52 -1
- package/templates/go/pkg/freedom/security.go +57 -7
- 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/toolchain.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 工具链自动装配扩展:探测 Go / Rust / C++ 是否就位,缺失时给出安装动作(--apply 才真跑),
|
|
4
|
+
// 并把镜像源等编译配置优化写进各工具自己的配置位。
|
|
5
|
+
//
|
|
6
|
+
// 「检测到配置好就不去检测」:探测要 spawn 子进程跑 --version,三平台冷启动累计几百毫秒,
|
|
7
|
+
// 故 ok 结论进缓存(按 PATH 指纹 + TTL 失效)。失败结论**永不入缓存**——刚装好的工具必须
|
|
8
|
+
// 当场被认出来,否则用户装完再跑一次会说「没装」,那是比慢得多的缺陷。
|
|
9
|
+
//
|
|
10
|
+
// 安装动作默认只打印不执行:这些命令要联网、要管理员权限、会改系统,属高危面(见 AGENTS.md
|
|
11
|
+
// 铁律 8 口径),必须 --apply 显式授权。
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { spawnSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const CACHE_VERSION = 1;
|
|
19
|
+
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
const SPECS = [
|
|
22
|
+
{
|
|
23
|
+
key: 'go',
|
|
24
|
+
name: 'Go 工具链',
|
|
25
|
+
why: '编译 Freedom 壳与内嵌 Go 后端(freedom shell build / cmd/freedom build)',
|
|
26
|
+
probes: [{ exe: 'go', args: ['version'], re: /go(\d+\.\d+(?:\.\d+)?)/ }],
|
|
27
|
+
manual: 'https://go.dev/dl/',
|
|
28
|
+
install: {
|
|
29
|
+
win32: [{ cmd: 'winget', args: ['install', '-e', '--id', 'GoLang.Go'] }],
|
|
30
|
+
darwin: [{ cmd: 'brew', args: ['install', 'go'] }],
|
|
31
|
+
linux: [{ cmd: 'sudo', args: ['apt-get', 'install', '-y', 'golang-go'] }],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'rust',
|
|
36
|
+
name: 'Rust 工具链',
|
|
37
|
+
why: '编译 examples/multiproc 的 Rust 后端(rustc -O 单文件,不走 cargo)',
|
|
38
|
+
probes: [
|
|
39
|
+
{ exe: 'rustc', args: ['--version'], re: /rustc\s+(\d+\.\d+(?:\.\d+)?)/ },
|
|
40
|
+
{ exe: 'rustc', args: ['--version'], re: /rustc\s+(\d+\.\d+(?:\.\d+)?)/, homeRelative: true },
|
|
41
|
+
],
|
|
42
|
+
manual: 'https://rustup.rs/',
|
|
43
|
+
install: {
|
|
44
|
+
win32: [{ cmd: 'winget', args: ['install', '-e', '--id', 'Rustlang.Rustup'] }],
|
|
45
|
+
darwin: [{ cmd: 'brew', args: ['install', 'rust'] }],
|
|
46
|
+
linux: [{ cmd: 'sudo', args: ['apt-get', 'install', '-y', 'rustc'] }],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: 'cpp',
|
|
51
|
+
name: 'C/C++ 工具链',
|
|
52
|
+
why: 'cgo 依赖:Linux WebKitGTK 壳与 Windows WebView2 层需要宿主 C 编译器',
|
|
53
|
+
probes: [
|
|
54
|
+
{ exe: 'cl', args: [], re: /Version\s+(\d+\.\d+)/, okNonZero: true },
|
|
55
|
+
{ exe: 'g++', args: ['--version'], re: /(\d+\.\d+(?:\.\d+)?)/ },
|
|
56
|
+
{ exe: 'clang++', args: ['--version'], re: /(\d+\.\d+(?:\.\d+)?)/ },
|
|
57
|
+
],
|
|
58
|
+
manual: 'https://visualstudio.microsoft.com/visual-cpp-build-tools/',
|
|
59
|
+
install: {
|
|
60
|
+
win32: [{
|
|
61
|
+
cmd: 'winget',
|
|
62
|
+
args: ['install', '-e', '--id', 'Microsoft.VisualStudio.2022.BuildTools',
|
|
63
|
+
'--override', '"--quiet --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"'],
|
|
64
|
+
}],
|
|
65
|
+
darwin: [{ cmd: 'xcode-select', args: ['--install'] }],
|
|
66
|
+
linux: [{ cmd: 'sudo', args: ['apt-get', 'install', '-y', 'build-essential', 'pkg-config'] }],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
function toolchainHome(opts = {}) {
|
|
72
|
+
return opts.home || process.env.FREEDOM_TOOLCHAIN_HOME || os.homedir();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cacheFile(opts) {
|
|
76
|
+
return path.join(toolchainHome(opts), '.freedom', 'toolchain-cache.json');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function findInPath(exe, env) {
|
|
80
|
+
const dirs = String(env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
81
|
+
const exts = env.FREEDOM_HOST_PLATFORM === 'win32' || process.platform === 'win32'
|
|
82
|
+
? String(env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').concat([''])
|
|
83
|
+
: [''];
|
|
84
|
+
for (const dir of dirs) {
|
|
85
|
+
for (const ext of exts) {
|
|
86
|
+
const full = path.join(dir, exe + ext);
|
|
87
|
+
try { if (fs.existsSync(full) && fs.statSync(full).isFile()) return full; } catch (e) { /* 无权访问的 PATH 项:跳过 */ }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// PATH 指纹:工具可见性只取决于 PATH 变了没有,变了就必须重探(新装工具通常在 PATH 尾部)。
|
|
94
|
+
function pathFingerprint(env) {
|
|
95
|
+
let h = 0;
|
|
96
|
+
const s = String(env.PATH || '');
|
|
97
|
+
for (let i = 0; i < s.length; i++) h = (Math.imul(h, 31) + s.charCodeAt(i)) | 0;
|
|
98
|
+
return h.toString(16);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readCache(opts) {
|
|
102
|
+
try {
|
|
103
|
+
const raw = JSON.parse(fs.readFileSync(cacheFile(opts), 'utf8'));
|
|
104
|
+
if (raw && raw.version === CACHE_VERSION && raw.host === hostPlatform(opts)) return raw;
|
|
105
|
+
} catch (e) { /* 无缓存 / 损坏 / 跨平台搬运:一律当未命中 */ }
|
|
106
|
+
return { version: CACHE_VERSION, host: hostPlatform(opts), pathHash: null, tools: {} };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeCache(opts, cache) {
|
|
110
|
+
const file = cacheFile(opts);
|
|
111
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
112
|
+
fs.writeFileSync(file, JSON.stringify(cache, null, 2) + '\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hostPlatform(opts) {
|
|
116
|
+
return (opts.env && opts.env.FREEDOM_HOST_PLATFORM) || process.platform;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 单个工具链探测:返回 { ok, version, exe, reason }。
|
|
120
|
+
function resolveProbe(p, o) {
|
|
121
|
+
if (p.homeRelative) {
|
|
122
|
+
// rustup 的默认安装位不进 PATH 时也要认出来(装完没重开终端是常态)。
|
|
123
|
+
const bin = path.join(o.env.CARGO_HOME || path.join(toolchainHome(o), '.cargo'), 'bin');
|
|
124
|
+
const exts = hostPlatform(o) === 'win32' ? ['.exe', ''] : [''];
|
|
125
|
+
for (const ext of exts) {
|
|
126
|
+
const full = path.join(bin, p.exe + ext);
|
|
127
|
+
try { if (fs.statSync(full).isFile()) return full; } catch (e) { /* 不存在 */ }
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return findInPath(p.exe, o.env);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function probe(spec, opts) {
|
|
135
|
+
const o = Object.assign({ env: process.env, exec: spawnSync }, opts);
|
|
136
|
+
const reasons = [];
|
|
137
|
+
for (const p of spec.probes) {
|
|
138
|
+
const resolved = resolveProbe(p, o);
|
|
139
|
+
if (!resolved) continue;
|
|
140
|
+
let r;
|
|
141
|
+
try { r = o.exec(resolved, p.args, { encoding: 'utf8', timeout: 8000 }); } catch (e) { reasons.push(`${resolved}: 无法启动`); continue; }
|
|
142
|
+
const text = `${(r && r.stdout) || ''}${(r && r.stderr) || ''}`;
|
|
143
|
+
const m = text.match(p.re);
|
|
144
|
+
if (m && (r.status === 0 || p.okNonZero)) return { ok: true, version: m[1], exe: resolved };
|
|
145
|
+
if (r && r.error) { reasons.push(`${resolved}: ${r.error.code || r.error.message}`); continue; }
|
|
146
|
+
// 找得到可执行但跑不通(cl 未加载 vcvarsall 环境是最常见的一种):记原因再试下一个候选。
|
|
147
|
+
reasons.push(`${resolved}: 可执行但输出无法识别(MSVC 需先加载 vcvarsall 环境)`);
|
|
148
|
+
}
|
|
149
|
+
return { ok: false, reason: reasons.length ? reasons[reasons.length - 1] : 'PATH 中未找到可执行' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// status:命中缓存的 ok 记录直接跳过 spawn。
|
|
153
|
+
function status(opts = {}) {
|
|
154
|
+
const o = Object.assign({ env: process.env, refresh: false }, opts);
|
|
155
|
+
const cache = readCache(o);
|
|
156
|
+
const hash = pathFingerprint(o.env);
|
|
157
|
+
const stale = cache.pathHash !== hash;
|
|
158
|
+
const rows = [];
|
|
159
|
+
for (const spec of SPECS) {
|
|
160
|
+
const hit = !o.refresh && !stale && cache.tools[spec.key] && cache.tools[spec.key].ok
|
|
161
|
+
&& (Date.now() - cache.tools[spec.key].at) < CACHE_TTL_MS;
|
|
162
|
+
if (hit) {
|
|
163
|
+
rows.push({ key: spec.key, name: spec.name, why: spec.why, source: 'cache', cached: true, ...cache.tools[spec.key] });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const res = probe(spec, o);
|
|
167
|
+
rows.push({ key: spec.key, name: spec.name, why: spec.why, source: 'probe', cached: false, ...res });
|
|
168
|
+
if (res.ok) cache.tools[spec.key] = { ok: true, version: res.version, exe: res.exe, at: Date.now() };
|
|
169
|
+
else delete cache.tools[spec.key]; // 失败不入缓存:下次仍探
|
|
170
|
+
}
|
|
171
|
+
cache.pathHash = hash;
|
|
172
|
+
writeCache(o, cache);
|
|
173
|
+
return { rows, skipped: rows.filter((r) => r.cached).length };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function installPlan(spec, opts) {
|
|
177
|
+
const list = spec.install[hostPlatform(opts)] || [];
|
|
178
|
+
return list.map((s) => [s.cmd].concat(s.args).join(' '));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// install:默认只打印计划;--apply 才真跑(联网 + 提权 + 改系统,属高危动作)。
|
|
182
|
+
function install(targets, opts = {}) {
|
|
183
|
+
const o = Object.assign({ env: process.env, apply: false, exec: spawnSync }, opts);
|
|
184
|
+
const want = resolveTargets(targets);
|
|
185
|
+
const st = status(Object.assign({}, o, { refresh: o.apply ? true : false }));
|
|
186
|
+
const out = [];
|
|
187
|
+
for (const spec of want) {
|
|
188
|
+
const row = st.rows.find((r) => r.key === spec.key);
|
|
189
|
+
if (row.ok && !o.force) {
|
|
190
|
+
out.push({ key: spec.key, action: 'skip', version: row.version, note: '已就位' });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const cmds = installPlan(spec, o);
|
|
194
|
+
if (!cmds.length) { out.push({ key: spec.key, action: 'manual', note: `本平台无自动安装命令,请手动获取:${spec.manual}` }); continue; }
|
|
195
|
+
if (!o.apply) { out.push({ key: spec.key, action: 'plan', commands: cmds, manual: spec.manual }); continue; }
|
|
196
|
+
const ran = [];
|
|
197
|
+
for (const line of cmds) {
|
|
198
|
+
const [cmd, ...args] = splitCmd(line);
|
|
199
|
+
let r;
|
|
200
|
+
try { r = o.exec(cmd, args, { stdio: 'inherit', env: o.env }); } catch (e) { r = { status: -1, error: e }; }
|
|
201
|
+
ran.push({ command: line, status: r ? r.status : -1 });
|
|
202
|
+
if (!r || r.status !== 0) break; // 前置步骤失败就别往下跑
|
|
203
|
+
}
|
|
204
|
+
out.push({ key: spec.key, action: 'applied', ran });
|
|
205
|
+
}
|
|
206
|
+
return { rows: out, dryRun: !o.apply };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolveTargets(raw) {
|
|
210
|
+
const keys = !raw || !raw.length || raw === 'missing' ? SPECS.map((s) => s.key) : String(raw).split(',').map((s) => s.trim()).filter(Boolean);
|
|
211
|
+
return SPECS.filter((s) => keys.includes(s.key));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function splitCmd(line) {
|
|
215
|
+
const m = String(line).match(/(?:[^\s"']+|"[^"]*")+/g) || [];
|
|
216
|
+
return m.map((t) => t.replace(/^"|"$/g, ''));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---- 配置优化:只写各工具自己的配置位,幂等 + .bak,缺省只打印 diff ----
|
|
220
|
+
|
|
221
|
+
// 大陆网络提示:locale 或时区命中才提议换镜像(不误伤海外机器),也可 FREEDOM_CN_MIRROR=1/0 强制。
|
|
222
|
+
function mainlandHint(env) {
|
|
223
|
+
const force = env.FREEDOM_CN_MIRROR;
|
|
224
|
+
if (force === '0' || force === 'false') return false;
|
|
225
|
+
if (force === '1' || force === 'true') return true;
|
|
226
|
+
const loc = `${env.LANG || ''}${env.LC_ALL || ''}${env.LC_CTYPE || ''}`;
|
|
227
|
+
const tz = env.TZ || '';
|
|
228
|
+
return /zh[-_]CN|chinese/i.test(loc) || /Asia\/(Shanghai|Chongqing|Harbin|Urumqi|Beijing|Kashgar)/i.test(tz);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const GO_PROXY = 'https://goproxy.cn,direct';
|
|
232
|
+
const CARGO_SOURCE = {
|
|
233
|
+
'source.crates-io': { 'replace-with': 'rsproxy-sparse' },
|
|
234
|
+
'source.rsproxy-sparse': { registry: 'sparse+https://rsproxy.cn/index/' },
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// cargo 的 config.toml:表名可含点([source.crates-io]),按表头整块替换/追加。
|
|
238
|
+
function upsertTomlTable(text, header, fields) {
|
|
239
|
+
const lines = String(text).split(/\r?\n/);
|
|
240
|
+
const block = [header];
|
|
241
|
+
for (const [k, v] of Object.entries(fields)) block.push(`${k} = ${JSON.stringify(v)}`);
|
|
242
|
+
const start = lines.findIndex((l) => l.trim() === header);
|
|
243
|
+
if (start < 0) {
|
|
244
|
+
while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
|
|
245
|
+
return { text: lines.concat('', block).join('\n') + '\n', had: false };
|
|
246
|
+
}
|
|
247
|
+
let end = start + 1;
|
|
248
|
+
while (end < lines.length && !/^\s*\[/.test(lines[end])) end++;
|
|
249
|
+
const out = lines.slice(0, start).concat(block, lines.slice(end));
|
|
250
|
+
return { text: out.join('\n').replace(/\n*$/, '\n'), had: true };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function cargoConfigFile(env, home) {
|
|
254
|
+
const root = env.CARGO_HOME ? env.CARGO_HOME : path.join(home, '.cargo');
|
|
255
|
+
return path.join(root, 'config.toml');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// optimize:返回每个工具链的配置动作。apply=false 只回报将要写什么。
|
|
259
|
+
function optimize(opts = {}) {
|
|
260
|
+
const o = Object.assign({ env: process.env, apply: false, exec: spawnSync }, opts);
|
|
261
|
+
const home = toolchainHome(o);
|
|
262
|
+
const actions = [];
|
|
263
|
+
const st = status(Object.assign({}, o, { refresh: false }));
|
|
264
|
+
|
|
265
|
+
const go = st.rows.find((r) => r.key === 'go');
|
|
266
|
+
if (!mainlandHint(o.env)) {
|
|
267
|
+
actions.push({ key: 'go', action: 'skip', note: '非大陆网络环境提示,保留官方 GOPROXY' });
|
|
268
|
+
} else if (!go || !go.ok) {
|
|
269
|
+
actions.push({ key: 'go', action: 'blocked', note: '未检测到 Go,先 freedom toolchain install go' });
|
|
270
|
+
} else {
|
|
271
|
+
const cur = runCapture(o.exec, go.exe, ['env', 'GOPROXY']);
|
|
272
|
+
if (cur && cur.includes('goproxy.cn')) {
|
|
273
|
+
actions.push({ key: 'go', action: 'skip', note: `GOPROXY 已是镜像(${cur})` });
|
|
274
|
+
} else {
|
|
275
|
+
const cmdLine = `${go.exe} env -w GOPROXY=${GO_PROXY}`;
|
|
276
|
+
if (!o.apply) { actions.push({ key: 'go', action: 'plan', commands: [cmdLine], from: cur }); }
|
|
277
|
+
else {
|
|
278
|
+
const r = o.exec(go.exe, ['env', '-w', `GOPROXY=${GO_PROXY}`], { env: o.env, encoding: 'utf8' });
|
|
279
|
+
actions.push({ key: 'go', action: 'applied', to: GO_PROXY, status: r ? r.status : -1, error: r && r.error ? String(r.error.message) : null });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const rust = st.rows.find((r) => r.key === 'rust');
|
|
285
|
+
if (!mainlandHint(o.env)) {
|
|
286
|
+
actions.push({ key: 'rust', action: 'skip', note: '非大陆网络环境提示,不改 cargo 源' });
|
|
287
|
+
} else if (!rust || !rust.ok) {
|
|
288
|
+
actions.push({ key: 'rust', action: 'blocked', note: '未检测到 Rust,先 freedom toolchain install rust' });
|
|
289
|
+
} else {
|
|
290
|
+
const file = cargoConfigFile(o.env, home);
|
|
291
|
+
let text = '';
|
|
292
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch (e) { /* 首次创建 */ }
|
|
293
|
+
if (/rsproxy\.cn/.test(text)) {
|
|
294
|
+
actions.push({ key: 'rust', action: 'skip', note: `cargo 源已是 rsproxy(${file})`, file });
|
|
295
|
+
} else {
|
|
296
|
+
let next = text;
|
|
297
|
+
for (const [header, fields] of Object.entries(CARGO_SOURCE)) next = upsertTomlTable(next, `[${header}]`, fields).text;
|
|
298
|
+
if (!o.apply) actions.push({ key: 'rust', action: 'plan', file, writes: next });
|
|
299
|
+
else {
|
|
300
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
301
|
+
if (fs.existsSync(file)) fs.copyFileSync(file, `${file}.bak`);
|
|
302
|
+
fs.writeFileSync(file, next);
|
|
303
|
+
actions.push({ key: 'rust', action: 'applied', file, backup: fs.existsSync(`${file}.bak`) ? `${file}.bak` : null });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const cpp = st.rows.find((r) => r.key === 'cpp');
|
|
309
|
+
actions.push({
|
|
310
|
+
key: 'cpp',
|
|
311
|
+
action: 'report',
|
|
312
|
+
ok: !!(cpp && cpp.ok),
|
|
313
|
+
note: cpp && cpp.ok
|
|
314
|
+
? `已就位:${cpp.exe}(cgo 可直接使用,无用户配置需改)`
|
|
315
|
+
: `${cpp && cpp.reason ? cpp.reason + ';' : ''}Windows 需经 vcvarsall 注入环境(freedom build 已在子进程内处理),或安装 Build Tools`,
|
|
316
|
+
});
|
|
317
|
+
return { rows: actions, dryRun: !o.apply };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function runCapture(exec, exe, args) {
|
|
321
|
+
try {
|
|
322
|
+
const r = exec(exe, args, { encoding: 'utf8', timeout: 8000 });
|
|
323
|
+
if (!r || r.status !== 0) return null;
|
|
324
|
+
return String(r.stdout || '').trim();
|
|
325
|
+
} catch (e) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 清理缓存(测试与「重装后重探」用)。
|
|
331
|
+
function clearCache(opts = {}) {
|
|
332
|
+
try { fs.unlinkSync(cacheFile(opts)); return true; } catch (e) { return false; }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = {
|
|
336
|
+
SPECS, status, install, optimize, probe, mainlandHint, upsertTomlTable,
|
|
337
|
+
cacheFile, clearCache, findInPath, pathFingerprint, installPlan, splitCmd, cargoConfigFile,
|
|
338
|
+
GO_PROXY, CACHE_VERSION, CACHE_TTL_MS,
|
|
339
|
+
};
|
package/lib/utils.js
CHANGED
|
@@ -25,6 +25,9 @@ function goTemplateDir() {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function shellDir() {
|
|
28
|
+
// 壳目录可经 env 覆写(离线/测试注入假壳;与 FREEDOM_SHELL_REPO 等既有旋钮同族)。
|
|
29
|
+
const override = process.env.FREEDOM_SHELL_DIR;
|
|
30
|
+
if (override) return path.resolve(override);
|
|
28
31
|
return path.join(PKG_ROOT, 'shell');
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -81,6 +84,53 @@ function nativePlatform() {
|
|
|
81
84
|
return `${plat}-${arch}`;
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
// 平台别名归一化:把口语写法(win / mac / linux / windows / osx / ubuntu)与架构同义词
|
|
88
|
+
// (x86_64 / amd64 / aarch64)统一到 ALL_PLATFORMS 的 key,无法识别返回 null。
|
|
89
|
+
// 缺架构时的默认值与历史 build.js keyMap 一致:win→x64、darwin→arm64(已不支持 Intel Mac)、
|
|
90
|
+
// linux→x64。
|
|
91
|
+
const PLAT_FAMILY = {
|
|
92
|
+
win: 'win', win32: 'win', win64: 'win', windows: 'win', pc: 'win',
|
|
93
|
+
mac: 'darwin', macos: 'darwin', 'mac-os': 'darwin', 'mac-os-x': 'darwin',
|
|
94
|
+
osx: 'darwin', darwin: 'darwin', apple: 'darwin',
|
|
95
|
+
linux: 'linux', lin: 'linux', ubuntu: 'linux', debian: 'linux',
|
|
96
|
+
};
|
|
97
|
+
const PLAT_ARCH = {
|
|
98
|
+
x64: 'x64', x86_64: 'x64', 'x86-64': 'x64', amd64: 'x64',
|
|
99
|
+
arm64: 'arm64', aarch64: 'arm64', 'aarch-64': 'arm64',
|
|
100
|
+
};
|
|
101
|
+
const PLAT_DEFAULT_ARCH = { win: 'x64', darwin: 'arm64', linux: 'x64' };
|
|
102
|
+
|
|
103
|
+
function normalizePlatform(raw) {
|
|
104
|
+
if (typeof raw !== 'string') return null;
|
|
105
|
+
const s = raw.trim().toLowerCase().replace(/\s+/g, '-').replace(/_/g, '-');
|
|
106
|
+
if (!s) return null;
|
|
107
|
+
if (ALL_PLATFORMS.includes(s)) return s;
|
|
108
|
+
// 从右往左试两种切法(架构名自身可能带分隔符,如 x86-64;族名也一样,如 mac-os-x):
|
|
109
|
+
// 先按最后一个分隔符切,切不出架构再按倒数第二个切,还不行就整串当族名。
|
|
110
|
+
let fam = s;
|
|
111
|
+
let arch;
|
|
112
|
+
const cuts = [];
|
|
113
|
+
const last = s.lastIndexOf('-');
|
|
114
|
+
if (last > 0) {
|
|
115
|
+
cuts.push([last, s.slice(last + 1)]);
|
|
116
|
+
const prev = s.lastIndexOf('-', last - 1);
|
|
117
|
+
if (prev > 0) cuts.push([prev, s.slice(prev + 1)]);
|
|
118
|
+
}
|
|
119
|
+
for (const [at, tail] of cuts) {
|
|
120
|
+
if (PLAT_ARCH[tail]) {
|
|
121
|
+
arch = PLAT_ARCH[tail];
|
|
122
|
+
fam = s.slice(0, at);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const family = PLAT_FAMILY[fam];
|
|
127
|
+
if (!family) return null;
|
|
128
|
+
const key = `${family}-${arch || PLAT_DEFAULT_ARCH[family]}`;
|
|
129
|
+
// 归一后仍须落在支持矩阵里:darwin-x64(Intel Mac 已停止支持)之类的组合
|
|
130
|
+
// 不能因为"语法像对的"就放行。
|
|
131
|
+
return ALL_PLATFORMS.includes(key) ? key : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
84
134
|
function localShellPath(plat) {
|
|
85
135
|
return path.join(shellDir(), plat, SHELL_EXE_NAME[plat] || 'freedom-shell');
|
|
86
136
|
}
|
|
@@ -134,6 +184,7 @@ module.exports = {
|
|
|
134
184
|
isMacPlat,
|
|
135
185
|
platformExeName,
|
|
136
186
|
nativePlatform,
|
|
187
|
+
normalizePlatform,
|
|
137
188
|
localShellPath,
|
|
138
189
|
tutorialFile,
|
|
139
190
|
loadConfig,
|
package/lib/verify.js
CHANGED
|
@@ -226,13 +226,14 @@ function collectTree(dir, depth, out, maxDepth) {
|
|
|
226
226
|
return out;
|
|
227
227
|
}
|
|
228
228
|
for (const e of entries) {
|
|
229
|
+
const abs = path.join(dir, e.name);
|
|
230
|
+
const stat = safeStat(abs);
|
|
229
231
|
if (e.name === '.integrity') {
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
+
// 清单内容是不对外展开的校验值,但大小必须照实报:写死 0 会让产物自检打印
|
|
233
|
+
// 「.integrity (0 B)」,在高模式安检语境下读起来像清单是空的,反而制造误判。
|
|
234
|
+
out.push({ rel: e.name, size: stat ? stat.size : 0, isDir: false, depth, hidden: true });
|
|
232
235
|
continue;
|
|
233
236
|
}
|
|
234
|
-
const abs = path.join(dir, e.name);
|
|
235
|
-
const stat = safeStat(abs);
|
|
236
237
|
if (e.isDirectory()) {
|
|
237
238
|
out.push({ rel: e.name, size: 0, isDir: true, depth });
|
|
238
239
|
if (depth < maxDepth) collectTree(abs, depth + 1, out, maxDepth);
|
package/lib/webkit.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Linux 构建的 WebKitGTK 依赖名选择。
|
|
4
|
+
//
|
|
5
|
+
// 背景:webview_go 的 cgo 依赖名在本地补丁副本(third_party/webview_go)里拆成了
|
|
6
|
+
// webkit2_40.go / webkit2_41.go 两个互斥标签文件。Ubuntu 24.04 / Debian 13 已移除
|
|
7
|
+
// webkit2gtk-4.0,只保留同一套 C API 的 webkit2gtk-4.1 —— 沿用 4.0 会让 `go build`
|
|
8
|
+
// 在新发行版上直接失败。
|
|
9
|
+
//
|
|
10
|
+
// 这里做的事:探测 pkg-config 实际装了哪个,缺 4.0 且有 4.1 时给 go 命令加
|
|
11
|
+
// `-tags webkit2_41`;否则返回原始环境(老发行版与 CI 产物基线零变化)。
|
|
12
|
+
// webkitgtk-6.0 是另一套 API,不能这样桥接(需上游适配)。
|
|
13
|
+
|
|
14
|
+
const { spawnSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
function pkgConfigExists(mod, exec = spawnSync) {
|
|
17
|
+
try {
|
|
18
|
+
return exec('pkg-config', ['--exists', mod], { stdio: 'ignore' }).status === 0;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 需要 4.1 标签吗:仅 Linux、仅当 4.0 不可用而 4.1 可用。
|
|
25
|
+
function needsWebkit241(env = process.env, exec = spawnSync) {
|
|
26
|
+
if (env.FREEDOM_WEBKIT_FORCE_41 === '1') return true;
|
|
27
|
+
const platform = env.FREEDOM_HOST_PLATFORM || process.platform;
|
|
28
|
+
if (platform !== 'linux') return false;
|
|
29
|
+
if (pkgConfigExists('webkit2gtk-4.0', exec)) return false;
|
|
30
|
+
return pkgConfigExists('webkit2gtk-4.1', exec);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 把标签并入 GOFLAGS(go 命令原生识别 GOFLAGS,故子进程与人工命令行都吃到)。
|
|
34
|
+
// 已有 GOFLAGS 时追加,不覆盖。
|
|
35
|
+
function applyWebkitTags(env = process.env, exec = spawnSync) {
|
|
36
|
+
const out = Object.assign({}, env);
|
|
37
|
+
if (!needsWebkit241(env, exec)) return { env: out, tags: [] };
|
|
38
|
+
const tags = ['webkit2_41'];
|
|
39
|
+
const existing = (env.GOFLAGS || '').trim();
|
|
40
|
+
const merged = existing ? `${existing} -tags=${tags.join(',')}` : `-tags=${tags.join(',')}`;
|
|
41
|
+
out.GOFLAGS = merged;
|
|
42
|
+
return { env: out, tags };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { needsWebkit241, applyWebkitTags, pkgConfigExists };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yufengtadian/freedom-cli",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.3",
|
|
4
4
|
"description": "Freedom WebView desktop shell packaging tool - no Go toolchain required, one command packs three-platform desktop apps",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"frontend",
|
|
16
16
|
"cross-platform"
|
|
17
17
|
],
|
|
18
|
-
"license": "
|
|
18
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
19
19
|
"publishConfig": {
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
Binary file
|
|
Binary file
|
package/skill/freedom/SKILL.md
CHANGED
|
@@ -121,9 +121,9 @@ freedom titlebar <native|frameless> | icon <path> | security <mode>
|
|
|
121
121
|
freedom shell list|download <plat>|build <plat>
|
|
122
122
|
freedom keygen # 生成自更新 ed25519 密钥对(私钥只在发布方)
|
|
123
123
|
freedom manifest --artifact <产物> --url <下载地址> [--version x] [--notes y]
|
|
124
|
-
freedom agents # Agent
|
|
125
|
-
freedom skill install --agent all [--dry-run]
|
|
126
|
-
freedom mcp install --agent <key> [--config <path> --format json|toml|yaml]
|
|
124
|
+
freedom agents # Agent 集成支持矩阵(本机足迹判定「是否安装」,未安装不写入)
|
|
125
|
+
freedom skill install --agent all [--dry-run] [--force]
|
|
126
|
+
freedom mcp install --agent <key> [--config <path> --format json|toml|toml-aot|yaml] [--force]
|
|
127
127
|
freedom mcp serve # stdio MCP 服务(一般由 agent 拉起,不手敲)
|
|
128
128
|
freedom update | version | tutorial | help
|
|
129
129
|
```
|
|
@@ -21,12 +21,18 @@ function write(msg) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// ---- CLI 定位:优先 cli-entry.json(freedom desktop 打包时写入),再退到环境变量 ----
|
|
24
|
+
// cliMeta.root 一并带出:high 模式下本文件被解到系统临时目录,
|
|
25
|
+
// 不能再靠 `__dirname/../package.json` 反推 CLI 包版本,只能读容器里的 root。
|
|
26
|
+
const cliMeta = { entry: null, root: null };
|
|
24
27
|
function cliEntry() {
|
|
25
28
|
const candidates = [];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
if (!cliMeta.entry) {
|
|
30
|
+
try {
|
|
31
|
+
const meta = JSON.parse(fs.readFileSync(path.join(__dirname, 'cli-entry.json'), 'utf8'));
|
|
32
|
+
if (meta && meta.entry) { cliMeta.entry = meta.entry; cliMeta.root = meta.root || null; }
|
|
33
|
+
} catch { /* 缺文件走后续回退 */ }
|
|
34
|
+
}
|
|
35
|
+
if (cliMeta.entry && fs.existsSync(cliMeta.entry)) return cliMeta.entry;
|
|
30
36
|
if (process.env.FREEDOM_CLI_ENTRY) candidates.push(process.env.FREEDOM_CLI_ENTRY);
|
|
31
37
|
for (const c of candidates) {
|
|
32
38
|
if (c && fs.existsSync(c)) return c;
|
|
@@ -108,10 +114,14 @@ function stopDev() {
|
|
|
108
114
|
const api = {
|
|
109
115
|
async 'app.info'() {
|
|
110
116
|
const entry = cliEntry();
|
|
111
|
-
const
|
|
117
|
+
const readVersion = (base) => {
|
|
118
|
+
try { return JSON.parse(fs.readFileSync(path.join(base, 'package.json'), 'utf8')).version || null; } catch { return null; }
|
|
119
|
+
};
|
|
120
|
+
const cliVersion = (entry ? readVersion(path.join(path.dirname(entry), '..')) : null)
|
|
121
|
+
|| (cliMeta.root ? readVersion(cliMeta.root) : null);
|
|
112
122
|
return {
|
|
113
123
|
cliEntry: entry,
|
|
114
|
-
cliVersion
|
|
124
|
+
cliVersion,
|
|
115
125
|
platform: `${process.platform}-${process.arch}`,
|
|
116
126
|
nodeVersion: process.version,
|
|
117
127
|
home: os.homedir(),
|
|
@@ -15,7 +15,10 @@ export default {
|
|
|
15
15
|
staticHtml: 'app.html',
|
|
16
16
|
|
|
17
17
|
titlebar: 'native',
|
|
18
|
-
|
|
18
|
+
// 闭源自举:Desktop 自身也要经得起逆向——前端 app.html、config.json 与后端
|
|
19
|
+
// backend/*(含 cli-entry.json)整体进 FRDM2 容器,磁盘只留 app.bin + .integrity,
|
|
20
|
+
// 运行期由壳解密到私有临时目录(退出即删,崩溃残留由下次启动回收)。
|
|
21
|
+
security: 'high',
|
|
19
22
|
outDir: 'dist',
|
|
20
23
|
|
|
21
24
|
// 应用图标:构建时注入 exe(Windows 用 rcedit 写 PE 资源),运行时壳层经
|
package/templates/go/go.mod
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//go:build linux
|
|
2
|
+
|
|
3
|
+
package freedom
|
|
4
|
+
|
|
5
|
+
// 反调试(Linux 实现):high 安全模式下由 Run() 在资源解密前后各调用一次,
|
|
6
|
+
// 与 Windows 侧同一语义——**探测失败一律记未命中**,只在确证被附加调试器时退出,
|
|
7
|
+
// 误杀正常用户的代价高于多一路信号的收益。
|
|
8
|
+
//
|
|
9
|
+
// 两道信号:
|
|
10
|
+
// 1. ptrace(PTRACE_TRACEME):本进程已被别的进程 attach 时返回 EPERM —— 这是
|
|
11
|
+
// "GDB 已挂上来"的确证。TRACEME 是一次性的(成功后无法再 DETACH 复位),
|
|
12
|
+
// 故整个进程生命周期只探测一次并缓存结论;Run 里的"解密前 / 解密后"两次调用
|
|
13
|
+
// 因此共享同一结论——本就只能挡住「启动时已挂调试器」,运行中途 attach
|
|
14
|
+
// (gdb -p)要等下次启动才被抓到,这是诚实的能力边界。
|
|
15
|
+
// **已知边界**:本进程此前处于停止/跟踪-停止状态(被 SIGSTOP 过)时 TRACEME
|
|
16
|
+
// 也会 EPERM,理论上存在误判窗口;桌面壳正常生命周期内不走这条路,且
|
|
17
|
+
// 非 high 模式完全不检测,可用 FREEDOM_DISABLE_ANTIDEBUG=1 关闭。
|
|
18
|
+
// 2. PR_SET_DUMPABLE=0:不是检测而是加固——内核不再产出 core dump,且非属主、
|
|
19
|
+
// 非 ptrace 授权的进程读不到 /proc/<pid>/mem 与 /proc/<pid>/maps。
|
|
20
|
+
// 副作用是本进程写的文件属主不再是 dumpable 语义下的自己(/tmp 下按目录名
|
|
21
|
+
// 隔离的私有临时目录不受影响)。
|
|
22
|
+
//
|
|
23
|
+
// 未纳入:/proc/<pid>/status 的 TracerPid 字段——读它本身没问题,但攻击者 attach
|
|
24
|
+
// 前后改一行 status 即可绕过(同一函数里 ptrace 系统调用才是硬证据),徒增误报源。
|
|
25
|
+
|
|
26
|
+
import (
|
|
27
|
+
"os"
|
|
28
|
+
"sync"
|
|
29
|
+
"syscall"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// antiDebugCheck 命中即退出(退出码 77,与 Windows 侧一致,不显示窗口、不回显原因)。
|
|
33
|
+
func antiDebugCheck() {
|
|
34
|
+
if debuggerPresent() {
|
|
35
|
+
os.Exit(77)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// debuggerPresent 汇总 Linux 侧信号。
|
|
40
|
+
func debuggerPresent() bool {
|
|
41
|
+
if ptracemeRejectedOnce() {
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
makeNonDumpable() // 未命中也要落地下一步加固(core dump / /proc 读内存收紧)
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var (
|
|
49
|
+
tracemeOnce = new(sync.Once) // 指针:单测需要可复位的实例
|
|
50
|
+
tracemeDetected bool
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// ptracemeRejectedOnce 缓存一次性 TRACEME 结论(见文件头:TRACEME 不可复位)。
|
|
54
|
+
func ptracemeRejectedOnce() bool {
|
|
55
|
+
tracemeOnce.Do(func() { tracemeDetected = ptracemeRejected() })
|
|
56
|
+
return tracemeDetected
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ptracemeRejected 报告 PTRACE_TRACEME 是否被拒(被拒 = 已有调试器附加);
|
|
60
|
+
// 变量形式便于单测替换,生产路径只会被调用一次。
|
|
61
|
+
var ptracemeRejected = func() bool {
|
|
62
|
+
r, _, errno := syscall.RawSyscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0)
|
|
63
|
+
return r != 0 || errno != 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// makeNonDumpable 关闭 core dump 并收紧 /proc/<pid>/* 的读取授权;失败按未命中处理。
|
|
67
|
+
var makeNonDumpable = func() {
|
|
68
|
+
syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0)
|
|
69
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
//go:build !windows
|
|
1
|
+
//go:build !windows && !linux
|
|
2
2
|
|
|
3
3
|
package freedom
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
// 反调试(macOS 占位实现)。Linux 侧已实装在 anti_debug_linux.go
|
|
6
|
+
// (PTRACE_TRACEME 一次性探测 + PR_SET_DUMPABLE=0 加固);macOS 需要
|
|
7
|
+
// ptrace(PT_DENY_ATTACH) / task_for_pid 等平台 API,本机无 Apple 设备无法验证,
|
|
8
|
+
// 故留空不装——诚实的"未实现"优于未验证的代码。
|
|
7
9
|
func antiDebugCheck() {}
|