dsh-code-server-app 0.3.46 → 0.3.49

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.
@@ -19,7 +19,7 @@
19
19
  // 不给 --from 时自动准备源树(按 vendor/VENDOR.json 的版本 npm install --ignore-scripts,再在
20
20
  // lib/vscode 里解包 + rebuild;耗时且需要工具链,维护者换版本时用)。--pack 生成 repack/tgz/*.tgz。
21
21
  import { existsSync, readFileSync, readdirSync, mkdirSync, rmSync, writeFileSync, cpSync } from 'node:fs';
22
- import { dirname, join, relative, resolve } from 'node:path';
22
+ import { basename, dirname, join, relative, resolve } from 'node:path';
23
23
  import { fileURLToPath } from 'node:url';
24
24
  import { spawnSync } from 'node:child_process';
25
25
 
@@ -47,6 +47,17 @@ const DO_PACK = process.argv.includes('--pack');
47
47
  // VS Code 树包、平台聚合包与插件 package.json。
48
48
  // 适用于「原生包没变、只调整打包结构」的场景(本机已无可分析的完整源树时也用它)。
49
49
  const REUSE = process.argv.includes('--reuse');
50
+ // --skip-independent:不重建「平台无关」的重打包包。平台无关的包由**单独一条腿**产出并发布
51
+ // (四个目标内容一致,不能四边都发同一个版本号),平台专属腿重建它们纯属浪费,而且会把一份"在别的宿主上
52
+ // 打出来的同名包"塞进 artifact —— 谁要是从 artifact 手工发布,发出去的就是错的内容。
53
+ const SKIP_INDEPENDENT = process.argv.includes('--skip-independent');
54
+ // --only-independent:只产「平台无关」的重打包包 + VS Code 树包,**完全不碰平台专属包**。
55
+ // 独立那条腿用的就是它:平台无关层是所有平台共用的同一份产物,先于平台专属腿执行、只发一次;
56
+ // 平台专属腿各自带 --skip-independent。两者互为反面,同时给直接报错。
57
+ const ONLY_INDEPENDENT = process.argv.includes('--only-independent');
58
+ if (SKIP_INDEPENDENT && ONLY_INDEPENDENT) {
59
+ throw new Error('--skip-independent 与 --only-independent 不能同时给(一个是不产平台无关包,一个是只产它)');
60
+ }
50
61
  const TARGETS = argValues('--target');
51
62
  // Copilot 整树排除:620MB、依赖链里还有带脚本的包,且 code-server 里用不到。
52
63
  const EXCLUDE = [/^@github\//, /^@vscode\/copilot-api$/];
@@ -283,8 +294,14 @@ function prepareSourceTree() {
283
294
  rmSync(tmp, { recursive: true, force: true });
284
295
  mkdirSync(tmp, { recursive: true });
285
296
  console.log(`[repack] 准备完整源树 code-server@${vendorVersion} → ${tmp}`);
297
+ // **必须显式 --prefix**:不给它时 npm 会往上找"最近的 package.json"当项目根 —— 而 tmp 就在
298
+ // 仓库目录里面(<pkgRoot>/.vendor-tmp/repack-src-<pid>),于是包被装进 <pkgRoot>/node_modules,
299
+ // 而下面按 <tmp>/node_modules/code-server 去找 ⇒ readJson() 拿到 null ⇒ analyze() 读
300
+ // `.dependencies` 时直接 TypeError。2026-09-16 在 ubuntu-latest 上实测到:日志里连
301
+ // 「解包内部依赖」都没有(innerDirs 是空的),因为树根本不在那儿。vendor-vscode-server.mjs
302
+ // 的同一步带了 --prefix,所以那条路一直是对的 —— 这里补齐。
286
303
  npm(['install', `code-server@${vendorVersion}`, '--ignore-scripts', '--omit=dev',
287
- '--no-audit', '--no-fund', '--no-save'], tmp);
304
+ '--no-audit', '--no-fund', '--no-save', '--prefix', tmp], tmp);
288
305
  const tree = join(tmp, 'node_modules', 'code-server');
289
306
  const innerDirs = [join('lib', 'vscode'), join('lib', 'vscode', 'extensions')]
290
307
  .map((rel) => join(tree, rel))
@@ -300,10 +317,35 @@ function prepareSourceTree() {
300
317
  for (const dir of innerDirs) {
301
318
  if (!existsSync(join(dir, 'node_modules'))) continue;
302
319
  console.log(`[repack] 编译内部依赖原生包: ${relative(pkgRoot, dir)}`);
320
+ // ① 整树 rebuild 一次:覆盖声明里没登记的新包。失败不算错 —— 因为 npm 会在**第一个失败**处中断,
321
+ // 一个包编不出来(例如 Windows 上缺 v145 工具集,或 Linux 上缺某个头文件)就会让它后面的包
322
+ // 再也没轮到自己,于是"一个包失败"级联成"一批包没编出来"。
303
323
  try {
304
324
  npm(['rebuild'], dir);
305
325
  } catch (e) {
306
- console.warn(`[repack] ${relative(pkgRoot, dir)} rebuild 失败(${e.message});后续按缺失处理`);
326
+ console.warn(`[repack] 整树 rebuild 未全部成功(${e.message})⇒ 改为逐个包重试`);
327
+ }
328
+ // ② 逐个「声明为平台专属、且本平台需要」的模块 rebuild:幂等,失败只影响它自己,并逐条报出来。
329
+ // 这样后面 analyze() 的「有没有 .node」判断才是可靠的,而不是随整树中断位置漂移。
330
+ const policy = readRepackPlatforms();
331
+ const hostKey = `${process.platform}-${process.arch}`;
332
+ const failedRebuild = [];
333
+ for (const [alias, decl] of policy.perModule) {
334
+ if (decl.platform !== true) continue; // 平台无关的包靠 prebuilds,不需要现编
335
+ const wanted = decl.targets ?? policy.allTargets ?? null;
336
+ if (Array.isArray(wanted) && !wanted.includes(hostKey)) continue; // 本平台不需要它
337
+ const pkgDir = join(dir, 'node_modules', ...alias.split('/'));
338
+ if (!existsSync(join(pkgDir, 'package.json'))) continue;
339
+ try {
340
+ npm(['rebuild', alias], dir);
341
+ } catch (e) {
342
+ failedRebuild.push(alias);
343
+ console.warn(` ⚠ ${hostKey}: ${alias} rebuild 失败(${e.message})`);
344
+ }
345
+ }
346
+ if (failedRebuild.length > 0) {
347
+ console.warn(` ⚠ ${relative(pkgRoot, dir)}:${failedRebuild.length} 个原生包 rebuild 失败`
348
+ + `(${failedRebuild.join(', ')})—— 若它们在白名单里却没有 .node,最后的硬闸门会直接判失败`);
307
349
  }
308
350
  }
309
351
  return tree;
@@ -320,11 +362,26 @@ function peMachine(file) {
320
362
  } catch { return null; }
321
363
  }
