tanmi-dock 0.9.3 → 1.0.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.
Files changed (51) hide show
  1. package/README.md +10 -4
  2. package/dist/commands/check.d.ts +36 -0
  3. package/dist/commands/check.d.ts.map +1 -1
  4. package/dist/commands/check.js +61 -7
  5. package/dist/commands/check.js.map +1 -1
  6. package/dist/commands/config.d.ts.map +1 -1
  7. package/dist/commands/config.js +10 -0
  8. package/dist/commands/config.js.map +1 -1
  9. package/dist/commands/link.d.ts +3 -2
  10. package/dist/commands/link.d.ts.map +1 -1
  11. package/dist/commands/link.js +877 -598
  12. package/dist/commands/link.js.map +1 -1
  13. package/dist/commands/reset.d.ts.map +1 -1
  14. package/dist/commands/reset.js +46 -17
  15. package/dist/commands/reset.js.map +1 -1
  16. package/dist/commands/status.d.ts.map +1 -1
  17. package/dist/commands/status.js +145 -20
  18. package/dist/commands/status.js.map +1 -1
  19. package/dist/commands/unavailable.d.ts.map +1 -1
  20. package/dist/commands/unavailable.js +7 -2
  21. package/dist/commands/unavailable.js.map +1 -1
  22. package/dist/core/codepac.d.ts +46 -0
  23. package/dist/core/codepac.d.ts.map +1 -1
  24. package/dist/core/codepac.js +599 -63
  25. package/dist/core/codepac.js.map +1 -1
  26. package/dist/core/config.d.ts.map +1 -1
  27. package/dist/core/config.js +14 -0
  28. package/dist/core/config.js.map +1 -1
  29. package/dist/core/linker.d.ts +7 -0
  30. package/dist/core/linker.d.ts.map +1 -1
  31. package/dist/core/linker.js +47 -0
  32. package/dist/core/linker.js.map +1 -1
  33. package/dist/core/parser.d.ts +30 -5
  34. package/dist/core/parser.d.ts.map +1 -1
  35. package/dist/core/parser.js +375 -56
  36. package/dist/core/parser.js.map +1 -1
  37. package/dist/core/platform.js +2 -2
  38. package/dist/core/platform.js.map +1 -1
  39. package/dist/core/store.d.ts +2 -0
  40. package/dist/core/store.d.ts.map +1 -1
  41. package/dist/core/store.js +32 -6
  42. package/dist/core/store.js.map +1 -1
  43. package/dist/types/index.d.ts +63 -11
  44. package/dist/types/index.d.ts.map +1 -1
  45. package/dist/types/index.js +3 -2
  46. package/dist/types/index.js.map +1 -1
  47. package/dist/utils/git.d.ts +20 -1
  48. package/dist/utils/git.d.ts.map +1 -1
  49. package/dist/utils/git.js +181 -4
  50. package/dist/utils/git.js.map +1 -1
  51. package/package.json +1 -1
@@ -7,7 +7,7 @@ import { promisify } from 'util';
7
7
  import path from 'path';
8
8
  import fs from 'fs/promises';
9
9
  import os from 'os';
10
- import { isPlatformDir, normalizePlatformValue, getBaseKeyForCodepac, collectRequestedPlatformTargets, } from './platform.js';
10
+ import { isPlatformDir, normalizePlatformValue, getBaseKeyForCodepac, platformValueToKey, collectRequestedPlatformTargets, getRequestedPlatformTargets, resolveSparseConfig, } from './platform.js';
11
11
  import * as logger from '../utils/logger.js';
12
12
  const execAsync = promisify(exec);
13
13
  // 代理配置缓存
@@ -47,6 +47,8 @@ function getEnvWithProxy() {
47
47
  * codepac 命令名称
48
48
  */
49
49
  const CODEPAC_CMD = 'codepac';
50
+ const GIT_CMD = 'git';
51
+ const MIN_GIT_VERSION = '2.22.0';
50
52
  /**
51
53
  * 检查 codepac 是否已安装
52
54
  */
@@ -59,6 +61,67 @@ export async function isCodepacInstalled() {
59
61
  return false;
60
62
  }
61
63
  }