322
364
 
323
- /** ① VS Code 树(平台无关):vendor/vscode → repack/build/vscode/vscode/。
365
+ /** ELF e_machine(0x3e=x86-64 / 0xb7=AArch64),用于 Linux 目标的静态校验。 */
366
+ function elfMachine(file) {
367
+ try {
368
+ const b = readFileSync(file);
369
+ if (b[0] !== 0x7f || b[1] !== 0x45 || b[2] !== 0x4c || b[3] !== 0x46) return null;
370
+ return `0x${b.readUInt16LE(0x12).toString(16)}`;
371
+ } catch { return null; }
372
+ }
373
+
374
+ /** ① VS Code 树(**必须平台无关**):vendor/vscode → repack/build/vscode/vscode/。
324
375
  * 排除 VS Code 的「内部依赖目录」(lib/vscode/node_modules、lib/vscode/extensions/node_modules)
325
- * —— 它们由包管理器安装 + 平台聚合包按架构提供,树里只留 lib/vscode 本体与静态资源。
326
- * 不含 code-server 的 out/node 与服务层依赖(已由 lib/launcher.mjs 取代)。 */
327
- function buildVscodeServerPackage() {
376
+ * —— 核心原生模块(node-pty / @vscode/sqlite3 / spdlog / kerberos / deviceid / native-watchdog /
377
+ * windows-* / @parcel/watcher …)全在那两个目录里,一律由 `@jinsiyu/dshcs-*-<目标>` 按平台供给;
378
+ * 树里只留 lib/vscode 本体与静态资源(browser 资源、语法/主题、字体、.wasm ⇒ 都跨平台)。
379
+ * 为什么树包不按 OS 区分(与上游 VS Code 相反):上游每个平台发一份 server 包,是因为它把
380
+ * **node 运行时 + 原生模块**塞在里面;本产品把这两件事分别交给宿主 DSH 的 node 与平台专属子包
381
+ * (0.1.37 的 `dshcs-code-server-<平台>-<架构>` 全量树就是这么被取代的,见 docs/desktop-first-install-root-cause.md)。
382
+ * 体积与下载账:一份 52MB / 解包 197MB 供所有平台复用,而不是 4 × 52MB。
383
+ * @param moduleNames - 本次重打包模块的原始名(用于校验核心原生模块没有泄漏进树里) */
384
+ function buildVscodeServerPackage(moduleNames = []) {
328
385
  const vendorTree = join(pkgRoot, 'vendor', 'vscode');
329
386
  if (!existsSync(join(vendorTree, 'lib', 'vscode', 'out', 'server-main.js'))) {
330
387
  throw new Error('缺少 vendor/vscode;先运行 `node scripts/vendor-vscode-server.mjs`');
@@ -350,6 +407,38 @@ function buildVscodeServerPackage() {
350
407
  },
351
408
  });
352
409
  const name = `${SCOPE}/dshcs-vscode-server`;
410
+ // 不变式校验:核心原生模块**不许**出现在树里 —— 一旦泄漏,树就变成平台相关(在 win32 腿上打出来的
411
+ // tgz 会被 Linux 用户装上),而它只发布一次、不按 OS 区分 ⇒ 这种错必须在打包期就炸,不能悄悄发出去。
412
+ // 顺带把树里**其它**平台二进制列出来留痕:那些是可选扩展自带的(js-debug 的 win32 预编译、
413
+ // microsoft-authentication 的 msal 运行时、终端 PSReadLine 的 dll),不参与核心运行。
414
+ const leaked = [];
415
+ const others = [];
416
+ const aliasHints = moduleNames.map((n) => String(n).split('/').pop()).filter(Boolean);
417
+ const scan = (cur) => {
418
+ let entries; try { entries = readdirSync(cur, { withFileTypes: true }); } catch { return; }
419
+ for (const e of entries) {
420
+ const full = join(cur, e.name);
421
+ if (e.isDirectory()) {
422
+ if (e.name === 'node_modules') continue; // 已被排除,不该出现在这里
423
+ scan(full);
424
+ continue;
425
+ }
426
+ if (!/\.(node|so|dll|dylib|exe)$/i.test(e.name)) continue;
427
+ const rel = relative(dir, full).replaceAll('\\', '/');
428
+ if (aliasHints.some((h) => e.name.includes(h))) leaked.push(rel);
429
+ else others.push(rel);
430
+ }
431
+ };
432
+ scan(join(dir, 'vscode'));
433
+ if (leaked.length > 0) {
434
+ throw new Error(`${name}: 树里出现了核心原生模块的二进制,树包就不再平台无关了:${leaked.join(', ')}`
435
+ + ' —— 它们必须留在「内部依赖目录」(lib/vscode/node_modules 或 extensions/node_modules)里,'
436
+ + '由平台专属子包按目标供给;请检查 vendor/vscode 的结构变化与 buildVscodeServerPackage 的排除规则');
437
+ }
438
+ if (others.length > 0) {
439
+ console.log(`[repack] 树里另有 ${others.length} 个可选扩展自带的平台二进制(不参与核心运行,不影响"一份树包供所有平台"):`);
440
+ for (const o of others) console.log(` · ${o}`);
441
+ }
353
442
  writeFileSync(join(dir, 'package.json'), JSON.stringify({
354
443
  name,
355
444
  version,
@@ -493,6 +582,113 @@ function readModules(targets) {
493
582
  return modules;
494
583
  }
495
584
 
585
+ /** 重打包模块的平台政策(scripts/repack-platforms.json,**人工评审**;本脚本只读不写)。
586
+ * @returns {{allTargets:string[]|null, publishedTargets:string[]|null,
587
+ * perModule:Map<string,{platform:boolean|null, targets:string[]|null}>}} */
588
+ function readRepackPlatforms() {
589
+ const doc = readJson(join(here, 'repack-platforms.json'));
590
+ const list = (v) => (Array.isArray(v) ? v.filter((t) => typeof t === 'string' && t !== '') : null);
591
+ const allTargets = list(doc?.targets);
592
+ if (allTargets === null || allTargets.length === 0) {
593
+ console.warn(' ⚠ 读不到 scripts/repack-platforms.json 的 targets ⇒ 平台政策退化为「构建目标即全部目标」,'
594
+ + '每模块白名单与跨平台保留都会失效');
595
+ }
596
+ const perModule = new Map();
597
+ for (const [alias, spec] of Object.entries(doc?.modules ?? {})) {
598
+ if (spec === null || typeof spec !== 'object') continue;
599
+ perModule.set(alias, {
600
+ platform: typeof spec.platform === 'boolean' ? spec.platform : null,
601
+ targets: list(spec.targets),
602
+ // 可选:我们自己定义的版本(重打包内容变了、而上游版本没变时用它,例如 2.1.1-dshcs.2)。
603
+ version: typeof spec.version === 'string' && spec.version !== '' ? spec.version : null,
604
+ });
605
+ }
606
+ return { allTargets, publishedTargets: list(doc?.publishedTargets), perModule };
607
+ }
608
+
609
+ /** 平台政策落到分析结果上:
610
+ * ① 分类(是不是平台专属)以声明为准 —— analyze() 的结论随宿主漂移(Linux 上 windows-registry
611
+ * 没有 .node 会被判成平台无关,一旦写进 dependencies,Windows 运行时反而找不到 -win32-* 子包);
612
+ * ② 平台专属模块补上目标白名单(缺席 = 全部目标);
613
+ * ③ **保留本次分析看不到的条目**:用 --target 只构建本机目标时,表里其它平台的模块必须原样留下;
614
+ * ④ 未在声明里登记的模块给出提醒(新模块).
615
+ * @param modules - 就地修改的重打包条目(来自 analyze 或 --reuse) */
616
+ function applyPlatformPolicy(modules, platforms, allTargets) {
617
+ const declared = (alias) => platforms.perModule.get(alias);
618
+ const existing = readJson(join(pkgRoot, 'lib', 'vendored.json'));
619
+ const previous = new Map((Array.isArray(existing?.modules) ? existing.modules : [])
620
+ .filter((e) => e !== null && typeof e === 'object' && typeof e.alias === 'string')
621
+ .map((e) => [e.alias, e]));
622
+
623
+ for (const m of modules) {
624
+ const decl = declared(m.alias);
625
+ if (decl === undefined) {
626
+ console.warn(` ⚠ ${m.alias} 不在 scripts/repack-platforms.json 的模块表里 ⇒ 沿用分析结论`
627
+ + `(platform=${m.platform});建议登记它,否则换宿主平台时分类会漂移`);
628
+ } else if (decl.platform !== null && decl.platform !== m.platform) {
629
+ console.warn(` ⚠ ${m.alias}:分析结论 platform=${m.platform},声明写的是 platform=${decl.platform} ⇒ 以声明为准`);
630
+ m.platform = decl.platform;
631
+ }
632
+ // 版本政策(**必须宿主与时点都无关**):① 声明里钉的 `version` 优先;② 否则沿用表里**已发布**的
633
+ // 版本;③ 只有表里没有这个模块(新模块)时才采用源树版本。
634
+ // 为什么不能照源树写:表里的版本就是 registry 上我们发布的那个号,而源树的版本会漂 ——
635
+ // · kerberos:源树上游 2.1.1,我们发布的是 2.1.1-dshcs.1(聚合包时代的后缀);
636
+ // · @vscode/proxy-agent:同一个 code-server 版本,维护者当时装到 0.44.0,今天全新装到 0.45.0
637
+ // —— 依赖范围允许漂移,而 0.45.0 这个子包我们根本没发过。
638
+ // 照源树写 ⇒ 换个宿主平台或换一天重跑,插件依赖就会指向不存在的版本,装上直接解析失败。
639
+ // 要让插件升到新上游版本:在 scripts/repack-platforms.json 里给该模块钉 `version`,重跑本脚本
640
+ // 并把新子包发出去(下面这行会说明清楚)。
641
+ const prevVersion = typeof previous.get(m.alias)?.version === 'string' ? previous.get(m.alias).version : null;
642
+ if (decl?.version !== null && decl?.version !== undefined) {
643
+ if (decl.version !== m.version) console.log(` · ${m.alias}:按声明钉版本 ${decl.version}(源树里是 ${m.version})`);
644
+ m.version = decl.version;
645
+ } else if (prevVersion !== null) {
646
+ if (prevVersion !== m.version) {
647
+ console.log(` · ${m.alias}:沿用表里已发布的版本 ${prevVersion}(源树里是 ${m.version});`
648
+ + `要让插件改用它,请在 scripts/repack-platforms.json 里钉 version=${m.version} 并先发布对应的子包`);
649
+ }
650
+ m.version = prevVersion;
651
+ } else {
652
+ console.log(` · ${m.alias}:新模块 ⇒ 采用源树版本 ${m.version}(记得先发布子包再把它写进依赖表)`);
653
+ }
654
+ }
655
+
656
+ const seen = new Set(modules.map((m) => m.alias));
657
+ const kept = [];
658
+ for (const e of [...previous.values()]) {
659
+ if (e === null || typeof e !== 'object' || typeof e.alias !== 'string' || seen.has(e.alias)) continue;
660
+ const decl = declared(e.alias);
661
+ kept.push({
662
+ alias: e.alias,
663
+ package: typeof e.package === 'string' && e.package !== '' ? e.package : `${SCOPE}/dshcs-${flat(e.alias)}`,
664
+ version: typeof e.version === 'string' ? e.version : null,
665
+ platform: decl?.platform ?? e.platform === true,
666
+ targets: decl?.targets ?? (Array.isArray(e.targets) ? e.targets : null),
667
+ carried: true,
668
+ });
669
+ seen.add(e.alias);
670
+ }
671
+ if (kept.length > 0) {
672
+ console.log(`[repack] 保留 ${kept.length} 个本次分析看不到的条目(其它平台的模块):${kept.map((k) => k.alias).join(', ')}`);
673
+ modules.push(...kept);
674
+ modules.sort((a, b) => a.alias.localeCompare(b.alias));
675
+ }
676
+
677
+ for (const m of modules) {
678
+ if (!m.platform) continue;
679
+ const wanted = m.targets ?? declared(m.alias)?.targets ?? null;
680
+ m.targets = wanted === null ? [...allTargets] : allTargets.filter((t) => wanted.includes(t));
681
+ if (m.targets.length === 0) {
682
+ console.warn(` ⚠ ${m.alias}(平台专属)在白名单里没有任何目标 ⇒ 不会产出任何子包`);
683
+ }
684
+ }
685
+ for (const [alias, decl] of platforms.perModule) {
686
+ if (seen.has(alias) || decl.platform !== true) continue;
687
+ console.warn(` ⚠ 声明里的 ${alias} 既不在本次分析结果、也不在 lib/vendored.json 里 ⇒ 无法为它产出子包`);
688
+ }
689
+ return modules;
690
+ }
691
+
496
692
  /** 平台专属重打包包(写在插件 `optionalDependencies`)自带的**注册表依赖** —— 必须同时提成插件
497
693
  * 自己的直接依赖。原因(0.3.46 实测,复现与证据见 docs/desktop-first-install-root-cause.md 第 5 节):
498
694
  * pnpm 的增量 hoisted 安装会把「optional 子树里 depth≥2」的依赖整支丢进 `node_modules/.modules.yaml`
@@ -510,9 +706,14 @@ const OPTIONAL_SUBTREE_DEPS = {
510
706
  uuid: '14.0.2',
511
707
  };
512
708
 
513
- /** 构建期闸门:平台专属重打包清单里的每个非别名依赖都必须在 OPTIONAL_SUBTREE_DEPS 里。
514
- * @param dirs - 已生成的平台专属重打包目录。 */
515
- function verifyOptionalSubtreeDeps(dirs) {
709
+ /** 构建期闸门:平台专属重打包清单里的每个非别名依赖都必须落在**插件根依赖**里,
710
+ * 否则 pnpm 会把它丢进 skipped。两条来源都算数:
711
+ * · OPTIONAL_SUBTREE_DEPS(仅这些包才需要、由本表钉版本的,例如 bindings);
712
+ * · 树自己的直装集 `declare`(例如 node-addon-api —— VS Code 树本来就把它列为直接依赖,
713
+ * 生成器会按树里的版本写进插件 dependencies,同样落在 profile 根 node_modules)。
714
+ * @param dirs - 已生成的平台专属重打包目录。
715
+ * @param directDeps - 本次将写进插件 dependencies 的名字集合。 */
716
+ function verifyOptionalSubtreeDeps(dirs, directDeps) {
516
717
  const seen = new Set();
517
718
  for (const dir of dirs) {
518
719
  const m = readJson(join(dir, 'package.json'));
@@ -521,9 +722,9 @@ function verifyOptionalSubtreeDeps(dirs) {
521
722
  for (const [name, spec] of Object.entries(m[fld] ?? {})) {
522
723
  if (String(spec).startsWith('npm:')) continue; // 已重打包(真名子包),不走注册表
523
724
  seen.add(name);
524
- if (!Object.hasOwn(OPTIONAL_SUBTREE_DEPS, name)) {
525
- throw new Error(`${m.name}: 注册表依赖 ${name}@${spec} 不在 OPTIONAL_SUBTREE_DEPS 里 ——`
526
- + ' 它位于 optional 子树(depth≥2),pnpm 的增量 hoisted 安装会丢包,'
725
+ if (!Object.hasOwn(OPTIONAL_SUBTREE_DEPS, name) && !directDeps.has(name)) {
726
+ throw new Error(`${m.name}: 注册表依赖 ${name}@${spec} 既不在 OPTIONAL_SUBTREE_DEPS、也不在插件的`
727
+ + ' 直装依赖里 —— 它位于 optional 子树(depth≥2),pnpm 的增量 hoisted 安装会丢包,'
527
728
  + ' dsh-desktop 安装后立刻校验必报 requires missing;请把它提为插件直接依赖'
528
729
  + '(见 docs/desktop-first-install-root-cause.md 第 5 节)');
529
730
  }
@@ -538,7 +739,20 @@ function verifyOptionalSubtreeDeps(dirs) {
538
739
 
539
740
  function main() {
540
741
  const hostKey = `${process.platform}-${process.arch}`;
541
- const targets = TARGETS.length > 0 ? TARGETS : [hostKey];
742
+ // 「构建哪些目标」(--target,缺省本机)与「产品覆盖哪些目标」是两件事:前者只影响本次构建,
743
+ // 后者来自 scripts/repack-platforms.json,决定 lib/vendored.json 的 targets、每模块白名单与插件
744
+ // optionalDependencies —— 否则在 Windows 上构建会把 Linux 的条目从依赖表里挤掉(反之亦然)。
745
+ const platforms = readRepackPlatforms();
746
+ const buildTargets = TARGETS.length > 0 ? TARGETS : [hostKey];
747
+ const allTargets = platforms.allTargets ?? buildTargets;
748
+ const publishedTargets = (platforms.publishedTargets ?? buildTargets).filter((t) => allTargets.includes(t));
749
+ for (const t of buildTargets) {
750
+ if (!allTargets.includes(t)) console.warn(` ⚠ --target ${t} 不在 repack-platforms.json 的 targets 里(仍按请求构建)`);
751
+ }
752
+ if (publishedTargets.length === 0) {
753
+ throw new Error('repack-platforms.json 的 publishedTargets 与 targets 没有交集 ⇒ 生成不出插件依赖表');
754
+ }
755
+ console.log(`[repack] 平台政策:产品目标 ${allTargets.join(', ')};已发布(可写进插件依赖)${publishedTargets.join(', ')}`);
542
756
 
543
757
  let repack = null;
544
758
  let modules;
@@ -546,7 +760,7 @@ function main() {
546
760
  let reusedItems = [];
547
761
  let autoSourceTree = null;
548
762
  if (REUSE) {
549
- ({ modules, declare, reusedItems } = reuseExisting(targets));
763
+ ({ modules, declare, reusedItems } = reuseExisting(buildTargets));
550
764
  } else {
551
765
  const autoSource = FROM === null;
552
766
  const sourceTree = autoSource ? prepareSourceTree() : FROM;
@@ -562,9 +776,20 @@ function main() {
562
776
  .sort((a, b) => a.alias.localeCompare(b.alias));
563
777
  rmSync(OUT, { recursive: true, force: true });
564
778
  }
779
+ applyPlatformPolicy(modules, platforms, allTargets);
565
780
  console.log(`\n重打包集(${modules.length}):`);
566
781
  for (const m of modules) {
567
- console.log(` ${m.alias}@${m.version} → ${m.package}${m.platform ? `-<平台>` : ''} ${m.platform ? '[平台专属]' : '[全平台]'}`);
782
+ const tag = m.platform ? `[平台专属 ${m.targets.join('|')}]` : '[全平台]';
783
+ console.log(` ${m.alias}@${m.version} → ${m.package}${m.platform ? '-<平台>' : ''}${m.carried ? ' (保留)' : ''} ${tag}`);
784
+ }
785
+ const rootPkgFile = join(pkgRoot, 'package.json');
786
+ const previousDeps = readJson(rootPkgFile)?.dependencies ?? {};
787
+ for (const d of declare) {
788
+ const prev = previousDeps[d.name];
789
+ if (typeof prev === 'string' && prev !== d.version) {
790
+ console.log(` · ${d.name}:源树版本 ${d.version}(仓库依赖表里是 ${prev})—— 上游纯 JS 包,跟着树走安全`
791
+ + '(树里的版本一定来自 npm;这类差异是**时间**相关而非宿主相关)');
792
+ }
568
793
  }
569
794
  console.log(`\n直装集(${declare.length}): ${declare.map((d) => d.name).join(', ')}\n`);
570
795
 
@@ -572,7 +797,7 @@ function main() {
572
797
 
573
798
  // 每个目标的包名映射:平台专属的按目标拼后缀,平台无关的到处同名
574
799
  const byTarget = new Map();
575
- for (const target of targets) {
800
+ for (const target of buildTargets) {
576
801
  const map = new Map(); // 原包名 -> { pkgName, version, platformSpecific }
577
802
  for (const m of modules) {
578
803
  map.set(m.alias, {
@@ -625,6 +850,20 @@ function main() {
625
850
  if (wrong.length > 0) {
626
851
  console.warn(` ⚠ ${pkg.pkgName}: ${wrong.length} 个 .node 架构不符(期望 ${want})`);
627
852
  }
853
+ } else if (platform === 'linux') {
854
+ // 同上,ELF 版:e_machine 必须是目标架构(0x3e=x86-64 / 0xb7=AArch64)
855
+ const want = arch === 'x64' ? '0x3e' : '0xb7';
856
+ const files = nodeFilesOf(dir);
857
+ const wrong = files
858
+ .map((file) => [file, elfMachine(file)])
859
+ .filter(([, machine]) => machine !== null && machine !== want);
860
+ if (files.length > 0 && wrong.length === files.length) {
861
+ throw new Error(`${pkg.pkgName}: 所有 .node 架构都不是 ${want}`
862
+ + `(${wrong.map(([file, machine]) => `${relative(dir, file)}=${machine}`).join(', ')})`);
863
+ }
864
+ if (wrong.length > 0) {
865
+ console.warn(` ⚠ ${pkg.pkgName}: ${wrong.length} 个 .node 架构不符(期望 ${want})`);
866
+ }
628
867
  }
629
868
  }
630
869
  for (const fld of ['dependencies', 'optionalDependencies']) {
@@ -642,51 +881,173 @@ function main() {
642
881
  // 0) 已打包的原生包(--reuse 模式)
643
882
  for (const item of reusedItems) plan.push(item);
644
883
  // 1) VS Code 树(平台无关,1 个包)
645
- const vscodePkg = buildVscodeServerPackage();
884
+ const vscodePkg = buildVscodeServerPackage(modules.map((m) => m.alias));
646
885
  plan.push({ dir: vscodePkg.dir, file: vscodePkg.file });
647
886
  // 2) 全平台重打包包(从 host 树;--reuse 时用已打包目录)
887
+ // 「要不要按平台分包」一律以**声明**为准(applyPlatformPolicy 已把结论钉到 modules 上),
888
+ // 源树分析只提供源目录与上游版本。照分析结论走的话,本机工具链编不出 .node 时这些模块会被
889
+ // 静默当成平台无关、打成不带目标后缀的包,而依赖表里照旧写着「每目标一份」。
648
890
  if (!REUSE) {
649
- for (const [name, r] of repack) {
650
- if (r.platformSpecific) continue; // 平台专属的按目标处理
651
- const dir = writeRepack(r.dir, hostMap.get(name), flat(name));
652
- plan.push({ dir, file: tgzName(hostMap.get(name).pkgName, r.pkg.version) });
891
+ const independentModules = modules.filter((m) => !m.platform);
892
+ if (SKIP_INDEPENDENT) {
893
+ console.log(`[repack] --skip-independent:跳过 ${independentModules.length} 个平台无关包`
894
+ + '(它们由 win32-x64 腿产出并发布,本腿只产自己的平台专属包)');
895
+ } else for (const m of independentModules) {
896
+ const r = repack.get(m.alias);
897
+ if (r === undefined) continue; // 「保留」条目(本宿主看不到它的包)⇒ 没有源目录可打包
898
+ const dir = writeRepack(r.dir, hostMap.get(m.alias), flat(m.alias));
899
+ plan.push({ dir, file: tgzName(hostMap.get(m.alias).pkgName, m.version) });
653
900
  }
654
901
  }
655
- // 3) 平台专属包:host 用现成树,其它目标用交叉安装树(--reuse 时用已打包目录)
902
+ // 3) 平台专属包:host 用现成树,其它目标用交叉安装树(--reuse 时用已打包目录)。
903
+ // · 白名单不含该目标 ⇒ 不构建(Windows-only 模块不产 -linux-*,反之亦然);
904
+ // · 该目标上没编出 .node / 源树里没这个模块 ⇒ 也跳过,但**是否致命交给下面的硬闸门**。
656
905
  const platformDirs = [];
657
- if (!REUSE) for (const target of targets) {
906
+ const builtByTarget = new Map(buildTargets.map((t) => [t, []]));
907
+ const gradedSpecific = new Map(buildTargets.map((t) => [t, []]));
908
+ const skipped = { policy: [], noBinary: [], missing: [] };
909
+ if (!REUSE && !ONLY_INDEPENDENT) for (const target of buildTargets) {
658
910
  const map = byTarget.get(target);
659
- const specs = [...repack].filter(([, r]) => r.platformSpecific).map(([name, r]) => ({ name, version: r.pkg.version }));
911
+ const eligible = modules.filter((m) => m.platform === true
912
+ && (m.targets ?? allTargets).includes(target) && repack.has(m.alias));
913
+ for (const m of modules) {
914
+ if (m.platform === true && repack.has(m.alias) && !(m.targets ?? allTargets).includes(target)) {
915
+ skipped.policy.push(`${m.alias}@${target}`);
916
+ }
917
+ }
918
+ gradedSpecific.set(target, eligible.map((m) => m.alias));
919
+ const specs = eligible.map((m) => ({ name: m.alias, version: repack.get(m.alias).pkg.version }));
660
920
  let crossTree = null;
661
921
  if (target !== hostKey && specs.length > 0) crossTree = prepareCrossTree(specs, target);
662
- for (const [name, r] of repack) {
663
- if (!r.platformSpecific) continue;
664
- const info = map.get(name);
665
- const srcDir = target === hostKey ? r.dir : join(crossTree, 'node_modules', name);
922
+ for (const m of eligible) {
923
+ const r = repack.get(m.alias);
924
+ const info = map.get(m.alias);
925
+ const srcDir = target === hostKey ? r.dir : join(crossTree, 'node_modules', m.alias);
666
926
  if (!existsSync(join(srcDir, 'package.json'))) {
667
- console.warn(` ⚠ ${target}: 缺少 ${name},跳过`);
927
+ console.warn(` ⚠ ${target}: 缺少 ${m.alias},跳过(硬闸门会判定是否致命)`);
928
+ skipped.missing.push(`${m.alias}@${target}`);
929
+ continue;
930
+ }
931
+ if (nodeFilesOf(srcDir).length === 0) {
932
+ console.warn(` ⚠ ${target}: ${m.alias} 没有产出 .node 二进制 ⇒ 跳过(硬闸门会判定是否致命)`);
933
+ skipped.noBinary.push(`${m.alias}@${target}`);
668
934
  continue;
669
935
  }
670
- const dir = writeRepack(srcDir, info, `${flat(name)}-${target}`);
936
+ const dir = writeRepack(srcDir, info, `${flat(m.alias)}-${target}`);
671
937
  platformDirs.push(dir);
672
- plan.push({ dir, file: tgzName(info.pkgName, r.pkg.version) });
938
+ builtByTarget.get(target).push(m.alias);
939
+ plan.push({ dir, file: tgzName(info.pkgName, m.version) });
673
940
  }
674
941
  if (crossTree !== null) rmSync(crossTree, { recursive: true, force: true });
675
942
  }
676
- // 3b) --reuse 时平台专属目录已存在,就地复查闸门
943
+ // 3b) --reuse 时平台专属目录已存在(不重新构建),就地复查闸门
677
944
  if (REUSE) for (const item of reusedItems) {
678
945
  if (PLATFORM_SUFFIX.test(String(readJson(join(item.dir, 'package.json'))?.name ?? ''))) platformDirs.push(item.dir);
679
946
  }
680
- verifyOptionalSubtreeDeps(platformDirs);
947
+ // 产物 → (目标, 模块) 反查:目录名就是「flat(别名)-目标」。记账(`builtByTarget`)与实际产出的
948
+ // 集合必须**逐个模块**一致 —— 只比个数会漏掉「各 2 个但不是同一批」这种错位
949
+ // (2026-09-16 实测:原生包没编出来时,kerberos 被判成平台无关、目录不带后缀,而 tgz 名却按声明带了
950
+ // -linux-x64 后缀,个数还刚好相等)。
951
+ const packedByTarget = new Map(buildTargets.map((t) => [t, new Set()]));
952
+ for (const dir of platformDirs) {
953
+ const base = basename(dir);
954
+ for (const target of buildTargets) {
955
+ for (const m of modules) {
956
+ if (m.platform === true && base === `${flat(m.alias)}-${target}`) packedByTarget.get(target).add(m.alias);
957
+ }
958
+ }
959
+ }
960
+ // 每个构建目标一行结论(CI 里同时发 annotation:Actions 原始日志要鉴权读不回,
961
+ // annotation 匿名可读,且 ≤20 条上限 —— 所以这里**只发汇总行**,不逐个模块发)。
962
+ if (REUSE) {
963
+ const line = `[repack] --reuse:复用 ${platformDirs.length} 个平台专属目录(不重新构建,故不做白名单/二进制过滤)`;
964
+ console.log(line);
965
+ if (process.env.GITHUB_ACTIONS === 'true') console.log(`::notice::${line}`);
966
+ } else if (ONLY_INDEPENDENT) {
967
+ const indep = modules.filter((m) => !m.platform);
968
+ const line = `[repack] --only-independent:产出 ${indep.length} 个平台无关包(${indep.map((m) => m.alias).join(', ')})`
969
+ + ` + VS Code 树包;平台专属包一律不产(那条腿不跑平台专属循环)`;
970
+ console.log(line);
971
+ if (process.env.GITHUB_ACTIONS === 'true') console.log(`::notice::${line}`);
972
+ } else for (const target of buildTargets) {
973
+ const built = builtByTarget.get(target) ?? [];
974
+ const graded = gradedSpecific.get(target) ?? [];
975
+ const packed = packedByTarget.get(target) ?? new Set();
976
+ const n = (list) => list.filter((s) => s.endsWith(`@${target}`)).length;
977
+ const line = `[repack] ${target}: 源树判定平台专属 ${graded.length} 个(${graded.join(', ') || '无'})`
978
+ + ` ⇒ 打包 ${packed.size} 个(${[...packed].join(', ') || '无'});白名单排除 ${n(skipped.policy)}`
979
+ + `;无二进制跳过 ${n(skipped.noBinary)}`
980
+ + `${n(skipped.missing) > 0 ? `;目录缺失跳过 ${n(skipped.missing)}` : ''}`
981
+ + `;平台无关 ${modules.filter((m) => !m.platform).length} 个`;
982
+ console.log(line);
983
+ if (process.env.GITHUB_ACTIONS === 'true') console.log(`::notice::${line}`);
984
+ const onlyBuilt = built.filter((a) => !packed.has(a));
985
+ const onlyPacked = [...packed].filter((a) => !built.includes(a));
986
+ if (onlyBuilt.length > 0 || onlyPacked.length > 0) {
987
+ const detail = `记账与产物不一致:只记账 ${onlyBuilt.join(', ') || '(无)'};只有产物 ${onlyPacked.join(', ') || '(无)'}`;
988
+ console.warn(` ⚠ ${target}: ${detail}`);
989
+ if (process.env.GITHUB_ACTIONS === 'true') console.log(`::warning::${target}: ${detail}`);
990
+ }
991
+ }
992
+ if (skipped.noBinary.length > 0) {
993
+ console.warn(` ⚠ 以下「模块×目标」在白名单里但没编出二进制:${skipped.noBinary.join(', ')};`
994
+ + '若该平台确实需要它,查编译日志;若不需要,请从 scripts/repack-platforms.json 里删掉该目标');
995
+ }
996
+ // ── 硬闸门:声明里承诺「某模块在某目标上有子包」,就必须真的产出 ──────────────────────────
997
+ // 为什么必须有它:`npm rebuild` 的失败是被 try/catch 吞掉的(单个原生包编不出来不该卡住整条链),
998
+ // 于是「本机工具链编不出 .node」会静默退化成「这个模块被当成平台无关、打成不带目标后缀的包」,
999
+ // 而依赖表里照旧写着「每目标一份」—— 发出去的就是一套装不起来的东西,而 CI 仍然是绿的。
1000
+ // 2026-09-16 维护者问「为什么 CI 输出里有 npm error 仍然通过了」时暴露:Linux 腿上
1001
+ // @vscode/spdlog / sqlite3 / kerberos 都 npm error,5 个承诺的包只产出 2 个,run 却是绿的。
1002
+ if (ONLY_INDEPENDENT) {
1003
+ // 这条腿只产平台无关层 ⇒ 闸门也只管这一层:声明里(或源树里)的平台无关模块必须都产出。
1004
+ const missing = modules
1005
+ .filter((m) => !m.platform && repack.has(m.alias))
1006
+ .filter((m) => !plan.some((item) => basename(String(item.dir ?? '')) === flat(m.alias)));
1007
+ if (missing.length > 0) {
1008
+ const detail = `--only-independent:这些平台无关重打包包没产出:${missing.map((m) => m.alias).join(', ')}`;
1009
+ console.error(`[repack] ✗ ${detail}`);
1010
+ if (process.env.GITHUB_ACTIONS === 'true') console.log(`::error::${detail}`);
1011
+ throw new Error(`${detail} —— 检查源树是否完整(npm install 失败?)和日志里的 npm error`);
1012
+ }
1013
+ } else {
1014
+ const missing = [];
1015
+ for (const target of buildTargets) {
1016
+ const produced = packedByTarget.get(target) ?? new Set();
1017
+ for (const m of modules) {
1018
+ if (m.platform !== true || !(m.targets ?? allTargets).includes(target)) continue;
1019
+ if (!produced.has(m.alias)) missing.push(`${m.alias}@${target}`);
1020
+ }
1021
+ }
1022
+ if (missing.length > 0) {
1023
+ const detail = `声明(scripts/repack-platforms.json)里承诺却没产出子包的「模块×目标」:${missing.join(', ')}`;
1024
+ const hint = '常见原因:① 本机工具链编不出该原生包 —— 日志里的 npm error / MSB8020 就是它'
1025
+ + '(Windows 上注意 MSVC 工具集版本;装对应工具集,或改在 CI 腿上构建);'
1026
+ + '② 源树里没有这个模块(npm install 失败,或上游把它去掉了);'
1027
+ + '③ 该目标本来就不该有这个模块 ⇒ 从该模块的 targets 里删掉这个目标。';
1028
+ console.error(`[repack] ✗ ${detail}`);
1029
+ if (process.env.GITHUB_ACTIONS === 'true') console.log(`::error::${detail} —— ${hint}`);
1030
+ throw new Error(`${detail} —— ${hint}`);
1031
+ }
1032
+ }
1033
+ verifyOptionalSubtreeDeps(platformDirs, new Set([
1034
+ ...declare.map((d) => d.name),
1035
+ ...Object.keys(OPTIONAL_SUBTREE_DEPS),
1036
+ ]));
681
1037
  // 3) 重打包表(lib/vendored.json,随插件发布):运行时用它把真名子包补成原始名字
682
1038
  const vendoredDoc = {
683
1039
  schemaVersion: 1,
684
1040
  generatedBy: 'scripts/vendor-repacks.mjs',
685
1041
  scope: SCOPE,
686
- targets,
1042
+ targets: allTargets,
687
1043
  modules: modules.map((m) => {
688
1044
  const entry = { alias: m.alias, package: m.package, version: m.version };
689
- if (m.platform) entry.platform = true;
1045
+ if (m.platform) {
1046
+ entry.platform = true;
1047
+ // 每模块目标白名单:运行时(lib/native.js)据此跳过本平台不适用的模块 ——
1048
+ // 否则 Linux 上会去找 @jinsiyu/dshcs-vscode-windows-registry-linux-x64,把缺包报成故障。
1049
+ entry.targets = [...(Array.isArray(m.targets) ? m.targets : allTargets)];
1050
+ }
690
1051
  return entry;
691
1052
  }),
692
1053
  };
@@ -711,15 +1072,21 @@ function main() {
711
1072
  ...independent.map((m) => [m.package, m.version]),
712
1073
  ...Object.entries(OPTIONAL_SUBTREE_DEPS),
713
1074
  ].sort(([a], [b]) => a.localeCompare(b)));
714
- pluginPkg.optionalDependencies = Object.fromEntries(targets
715
- .flatMap((t) => specific.map((m) => [`${m.package}-${t}`, m.version]))
1075
+ pluginPkg.optionalDependencies = Object.fromEntries(publishedTargets
1076
+ .flatMap((t) => specific.filter((m) => m.targets.includes(t)).map((m) => [`${m.package}-${t}`, m.version]))
716
1077
  .sort(([a], [b]) => a.localeCompare(b)));
717
1078
  writeFileSync(pkgFile, JSON.stringify(pluginPkg, null, 2) + '\n', 'utf8');
718
1079
  console.log(`[repack] 已写入 package.json:dependencies ${Object.keys(pluginPkg.dependencies).length} 个`
719
1080
  + `(VS Code 树 + 纯 JS + ${independent.length} 个全平台重打包包`
720
1081
  + ` + ${Object.keys(OPTIONAL_SUBTREE_DEPS).length} 个 optional 子树兜底依赖),`
721
1082
  + `optionalDependencies ${Object.keys(pluginPkg.optionalDependencies).length} 个`
722
- + `(${specific.length} 个平台专属重打包包 × ${targets.length} 个目标)`);
1083
+ + `(${specific.length} 个平台专属重打包包 × 已发布目标 ${publishedTargets.join('|')},按每模块白名单取交集)`);
1084
+ const pendingTargets = allTargets.filter((t) => !publishedTargets.includes(t));
1085
+ if (pendingTargets.length > 0) {
1086
+ console.log(`[repack] 注意:${pendingTargets.join(', ')} 的子包**还没发布到 npm** ⇒ 暂不写进插件依赖`
1087
+ + '(写进去会让 pnpm install 解析一个不存在的包)。发布后把 scripts/repack-platforms.json 的'
1088
+ + ' publishedTargets 补齐、再跑一次本脚本,并刷新 pnpm-lock.yaml');
1089
+ }
723
1090
 
724
1091
  // 5) 可选:直接打包
725
1092
  if (DO_PACK) {
@@ -3,10 +3,10 @@
3
3
  "vscodeVersion": "1.137.0",
4
4
  "productPath": "stable-b11dabdaca0d3369986975be285db92c8795cea5",
5
5
  "layout": "vscode-only",
6
- "preparedAt": "2026-09-13T13:24:33.857Z",
6
+ "preparedAt": "2026-09-17T16:48:15.827Z",
7
7
  "source": "registry",
8
- "node": "v24.21.0",
9
- "platform": "win32",
10
- "arch": "arm64",
8
+ "node": "v24.20.0",
9
+ "platform": "linux",
10
+ "arch": "x64",
11
11
  "sizeMB": 197.9
12
12
  }