64
+ export async function checkCodepacEnvironment() {
65
+ const commandProbe = await runShellProbe(`command -v ${CODEPAC_CMD}`);
66
+ logger.debug(`[codepac-env] CodePac 命令检测: command="${commandProbe.command}", ok=${commandProbe.ok}, stdout="${summarizeProbeText(commandProbe.stdout)}", stderr="${summarizeProbeText(commandProbe.stderr)}", error="${commandProbe.error ?? ''}"`);
67
+ const gitProbe = await runShellProbe(`${GIT_CMD} --version`);
68
+ const gitRaw = gitProbe.stdout.trim() || gitProbe.stderr.trim();
69
+ const gitVersion = extractVersion(gitRaw);
70
+ const gitOk = gitProbe.ok && !!gitVersion && compareVersions(gitVersion, MIN_GIT_VERSION) >= 0;
71
+ logger.debug(`[codepac-env] Git 版本检测: command="${gitProbe.command}", ok=${gitProbe.ok}, raw="${summarizeProbeText(gitRaw)}", parsed="${gitVersion ?? ''}", minimum="${MIN_GIT_VERSION}", pass=${gitOk}, error="${gitProbe.error ?? ''}"`);
72
+ const gitLfsVersion = await runShellProbe('git-lfs --version');
73
+ const gitLfsSubcommand = await runShellProbe(`${GIT_CMD} lfs --version`);
74
+ const gitLfsOk = gitLfsVersion.ok && gitLfsSubcommand.ok;
75
+ logger.debug(`[codepac-env] Git LFS 检测: git-lfs ok=${gitLfsVersion.ok}, stdout="${summarizeProbeText(gitLfsVersion.stdout)}", stderr="${summarizeProbeText(gitLfsVersion.stderr)}", error="${gitLfsVersion.error ?? ''}"; git lfs ok=${gitLfsSubcommand.ok}, stdout="${summarizeProbeText(gitLfsSubcommand.stdout)}", stderr="${summarizeProbeText(gitLfsSubcommand.stderr)}", error="${gitLfsSubcommand.error ?? ''}"`);
76
+ const versionProbe = await runShellProbe(`${CODEPAC_CMD} --version`);
77
+ const versionRaw = versionProbe.stdout.trim() || versionProbe.stderr.trim();
78
+ logger.debug(`[codepac-env] CodePac 版本检测: command="${versionProbe.command}", ok=${versionProbe.ok}, stdout="${summarizeProbeText(versionProbe.stdout)}", stderr="${summarizeProbeText(versionProbe.stderr)}", error="${versionProbe.error ?? ''}"`);
79
+ const codepacCommand = {
80
+ ok: commandProbe.ok,
81
+ command: commandProbe.command,
82
+ path: commandProbe.ok ? commandProbe.stdout.trim() : undefined,
83
+ message: commandProbe.ok ? `已找到: ${commandProbe.stdout.trim()}` : '未找到 codepac 命令',
84
+ error: commandProbe.error || commandProbe.stderr.trim() || undefined,
85
+ };
86
+ const git = {
87
+ ok: gitOk,
88
+ command: gitProbe.command,
89
+ minimumVersion: MIN_GIT_VERSION,
90
+ version: gitVersion ?? undefined,
91
+ raw: gitRaw || undefined,
92
+ message: gitOk
93
+ ? `${gitVersion} 可用`
94
+ : gitProbe.ok
95
+ ? `Git 版本需不低于 ${MIN_GIT_VERSION}${gitVersion ? `,当前 ${gitVersion}` : ''}`
96
+ : '无法执行 git --version',
97
+ error: gitProbe.error || gitProbe.stderr.trim() || undefined,
98
+ };
99
+ const gitLfs = {
100
+ ok: gitLfsOk,
101
+ commands: {
102
+ gitLfsVersion,
103
+ gitLfsSubcommand,
104
+ },
105
+ message: gitLfsOk ? '已安装' : 'Git LFS 不可用',
106
+ };
107
+ const codepacVersion = {
108
+ ok: versionProbe.ok,
109
+ command: versionProbe.command,
110
+ version: versionProbe.ok ? versionRaw : undefined,
111
+ raw: versionRaw || undefined,
112
+ message: versionProbe.ok ? versionRaw || '可执行' : 'codepac --version 执行失败',
113
+ error: versionProbe.error || versionProbe.stderr.trim() || undefined,
114
+ };
115
+ const result = {
116
+ ok: codepacCommand.ok && git.ok && gitLfs.ok && codepacVersion.ok,
117
+ codepacCommand,
118
+ git,
119
+ gitLfs,
120
+ codepacVersion,
121
+ };
122
+ logger.debug(`[codepac-env] 环境检测结果: ok=${result.ok}, codepac=${result.codepacCommand.ok}, git=${result.git.ok}, gitLfs=${result.gitLfs.ok}, version=${result.codepacVersion.ok}`);
123
+ return result;
124
+ }
62
125
  /**
63
126
  * 获取 codepac 版本
64
127
  */
@@ -71,6 +134,57 @@ export async function getVersion() {
71
134
  return null;
72
135
  }
73
136
  }
137
+ async function runShellProbe(command) {
138
+ try {
139
+ const output = await execAsync(command, { encoding: 'utf8' });
140
+ const { stdout, stderr } = normalizeExecOutput(output);
141
+ return { ok: true, command, stdout: stdout ?? '', stderr: stderr ?? '' };
142
+ }
143
+ catch (err) {
144
+ const execError = err;
145
+ return {
146
+ ok: false,
147
+ command,
148
+ stdout: execError.stdout ?? '',
149
+ stderr: execError.stderr ?? '',
150
+ error: execError.message,
151
+ };
152
+ }
153
+ }
154
+ function normalizeExecOutput(output) {
155
+ if (typeof output === 'string') {
156
+ return { stdout: output, stderr: '' };
157
+ }
158
+ if (output && typeof output === 'object') {
159
+ const value = output;
160
+ return {
161
+ stdout: typeof value.stdout === 'string' ? value.stdout : '',
162
+ stderr: typeof value.stderr === 'string' ? value.stderr : '',
163
+ };
164
+ }
165
+ return { stdout: '', stderr: '' };
166
+ }
167
+ function extractVersion(text) {
168
+ const match = text.match(/(\d+\.\d+\.\d+)/);
169
+ return match ? match[1] : null;
170
+ }
171
+ function compareVersions(left, right) {
172
+ const leftParts = left.split('.').map((part) => Number.parseInt(part, 10));
173
+ const rightParts = right.split('.').map((part) => Number.parseInt(part, 10));
174
+ const length = Math.max(leftParts.length, rightParts.length);
175
+ for (let index = 0; index < length; index++) {
176
+ const leftValue = leftParts[index] ?? 0;
177
+ const rightValue = rightParts[index] ?? 0;
178
+ if (leftValue > rightValue)
179
+ return 1;
180
+ if (leftValue < rightValue)
181
+ return -1;
182
+ }
183
+ return 0;
184
+ }
185
+ function summarizeProbeText(text) {
186
+ return text.replace(/\s+/g, ' ').trim().slice(0, 240);
187
+ }
74
188
  /**
75
189
  * 使用 codepac 安装依赖
76
190
  */
@@ -242,11 +356,7 @@ function generateTempDirName() {
242
356
  * @deprecated installSingle 已被本函数替代
243
357
  */
244
358
  export async function downloadToTemp(options) {
245
- const { url, commit, branch, libName, platforms, sparse, vars, onProgress, onHeartbeat, onTempDirCreated } = options;
246
- // 检查 codepac 是否安装
247
- if (!(await isCodepacInstalled())) {
248
- throw new Error('codepac 未安装,请先安装 codepac 工具');
249
- }
359
+ const { url, commit, branch, libName, platforms, sparse, vars, useGitLightweightDownload = true, onProgress, onHeartbeat, onTempDirCreated, } = options;
250
360
  // 创建唯一临时目录
251
361
  const tempDirName = generateTempDirName();
252
362
  const tempDir = path.join(os.tmpdir(), tempDirName);
@@ -278,58 +388,34 @@ export async function downloadToTemp(options) {
278
388
  tempConfig.vars = vars;
279
389
  }
280
390
  await fs.writeFile(configPath, JSON.stringify(tempConfig, null, 2), 'utf-8');
281
- // 构建 codepac 命令参数
282
- // 关键: codepac -p 需要基础 CLI key (mac/ios/android)
283
- // 无论用户请求 macOS 还是 macOS-asan,都传 mac codepac
284
- // codepac 会下载所有变体,之后我们做清理
285
- const baseKeys = [...new Set(platforms.map(getBaseKeyForCodepac))];
286
- const args = ['install', '-cf', configPath, '-td', tempDir, '-p', ...baseKeys];
287
- // 调用 codepac
288
- await spawnCodepac(args, tempDir, onProgress, onHeartbeat);
289
- // 分析下载结果,区分平台目录和共享文件
290
- const entries = await fs.readdir(libDir, { withFileTypes: true });
291
- const downloadedPlatforms = [];
292
- const sharedFiles = [];
293
- for (const entry of entries) {
294
- const name = entry.name;
295
- if (isPlatformDir(name)) {
296
- // 标准化平台目录名(例如 "macos" -> "macOS")
297
- downloadedPlatforms.push(normalizePlatformValue(name));
298
- }
299
- else {
300
- sharedFiles.push(name);
301
- }
302
- }
303
- // 后处理:清理用户未请求的平台目录
304
- // 例如用户只请求 macOS,但 codepac 下载了 macOS 和 macOS-asan
305
- const requestedTargets = new Set(collectRequestedPlatformTargets(platforms, sparse, vars));
306
- const platformDirs = [];
307
- const cleanedPlatforms = [];
308
- for (const platform of downloadedPlatforms) {
309
- if (requestedTargets.has(platform)) {
310
- // 请求平台直接命中,或通过 sparse 映射命中承载目录时保留
311
- platformDirs.push(platform);
312
- }
313
- else {
314
- // 用户未请求的平台,删除
315
- const platformPath = path.join(libDir, platform);
391
+ const gitDownloaded = useGitLightweightDownload
392
+ ? await tryDownloadWithGit(options, tempDir, libDir).catch(async (err) => {
393
+ const message = err instanceof Error ? err.message : String(err);
394
+ onProgress?.(`Git 轻量下载失败,切换 codepac 下载: ${message}`);
316
395
  try {
317
- await fs.rm(platformPath, { recursive: true, force: true });
318
- cleanedPlatforms.push(platform);
396
+ await fs.rm(libDir, { recursive: true, force: true });
319
397
  }
320
- catch (err) {
321
- logger.warn(`清理平台目录 ${platform} 失败: ${err.message}`);
398
+ catch {
399
+ // 忽略回退前的清理错误,后续 codepac 仍会在临时目录中重新下载。
322
400
  }
401
+ return false;
402
+ })
403
+ : false;
404
+ if (!gitDownloaded) {
405
+ // 检查 codepac 是否安装
406
+ if (!(await isCodepacInstalled())) {
407
+ throw new Error('codepac 未安装,请先安装 codepac 工具');
323
408
  }
409
+ // 构建 codepac 命令参数
410
+ // 关键: codepac -p 需要基础 CLI key (mac/ios/android)
411
+ // 无论用户请求 macOS 还是 macOS-asan,都传 mac 给 codepac
412
+ // codepac 会下载所有变体,之后我们做清理
413
+ const baseKeys = [...new Set(platforms.map(getBaseKeyForCodepac))];
414
+ const args = ['install', '-cf', configPath, '-td', tempDir, '-p', ...baseKeys];
415
+ // 调用 codepac
416
+ await spawnCodepac(args, tempDir, onProgress, onHeartbeat);
324
417
  }
325
- return {
326
- tempDir,
327
- libDir,
328
- allPlatformDirs: downloadedPlatforms,
329
- platformDirs,
330
- sharedFiles,
331
- cleanedPlatforms,
332
- };
418
+ return analyzeDownloadedLibrary({ tempDir, libDir, platforms, sparse, vars });
333
419
  }
334
420
  catch (error) {
335
421
  // 清理临时目录
@@ -344,16 +430,463 @@ export async function downloadToTemp(options) {
344
430
  throw new Error(`下载库失败: ${message}`);
345
431
  }
346
432
  }
433
+ async function tryDownloadWithGit(options, tempDir, libDir) {
434
+ const { url, commit, branch, platforms, sparse, vars, onProgress, onHeartbeat } = options;
435
+ if (platforms.length === 0 || !isCommitHash(commit)) {
436
+ return false;
437
+ }
438
+ const sparseConfig = resolveSparseConfig(sparse, vars);
439
+ if (sparse && !sparseConfig) {
440
+ return false;
441
+ }
442
+ onProgress?.('尝试使用 Git 轻量下载');
443
+ await spawnGit(['--version'], tempDir, onProgress, onHeartbeat);
444
+ await spawnGit(['lfs', 'version'], tempDir, onProgress, onHeartbeat);
445
+ await spawnGit([
446
+ 'clone',
447
+ '--filter=blob:none',
448
+ '--depth',
449
+ '1',
450
+ '--single-branch',
451
+ '--no-checkout',
452
+ '--branch',
453
+ branch,
454
+ url,
455
+ libDir,
456
+ ], tempDir, onProgress, onHeartbeat, { GIT_LFS_SKIP_SMUDGE: '1' });
457
+ const partialCloneFilter = await spawnGit(['-C', libDir, 'config', '--get', 'remote.origin.partialclonefilter'], tempDir, undefined, onHeartbeat, undefined, false);
458
+ if (partialCloneFilter.stdout.trim() !== 'blob:none') {
459
+ throw new Error('远端未启用 blob:none 轻量克隆');
460
+ }
461
+ const sparseCheckoutPaths = sparseConfig
462
+ ? buildSparseCheckoutPaths(platforms, sparseConfig)
463
+ : await buildRepositorySparseCheckoutPaths(libDir, platforms, sparse, vars, tempDir, onHeartbeat);
464
+ if (sparseCheckoutPaths.length === 0) {
465
+ throw new Error('无法推导 Git sparse checkout 路径');
466
+ }
467
+ await spawnGit(['-C', libDir, 'sparse-checkout', 'init', '--no-cone'], tempDir, onProgress, onHeartbeat);
468
+ await spawnGit(['-C', libDir, 'sparse-checkout', 'set', '--no-cone', ...sparseCheckoutPaths], tempDir, onProgress, onHeartbeat);
469
+ try {
470
+ await checkoutGitCommit(libDir, commit, tempDir, onProgress, onHeartbeat);
471
+ }
472
+ catch {
473
+ await spawnGit(['-C', libDir, 'fetch', '--depth', '1', 'origin', commit], tempDir, onProgress, onHeartbeat, {
474
+ GIT_LFS_SKIP_SMUDGE: '1',
475
+ });
476
+ await checkoutGitCommit(libDir, commit, tempDir, onProgress, onHeartbeat);
477
+ }
478
+ const includePaths = sparseConfig
479
+ ? buildSparseGitLfsIncludePaths(platforms, sparseConfig)
480
+ : await buildGitLfsIncludePathsFromRepository(libDir, platforms, sparse, vars, tempDir, onHeartbeat);
481
+ const expectedLfsPaths = sparseConfig
482
+ ? await buildGitLfsFilePathsForIncludes(libDir, includePaths, tempDir, onHeartbeat)
483
+ : includePaths;
484
+ if (includePaths.length > 0) {
485
+ await spawnGit(['-C', libDir, 'lfs', 'pull', '--include', includePaths.join(','), '--exclude', ''], tempDir, onProgress, onHeartbeat);
486
+ }
487
+ await verifyGitDownloadResult({ libDir, platforms, sparse, vars, expectedLfsPaths });
488
+ await finalizeMinisizeGitDownload(libDir, tempDir, onProgress, onHeartbeat);
489
+ onProgress?.('Git 轻量下载完成');
490
+ return true;
491
+ }
492
+ function isCommitHash(commit) {
493
+ return /^[0-9a-f]{7,40}$/i.test(commit);
494
+ }
495
+ function buildSparseGitLfsIncludePaths(platforms, sparseConfig) {
496
+ return expandGitLfsIncludeRoots(buildSparseCheckoutPaths(platforms, sparseConfig));
497
+ }
498
+ function buildSparseCheckoutPaths(platforms, sparseConfig) {
499
+ const includeRoots = [];
500
+ const commonItems = sparseConfig.common;
501
+ if (Array.isArray(commonItems)) {
502
+ includeRoots.push(...commonItems.filter((item) => typeof item === 'string'));
503
+ }
504
+ for (const platform of platforms) {
505
+ const rawItems = getSparseItemsForPlatform(platform, sparseConfig);
506
+ if (Array.isArray(rawItems)) {
507
+ includeRoots.push(...rawItems.filter((item) => typeof item === 'string'));
508
+ }
509
+ else {
510
+ includeRoots.push(...getRequestedPlatformTargets(platform, sparseConfig));
511
+ }
512
+ }
513
+ return [...new Set(includeRoots.map((item) => normalizeGitPath(item)).filter((item) => Boolean(item)))];
514
+ }
515
+ function getSparseItemsForPlatform(platform, sparseConfig) {
516
+ const candidateKeys = [
517
+ platform,
518
+ platformValueToKey(platform),
519
+ normalizePlatformValue(platform),
520
+ getBaseKeyForCodepac(platform),
521
+ ];
522
+ for (const key of new Set(candidateKeys)) {
523
+ const items = sparseConfig[key];
524
+ if (Array.isArray(items)) {
525
+ return items;
526
+ }
527
+ }
528
+ return undefined;
529
+ }
530
+ async function buildGitLfsIncludePathsFromRepository(libDir, platforms, sparse, vars, cwd, onHeartbeat) {
531
+ const requestedTargets = new Set(collectRequestedPlatformTargets(platforms, sparse, vars));
532
+ const lfsPaths = await listGitLfsFilePaths(libDir, cwd, onHeartbeat);
533
+ const selectedPaths = lfsPaths.filter((line) => {
534
+ const [topLevel] = line.split('/');
535
+ if (!topLevel || !isPlatformDir(topLevel)) {
536
+ return true;
537
+ }
538
+ return requestedTargets.has(normalizePlatformValue(topLevel));
539
+ });
540
+ return [...new Set(selectedPaths)];
541
+ }
542
+ async function buildGitLfsFilePathsForIncludes(libDir, includePaths, cwd, onHeartbeat) {
543
+ const includeRoots = includePaths
544
+ .map((item) => normalizeGitPath(item.replace(/\/\*\*$/g, '')))
545
+ .filter((item) => Boolean(item));
546
+ const uniqueIncludeRoots = [...new Set(includeRoots)];
547
+ if (uniqueIncludeRoots.length === 0) {
548
+ return [];
549
+ }
550
+ const lfsPaths = await listGitLfsFilePaths(libDir, cwd, onHeartbeat);
551
+ return lfsPaths.filter((lfsPath) => uniqueIncludeRoots.some((root) => lfsPath === root || lfsPath.startsWith(`${root}/`)));
552
+ }
553
+ async function listGitLfsFilePaths(libDir, cwd, onHeartbeat) {
554
+ const result = await spawnGit(['-C', libDir, 'lfs', 'ls-files', '--all', '--name-only'], cwd, undefined, onHeartbeat, undefined, false);
555
+ return result.stdout
556
+ .split(/\r?\n/)
557
+ .map((line) => normalizeGitPath(line))
558
+ .filter((line) => Boolean(line));
559
+ }
560
+ async function buildRepositorySparseCheckoutPaths(libDir, platforms, sparse, vars, cwd, onHeartbeat) {
561
+ const requestedTargets = new Set(collectRequestedPlatformTargets(platforms, sparse, vars));
562
+ const result = await spawnGit(['-C', libDir, 'ls-tree', '--name-only', 'HEAD'], cwd, undefined, onHeartbeat, undefined, false);
563
+ const topLevelEntries = result.stdout
564
+ .split(/\r?\n/)
565
+ .map((line) => normalizeGitPath(line))
566
+ .filter((line) => Boolean(line));
567
+ const hasRequestedPlatform = topLevelEntries.some((entry) => (isPlatformDir(entry) && requestedTargets.has(normalizePlatformValue(entry))));
568
+ if (!hasRequestedPlatform) {
569
+ throw new Error('仓库顶层目录未包含请求平台');
570
+ }
571
+ const selectedEntries = topLevelEntries.filter((entry) => {
572
+ if (!isPlatformDir(entry)) {
573
+ return true;
574
+ }
575
+ return requestedTargets.has(normalizePlatformValue(entry));
576
+ });
577
+ return selectedEntries.flatMap((entry) => [entry, `${entry}/**`]);
578
+ }
579
+ function expandGitLfsIncludeRoots(paths) {
580
+ const result = [];
581
+ for (const rawPath of paths) {
582
+ const normalized = normalizeGitPath(rawPath);
583
+ if (!normalized)
584
+ continue;
585
+ result.push(normalized);
586
+ result.push(`${normalized}/**`);
587
+ }
588
+ return [...new Set(result)];
589
+ }
590
+ function normalizeGitPath(value) {
591
+ const normalized = value
592
+ .trim()
593
+ .replace(/\\/g, '/')
594
+ .replace(/^\.\//, '')
595
+ .replace(/\/+$/g, '');
596
+ if (!normalized || normalized === '.' || normalized.startsWith('/')) {
597
+ return null;
598
+ }
599
+ if (normalized.split('/').some((part) => part === '..')) {
600
+ return null;
601
+ }
602
+ return normalized;
603
+ }
604
+ async function checkoutGitCommit(libDir, commit, cwd, onProgress, onHeartbeat) {
605
+ await spawnGit(['-C', libDir, 'checkout', commit], cwd, onProgress, onHeartbeat, {
606
+ GIT_LFS_SKIP_SMUDGE: '1',
607
+ });
608
+ }
609
+ async function verifyGitDownloadResult(options) {
610
+ const { libDir, platforms, sparse, vars, expectedLfsPaths } = options;
611
+ const result = await inspectDownloadedLibrary(libDir);
612
+ const requestedTargets = new Set(collectRequestedPlatformTargets(platforms, sparse, vars));
613
+ const missingTargets = [...requestedTargets].filter((platform) => !result.downloadedPlatforms.includes(platform));
614
+ if (missingTargets.length > 0) {
615
+ throw new Error(`Git 下载结果缺少请求平台目录: ${missingTargets.join(', ')}`);
616
+ }
617
+ const unresolvedPointer = await findFirstUnresolvedLfsPointer(libDir, expectedLfsPaths);
618
+ if (unresolvedPointer) {
619
+ throw new Error(`Git LFS 文件未完成下载: ${unresolvedPointer}`);
620
+ }
621
+ }
622
+ async function finalizeMinisizeGitDownload(libDir, cwd, onProgress, onHeartbeat) {
623
+ const gitDir = await resolveGitDir(libDir);
624
+ if (!gitDir) {
625
+ onProgress?.('Git minisize 收尾跳过: 未发现 .git 目录');
626
+ return;
627
+ }
628
+ const gitLog = await spawnGit(['-C', libDir, 'log', '-1'], cwd, undefined, onHeartbeat, undefined, false);
629
+ const head = await spawnGit(['-C', libDir, 'rev-parse', 'HEAD'], cwd, undefined, onHeartbeat, undefined, false);
630
+ const commit = head.stdout.trim();
631
+ onProgress?.(`Git minisize 收尾: gitdir=${gitDir}`);
632
+ const cleanupSummary = await cleanupRepoAndSubmoduleGitPayloads(libDir);
633
+ onProgress?.(`Git minisize 收尾: 清理 .git/lfs 和 .git/objects,仓库数=${cleanupSummary.repoCount}, 目录数=${cleanupSummary.cleanedDirectoryCount}`);
634
+ if (cleanupSummary.submodulePaths.length > 0) {
635
+ onProgress?.(`Git minisize 收尾: 已处理 submodule=${cleanupSummary.submodulePaths.join(',')}`);
636
+ }
637
+ const repoName = path.basename(libDir);
638
+ const commitMessagePath = path.join(gitDir, 'commit_message');
639
+ const commitHashPath = path.join(gitDir, 'commit_hash');
640
+ await fs.writeFile(commitMessagePath, `=============== ${repoName} ===============\n${gitLog.stdout}`, 'utf-8');
641
+ await fs.writeFile(commitHashPath, commit, 'utf-8');
642
+ onProgress?.(`Git minisize 收尾完成: commit_hash=${commit.slice(0, 12)}, marker=${commitHashPath}`);
643
+ }
644
+ async function cleanupRepoAndSubmoduleGitPayloads(repoDir) {
645
+ let repoCount = 0;
646
+ let cleanedDirectoryCount = 0;
647
+ const rootCleaned = await cleanupGitPayload(repoDir);
648
+ repoCount += rootCleaned === null ? 0 : 1;
649
+ cleanedDirectoryCount += rootCleaned ?? 0;
650
+ const submodulePaths = await collectGitSubmodulePaths(repoDir);
651
+ for (const submodulePath of submodulePaths) {
652
+ const submoduleCleaned = await cleanupGitPayload(path.join(repoDir, submodulePath));
653
+ repoCount += submoduleCleaned === null ? 0 : 1;
654
+ cleanedDirectoryCount += submoduleCleaned ?? 0;
655
+ }
656
+ return { repoCount, cleanedDirectoryCount, submodulePaths };
657
+ }
658
+ async function cleanupGitPayload(repoDir) {
659
+ const gitDir = await resolveGitDir(repoDir);
660
+ if (!gitDir)
661
+ return null;
662
+ return (await cleanupGitPayloadDirectory(path.join(gitDir, 'lfs'))
663
+ + await cleanupGitPayloadDirectory(path.join(gitDir, 'objects')));
664
+ }
665
+ async function cleanupGitPayloadDirectory(payloadDir) {
666
+ let entries = [];
667
+ try {
668
+ const dirEntries = await fs.readdir(payloadDir, { withFileTypes: true });
669
+ entries = Array.isArray(dirEntries)
670
+ ? dirEntries
671
+ : [];
672
+ }
673
+ catch {
674
+ return 0;
675
+ }
676
+ let cleanedCount = 0;
677
+ for (const entry of entries) {
678
+ const name = typeof entry === 'string' ? entry : entry.name;
679
+ const isDirectory = typeof entry === 'string' ? true : entry.isDirectory?.() ?? false;
680
+ if (!isDirectory)
681
+ continue;
682
+ const entryPath = path.join(payloadDir, name);
683
+ await fs.rm(entryPath, { recursive: true, force: true });
684
+ await fs.mkdir(entryPath, { recursive: true });
685
+ cleanedCount++;
686
+ }
687
+ return cleanedCount;
688
+ }
689
+ async function resolveGitDir(repoDir) {
690
+ const gitPath = path.join(repoDir, '.git');
691
+ try {
692
+ const stat = await fs.stat(gitPath);
693
+ if (stat.isDirectory()) {
694
+ return gitPath;
695
+ }
696
+ if (!stat.isFile()) {
697
+ return null;
698
+ }
699
+ }
700
+ catch {
701
+ return null;
702
+ }
703
+ try {
704
+ const content = await fs.readFile(gitPath, 'utf-8');
705
+ if (typeof content !== 'string') {
706
+ return null;
707
+ }
708
+ const match = content.match(/^gitdir:\s*(.+)$/m);
709
+ if (!match) {
710
+ return null;
711
+ }
712
+ return path.resolve(repoDir, match[1].trim());
713
+ }
714
+ catch {
715
+ return null;
716
+ }
717
+ }
718
+ async function collectGitSubmodulePaths(repoDir) {
719
+ const gitmodulesPath = path.join(repoDir, '.gitmodules');
720
+ let content;
721
+ try {
722
+ content = await fs.readFile(gitmodulesPath, 'utf-8');
723
+ }
724
+ catch {
725
+ return [];
726
+ }
727
+ if (typeof content !== 'string') {
728
+ return [];
729
+ }
730
+ const paths = [];
731
+ for (const rawLine of content.split(/\r?\n/)) {
732
+ const match = rawLine.trim().match(/^path\s*=\s*(.+)$/);
733
+ if (!match)
734
+ continue;
735
+ const submodulePath = normalizeGitPath(match[1]);
736
+ if (submodulePath) {
737
+ paths.push(submodulePath);
738
+ }
739
+ }
740
+ const nestedPaths = await Promise.all(paths.map(async (submodulePath) => {
741
+ const childPaths = await collectGitSubmodulePaths(path.join(repoDir, submodulePath));
742
+ return childPaths.map((childPath) => `${submodulePath}/${childPath}`);
743
+ }));
744
+ return [...new Set([...paths, ...nestedPaths.flat()])];
745
+ }
746
+ async function findFirstUnresolvedLfsPointer(libDir, relativePaths) {
747
+ for (const relativePath of relativePaths) {
748
+ const content = await readSmallTextFile(path.join(libDir, relativePath));
749
+ if (content?.startsWith('version https://git-lfs.github.com/spec/v1')) {
750
+ return relativePath;
751
+ }
752
+ }
753
+ return null;
754
+ }
755
+ async function readSmallTextFile(filePath) {
756
+ try {
757
+ const stat = await fs.stat(filePath);
758
+ if (!stat.isFile() || stat.size > 1024) {
759
+ return null;
760
+ }
761
+ return await fs.readFile(filePath, 'utf-8');
762
+ }
763
+ catch {
764
+ return null;
765
+ }
766
+ }
767
+ async function analyzeDownloadedLibrary(options) {
768
+ const { tempDir, libDir, platforms, sparse, vars } = options;
769
+ const { downloadedPlatforms, sharedFiles } = await inspectDownloadedLibrary(libDir);
770
+ // 后处理:清理用户未请求的平台目录
771
+ // 例如用户只请求 macOS,但 codepac 下载了 macOS 和 macOS-asan
772
+ const requestedTargets = new Set(collectRequestedPlatformTargets(platforms, sparse, vars));
773
+ const platformDirs = [];
774
+ const cleanedPlatforms = [];
775
+ for (const platform of downloadedPlatforms) {
776
+ if (requestedTargets.has(platform)) {
777
+ // 请求平台直接命中,或通过 sparse 映射命中承载目录时保留
778
+ platformDirs.push(platform);
779
+ }
780
+ else {
781
+ // 用户未请求的平台,删除
782
+ const platformPath = path.join(libDir, platform);
783
+ try {
784
+ await fs.rm(platformPath, { recursive: true, force: true });
785
+ cleanedPlatforms.push(platform);
786
+ }
787
+ catch (err) {
788
+ logger.warn(`清理平台目录 ${platform} 失败: ${err.message}`);
789
+ }
790
+ }
791
+ }
792
+ return {
793
+ tempDir,
794
+ libDir,
795
+ allPlatformDirs: downloadedPlatforms,
796
+ platformDirs,
797
+ sharedFiles,
798
+ cleanedPlatforms,
799
+ };
800
+ }
801
+ async function inspectDownloadedLibrary(libDir) {
802
+ let entries = [];
803
+ try {
804
+ const dirEntries = await fs.readdir(libDir, { withFileTypes: true });
805
+ entries = Array.isArray(dirEntries)
806
+ ? dirEntries
807
+ : [];
808
+ }
809
+ catch {
810
+ entries = [];
811
+ }
812
+ const downloadedPlatforms = [];
813
+ const sharedFiles = [];
814
+ for (const entry of entries) {
815
+ const name = typeof entry === 'string' ? entry : entry.name;
816
+ const isDirectory = typeof entry === 'string' ? false : entry.isDirectory();
817
+ if (isDirectory && isPlatformDir(name)) {
818
+ const platformPath = path.join(libDir, name);
819
+ const platformHasFiles = await directoryHasFiles(platformPath);
820
+ if (platformHasFiles) {
821
+ // 标准化平台目录名(例如 "macos" -> "macOS")
822
+ downloadedPlatforms.push(normalizePlatformValue(name));
823
+ }
824
+ else {
825
+ sharedFiles.push(name);
826
+ }
827
+ }
828
+ else {
829
+ sharedFiles.push(name);
830
+ }
831
+ }
832
+ return { downloadedPlatforms, sharedFiles };
833
+ }
834
+ async function directoryHasFiles(dirPath, depth = 0) {
835
+ if (depth > 32) {
836
+ return true;
837
+ }
838
+ try {
839
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
840
+ for (const entry of entries) {
841
+ const name = typeof entry === 'string' ? entry : entry.name;
842
+ if (name.startsWith('.'))
843
+ continue;
844
+ if (typeof entry === 'string')
845
+ return true;
846
+ if (typeof entry.isFile === 'function' && entry.isFile())
847
+ return true;
848
+ if (typeof entry.isSymbolicLink === 'function' && entry.isSymbolicLink())
849
+ return true;
850
+ if (entry.isDirectory() && await directoryHasFiles(path.join(dirPath, name), depth + 1)) {
851
+ return true;
852
+ }
853
+ }
854
+ return false;
855
+ }
856
+ catch {
857
+ return false;
858
+ }
859
+ }
860
+ function spawnGit(args, cwd, onProgress, onHeartbeat, extraEnv, forwardStdout = true) {
861
+ return spawnCommand(GIT_CMD, args, cwd, {
862
+ onProgress,
863
+ onHeartbeat,
864
+ env: { ...getEnvWithProxy(), ...extraEnv },
865
+ forwardStdout,
866
+ });
867
+ }
347
868
  /**
348
869
  * 内部辅助函数: 执行 codepac 命令
349
870
  */
350
871
  function spawnCodepac(args, cwd, onProgress, onHeartbeat) {
872
+ return spawnCommand(CODEPAC_CMD, args, cwd, {
873
+ onProgress,
874
+ onHeartbeat,
875
+ env: getEnvWithProxy(),
876
+ }).then(() => undefined);
877
+ }
878
+ function spawnCommand(command, args, cwd, options = {}) {
351
879
  return new Promise((resolve, reject) => {
352
- const proc = spawn(CODEPAC_CMD, args, {
880
+ const proc = spawn(command, args, {
353
881
  cwd,
354
882
  stdio: 'pipe',
355
- env: getEnvWithProxy(),
883
+ env: options.env ?? getEnvWithProxy(),
356
884
  });
885
+ if (!proc) {
886
+ reject(new Error(`无法执行 ${command} 命令`));
887
+ return;
888
+ }
889
+ let stdout = '';
357
890
  let stderr = '';
358
891
  const heartbeatIntervalMs = 10000;
359
892
  let heartbeatTimer = null;
@@ -364,11 +897,11 @@ function spawnCodepac(args, cwd, onProgress, onHeartbeat) {
364
897
  }
365
898
  };
366
899
  const resetHeartbeat = () => {
367
- if (!onHeartbeat)
900
+ if (!options.onHeartbeat)
368
901
  return;
369
902
  clearHeartbeat();
370
903
  heartbeatTimer = setTimeout(() => {
371
- onHeartbeat('仍在处理中,可能在同步大文件,期间无新日志');
904
+ options.onHeartbeat?.('仍在处理中,可能在同步大文件,期间无新日志');
372
905
  resetHeartbeat();
373
906
  }, heartbeatIntervalMs);
374
907
  };
@@ -376,9 +909,11 @@ function spawnCodepac(args, cwd, onProgress, onHeartbeat) {
376
909
  if (proc.stdout) {
377
910
  proc.stdout.on('data', (data) => {
378
911
  resetHeartbeat();
379
- const message = data.toString().trim();
380
- if (message && onProgress) {
381
- onProgress(message);
912
+ const chunk = data.toString();
913
+ stdout += chunk;
914
+ const message = chunk.trim();
915
+ if (message && options.onProgress && options.forwardStdout !== false) {
916
+ options.onProgress(message);
382
917
  }
383
918
  });
384
919
  }
@@ -390,15 +925,15 @@ function spawnCodepac(args, cwd, onProgress, onHeartbeat) {
390
925
  }
391
926
  proc.on('error', (err) => {
392
927
  clearHeartbeat();
393
- reject(new Error(`无法执行 codepac 命令: ${err.message}`));
928
+ reject(new Error(`无法执行 ${command} 命令: ${err.message}`));
394
929
  });
395
930
  proc.on('close', (code) => {
396
931
  clearHeartbeat();
397
932
  if (code === 0) {
398
- resolve();
933
+ resolve({ stdout, stderr });
399
934
  }
400
935
  else {
401
- const errorMsg = stderr.trim() || `codepac 命令执行失败,退出码: ${code}`;
936
+ const errorMsg = stderr.trim() || `${command} 命令执行失败,退出码: ${code}`;
402
937
  reject(new Error(errorMsg));
403
938
  }
404
939
  });
@@ -406,6 +941,7 @@ function spawnCodepac(args, cwd, onProgress, onHeartbeat) {
406
941
  }
407
942
  export default {
408
943
  isCodepacInstalled,
944
+ checkCodepacEnvironment,
409
945
  getVersion,
410
946
  install,
411
947
  installSingle,