tanmi-dock 0.10.0-beta → 1.0.4
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/README.md +26 -5
- package/dist/commands/calibrate.d.ts +10 -0
- package/dist/commands/calibrate.d.ts.map +1 -0
- package/dist/commands/calibrate.js +158 -0
- package/dist/commands/calibrate.js.map +1 -0
- package/dist/commands/check.d.ts +45 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +322 -101
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/clean.d.ts +2 -6
- package/dist/commands/clean.d.ts.map +1 -1
- package/dist/commands/clean.js +224 -209
- package/dist/commands/clean.js.map +1 -1
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +56 -28
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/link.d.ts +8 -7
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/link.js +717 -221
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/migrate.d.ts +11 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +277 -57
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/reset.d.ts.map +1 -1
- package/dist/commands/reset.js +65 -41
- package/dist/commands/reset.js.map +1 -1
- package/dist/commands/status.d.ts +4 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +182 -40
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/unavailable.d.ts.map +1 -1
- package/dist/commands/unavailable.js +7 -2
- package/dist/commands/unavailable.js.map +1 -1
- package/dist/commands/unlink.d.ts.map +1 -1
- package/dist/commands/unlink.js +43 -39
- package/dist/commands/unlink.js.map +1 -1
- package/dist/core/codepac.d.ts +44 -0
- package/dist/core/codepac.d.ts.map +1 -1
- package/dist/core/codepac.js +287 -5
- package/dist/core/codepac.js.map +1 -1
- package/dist/core/config.d.ts +3 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +7 -3
- package/dist/core/config.js.map +1 -1
- package/dist/core/linker.d.ts +7 -0
- package/dist/core/linker.d.ts.map +1 -1
- package/dist/core/linker.js +47 -0
- package/dist/core/linker.js.map +1 -1
- package/dist/core/parser.d.ts +30 -5
- package/dist/core/parser.d.ts.map +1 -1
- package/dist/core/parser.js +376 -57
- package/dist/core/parser.js.map +1 -1
- package/dist/core/registry.d.ts +11 -29
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/registry.js +46 -113
- package/dist/core/registry.js.map +1 -1
- package/dist/core/space/calibration-executor.d.ts +16 -0
- package/dist/core/space/calibration-executor.d.ts.map +1 -0
- package/dist/core/space/calibration-executor.js +142 -0
- package/dist/core/space/calibration-executor.js.map +1 -0
- package/dist/core/space/calibration-planner.d.ts +5 -0
- package/dist/core/space/calibration-planner.d.ts.map +1 -0
- package/dist/core/space/calibration-planner.js +157 -0
- package/dist/core/space/calibration-planner.js.map +1 -0
- package/dist/core/space/cleanup-executor.d.ts +15 -0
- package/dist/core/space/cleanup-executor.d.ts.map +1 -0
- package/dist/core/space/cleanup-executor.js +188 -0
- package/dist/core/space/cleanup-executor.js.map +1 -0
- package/dist/core/space/cleanup-journal.d.ts +43 -0
- package/dist/core/space/cleanup-journal.d.ts.map +1 -0
- package/dist/core/space/cleanup-journal.js +96 -0
- package/dist/core/space/cleanup-journal.js.map +1 -0
- package/dist/core/space/cleanup-planner.d.ts +14 -0
- package/dist/core/space/cleanup-planner.d.ts.map +1 -0
- package/dist/core/space/cleanup-planner.js +166 -0
- package/dist/core/space/cleanup-planner.js.map +1 -0
- package/dist/core/space/cleanup-selection.d.ts +10 -0
- package/dist/core/space/cleanup-selection.d.ts.map +1 -0
- package/dist/core/space/cleanup-selection.js +84 -0
- package/dist/core/space/cleanup-selection.js.map +1 -0
- package/dist/core/space/estimator.d.ts +11 -0
- package/dist/core/space/estimator.d.ts.map +1 -0
- package/dist/core/space/estimator.js +142 -0
- package/dist/core/space/estimator.js.map +1 -0
- package/dist/core/space/inventory.d.ts +15 -0
- package/dist/core/space/inventory.d.ts.map +1 -0
- package/dist/core/space/inventory.js +587 -0
- package/dist/core/space/inventory.js.map +1 -0
- package/dist/core/space/meter.d.ts +5 -0
- package/dist/core/space/meter.d.ts.map +1 -0
- package/dist/core/space/meter.js +87 -0
- package/dist/core/space/meter.js.map +1 -0
- package/dist/core/space/migration-journal.d.ts +31 -0
- package/dist/core/space/migration-journal.d.ts.map +1 -0
- package/dist/core/space/migration-journal.js +121 -0
- package/dist/core/space/migration-journal.js.map +1 -0
- package/dist/core/space/service.d.ts +26 -0
- package/dist/core/space/service.d.ts.map +1 -0
- package/dist/core/space/service.js +54 -0
- package/dist/core/space/service.js.map +1 -0
- package/dist/core/space/types.d.ts +241 -0
- package/dist/core/space/types.d.ts.map +1 -0
- package/dist/core/space/types.js +13 -0
- package/dist/core/space/types.js.map +1 -0
- package/dist/core/store.d.ts +2 -0
- package/dist/core/store.d.ts.map +1 -1
- package/dist/core/store.js +44 -12
- package/dist/core/store.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +63 -10
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/atomic-file.d.ts +6 -0
- package/dist/utils/atomic-file.d.ts.map +1 -0
- package/dist/utils/atomic-file.js +37 -0
- package/dist/utils/atomic-file.js.map +1 -0
- package/dist/utils/fs-utils.d.ts +18 -1
- package/dist/utils/fs-utils.d.ts.map +1 -1
- package/dist/utils/fs-utils.js +150 -2
- package/dist/utils/fs-utils.js.map +1 -1
- package/dist/utils/git.d.ts +20 -1
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +181 -4
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/logger.d.ts +1 -1
- package/dist/utils/logger.js +2 -2
- package/dist/utils/prompt.d.ts.map +1 -1
- package/dist/utils/prompt.js +4 -4
- package/dist/utils/prompt.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/link.js
CHANGED
|
@@ -7,13 +7,13 @@ import { Command } from 'commander';
|
|
|
7
7
|
import { ensureInitialized } from '../core/guard.js';
|
|
8
8
|
import * as config from '../core/config.js';
|
|
9
9
|
import { setLogLevel } from '../utils/logger.js';
|
|
10
|
-
import { parseProjectDependencies, getRelativeConfigPath, parseCodepacDep, extractActions,
|
|
10
|
+
import { parseProjectDependencies, getRelativeConfigPath, parseCodepacDep, extractActions, buildActionExecutionPlan, extractNestedDependencies, findAllCodepacConfigs, extractDependencies, resolveProjectRootPath, } from '../core/parser.js';
|
|
11
11
|
import { getRegistry } from '../core/registry.js';
|
|
12
12
|
import * as store from '../core/store.js';
|
|
13
13
|
import * as linker from '../core/linker.js';
|
|
14
14
|
import * as codepac from '../core/codepac.js';
|
|
15
15
|
import { setProxyConfig } from '../core/codepac.js';
|
|
16
|
-
import { getPlatformHelpText, GENERAL_PLATFORM, SHARED_PLATFORM, pathsEqual, isSparseOnlyCommon, KNOWN_PLATFORM_VALUES, getRequestedPlatformTargets, } from '../core/platform.js';
|
|
16
|
+
import { getPlatformHelpText, GENERAL_PLATFORM, SHARED_PLATFORM, pathsEqual, isSparseOnlyCommon, KNOWN_PLATFORM_VALUES, getRequestedPlatformTargets, resolveSparseConfig, getBaseKeyForCodepac, } from '../core/platform.js';
|
|
17
17
|
import { Transaction } from '../core/transaction.js';
|
|
18
18
|
import { formatSize, checkDiskSpace } from '../utils/disk.js';
|
|
19
19
|
import { getDirSize } from '../utils/fs-utils.js';
|
|
@@ -21,6 +21,7 @@ import { ProgressTracker, DownloadMonitor, MultiBarManager } from '../utils/prog
|
|
|
21
21
|
import { success, warn, error, info, hint, blank, separator, debug } from '../utils/logger.js';
|
|
22
22
|
import { verifyLocalCommit, findSubmoduleConfigs } from '../utils/git.js';
|
|
23
23
|
import { DependencyStatus } from '../types/index.js';
|
|
24
|
+
import { SpaceService, buildHalfCleanupSelectionSummary, buildUnreferencedSelectionSummary, estimateDownloadSpace, } from '../core/space/service.js';
|
|
24
25
|
import { withGlobalLock } from '../utils/global-lock.js';
|
|
25
26
|
import { selectPlatforms, parsePlatformArgs, selectOption, selectOptionalConfigs, PROMPT_CANCELLED } from '../utils/prompt.js';
|
|
26
27
|
import pLimit from 'p-limit';
|
|
@@ -37,6 +38,7 @@ export function createLinkCommand() {
|
|
|
37
38
|
.option('--no-download', '不自动下载缺失库')
|
|
38
39
|
.option('--dry-run', '只显示将要执行的操作')
|
|
39
40
|
.option('--config <configs...>', '指定可选配置文件 (如 codepac-dep-inner.json)')
|
|
41
|
+
.option('--skip-action, --skip_action, -sa <names...>', '按 action.name 跳过指定嵌套 action')
|
|
40
42
|
.option('--no-submodules', '不检测 git submodule 依赖')
|
|
41
43
|
.addHelpText('after', `${getPlatformHelpText()}
|
|
42
44
|
|
|
@@ -48,6 +50,7 @@ export function createLinkCommand() {
|
|
|
48
50
|
td link --dry-run 预览操作,不实际执行
|
|
49
51
|
td link -y 跳过确认,自动执行
|
|
50
52
|
td link --config codepac-dep-inner.json 使用指定的可选配置
|
|
53
|
+
td link --skip-action prepareNested 跳过指定 action.name
|
|
51
54
|
td link --no-submodules 跳过 git submodule 检测`)
|
|
52
55
|
.action(async (projectPath, options) => {
|
|
53
56
|
await ensureInitialized();
|
|
@@ -73,6 +76,100 @@ function resolveRequestedPlatformsByActual(requestedPlatforms, sparse, actualPla
|
|
|
73
76
|
missingRequested: requestedPlatforms.filter((platform) => !satisfiedRequested.includes(platform)),
|
|
74
77
|
};
|
|
75
78
|
}
|
|
79
|
+
async function verifyGeneralStoreState(dependency) {
|
|
80
|
+
const result = await verifyExistingPlatforms(dependency.libName, dependency.commit, [GENERAL_PLATFORM]);
|
|
81
|
+
return {
|
|
82
|
+
valid: result.valid.length > 0,
|
|
83
|
+
repairRequired: result.repairRequired.length > 0,
|
|
84
|
+
corrupted: result.corrupted.length > 0,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function executeLinkStoreRepair(dependency, platforms, options) {
|
|
88
|
+
const registry = getRegistry();
|
|
89
|
+
const storePath = await store.getStorePath();
|
|
90
|
+
const spaceService = new SpaceService(registry, storePath);
|
|
91
|
+
const plan = await spaceService.buildCleanupPlan({
|
|
92
|
+
reason: 'repair',
|
|
93
|
+
selections: [{
|
|
94
|
+
id: { libName: dependency.libName, commit: dependency.commit },
|
|
95
|
+
platforms,
|
|
96
|
+
includeShared: options.includeShared,
|
|
97
|
+
}],
|
|
98
|
+
requireUnreferenced: false,
|
|
99
|
+
allowUnknownContent: false,
|
|
100
|
+
});
|
|
101
|
+
for (const warningMessage of plan.warnings)
|
|
102
|
+
warn(`[space] ${warningMessage}`);
|
|
103
|
+
debug(`[link] 调用统一清理执行器: category=${options.category}, plan=${plan.id}, lib=${dependency.libName}, commit=${dependency.commit}, platforms=${platforms === 'all' ? 'all' : platforms.join(',')}, includeShared=${Boolean(options.includeShared)}, groups=${plan.groups.length}, expected=${plan.expectedRelease.bytes}`);
|
|
104
|
+
if (plan.groups.length === 0)
|
|
105
|
+
return;
|
|
106
|
+
const result = await spaceService.createCleanupExecutor().execute(plan);
|
|
107
|
+
debug(`[link] 清理执行返回: category=${options.category}, plan=${plan.id}, status=${result.status}, succeeded=${result.succeeded.length}, failed=${result.failed.length}, released=${result.released.bytes}`);
|
|
108
|
+
if (result.status !== 'completed') {
|
|
109
|
+
throw new Error(`${dependency.libName}@${dependency.commit.slice(0, 8)} 缓存修复未完整完成: ${result.failed.map((item) => item.message).join('; ')}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function prepareGeneralStoreRepair(dependency, repairRequiredPlatforms) {
|
|
113
|
+
const generalState = await verifyGeneralStoreState(dependency);
|
|
114
|
+
if (!generalState.valid && (generalState.corrupted || generalState.repairRequired)) {
|
|
115
|
+
await executeLinkStoreRepair(dependency, 'all', { category: 'general-corruption' });
|
|
116
|
+
if (!repairRequiredPlatforms.includes(GENERAL_PLATFORM)) {
|
|
117
|
+
repairRequiredPlatforms.push(GENERAL_PLATFORM);
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
async function prepareSharedStoreRepair(dependency, requestedPlatforms, repairRequiredPlatforms) {
|
|
124
|
+
const storePath = await config.getStorePath();
|
|
125
|
+
if (!storePath)
|
|
126
|
+
return [];
|
|
127
|
+
const sharedPath = path.join(storePath, dependency.libName, dependency.commit, '_shared');
|
|
128
|
+
try {
|
|
129
|
+
await fs.access(sharedPath);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const result = await verifyExistingPlatforms(dependency.libName, dependency.commit, [SHARED_PLATFORM]);
|
|
135
|
+
if (result.valid.length > 0 && result.corrupted.length === 0 && result.repairRequired.length === 0) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const removedPlatforms = [];
|
|
139
|
+
for (const platform of requestedPlatforms) {
|
|
140
|
+
if (!repairRequiredPlatforms.includes(platform)) {
|
|
141
|
+
repairRequiredPlatforms.push(platform);
|
|
142
|
+
}
|
|
143
|
+
removedPlatforms.push(platform);
|
|
144
|
+
}
|
|
145
|
+
await executeLinkStoreRepair(dependency, 'all', {
|
|
146
|
+
includeShared: true,
|
|
147
|
+
category: 'shared-corruption',
|
|
148
|
+
});
|
|
149
|
+
debug(`${dependency.libName} _shared 本地缓存不完整,准备随平台 [${requestedPlatforms.join(', ') || '无'}] 重新下载`);
|
|
150
|
+
return removedPlatforms;
|
|
151
|
+
}
|
|
152
|
+
async function hasRequestedPlatformContent(dependency, requestedPlatforms, vars) {
|
|
153
|
+
if (requestedPlatforms.length === 0)
|
|
154
|
+
return false;
|
|
155
|
+
const requestedTargets = [...new Set(requestedPlatforms.flatMap((platform) => getRequestedPlatformTargets(platform, dependency.sparse, vars)))];
|
|
156
|
+
const storePath = await store.getStorePath();
|
|
157
|
+
const commitPath = path.join(storePath, dependency.libName, dependency.commit);
|
|
158
|
+
try {
|
|
159
|
+
const entries = await fs.readdir(commitPath, { withFileTypes: true });
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
if (!entry.isDirectory() || !requestedTargets.includes(entry.name))
|
|
162
|
+
continue;
|
|
163
|
+
if (await store.exists(dependency.libName, dependency.commit, entry.name)) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Store 目录不存在时继续使用历史记录或后续 General 检测
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
76
173
|
export function assessDownloadResult(requestedPlatforms, sparse, downloadResult, vars) {
|
|
77
174
|
const downloadedActual = new Set(downloadResult.platformDirs);
|
|
78
175
|
const { satisfiedRequested, missingRequested, } = resolveRequestedPlatformsByActual(requestedPlatforms, sparse, downloadResult.platformDirs, vars);
|
|
@@ -90,15 +187,28 @@ export function assessDownloadResult(requestedPlatforms, sparse, downloadResult,
|
|
|
90
187
|
isPureGeneral,
|
|
91
188
|
};
|
|
92
189
|
}
|
|
93
|
-
async function resolveRequestedStoreState(dependency, requestedPlatforms, vars) {
|
|
190
|
+
async function resolveRequestedStoreState(dependency, requestedPlatforms, vars, repairMissingRecords = true) {
|
|
94
191
|
const resolvedExisting = [];
|
|
192
|
+
const stalePlatforms = [];
|
|
95
193
|
const candidatePlatforms = [...new Set(requestedPlatforms.flatMap((platform) => getRequestedPlatformTargets(platform, dependency.sparse, vars)))];
|
|
96
194
|
for (const platform of candidatePlatforms) {
|
|
97
195
|
if (await store.exists(dependency.libName, dependency.commit, platform)) {
|
|
98
196
|
resolvedExisting.push(platform);
|
|
99
197
|
}
|
|
198
|
+
else {
|
|
199
|
+
const registry = getRegistry();
|
|
200
|
+
const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, platform);
|
|
201
|
+
if (registry.getStore(storeKey)) {
|
|
202
|
+
stalePlatforms.push(platform);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
100
205
|
}
|
|
101
|
-
|
|
206
|
+
if (repairMissingRecords && stalePlatforms.length > 0) {
|
|
207
|
+
await executeLinkStoreRepair(dependency, stalePlatforms, { category: 'missing-platform-path' });
|
|
208
|
+
}
|
|
209
|
+
const result = resolveRequestedPlatformsByActual(requestedPlatforms, dependency.sparse, resolvedExisting, vars);
|
|
210
|
+
debug(`[link-store-state] ${dependency.libName}@${dependency.commit.slice(0, 7)} 请求平台=${requestedPlatforms.join(', ') || '空'}, Store候选=${candidatePlatforms.join(', ') || '空'}, 实际存在=${result.actualExisting.join(', ') || '无'}, 满足请求=${result.satisfiedRequested.join(', ') || '无'}, 缺失请求=${result.missingRequested.join(', ') || '无'}`);
|
|
211
|
+
return result;
|
|
102
212
|
}
|
|
103
213
|
function getBlockedRequestedPlatforms(registry, dependency, requestedPlatforms, vars) {
|
|
104
214
|
const { manualPlatforms, autoPlatforms } = resolveUnavailablePlatforms(registry, dependency);
|
|
@@ -109,7 +219,9 @@ function getBlockedRequestedPlatforms(registry, dependency, requestedPlatforms,
|
|
|
109
219
|
const targets = getRequestedPlatformTargets(platform, dependency.sparse, vars);
|
|
110
220
|
return targets.length === 1 && targets[0] === platform;
|
|
111
221
|
});
|
|
112
|
-
|
|
222
|
+
const blocked = [...new Set([...manualBlocked, ...autoBlocked])];
|
|
223
|
+
debug(`[link-platforms] ${dependency.libName}@${dependency.commit.slice(0, 7)} 请求平台=${requestedPlatforms.join(', ') || '空'}, 手动跳过=${manualBlocked.join(', ') || '无'}, 自动跳过=${autoBlocked.join(', ') || '无'}, 最终跳过=${blocked.join(', ') || '无'}`);
|
|
224
|
+
return blocked;
|
|
113
225
|
}
|
|
114
226
|
function clearSatisfiedAutoUnavailablePlatforms(registry, dependency, satisfiedRequested) {
|
|
115
227
|
if (satisfiedRequested.length === 0)
|
|
@@ -154,12 +266,80 @@ function upsertLibraryAvailability(dependency, updates) {
|
|
|
154
266
|
lastAccess: new Date().toISOString(),
|
|
155
267
|
});
|
|
156
268
|
}
|
|
157
|
-
export async function resolveLibraryGeneralState(dependency) {
|
|
269
|
+
export async function resolveLibraryGeneralState(dependency, requestedPlatforms = [], vars, updateRegistry = true) {
|
|
158
270
|
const registry = getRegistry();
|
|
159
271
|
const libKey = registry.getLibraryKey(dependency.libName, dependency.commit);
|
|
160
272
|
const library = registry.getLibrary(libKey);
|
|
273
|
+
const sparseConfig = resolveSparseConfig(dependency.sparse, vars);
|
|
274
|
+
const hasPlatformSparse = sparseConfig
|
|
275
|
+
? Object.keys(sparseConfig).some((key) => key !== 'common')
|
|
276
|
+
: Boolean(dependency.sparse && !isSparseOnlyCommon(dependency.sparse));
|
|
277
|
+
const hasPlatformLibraryHistory = requestedPlatforms.length > 0
|
|
278
|
+
&& library?.isGeneral === false
|
|
279
|
+
&& library.platforms.some((platform) => platform !== GENERAL_PLATFORM);
|
|
280
|
+
if (requestedPlatforms.length > 0) {
|
|
281
|
+
const requestedState = await resolveRequestedStoreState(dependency, requestedPlatforms, vars, updateRegistry);
|
|
282
|
+
if (requestedState.actualExisting.length > 0) {
|
|
283
|
+
if (updateRegistry && library?.isGeneral === true) {
|
|
284
|
+
registry.updateLibrary(libKey, {
|
|
285
|
+
platforms: requestedState.actualExisting,
|
|
286
|
+
isGeneral: false,
|
|
287
|
+
lastAccess: new Date().toISOString(),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (requestedPlatforms.length > 0 && library?.isGeneral === true) {
|
|
294
|
+
if (await hasRequestedPlatformContent(dependency, requestedPlatforms, vars)) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (requestedPlatforms.length > 0 && hasPlatformSparse) {
|
|
299
|
+
if (updateRegistry && library?.isGeneral === true) {
|
|
300
|
+
registry.updateLibrary(libKey, {
|
|
301
|
+
isGeneral: false,
|
|
302
|
+
lastAccess: new Date().toISOString(),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
if (hasPlatformLibraryHistory) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
161
310
|
if (typeof library?.isGeneral === 'boolean') {
|
|
162
|
-
|
|
311
|
+
if (library.isGeneral) {
|
|
312
|
+
const generalState = await verifyGeneralStoreState(dependency);
|
|
313
|
+
if (!generalState.valid) {
|
|
314
|
+
if (updateRegistry && (generalState.corrupted || generalState.repairRequired)) {
|
|
315
|
+
await executeLinkStoreRepair(dependency, 'all', { category: 'general-state-repair' });
|
|
316
|
+
}
|
|
317
|
+
if (updateRegistry) {
|
|
318
|
+
registry.updateLibrary(libKey, {
|
|
319
|
+
isGeneral: false,
|
|
320
|
+
lastAccess: new Date().toISOString(),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
const actualGeneral = await store.isGeneralLib(dependency.libName, dependency.commit);
|
|
326
|
+
if (updateRegistry && !actualGeneral) {
|
|
327
|
+
registry.updateLibrary(libKey, {
|
|
328
|
+
isGeneral: false,
|
|
329
|
+
lastAccess: new Date().toISOString(),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return actualGeneral;
|
|
333
|
+
}
|
|
334
|
+
const actualGeneral = await store.isGeneralLib(dependency.libName, dependency.commit);
|
|
335
|
+
if (updateRegistry && actualGeneral) {
|
|
336
|
+
registry.updateLibrary(libKey, {
|
|
337
|
+
platforms: [GENERAL_PLATFORM],
|
|
338
|
+
isGeneral: true,
|
|
339
|
+
lastAccess: new Date().toISOString(),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return actualGeneral;
|
|
163
343
|
}
|
|
164
344
|
return store.isGeneralLib(dependency.libName, dependency.commit);
|
|
165
345
|
}
|
|
@@ -173,7 +353,7 @@ export function resolveUnavailablePlatforms(registry, dependency) {
|
|
|
173
353
|
autoPlatforms,
|
|
174
354
|
};
|
|
175
355
|
}
|
|
176
|
-
export async function collectProjectDependencyGraph(projectRoot) {
|
|
356
|
+
export async function collectProjectDependencyGraph(projectRoot, codepacPlatforms = []) {
|
|
177
357
|
const { normalizedPath, configPath } = await resolveProjectRootPath(projectRoot);
|
|
178
358
|
if (!configPath) {
|
|
179
359
|
throw new Error('只能在项目目录下使用');
|
|
@@ -182,11 +362,17 @@ export async function collectProjectDependencyGraph(projectRoot) {
|
|
|
182
362
|
const seenDeps = new Set();
|
|
183
363
|
const visitedConfigs = new Set();
|
|
184
364
|
const queue = [
|
|
185
|
-
{ configPath, source: 'direct' },
|
|
365
|
+
{ configPath, targetDir: path.dirname(configPath), codepacPlatforms, source: 'direct' },
|
|
186
366
|
];
|
|
187
|
-
const submodules = await findSubmoduleConfigs(normalizedPath);
|
|
367
|
+
const submodules = await findSubmoduleConfigs(normalizedPath, '', 0, codepacPlatforms);
|
|
188
368
|
for (const submodule of submodules) {
|
|
189
|
-
queue.push({
|
|
369
|
+
queue.push({
|
|
370
|
+
configPath: submodule.configPath,
|
|
371
|
+
targetDir: path.dirname(submodule.configPath),
|
|
372
|
+
codepacPlatforms,
|
|
373
|
+
scope: submodule.relativePath,
|
|
374
|
+
source: 'submodule',
|
|
375
|
+
});
|
|
190
376
|
}
|
|
191
377
|
while (queue.length > 0) {
|
|
192
378
|
const current = queue.shift();
|
|
@@ -200,25 +386,32 @@ export async function collectProjectDependencyGraph(projectRoot) {
|
|
|
200
386
|
catch {
|
|
201
387
|
continue;
|
|
202
388
|
}
|
|
203
|
-
|
|
389
|
+
const extractOptions = { platforms: current.codepacPlatforms, configPath: current.configPath };
|
|
390
|
+
for (const dependency of extractDependencies(parsedConfig, extractOptions)) {
|
|
204
391
|
const key = `${dependency.libName}:${dependency.commit}:${current.scope ?? ''}`;
|
|
205
392
|
if (!seenDeps.has(key)) {
|
|
206
393
|
seenDeps.add(key);
|
|
207
394
|
results.push({ ...dependency, scope: current.scope, source: current.source });
|
|
208
395
|
}
|
|
209
396
|
}
|
|
210
|
-
for (const action of extractActions(parsedConfig)) {
|
|
211
|
-
let
|
|
397
|
+
for (const action of extractActions(parsedConfig, extractOptions)) {
|
|
398
|
+
let plan;
|
|
212
399
|
try {
|
|
213
|
-
|
|
400
|
+
plan = buildActionExecutionPlan(action, {
|
|
401
|
+
parentConfigPath: current.configPath,
|
|
402
|
+
parentTargetDir: current.targetDir,
|
|
403
|
+
inheritedCodepacPlatforms: current.codepacPlatforms,
|
|
404
|
+
});
|
|
214
405
|
}
|
|
215
406
|
catch {
|
|
216
407
|
continue;
|
|
217
408
|
}
|
|
218
|
-
const nestedConfigPath = path.resolve(path.dirname(current.configPath), parsedAction.configDir, 'codepac-dep.json');
|
|
219
409
|
let nested;
|
|
220
410
|
try {
|
|
221
|
-
nested = await extractNestedDependencies(nestedConfigPath,
|
|
411
|
+
nested = await extractNestedDependencies(plan.nestedConfigPath, plan.libraries, {
|
|
412
|
+
platforms: plan.effectiveCodepacPlatforms,
|
|
413
|
+
configPath: plan.nestedConfigPath,
|
|
414
|
+
});
|
|
222
415
|
}
|
|
223
416
|
catch {
|
|
224
417
|
continue;
|
|
@@ -230,7 +423,15 @@ export async function collectProjectDependencyGraph(projectRoot) {
|
|
|
230
423
|
results.push({ ...dependency, scope: current.scope, source: 'nested' });
|
|
231
424
|
}
|
|
232
425
|
}
|
|
233
|
-
|
|
426
|
+
if (!plan.parsed.disableAction) {
|
|
427
|
+
queue.push({
|
|
428
|
+
configPath: plan.nestedConfigPath,
|
|
429
|
+
targetDir: plan.nestedTargetDir,
|
|
430
|
+
codepacPlatforms: plan.effectiveCodepacPlatforms,
|
|
431
|
+
scope: current.scope,
|
|
432
|
+
source: 'nested',
|
|
433
|
+
});
|
|
434
|
+
}
|
|
234
435
|
}
|
|
235
436
|
}
|
|
236
437
|
return results;
|
|
@@ -276,6 +477,16 @@ async function selectSubmodules(configs, options, remembered) {
|
|
|
276
477
|
}
|
|
277
478
|
return selected.map(s => ({ ...s }));
|
|
278
479
|
}
|
|
480
|
+
function buildSubmoduleOptionalConfigState(submodules) {
|
|
481
|
+
const state = {};
|
|
482
|
+
for (const submodule of submodules) {
|
|
483
|
+
const names = submodule.selectedOptionalConfigs?.map((config) => config.name) ?? [];
|
|
484
|
+
if (names.length > 0) {
|
|
485
|
+
state[submodule.relativePath] = names;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return Object.keys(state).length > 0 ? state : undefined;
|
|
489
|
+
}
|
|
279
490
|
/**
|
|
280
491
|
* 单个 scope(主项目或 submodule)的链接处理
|
|
281
492
|
*
|
|
@@ -284,6 +495,7 @@ async function selectSubmodules(configs, options, remembered) {
|
|
|
284
495
|
async function linkScope(params) {
|
|
285
496
|
const { scopeName, configPath, platforms, scanExtraPlatforms, registry, tx, projectHash, projectRoot, storePath, download, dryRun, yes, concurrency, gitLightweightDownload, optionalConfigs, scope, } = params;
|
|
286
497
|
let finalLinkPlatforms = [...params.finalLinkPlatforms];
|
|
498
|
+
const codepacPlatforms = [...new Set(platforms.map((platform) => getBaseKeyForCodepac(platform)))];
|
|
287
499
|
// 记录 General 类型库
|
|
288
500
|
const generalLibs = new Set();
|
|
289
501
|
const downloadedLibs = [];
|
|
@@ -292,17 +504,32 @@ async function linkScope(params) {
|
|
|
292
504
|
info(`分析 ${scopeName}: ${configPath}`);
|
|
293
505
|
let dependencies;
|
|
294
506
|
let configVars;
|
|
507
|
+
const selectedActions = [];
|
|
508
|
+
const cacheConfigPaths = [configPath];
|
|
295
509
|
try {
|
|
296
|
-
const result = await parseProjectDependencies(path.dirname(path.dirname(configPath))
|
|
510
|
+
const result = await parseProjectDependencies(path.dirname(path.dirname(configPath)), {
|
|
511
|
+
platforms: codepacPlatforms,
|
|
512
|
+
});
|
|
297
513
|
dependencies = result.dependencies;
|
|
298
514
|
configVars = result.vars;
|
|
515
|
+
const topLevelConfig = await parseCodepacDep(configPath);
|
|
516
|
+
selectedActions.push(...extractActions(topLevelConfig, { platforms: codepacPlatforms, configPath }));
|
|
299
517
|
// 合并可选配置依赖
|
|
300
518
|
if (optionalConfigs && optionalConfigs.length > 0) {
|
|
301
519
|
for (const optionalConfig of optionalConfigs) {
|
|
302
520
|
try {
|
|
303
521
|
const optionalResult = await parseCodepacDep(optionalConfig.path);
|
|
304
|
-
const optionalDeps = extractDependencies(optionalResult
|
|
522
|
+
const optionalDeps = extractDependencies(optionalResult, {
|
|
523
|
+
platforms: codepacPlatforms,
|
|
524
|
+
configPath: optionalConfig.path,
|
|
525
|
+
});
|
|
305
526
|
dependencies = mergeDepLists(dependencies, optionalDeps);
|
|
527
|
+
configVars = { ...configVars, ...optionalResult.vars };
|
|
528
|
+
selectedActions.push(...extractActions(optionalResult, {
|
|
529
|
+
platforms: codepacPlatforms,
|
|
530
|
+
configPath: optionalConfig.path,
|
|
531
|
+
}));
|
|
532
|
+
cacheConfigPaths.push(optionalConfig.path);
|
|
306
533
|
info(` + ${optionalConfig.name}: ${optionalDeps.length} 个依赖`);
|
|
307
534
|
}
|
|
308
535
|
catch (optErr) {
|
|
@@ -319,7 +546,7 @@ async function linkScope(params) {
|
|
|
319
546
|
blank();
|
|
320
547
|
// 2. 分类依赖
|
|
321
548
|
const scopePath = path.dirname(path.dirname(configPath));
|
|
322
|
-
const classified = await classifyDependencies(dependencies, scopePath, configPath, platforms, configVars);
|
|
549
|
+
const classified = await classifyDependencies(dependencies, scopePath, configPath, platforms, configVars, !dryRun);
|
|
323
550
|
const stats = {
|
|
324
551
|
linked: 0, relink: 0, replace: 0, absorb: 0, missing: 0, linkNew: 0,
|
|
325
552
|
};
|
|
@@ -336,14 +563,31 @@ async function linkScope(params) {
|
|
|
336
563
|
}
|
|
337
564
|
// 4. 磁盘空间预检
|
|
338
565
|
if (stats.missing > 0 && download) {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
566
|
+
const downloadRequests = classified
|
|
567
|
+
.filter((item) => item.status === DependencyStatus.MISSING)
|
|
568
|
+
.map((item) => ({
|
|
569
|
+
libName: item.dependency.libName,
|
|
570
|
+
commit: item.dependency.commit,
|
|
571
|
+
platforms: platforms.filter((platform) => !getBlockedRequestedPlatforms(registry, item.dependency, platforms, configVars).includes(platform)),
|
|
572
|
+
}))
|
|
573
|
+
.filter((request) => request.platforms.length > 0);
|
|
574
|
+
const estimate = estimateDownloadSpace(registry, downloadRequests);
|
|
575
|
+
for (const evidence of estimate.evidence)
|
|
576
|
+
debug(`[space] 下载容量依据: ${evidence}`);
|
|
577
|
+
if (estimate.status === 'unknown' || !estimate.amount) {
|
|
578
|
+
warn(`下载容量未知: ${estimate.warnings.join('; ')}`);
|
|
579
|
+
debug(`[space] 下载容量预检跳过: requests=${downloadRequests.length}, status=${estimate.status}, warnings=${estimate.warnings.length}`);
|
|
580
|
+
}
|
|
581
|
+
else if (downloadRequests.length > 0) {
|
|
582
|
+
const spaceCheck = await checkDiskSpace(storePath, estimate.amount.bytes);
|
|
583
|
+
debug(`[space] 下载容量预检: requests=${downloadRequests.length}, estimated=${estimate.amount.bytes}, available=${spaceCheck.available}, required=${spaceCheck.required}, sufficient=${spaceCheck.sufficient}`);
|
|
584
|
+
if (!spaceCheck.sufficient) {
|
|
585
|
+
error(`磁盘空间不足: 预计需要 ${formatSize(spaceCheck.required)},可用 ${formatSize(spaceCheck.available)}(含 1GB 安全余量)`);
|
|
586
|
+
process.exit(EXIT_CODES.IOERR);
|
|
587
|
+
}
|
|
588
|
+
if (spaceCheck.available === 0) {
|
|
589
|
+
warn('无法获取磁盘空间信息,继续执行');
|
|
590
|
+
}
|
|
347
591
|
}
|
|
348
592
|
}
|
|
349
593
|
// 5. 预扫描额外平台(仅主项目 scope)
|
|
@@ -457,17 +701,18 @@ async function linkScope(params) {
|
|
|
457
701
|
}
|
|
458
702
|
// 7. 逐个处理依赖(LINKED/RELINK/REPLACE/ABSORB/LINK_NEW)
|
|
459
703
|
for (const item of classified) {
|
|
460
|
-
const { dependency, status, localPath } = item;
|
|
704
|
+
const { dependency, status, localPath, repairRequiredPlatforms } = item;
|
|
461
705
|
await store.ensureCompatibleStore(storePath, dependency.libName, dependency.commit);
|
|
462
706
|
switch (status) {
|
|
463
707
|
case DependencyStatus.LINKED: {
|
|
464
708
|
const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, configVars);
|
|
465
709
|
const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
|
|
466
|
-
const isLinkedGeneral = await resolveLibraryGeneralState(dependency);
|
|
710
|
+
const isLinkedGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
|
|
467
711
|
if (!isLinkedGeneral) {
|
|
468
712
|
const supplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, {
|
|
469
713
|
vars: configVars,
|
|
470
714
|
useGitLightweightDownload: gitLightweightDownload,
|
|
715
|
+
repairRequiredPlatforms,
|
|
471
716
|
});
|
|
472
717
|
await registerNestedLibraries(supplementResult.nestedLibraries, projectHash);
|
|
473
718
|
if (supplementResult.downloaded.length > 0) {
|
|
@@ -489,7 +734,7 @@ async function linkScope(params) {
|
|
|
489
734
|
const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, configVars);
|
|
490
735
|
const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
|
|
491
736
|
const relinkCommitPath = path.join(storePath, dependency.libName, dependency.commit);
|
|
492
|
-
const isRelinkGeneral = await resolveLibraryGeneralState(dependency);
|
|
737
|
+
const isRelinkGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
|
|
493
738
|
if (isRelinkGeneral) {
|
|
494
739
|
tx.recordOp('unlink', localPath);
|
|
495
740
|
await linker.unlink(localPath);
|
|
@@ -504,6 +749,7 @@ async function linkScope(params) {
|
|
|
504
749
|
const relinkSupplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, {
|
|
505
750
|
vars: configVars,
|
|
506
751
|
useGitLightweightDownload: gitLightweightDownload,
|
|
752
|
+
repairRequiredPlatforms,
|
|
507
753
|
});
|
|
508
754
|
await registerNestedLibraries(relinkSupplementResult.nestedLibraries, projectHash);
|
|
509
755
|
const { actualExisting: relinkExisting, satisfiedRequested, } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
|
|
@@ -547,7 +793,7 @@ async function linkScope(params) {
|
|
|
547
793
|
const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
|
|
548
794
|
const replaceSize = await getDirSize(localPath);
|
|
549
795
|
const replaceCommitPath = path.join(storePath, dependency.libName, dependency.commit);
|
|
550
|
-
const isReplaceGeneral = await resolveLibraryGeneralState(dependency);
|
|
796
|
+
const isReplaceGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
|
|
551
797
|
if (isReplaceGeneral) {
|
|
552
798
|
const sharedPath = path.join(replaceCommitPath, '_shared');
|
|
553
799
|
tx.recordOp('replace', localPath, sharedPath);
|
|
@@ -561,6 +807,7 @@ async function linkScope(params) {
|
|
|
561
807
|
const replaceSupplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, {
|
|
562
808
|
vars: configVars,
|
|
563
809
|
useGitLightweightDownload: gitLightweightDownload,
|
|
810
|
+
repairRequiredPlatforms,
|
|
564
811
|
});
|
|
565
812
|
await registerNestedLibraries(replaceSupplementResult.nestedLibraries, projectHash);
|
|
566
813
|
const { actualExisting: replaceExisting, satisfiedRequested, } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
|
|
@@ -670,6 +917,7 @@ async function linkScope(params) {
|
|
|
670
917
|
const absorbSupplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, {
|
|
671
918
|
vars: configVars,
|
|
672
919
|
useGitLightweightDownload: gitLightweightDownload,
|
|
920
|
+
repairRequiredPlatforms,
|
|
673
921
|
});
|
|
674
922
|
await registerNestedLibraries(absorbSupplementResult.nestedLibraries, projectHash);
|
|
675
923
|
if (absorbSupplementResult.downloaded.length > 0) {
|
|
@@ -736,9 +984,20 @@ async function linkScope(params) {
|
|
|
736
984
|
const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, configVars);
|
|
737
985
|
const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
|
|
738
986
|
const { missingRequested: missing } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
|
|
987
|
+
const repairRequiredSet = new Set(repairRequiredPlatforms ?? []);
|
|
988
|
+
const repairDownloadTargets = missing.filter((platform) => repairRequiredSet.has(platform));
|
|
989
|
+
const isRepairingGeneral = repairRequiredSet.has(GENERAL_PLATFORM);
|
|
739
990
|
const linkNewLibId = `${dependency.libName}@${dependency.commit}`;
|
|
740
991
|
if (missing.length > 0 && !skipAllDownloads && downloadConfirmedLibs.has(linkNewLibId)) {
|
|
741
|
-
|
|
992
|
+
if (repairDownloadTargets.length > 0 || isRepairingGeneral) {
|
|
993
|
+
const repairTargetText = repairDownloadTargets.length > 0
|
|
994
|
+
? repairDownloadTargets.join(', ')
|
|
995
|
+
: GENERAL_PLATFORM;
|
|
996
|
+
debug(`${dependency.libName} ${repairTargetText} 本地缓存不完整,开始重新下载`);
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
info(`${dependency.libName} 缺少平台 [${missing.join(', ')}],开始下载...`);
|
|
1000
|
+
}
|
|
742
1001
|
const linkNewLibKey = registry.getLibraryKey(dependency.libName, dependency.commit);
|
|
743
1002
|
const linkNewHistoryLib = registry.getLibrary(linkNewLibKey);
|
|
744
1003
|
const linkNewMonitor = new DownloadMonitor({
|
|
@@ -754,6 +1013,17 @@ async function linkScope(params) {
|
|
|
754
1013
|
onHeartbeat: (message) => { linkNewMonitor.heartbeat(message); },
|
|
755
1014
|
});
|
|
756
1015
|
}
|
|
1016
|
+
catch (err) {
|
|
1017
|
+
if (repairDownloadTargets.length > 0 || isRepairingGeneral) {
|
|
1018
|
+
const repairTargetText = repairDownloadTargets.length > 0
|
|
1019
|
+
? repairDownloadTargets.join(', ')
|
|
1020
|
+
: GENERAL_PLATFORM;
|
|
1021
|
+
error(`${dependency.libName} ${repairTargetText} 本地缓存不完整,自动重新下载失败`);
|
|
1022
|
+
hint(`可运行 td reset ${dependency.libName} 后重试`);
|
|
1023
|
+
debug(`${dependency.libName} 下载失败: ${err.message}`);
|
|
1024
|
+
}
|
|
1025
|
+
throw err;
|
|
1026
|
+
}
|
|
757
1027
|
finally {
|
|
758
1028
|
await linkNewMonitor.stop();
|
|
759
1029
|
}
|
|
@@ -768,6 +1038,10 @@ async function linkScope(params) {
|
|
|
768
1038
|
tx.recordOp('absorb', linkNewCommitPath, downloadResult.libDir);
|
|
769
1039
|
const linkNewAbsorbResult = await store.absorbLib(downloadResult.libDir, filteredDownloaded, dependency.libName, dependency.commit);
|
|
770
1040
|
await registerNestedLibraries(linkNewAbsorbResult.nestedLibraries, projectHash);
|
|
1041
|
+
const repaired = filteredDownloaded.filter((platform) => repairRequiredSet.has(platform));
|
|
1042
|
+
if (repaired.length > 0) {
|
|
1043
|
+
warn(`${dependency.libName} ${repaired.join(', ')} 本地缓存不完整,已重新下载`);
|
|
1044
|
+
}
|
|
771
1045
|
}
|
|
772
1046
|
}
|
|
773
1047
|
finally {
|
|
@@ -776,14 +1050,19 @@ async function linkScope(params) {
|
|
|
776
1050
|
}
|
|
777
1051
|
const { actualExisting: linkNewExisting, satisfiedRequested, } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
|
|
778
1052
|
clearSatisfiedAutoUnavailablePlatforms(registry, dependency, satisfiedRequested);
|
|
779
|
-
const isLinkNewGeneral = await resolveLibraryGeneralState(dependency);
|
|
1053
|
+
const isLinkNewGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
|
|
780
1054
|
if (isLinkNewGeneral) {
|
|
781
1055
|
const sharedPath = path.join(linkNewCommitPath, '_shared');
|
|
782
1056
|
tx.recordOp('link', localPath, sharedPath);
|
|
783
1057
|
await linker.linkGeneral(localPath, sharedPath);
|
|
784
1058
|
await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, [GENERAL_PLATFORM], projectHash, true);
|
|
785
1059
|
generalLibs.add(dependency.libName);
|
|
786
|
-
|
|
1060
|
+
if (isRepairingGeneral) {
|
|
1061
|
+
warn(`${dependency.libName} ${GENERAL_PLATFORM} 本地缓存不完整,已重新下载`);
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - General 库,创建链接`);
|
|
1065
|
+
}
|
|
787
1066
|
}
|
|
788
1067
|
else if (linkNewExisting.length === 0) {
|
|
789
1068
|
const { KNOWN_PLATFORM_VALUES } = await import('../core/platform.js');
|
|
@@ -833,21 +1112,47 @@ async function linkScope(params) {
|
|
|
833
1112
|
const multiBarManager = isTTY ? new MultiBarManager() : null;
|
|
834
1113
|
const downloadLimit = pLimit(concurrency);
|
|
835
1114
|
const downloadTasks = toDownload.map((item) => downloadLimit(async () => {
|
|
836
|
-
const { dependency, localPath } = item;
|
|
1115
|
+
const { dependency, localPath, repairRequiredPlatforms = [] } = item;
|
|
1116
|
+
const repairRequiredSet = new Set(repairRequiredPlatforms);
|
|
837
1117
|
const pLog = {
|
|
838
1118
|
info: (msg) => multiBarManager ? multiBarManager.log(`[info] ${msg}`) : info(msg),
|
|
839
1119
|
success: (msg) => multiBarManager ? multiBarManager.log(`[ok] ${msg}`) : success(msg),
|
|
840
1120
|
hint: (msg) => multiBarManager ? multiBarManager.log(`[hint] ${msg}`) : hint(msg),
|
|
841
1121
|
warn: (msg) => multiBarManager ? multiBarManager.log(`[warn] ${msg}`) : warn(msg),
|
|
842
|
-
error: (msg) => multiBarManager ? multiBarManager.log(`[
|
|
1122
|
+
error: (msg) => multiBarManager ? multiBarManager.log(`[err] ${msg}`) : error(msg),
|
|
843
1123
|
};
|
|
844
1124
|
try {
|
|
845
1125
|
const storeCommitPath = path.join(storePath, dependency.libName, dependency.commit);
|
|
846
1126
|
const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, configVars);
|
|
847
1127
|
const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
|
|
1128
|
+
if (requestedPlatforms.length === 0) {
|
|
1129
|
+
pLog.warn(`${dependency.libName} 请求平台 [${platforms.join(', ')}] 均已按规则跳过,没有可链接产物`);
|
|
1130
|
+
if (await linker.cleanupEmptyLocalDirectory(localPath)) {
|
|
1131
|
+
pLog.warn(`${dependency.libName} 已清理历史遗留的空本地目录`);
|
|
1132
|
+
}
|
|
1133
|
+
return {
|
|
1134
|
+
success: false,
|
|
1135
|
+
name: dependency.libName,
|
|
1136
|
+
skipped: true,
|
|
1137
|
+
skippedPlatforms: blockedPlatforms,
|
|
1138
|
+
unsupported: true,
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
848
1141
|
const { actualExisting: existing, missingRequested: missing, satisfiedRequested, } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
|
|
849
1142
|
clearSatisfiedAutoUnavailablePlatforms(registry, dependency, satisfiedRequested);
|
|
850
1143
|
if (missing.length === 0) {
|
|
1144
|
+
if (existing.length === 0) {
|
|
1145
|
+
pLog.warn(`${dependency.libName} 请求平台 [${requestedPlatforms.join(', ')}] 在 Store 中没有可链接产物,已拒绝创建空目录`);
|
|
1146
|
+
if (await linker.cleanupEmptyLocalDirectory(localPath)) {
|
|
1147
|
+
pLog.warn(`${dependency.libName} 已清理历史遗留的空本地目录`);
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
success: false,
|
|
1151
|
+
name: dependency.libName,
|
|
1152
|
+
skipped: true,
|
|
1153
|
+
skippedPlatforms: requestedPlatforms,
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
851
1156
|
pLog.info(`${dependency.libName} 所有平台已存在,直接链接...`);
|
|
852
1157
|
tx.recordOp('link', localPath, storeCommitPath);
|
|
853
1158
|
await linker.linkLib(localPath, storeCommitPath, existing);
|
|
@@ -864,9 +1169,22 @@ async function linkScope(params) {
|
|
|
864
1169
|
if (knownUnavailable.length > 0) {
|
|
865
1170
|
pLog.warn(`${dependency.libName} 平台 [${knownUnavailable.join(', ')}] 已按规则跳过`);
|
|
866
1171
|
}
|
|
1172
|
+
if (await linker.cleanupEmptyLocalDirectory(localPath)) {
|
|
1173
|
+
pLog.warn(`${dependency.libName} 已清理历史遗留的空本地目录`);
|
|
1174
|
+
}
|
|
867
1175
|
return { success: false, name: dependency.libName, skipped: true, skippedPlatforms: missing, unsupported: true };
|
|
868
1176
|
}
|
|
869
|
-
|
|
1177
|
+
const repairDownloadTargets = dlToDownload.filter((platform) => repairRequiredSet.has(platform));
|
|
1178
|
+
const isRepairingGeneral = repairRequiredSet.has(GENERAL_PLATFORM);
|
|
1179
|
+
if (repairDownloadTargets.length === 0 && !isRepairingGeneral) {
|
|
1180
|
+
pLog.info(`下载 ${dependency.libName} [${dlToDownload.join(', ')}]...`);
|
|
1181
|
+
}
|
|
1182
|
+
else {
|
|
1183
|
+
const repairTargetText = repairDownloadTargets.length > 0
|
|
1184
|
+
? repairDownloadTargets.join(', ')
|
|
1185
|
+
: GENERAL_PLATFORM;
|
|
1186
|
+
debug(`${dependency.libName} ${repairTargetText} 本地缓存不完整,开始重新下载`);
|
|
1187
|
+
}
|
|
870
1188
|
const estimatedSize = historyLib?.size;
|
|
871
1189
|
const downloadMonitor = new DownloadMonitor({
|
|
872
1190
|
name: ` ${dependency.libName}`, estimatedSize, getDirSize,
|
|
@@ -904,7 +1222,15 @@ async function linkScope(params) {
|
|
|
904
1222
|
await linker.linkGeneral(localPath, sharedPath);
|
|
905
1223
|
await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, [GENERAL_PLATFORM], projectHash, true);
|
|
906
1224
|
generalLibs.add(dependency.libName);
|
|
907
|
-
|
|
1225
|
+
if (isRepairingGeneral) {
|
|
1226
|
+
pLog.warn(`${dependency.libName} ${GENERAL_PLATFORM} 本地缓存不完整,已重新下载`);
|
|
1227
|
+
}
|
|
1228
|
+
else if (repairDownloadTargets.length > 0) {
|
|
1229
|
+
pLog.warn(`${dependency.libName} ${repairDownloadTargets.join(', ')} 本地缓存不完整,已重新下载`);
|
|
1230
|
+
}
|
|
1231
|
+
else {
|
|
1232
|
+
pLog.success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - General 库,下载完成`);
|
|
1233
|
+
}
|
|
908
1234
|
return { success: true, name: dependency.libName, downloadedPlatforms: [GENERAL_PLATFORM], skippedPlatforms: [], isGeneral: true };
|
|
909
1235
|
}
|
|
910
1236
|
const filteredDownloaded = assessment.downloadedRequested;
|
|
@@ -924,6 +1250,10 @@ async function linkScope(params) {
|
|
|
924
1250
|
}
|
|
925
1251
|
const linkPlatforms = [...existing, ...filteredDownloaded];
|
|
926
1252
|
if (linkPlatforms.length === 0) {
|
|
1253
|
+
pLog.warn(`${dependency.libName} 下载后没有生成请求平台 [${requestedPlatforms.join(', ')}] 的可链接产物,已拒绝创建空目录`);
|
|
1254
|
+
if (await linker.cleanupEmptyLocalDirectory(localPath)) {
|
|
1255
|
+
pLog.warn(`${dependency.libName} 已清理历史遗留的空本地目录`);
|
|
1256
|
+
}
|
|
927
1257
|
return {
|
|
928
1258
|
success: false,
|
|
929
1259
|
name: dependency.libName,
|
|
@@ -935,7 +1265,13 @@ async function linkScope(params) {
|
|
|
935
1265
|
await linker.linkLib(localPath, storeCommitPath, linkPlatforms);
|
|
936
1266
|
await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, linkPlatforms, projectHash, false);
|
|
937
1267
|
const notLinkedPlatforms = resolveRequestedPlatformsByActual(requestedPlatforms, dependency.sparse, linkPlatforms, configVars).missingRequested;
|
|
938
|
-
|
|
1268
|
+
const repaired = filteredDownloaded.filter((platform) => repairRequiredSet.has(platform));
|
|
1269
|
+
if (repaired.length > 0) {
|
|
1270
|
+
pLog.warn(`${dependency.libName} ${repaired.join(', ')} 本地缓存不完整,已重新下载`);
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
pLog.success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 下载完成 [${linkPlatforms.join(', ')}]`);
|
|
1274
|
+
}
|
|
939
1275
|
return { success: true, name: dependency.libName, downloadedPlatforms: linkPlatforms, skippedPlatforms: notLinkedPlatforms };
|
|
940
1276
|
}
|
|
941
1277
|
finally {
|
|
@@ -943,8 +1279,22 @@ async function linkScope(params) {
|
|
|
943
1279
|
}
|
|
944
1280
|
}
|
|
945
1281
|
catch (err) {
|
|
946
|
-
|
|
947
|
-
|
|
1282
|
+
const repairTargets = repairRequiredPlatforms.filter((platform) => platforms.includes(platform) || platform === GENERAL_PLATFORM);
|
|
1283
|
+
const repairFailure = repairTargets.length > 0;
|
|
1284
|
+
if (repairTargets.length > 0) {
|
|
1285
|
+
pLog.error(`${dependency.libName} ${repairTargets.join(', ')} 本地缓存不完整,自动重新下载失败`);
|
|
1286
|
+
pLog.hint(`可运行 td reset ${dependency.libName} 后重试`);
|
|
1287
|
+
debug(`${dependency.libName} 下载失败: ${err.message}`);
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
pLog.error(`${dependency.libName} 下载失败: ${err.message}`);
|
|
1291
|
+
}
|
|
1292
|
+
return {
|
|
1293
|
+
success: false,
|
|
1294
|
+
name: dependency.libName,
|
|
1295
|
+
error: err.message,
|
|
1296
|
+
repairFailure,
|
|
1297
|
+
};
|
|
948
1298
|
}
|
|
949
1299
|
}));
|
|
950
1300
|
const results = await Promise.all(downloadTasks);
|
|
@@ -955,6 +1305,12 @@ async function linkScope(params) {
|
|
|
955
1305
|
info(`下载完成: ${succeeded.length}/${toDownload.length} 个库`);
|
|
956
1306
|
if (failed.length > 0)
|
|
957
1307
|
warn(`${failed.length} 个库下载失败`);
|
|
1308
|
+
const failedRepairs = failed.filter((result) => 'repairFailure' in result && result.repairFailure);
|
|
1309
|
+
if (failedRepairs.length > 0) {
|
|
1310
|
+
const names = failedRepairs.map((result) => result.name).join(', ');
|
|
1311
|
+
debug(`[link] 缓存修复下载失败,终止链接: libraries=${names}`);
|
|
1312
|
+
throw new Error(`缓存修复下载失败: ${names}`);
|
|
1313
|
+
}
|
|
958
1314
|
const allSkipped = [];
|
|
959
1315
|
for (const r of results) {
|
|
960
1316
|
if ('skippedPlatforms' in r && r.skippedPlatforms && r.skippedPlatforms.length > 0) {
|
|
@@ -978,26 +1334,32 @@ async function linkScope(params) {
|
|
|
978
1334
|
}
|
|
979
1335
|
}
|
|
980
1336
|
// 9. 处理嵌套依赖 (actions)
|
|
981
|
-
const
|
|
982
|
-
const actions = extractActions(topLevelConfig);
|
|
1337
|
+
const actions = selectedActions;
|
|
983
1338
|
const nestedLinkedDeps = [];
|
|
984
1339
|
if (actions.length > 0) {
|
|
985
1340
|
blank();
|
|
986
1341
|
separator();
|
|
987
1342
|
info(`发现 ${actions.length} 个嵌套依赖配置`);
|
|
988
1343
|
const nestedContext = {
|
|
989
|
-
depth: 0,
|
|
1344
|
+
depth: 0,
|
|
1345
|
+
processedConfigs: new Set([buildActionVisitKey(configPath, path.dirname(configPath))]),
|
|
1346
|
+
platforms,
|
|
1347
|
+
codepacPlatforms,
|
|
1348
|
+
vars: configVars,
|
|
990
1349
|
};
|
|
991
1350
|
const thirdPartyDir = path.dirname(configPath);
|
|
992
1351
|
for (const action of actions) {
|
|
993
|
-
await processAction(action, nestedContext, thirdPartyDir, {
|
|
1352
|
+
await processAction(action, nestedContext, configPath, thirdPartyDir, {
|
|
994
1353
|
tx, registry, projectHash, projectRoot, dryRun, download, yes,
|
|
995
|
-
gitLightweightDownload,
|
|
1354
|
+
gitLightweightDownload, skipActions: new Set(params.skipActions ?? []),
|
|
1355
|
+
generalLibs, downloadedLibs, nestedLinkedDeps,
|
|
996
1356
|
});
|
|
997
1357
|
}
|
|
998
1358
|
}
|
|
999
1359
|
// 10. 同步 cache 文件
|
|
1000
|
-
|
|
1360
|
+
for (const cacheConfigPath of cacheConfigPaths) {
|
|
1361
|
+
await syncCacheFile(cacheConfigPath);
|
|
1362
|
+
}
|
|
1001
1363
|
// 11. 构建返回结果
|
|
1002
1364
|
const linkedDeps = [];
|
|
1003
1365
|
for (const item of classified) {
|
|
@@ -1025,7 +1387,10 @@ async function linkScope(params) {
|
|
|
1025
1387
|
export async function linkProject(projectPath, options) {
|
|
1026
1388
|
const { absolutePath, normalizedPath, configPath: discoveredConfigPath, } = await resolveProjectRootPath(projectPath);
|
|
1027
1389
|
// === 阶段 1: 初始化 ===
|
|
1028
|
-
const cfg = await config.load();
|
|
1390
|
+
const cfg = await config.load({ migrate: !options.dryRun });
|
|
1391
|
+
if (options.dryRun && cfg && config.checkConfigVersion(cfg) === 'migrate') {
|
|
1392
|
+
throw new Error('配置需要迁移;dry-run 不会修改配置,请先运行一次普通命令完成迁移');
|
|
1393
|
+
}
|
|
1029
1394
|
if (cfg?.logLevel)
|
|
1030
1395
|
setLogLevel(cfg.logLevel);
|
|
1031
1396
|
if (cfg?.proxy)
|
|
@@ -1033,7 +1398,7 @@ export async function linkProject(projectPath, options) {
|
|
|
1033
1398
|
const concurrency = cfg?.concurrency ?? 5;
|
|
1034
1399
|
const gitLightweightDownload = cfg?.gitLightweightDownload ?? true;
|
|
1035
1400
|
const registry = getRegistry();
|
|
1036
|
-
await registry.load();
|
|
1401
|
+
await registry.load({ migrate: !options.dryRun });
|
|
1037
1402
|
const existingProject = registry.getProjectByPath(normalizedPath);
|
|
1038
1403
|
const rememberedPlatforms = existingProject?.platforms;
|
|
1039
1404
|
// 确定平台列表
|
|
@@ -1070,9 +1435,10 @@ export async function linkProject(projectPath, options) {
|
|
|
1070
1435
|
error(`路径不存在: ${absolutePath}`);
|
|
1071
1436
|
process.exit(EXIT_CODES.NOINPUT);
|
|
1072
1437
|
}
|
|
1073
|
-
//
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1438
|
+
// 发现可选配置文件,扫描位置跟随实际主配置所在目录。
|
|
1439
|
+
const configDiscovery = discoveredConfigPath
|
|
1440
|
+
? await findAllCodepacConfigs(path.dirname(discoveredConfigPath))
|
|
1441
|
+
: null;
|
|
1076
1442
|
let selectedOptionalConfigs = [];
|
|
1077
1443
|
if (configDiscovery && configDiscovery.optionalConfigs.length > 0) {
|
|
1078
1444
|
if (options.config && options.config.length > 0) {
|
|
@@ -1119,14 +1485,21 @@ export async function linkProject(projectPath, options) {
|
|
|
1119
1485
|
// === 阶段 2: Submodule 检测 ===
|
|
1120
1486
|
let selectedSubmodules = [];
|
|
1121
1487
|
if (options.submodules !== false) {
|
|
1122
|
-
const
|
|
1488
|
+
const codepacPlatforms = [...new Set(platforms.map((platform) => getBaseKeyForCodepac(platform)))];
|
|
1489
|
+
const submoduleConfigs = await findSubmoduleConfigs(normalizedPath, '', 0, codepacPlatforms);
|
|
1123
1490
|
if (submoduleConfigs.length > 0) {
|
|
1124
1491
|
selectedSubmodules = await selectSubmodules(submoduleConfigs, options, existingProject?.submodules);
|
|
1125
1492
|
// 为选中的 submodule 处理可选配置
|
|
1126
1493
|
for (const sub of selectedSubmodules) {
|
|
1127
|
-
if (sub.optionalConfigs.length
|
|
1494
|
+
if (sub.optionalConfigs.length === 0)
|
|
1495
|
+
continue;
|
|
1496
|
+
const rememberedConfigs = existingProject?.submoduleOptionalConfigs?.[sub.relativePath] ?? [];
|
|
1497
|
+
if (options.yes) {
|
|
1498
|
+
sub.selectedOptionalConfigs = sub.optionalConfigs.filter((configInfo) => rememberedConfigs.includes(configInfo.name));
|
|
1499
|
+
}
|
|
1500
|
+
else if (process.stdout.isTTY) {
|
|
1128
1501
|
info(`${sub.name} 发现可选配置:`);
|
|
1129
|
-
const selected = await selectOptionalConfigs(sub.optionalConfigs, { isTTY: true, specifiedConfigs:
|
|
1502
|
+
const selected = await selectOptionalConfigs(sub.optionalConfigs, { isTTY: true, specifiedConfigs: rememberedConfigs });
|
|
1130
1503
|
if (selected !== PROMPT_CANCELLED) {
|
|
1131
1504
|
sub.selectedOptionalConfigs = selected;
|
|
1132
1505
|
}
|
|
@@ -1158,24 +1531,37 @@ export async function linkProject(projectPath, options) {
|
|
|
1158
1531
|
}
|
|
1159
1532
|
}
|
|
1160
1533
|
const finalPath = normalizedRoot;
|
|
1161
|
-
//
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1534
|
+
// 预演模式只读取现状,不恢复历史事务,也不创建新的持久事务日志。
|
|
1535
|
+
if (!options.dryRun) {
|
|
1536
|
+
const pendingTx = await Transaction.findPending();
|
|
1537
|
+
if (pendingTx) {
|
|
1538
|
+
warn(`发现未完成的事务 (${pendingTx.id.slice(0, 8)})`);
|
|
1539
|
+
info('正在尝试回滚...');
|
|
1540
|
+
try {
|
|
1541
|
+
await pendingTx.rollback();
|
|
1542
|
+
success('事务回滚完成');
|
|
1543
|
+
}
|
|
1544
|
+
catch (err) {
|
|
1545
|
+
error(`回滚失败: ${err.message}`);
|
|
1546
|
+
hint('请手动检查 Store 和项目目录状态');
|
|
1547
|
+
}
|
|
1173
1548
|
}
|
|
1174
1549
|
}
|
|
1550
|
+
else {
|
|
1551
|
+
const pendingTransactions = await Transaction.getPendingTransactions();
|
|
1552
|
+
debug(`[link] dry-run 跳过事务恢复与事务日志创建: pending=${pendingTransactions.length}`);
|
|
1553
|
+
}
|
|
1175
1554
|
const storePath = await store.getStorePath();
|
|
1555
|
+
if (!options.dryRun) {
|
|
1556
|
+
const recovered = await new SpaceService(registry, storePath)
|
|
1557
|
+
.createCleanupExecutor()
|
|
1558
|
+
.recoverPending();
|
|
1559
|
+
debug(`[link] 清理日志恢复完成: recovered=${recovered}, store=${storePath}`);
|
|
1560
|
+
}
|
|
1176
1561
|
const projectHash = registry.hashPath(finalPath);
|
|
1177
1562
|
const tx = new Transaction(`link:${finalPath}`);
|
|
1178
|
-
|
|
1563
|
+
if (!options.dryRun)
|
|
1564
|
+
await tx.begin();
|
|
1179
1565
|
// === 阶段 4: 依次执行 linkScope ===
|
|
1180
1566
|
try {
|
|
1181
1567
|
// 主项目
|
|
@@ -1194,6 +1580,7 @@ export async function linkProject(projectPath, options) {
|
|
|
1194
1580
|
concurrency,
|
|
1195
1581
|
gitLightweightDownload,
|
|
1196
1582
|
optionalConfigs: selectedOptionalConfigs,
|
|
1583
|
+
skipActions: options.skipAction ?? [],
|
|
1197
1584
|
});
|
|
1198
1585
|
const finalLinkPlatforms = mainResult.finalLinkPlatforms;
|
|
1199
1586
|
// 如果 dry-run 模式,linkScope 内已显示信息
|
|
@@ -1219,6 +1606,7 @@ export async function linkProject(projectPath, options) {
|
|
|
1219
1606
|
gitLightweightDownload,
|
|
1220
1607
|
optionalConfigs: sub.selectedOptionalConfigs,
|
|
1221
1608
|
scope: sub.relativePath,
|
|
1609
|
+
skipActions: options.skipAction ?? [],
|
|
1222
1610
|
});
|
|
1223
1611
|
}
|
|
1224
1612
|
return;
|
|
@@ -1245,6 +1633,7 @@ export async function linkProject(projectPath, options) {
|
|
|
1245
1633
|
gitLightweightDownload,
|
|
1246
1634
|
optionalConfigs: sub.selectedOptionalConfigs,
|
|
1247
1635
|
scope: sub.relativePath,
|
|
1636
|
+
skipActions: options.skipAction ?? [],
|
|
1248
1637
|
});
|
|
1249
1638
|
subResults.push(subResult);
|
|
1250
1639
|
}
|
|
@@ -1269,6 +1658,7 @@ export async function linkProject(projectPath, options) {
|
|
|
1269
1658
|
? selectedOptionalConfigs.map(c => c.name) : undefined,
|
|
1270
1659
|
submodules: selectedSubmodules.length > 0
|
|
1271
1660
|
? selectedSubmodules.map(s => s.relativePath) : undefined,
|
|
1661
|
+
submoduleOptionalConfigs: buildSubmoduleOptionalConfigState(selectedSubmodules),
|
|
1272
1662
|
});
|
|
1273
1663
|
// 更新 Store 引用关系
|
|
1274
1664
|
const newStoreKeys = allLinkedDeps.map((d) => registry.getStoreKey(d.libName, d.commit, d.platform));
|
|
@@ -1320,52 +1710,44 @@ export async function linkProject(projectPath, options) {
|
|
|
1320
1710
|
if (allSavedBytes > 0) {
|
|
1321
1711
|
info(`本次节省: ${formatSize(allSavedBytes)}`);
|
|
1322
1712
|
}
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1713
|
+
const spaceService = new SpaceService(registry, storePath);
|
|
1714
|
+
const spaceSnapshot = await spaceService.buildSnapshot('summary');
|
|
1715
|
+
info(`Store 总计: ${formatSize(spaceSnapshot.totals.physical.bytes)}`);
|
|
1716
|
+
const cleanupCandidates = spaceService.getCleanupCandidates(spaceSnapshot);
|
|
1717
|
+
const allCleanup = buildUnreferencedSelectionSummary(cleanupCandidates);
|
|
1718
|
+
if (allCleanup.platformCount > 0) {
|
|
1719
|
+
hint(`无引用: ${allCleanup.platformCount} 个 (约 ${formatSize(allCleanup.registeredBytes)}) - 可用 td clean 清理`);
|
|
1329
1720
|
// capacity 策略:检查是否超过阈值
|
|
1330
1721
|
const cleanStrategy = await config.get('cleanStrategy');
|
|
1331
1722
|
if (cleanStrategy === 'capacity') {
|
|
1332
1723
|
const threshold = (await config.get('unreferencedThreshold')) ?? 10 * 1024 * 1024 * 1024;
|
|
1333
|
-
if (
|
|
1724
|
+
if (allCleanup.registeredBytes > threshold) {
|
|
1334
1725
|
blank();
|
|
1335
|
-
warn(`无引用库容量 (${formatSize(
|
|
1726
|
+
warn(`无引用库容量 (约 ${formatSize(allCleanup.registeredBytes)}) 已超过阈值 (${formatSize(threshold)})`);
|
|
1336
1727
|
if (!options.yes && process.stdout.isTTY) {
|
|
1337
|
-
|
|
1338
|
-
const halfCleanStores = registry.getStoresForHalfClean();
|
|
1339
|
-
const halfCleanSize = halfCleanStores.reduce((sum, e) => sum + e.size, 0);
|
|
1728
|
+
const halfCleanup = buildHalfCleanupSelectionSummary(cleanupCandidates);
|
|
1340
1729
|
const action = await selectOption('选择清理方式:', [
|
|
1341
1730
|
{ name: '跳过本次', value: 'skip' },
|
|
1342
1731
|
{
|
|
1343
|
-
name: `清理最久未引用的 (释放约 ${formatSize(
|
|
1732
|
+
name: `清理最久未引用的 (释放约 ${formatSize(halfCleanup.registeredBytes)})`,
|
|
1344
1733
|
value: 'half',
|
|
1345
1734
|
},
|
|
1346
|
-
{ name: `全部清理 (
|
|
1735
|
+
{ name: `全部清理 (释放约 ${formatSize(allCleanup.registeredBytes)})`, value: 'all' },
|
|
1347
1736
|
]);
|
|
1348
1737
|
// ESC 取消视为跳过
|
|
1349
1738
|
if (action !== PROMPT_CANCELLED && action !== 'skip') {
|
|
1350
|
-
const
|
|
1739
|
+
const cleanup = action === 'half' ? halfCleanup : allCleanup;
|
|
1740
|
+
const plan = await spaceService.buildCleanupPlan({
|
|
1741
|
+
reason: 'capacity',
|
|
1742
|
+
selections: cleanup.selections,
|
|
1743
|
+
requireUnreferenced: true,
|
|
1744
|
+
});
|
|
1745
|
+
for (const planWarning of plan.warnings)
|
|
1746
|
+
warn(`[space] ${planWarning}`);
|
|
1351
1747
|
blank();
|
|
1352
|
-
info(
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
for (const entry of toClean) {
|
|
1356
|
-
try {
|
|
1357
|
-
await store.remove(entry.libName, entry.commit, entry.platform);
|
|
1358
|
-
const storeKey = registry.getStoreKey(entry.libName, entry.commit, entry.platform);
|
|
1359
|
-
registry.removeStore(storeKey);
|
|
1360
|
-
freedSize += entry.size;
|
|
1361
|
-
cleaned++;
|
|
1362
|
-
}
|
|
1363
|
-
catch (cleanErr) {
|
|
1364
|
-
warn(`清理 ${entry.libName}/${entry.platform} 失败: ${cleanErr.message}`);
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
await registry.save();
|
|
1368
|
-
success(`清理完成: 删除 ${cleaned} 个库,释放 ${formatSize(freedSize)}`);
|
|
1748
|
+
info(`正在执行清理计划 ${plan.id}...`);
|
|
1749
|
+
const cleanupResult = await spaceService.createCleanupExecutor().execute(plan);
|
|
1750
|
+
success(`清理完成: 删除 ${cleanupResult.succeeded.length} 项,释放 ${formatSize(cleanupResult.released.bytes)}`);
|
|
1369
1751
|
}
|
|
1370
1752
|
}
|
|
1371
1753
|
else {
|
|
@@ -1383,14 +1765,16 @@ export async function linkProject(projectPath, options) {
|
|
|
1383
1765
|
// 链接过程出错,回滚事务
|
|
1384
1766
|
error(`链接失败: ${err.message}`);
|
|
1385
1767
|
hintStoreRepairCommand(err);
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1768
|
+
if (!options.dryRun) {
|
|
1769
|
+
warn('正在回滚事务...');
|
|
1770
|
+
try {
|
|
1771
|
+
await tx.rollback();
|
|
1772
|
+
success('事务已回滚');
|
|
1773
|
+
}
|
|
1774
|
+
catch (rollbackErr) {
|
|
1775
|
+
error(`回滚失败: ${rollbackErr.message}`);
|
|
1776
|
+
hint('请手动检查 Store 和项目目录状态');
|
|
1777
|
+
}
|
|
1394
1778
|
}
|
|
1395
1779
|
process.exit(1);
|
|
1396
1780
|
}
|
|
@@ -1419,10 +1803,7 @@ function hintStoreRepairCommand(err) {
|
|
|
1419
1803
|
async function registerSharedStore(libName, commit, branch = '', url = '') {
|
|
1420
1804
|
const registry = getRegistry();
|
|
1421
1805
|
const storeKey = registry.getStoreKey(libName, commit, SHARED_PLATFORM);
|
|
1422
|
-
|
|
1423
|
-
if (registry.getStore(storeKey)) {
|
|
1424
|
-
return true;
|
|
1425
|
-
}
|
|
1806
|
+
const existingEntry = registry.getStore(storeKey);
|
|
1426
1807
|
// 检查 _shared 目录是否存在
|
|
1427
1808
|
const storePath = await config.getStorePath();
|
|
1428
1809
|
if (!storePath)
|
|
@@ -1440,17 +1821,27 @@ async function registerSharedStore(libName, commit, branch = '', url = '') {
|
|
|
1440
1821
|
}
|
|
1441
1822
|
// 计算 _shared 完整性信息并注册
|
|
1442
1823
|
const integrity = await store.captureIntegrity(libName, commit, SHARED_PLATFORM);
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1824
|
+
if (existingEntry) {
|
|
1825
|
+
registry.updateStore(storeKey, {
|
|
1826
|
+
branch: existingEntry.branch || branch,
|
|
1827
|
+
url: existingEntry.url || url,
|
|
1828
|
+
...integrity,
|
|
1829
|
+
lastAccess: new Date().toISOString(),
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
else {
|
|
1833
|
+
registry.addStore({
|
|
1834
|
+
libName,
|
|
1835
|
+
commit,
|
|
1836
|
+
platform: SHARED_PLATFORM,
|
|
1837
|
+
branch,
|
|
1838
|
+
url,
|
|
1839
|
+
...integrity,
|
|
1840
|
+
usedBy: [], // _shared 不追踪引用,生命周期跟随平台
|
|
1841
|
+
createdAt: new Date().toISOString(),
|
|
1842
|
+
lastAccess: new Date().toISOString(),
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1454
1845
|
debug(`注册 _shared: ${libName}:${commit.slice(0, 8)} (${formatSize(integrity.size)})`);
|
|
1455
1846
|
return true;
|
|
1456
1847
|
}
|
|
@@ -1472,9 +1863,13 @@ async function ensureStoreInfo(libName, commit, platform, branch, url, projectHa
|
|
|
1472
1863
|
lastAccess: new Date().toISOString(),
|
|
1473
1864
|
});
|
|
1474
1865
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
registry.updateStore(storeKey,
|
|
1866
|
+
const currentEntry = registry.getStore(storeKey);
|
|
1867
|
+
if (currentEntry && ((!currentEntry.branch && branch) || (!currentEntry.url && url))) {
|
|
1868
|
+
registry.updateStore(storeKey, {
|
|
1869
|
+
branch: currentEntry.branch || branch,
|
|
1870
|
+
url: currentEntry.url || url,
|
|
1871
|
+
lastAccess: new Date().toISOString(),
|
|
1872
|
+
});
|
|
1478
1873
|
}
|
|
1479
1874
|
if (projectHash) {
|
|
1480
1875
|
registry.addStoreReference(storeKey, projectHash);
|
|
@@ -1687,7 +2082,7 @@ export async function classifyLinkStatus(localPath, storeCommitPath, isGeneral,
|
|
|
1687
2082
|
* @param configPath 配置文件路径
|
|
1688
2083
|
* @param platforms 请求的平台列表
|
|
1689
2084
|
*/
|
|
1690
|
-
async function classifyDependencies(dependencies, projectPath, configPath, platforms, vars) {
|
|
2085
|
+
async function classifyDependencies(dependencies, projectPath, configPath, platforms, vars, repairStore = true) {
|
|
1691
2086
|
const result = [];
|
|
1692
2087
|
const thirdPartyDir = path.dirname(configPath);
|
|
1693
2088
|
const storePath = await store.getStorePath();
|
|
@@ -1701,24 +2096,33 @@ async function classifyDependencies(dependencies, projectPath, configPath, platf
|
|
|
1701
2096
|
const blockedPlatforms = getBlockedRequestedPlatforms(getRegistry(), dep, platforms, vars);
|
|
1702
2097
|
const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
|
|
1703
2098
|
// 检查 Store 中是否有任意请求的平台(支持 sparse 承载目录)
|
|
1704
|
-
const { actualExisting: rawExisting, satisfiedRequested, } = await resolveRequestedStoreState(dep, requestedPlatforms, vars);
|
|
2099
|
+
const { actualExisting: rawExisting, satisfiedRequested, } = await resolveRequestedStoreState(dep, requestedPlatforms, vars, repairStore);
|
|
2100
|
+
const registry = getRegistry();
|
|
2101
|
+
const libKey = registry.getLibraryKey(dep.libName, dep.commit);
|
|
2102
|
+
const library = registry.getLibrary(libKey);
|
|
1705
2103
|
// 完整性校验:验证 existing 平台的文件完整性
|
|
1706
|
-
const { valid: verifiedExisting, corrupted } = await verifyExistingPlatforms(dep.libName, dep.commit, rawExisting);
|
|
2104
|
+
const { valid: verifiedExisting, corrupted, repairRequired } = await verifyExistingPlatforms(dep.libName, dep.commit, rawExisting);
|
|
1707
2105
|
// 清除损坏的平台数据
|
|
1708
|
-
if (corrupted.length > 0) {
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
2106
|
+
if (repairStore && (corrupted.length > 0 || repairRequired.length > 0)) {
|
|
2107
|
+
await executeLinkStoreRepair(dep, [...new Set([...corrupted, ...repairRequired])], { category: 'classified-platform-corruption' });
|
|
2108
|
+
}
|
|
2109
|
+
let existing = verifiedExisting;
|
|
2110
|
+
const repairRequiredPlatforms = [...repairRequired];
|
|
2111
|
+
if (library?.isGeneral === true &&
|
|
2112
|
+
!(await hasRequestedPlatformContent(dep, requestedPlatforms, vars))) {
|
|
2113
|
+
if (repairStore)
|
|
2114
|
+
await prepareGeneralStoreRepair(dep, repairRequiredPlatforms);
|
|
2115
|
+
}
|
|
2116
|
+
if (repairStore && existing.length > 0) {
|
|
2117
|
+
const sharedRepairPlatforms = await prepareSharedStoreRepair(dep, existing, repairRequiredPlatforms);
|
|
2118
|
+
if (sharedRepairPlatforms.length > 0) {
|
|
2119
|
+
existing = existing.filter((platform) => !sharedRepairPlatforms.includes(platform));
|
|
1715
2120
|
}
|
|
1716
2121
|
}
|
|
1717
|
-
const existing = verifiedExisting;
|
|
1718
2122
|
// 也检查是否为 General 库(有 _shared 且有内容)
|
|
1719
|
-
const isGeneral = await resolveLibraryGeneralState(dep);
|
|
2123
|
+
const isGeneral = await resolveLibraryGeneralState(dep, requestedPlatforms, vars, repairStore);
|
|
1720
2124
|
const inStore = existing.length > 0 || isGeneral;
|
|
1721
|
-
if (satisfiedRequested.length > 0) {
|
|
2125
|
+
if (repairStore && satisfiedRequested.length > 0) {
|
|
1722
2126
|
clearSatisfiedAutoUnavailablePlatforms(getRegistry(), dep, satisfiedRequested);
|
|
1723
2127
|
}
|
|
1724
2128
|
// 用于非 inStore 情况的本地路径状态检查
|
|
@@ -1796,6 +2200,7 @@ async function classifyDependencies(dependencies, projectPath, configPath, platf
|
|
|
1796
2200
|
status,
|
|
1797
2201
|
localPath,
|
|
1798
2202
|
storePath: storeLibPath,
|
|
2203
|
+
repairRequiredPlatforms,
|
|
1799
2204
|
});
|
|
1800
2205
|
}
|
|
1801
2206
|
return result;
|
|
@@ -1804,6 +2209,7 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
|
|
|
1804
2209
|
const result = { downloaded: [], unavailable: [], nestedLibraries: [] };
|
|
1805
2210
|
const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, options.vars);
|
|
1806
2211
|
const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
|
|
2212
|
+
const repairRequiredPlatforms = new Set(options.repairRequiredPlatforms ?? []);
|
|
1807
2213
|
// 1. 检查平台完整性
|
|
1808
2214
|
const { missingRequested, satisfiedRequested } = await resolveRequestedStoreState(dependency, requestedPlatforms, options.vars);
|
|
1809
2215
|
const missing = missingRequested;
|
|
@@ -1817,6 +2223,7 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
|
|
|
1817
2223
|
const toDownload = options.forceDownload
|
|
1818
2224
|
? missing
|
|
1819
2225
|
: missing.filter((p) => !unavailablePlatforms.includes(p));
|
|
2226
|
+
const repairDownloadTargets = toDownload.filter((platform) => repairRequiredPlatforms.has(platform));
|
|
1820
2227
|
if (toDownload.length === 0) {
|
|
1821
2228
|
// 所有缺失平台都是已知不可用的
|
|
1822
2229
|
if (missing.length > 0) {
|
|
@@ -1826,7 +2233,12 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
|
|
|
1826
2233
|
return result;
|
|
1827
2234
|
}
|
|
1828
2235
|
// 4. 尝试下载
|
|
1829
|
-
|
|
2236
|
+
if (repairDownloadTargets.length > 0) {
|
|
2237
|
+
debug(`${dependency.libName} ${repairDownloadTargets.join(', ')} 本地缓存不完整,开始重新下载`);
|
|
2238
|
+
}
|
|
2239
|
+
else {
|
|
2240
|
+
info(`${dependency.libName} 缺少平台 [${toDownload.join(', ')}],尝试下载...`);
|
|
2241
|
+
}
|
|
1830
2242
|
try {
|
|
1831
2243
|
const downloadResult = await codepac.downloadToTemp({
|
|
1832
2244
|
url: dependency.url,
|
|
@@ -1848,6 +2260,10 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
|
|
|
1848
2260
|
const downloaded = assessment.downloadedRequested;
|
|
1849
2261
|
result.downloaded = downloaded;
|
|
1850
2262
|
clearSatisfiedAutoUnavailablePlatforms(registry, dependency, assessment.satisfiedRequested);
|
|
2263
|
+
const repaired = downloaded.filter((platform) => repairRequiredPlatforms.has(platform));
|
|
2264
|
+
if (repaired.length > 0) {
|
|
2265
|
+
warn(`${dependency.libName} ${repaired.join(', ')} 本地缓存不完整,已重新下载`);
|
|
2266
|
+
}
|
|
1851
2267
|
// 6. 记录新发现的不可用平台
|
|
1852
2268
|
const notFound = assessment.unavailableRequested;
|
|
1853
2269
|
if (notFound.length > 0) {
|
|
@@ -1887,7 +2303,9 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
|
|
|
1887
2303
|
}
|
|
1888
2304
|
// 注册 _shared 目录(如果存在)
|
|
1889
2305
|
await registerSharedStore(dependency.libName, dependency.commit, dependency.branch, dependency.url);
|
|
1890
|
-
|
|
2306
|
+
if (repaired.length === 0) {
|
|
2307
|
+
success(`${dependency.libName} 已补充平台 [${downloaded.join(', ')}]`);
|
|
2308
|
+
}
|
|
1891
2309
|
}
|
|
1892
2310
|
}
|
|
1893
2311
|
finally {
|
|
@@ -1896,7 +2314,16 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
|
|
|
1896
2314
|
}
|
|
1897
2315
|
}
|
|
1898
2316
|
catch (err) {
|
|
1899
|
-
|
|
2317
|
+
const repairTargets = toDownload.filter((platform) => repairRequiredPlatforms.has(platform));
|
|
2318
|
+
if (repairTargets.length > 0) {
|
|
2319
|
+
error(`${dependency.libName} ${repairTargets.join(', ')} 本地缓存不完整,自动重新下载失败`);
|
|
2320
|
+
hint(`可运行 td reset ${dependency.libName} 后重试`);
|
|
2321
|
+
debug(`${dependency.libName} 下载失败: ${err.message}`);
|
|
2322
|
+
throw err;
|
|
2323
|
+
}
|
|
2324
|
+
else {
|
|
2325
|
+
warn(`${dependency.libName} 下载失败: ${err.message}`);
|
|
2326
|
+
}
|
|
1900
2327
|
}
|
|
1901
2328
|
return result;
|
|
1902
2329
|
}
|
|
@@ -1953,19 +2380,20 @@ function getStatusKey(status) {
|
|
|
1953
2380
|
async function syncCacheFile(configPath) {
|
|
1954
2381
|
const configDir = path.dirname(configPath);
|
|
1955
2382
|
const cacheDir = path.join(configDir, '.cache');
|
|
1956
|
-
const
|
|
2383
|
+
const configFileName = path.basename(configPath);
|
|
2384
|
+
const cachePath = path.join(cacheDir, configFileName);
|
|
1957
2385
|
try {
|
|
1958
2386
|
// 确保 .cache 目录存在
|
|
1959
2387
|
await fs.mkdir(cacheDir, { recursive: true });
|
|
1960
2388
|
// 复制配置文件到 cache
|
|
1961
2389
|
await fs.copyFile(configPath, cachePath);
|
|
1962
2390
|
if (process.env.VERBOSE) {
|
|
1963
|
-
info(`已同步 cache: ${path.
|
|
2391
|
+
info(`已同步 cache: ${configFileName} -> ${path.relative(configDir, cachePath)}`);
|
|
1964
2392
|
}
|
|
1965
2393
|
}
|
|
1966
2394
|
catch (err) {
|
|
1967
2395
|
// cache 同步失败不应阻塞主流程,仅警告
|
|
1968
|
-
warn(`cache 同步失败: ${err.message}`);
|
|
2396
|
+
warn(`cache 同步失败: ${configFileName} -> ${path.relative(configDir, cachePath)}: ${err.message}`);
|
|
1969
2397
|
}
|
|
1970
2398
|
}
|
|
1971
2399
|
/**
|
|
@@ -1975,78 +2403,98 @@ async function syncCacheFile(configPath) {
|
|
|
1975
2403
|
* @param thirdPartyDir 3rdparty 目录路径
|
|
1976
2404
|
* @param options 处理选项
|
|
1977
2405
|
*/
|
|
1978
|
-
async function processAction(action, context,
|
|
2406
|
+
async function processAction(action, context, parentConfigPath, parentTargetDir, options) {
|
|
1979
2407
|
const indent = ' '.repeat(context.depth);
|
|
1980
|
-
|
|
1981
|
-
|
|
2408
|
+
if (action.name && options.skipActions.has(action.name)) {
|
|
2409
|
+
info(`${indent}跳过嵌套 action: name=${action.name}, 原因=skip-action`);
|
|
2410
|
+
debug(`${indent}CodePac action 跳过: name=${action.name}, command=${action.command}, skipActions=${Array.from(options.skipActions).join(',')}`);
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
// 1. 解析 action 命令和执行计划
|
|
2414
|
+
let plan;
|
|
1982
2415
|
try {
|
|
1983
|
-
|
|
2416
|
+
plan = buildActionExecutionPlan(action, {
|
|
2417
|
+
parentConfigPath,
|
|
2418
|
+
parentTargetDir,
|
|
2419
|
+
inheritedCodepacPlatforms: context.codepacPlatforms ?? context.platforms,
|
|
2420
|
+
});
|
|
1984
2421
|
}
|
|
1985
2422
|
catch (err) {
|
|
1986
|
-
warn(`${indent}
|
|
2423
|
+
warn(`${indent}跳过嵌套 action: name=${action.name ?? '(未命名)'}, 原因=无法解析命令, message=${err.message}`);
|
|
2424
|
+
debug(`${indent}CodePac action 解析失败: command=${action.command}`);
|
|
1987
2425
|
return;
|
|
1988
2426
|
}
|
|
1989
|
-
const libsDisplay =
|
|
1990
|
-
info(`${indent}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
if (context.processedConfigs.has(
|
|
1995
|
-
warn(`${indent}
|
|
2427
|
+
const libsDisplay = plan.libraries.length > 0 ? plan.libraries.join(', ') : '全部依赖';
|
|
2428
|
+
info(`${indent}处理嵌套 action: name=${plan.actionName ?? '(未命名)'}, config=${plan.nestedConfigPath}, target=${plan.nestedTargetDir}, libs=[${libsDisplay}]`);
|
|
2429
|
+
debug(`${indent}CodePac action 执行计划: command=${plan.command}, parentConfig=${parentConfigPath}, parentTarget=${parentTargetDir}, parsedConfigDir=${plan.configDir}, parsedTargetDir=${plan.targetDir}, resolvedConfig=${plan.nestedConfigPath}, resolvedTarget=${plan.nestedTargetDir}, inheritedPlatforms=${formatActionPlatforms(plan.inheritedPlatforms)}, effectivePlatforms=${formatActionPlatforms(plan.effectiveCodepacPlatforms)}, explicitPlatform=${plan.parsed.hasExplicitPlatform}, fullgit=${plan.parsed.hasExplicitFullGit}, unshallow=${plan.parsed.hasExplicitUnshallow}, disable_sparse=${plan.parsed.hasExplicitDisableSparse}`);
|
|
2430
|
+
// 2. 循环检测
|
|
2431
|
+
const visitKey = buildActionVisitKey(plan.nestedConfigPath, plan.nestedTargetDir);
|
|
2432
|
+
if (context.processedConfigs.has(visitKey)) {
|
|
2433
|
+
warn(`${indent} 跳过嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=循环配置, config=${plan.nestedConfigPath}, target=${plan.nestedTargetDir}`);
|
|
1996
2434
|
return;
|
|
1997
2435
|
}
|
|
1998
|
-
context.processedConfigs.add(
|
|
1999
|
-
//
|
|
2436
|
+
context.processedConfigs.add(visitKey);
|
|
2437
|
+
// 3. 检查配置文件是否存在
|
|
2000
2438
|
try {
|
|
2001
|
-
await fs.access(nestedConfigPath);
|
|
2439
|
+
await fs.access(plan.nestedConfigPath);
|
|
2002
2440
|
}
|
|
2003
2441
|
catch {
|
|
2004
|
-
warn(`${indent}
|
|
2005
|
-
hint(`${indent} 请确保 ${
|
|
2442
|
+
warn(`${indent} 跳过嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=配置不存在, config=${plan.nestedConfigPath}`);
|
|
2443
|
+
hint(`${indent} 请确保 ${plan.configDir} 库已被下载`);
|
|
2006
2444
|
return;
|
|
2007
2445
|
}
|
|
2008
|
-
//
|
|
2446
|
+
// 4. 提取指定库的依赖
|
|
2009
2447
|
let nestedResult;
|
|
2010
2448
|
try {
|
|
2011
|
-
nestedResult = await extractNestedDependencies(nestedConfigPath,
|
|
2449
|
+
nestedResult = await extractNestedDependencies(plan.nestedConfigPath, plan.libraries, {
|
|
2450
|
+
platforms: plan.effectiveCodepacPlatforms,
|
|
2451
|
+
configPath: plan.nestedConfigPath,
|
|
2452
|
+
});
|
|
2012
2453
|
}
|
|
2013
2454
|
catch (err) {
|
|
2014
|
-
warn(`${indent}
|
|
2455
|
+
warn(`${indent} 跳过嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=解析配置失败, message=${err.message}`);
|
|
2015
2456
|
return;
|
|
2016
2457
|
}
|
|
2017
2458
|
const { dependencies, vars, nestedActions } = nestedResult;
|
|
2018
2459
|
if (dependencies.length === 0) {
|
|
2019
|
-
warn(`${indent}
|
|
2460
|
+
warn(`${indent} 跳过嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=未找到依赖, libs=[${libsDisplay}], config=${plan.nestedConfigPath}`);
|
|
2020
2461
|
return;
|
|
2021
2462
|
}
|
|
2022
|
-
info(`${indent}
|
|
2023
|
-
//
|
|
2463
|
+
info(`${indent} 嵌套 action 命中 ${dependencies.length} 个依赖,继承平台: ${formatActionPlatforms(plan.inheritedPlatforms)}, 生效平台: ${formatActionPlatforms(plan.effectiveCodepacPlatforms)}`);
|
|
2464
|
+
// 5. 合并变量
|
|
2024
2465
|
const mergedVars = { ...context.vars, ...vars };
|
|
2025
|
-
//
|
|
2466
|
+
// 6. 处理这些依赖(targetDir 指定嵌套依赖的目标目录)
|
|
2026
2467
|
await linkNestedDependencies(dependencies, {
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
context: { ...context, vars: mergedVars },
|
|
2468
|
+
targetDir: plan.nestedTargetDir,
|
|
2469
|
+
nestedConfigPath: plan.nestedConfigPath,
|
|
2470
|
+
context: { ...context, codepacPlatforms: plan.effectiveCodepacPlatforms, vars: mergedVars },
|
|
2031
2471
|
options,
|
|
2032
2472
|
indent,
|
|
2033
2473
|
});
|
|
2034
2474
|
// 同步嵌套配置文件的 cache(兼容 checkValid.js 检测)
|
|
2035
|
-
await syncCacheFile(nestedConfigPath);
|
|
2036
|
-
//
|
|
2037
|
-
|
|
2038
|
-
if (!parsed.disableAction && nestedActions.length > 0) {
|
|
2475
|
+
await syncCacheFile(plan.nestedConfigPath);
|
|
2476
|
+
// 7. 递归处理嵌套 actions(如果没有 disable_action)
|
|
2477
|
+
if (!plan.parsed.disableAction && nestedActions.length > 0) {
|
|
2039
2478
|
const nestedContext = {
|
|
2040
2479
|
depth: context.depth + 1,
|
|
2041
2480
|
processedConfigs: context.processedConfigs,
|
|
2042
2481
|
platforms: context.platforms,
|
|
2482
|
+
codepacPlatforms: plan.effectiveCodepacPlatforms,
|
|
2043
2483
|
vars: mergedVars,
|
|
2044
2484
|
};
|
|
2045
|
-
const nestedThirdPartyDir = path.join(thirdPartyDir, parsed.targetDir);
|
|
2046
2485
|
for (const nestedAction of nestedActions) {
|
|
2047
|
-
await processAction(nestedAction, nestedContext,
|
|
2486
|
+
await processAction(nestedAction, nestedContext, plan.nestedConfigPath, plan.nestedTargetDir, options);
|
|
2048
2487
|
}
|
|
2049
2488
|
}
|
|
2489
|
+
else if (plan.parsed.disableAction && nestedActions.length > 0) {
|
|
2490
|
+
info(`${indent} 跳过后续嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=disable_action`);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
function formatActionPlatforms(platforms) {
|
|
2494
|
+
return platforms && platforms.length > 0 ? platforms.join(',') : 'all';
|
|
2495
|
+
}
|
|
2496
|
+
function buildActionVisitKey(configPath, targetDir) {
|
|
2497
|
+
return `${configPath}::${targetDir}`;
|
|
2050
2498
|
}
|
|
2051
2499
|
/**
|
|
2052
2500
|
* 统一的本地目录验证+吸收流程
|
|
@@ -2104,9 +2552,7 @@ async function resolveLocalAndAbsorb(localPath, libName, commit, options) {
|
|
|
2104
2552
|
* 链接嵌套依赖
|
|
2105
2553
|
*/
|
|
2106
2554
|
async function linkNestedDependencies(dependencies, params) {
|
|
2107
|
-
const {
|
|
2108
|
-
// 计算嵌套依赖的实际目标目录
|
|
2109
|
-
const nestedTargetDir = path.join(thirdPartyDir, targetDir);
|
|
2555
|
+
const { targetDir: nestedTargetDir, context, options, indent } = params;
|
|
2110
2556
|
const { tx, registry, projectHash, projectRoot, dryRun, download, generalLibs, downloadedLibs, nestedLinkedDeps } = options;
|
|
2111
2557
|
const { platforms, vars } = context;
|
|
2112
2558
|
for (const dep of dependencies) {
|
|
@@ -2130,7 +2576,8 @@ async function linkNestedDependencies(dependencies, params) {
|
|
|
2130
2576
|
const availablePlatforms = platforms.filter(p => !unavailablePlatforms.includes(p));
|
|
2131
2577
|
const knownUnavailable = platforms.filter(p => unavailablePlatforms.includes(p));
|
|
2132
2578
|
let storeHas = false;
|
|
2133
|
-
|
|
2579
|
+
let existingPlatforms = [];
|
|
2580
|
+
let repairRequiredPlatforms = [];
|
|
2134
2581
|
const initialStoreState = await resolveRequestedStoreState(dep, availablePlatforms, vars);
|
|
2135
2582
|
existingPlatforms.push(...initialStoreState.actualExisting);
|
|
2136
2583
|
if (initialStoreState.satisfiedRequested.length > 0) {
|
|
@@ -2139,18 +2586,31 @@ async function linkNestedDependencies(dependencies, params) {
|
|
|
2139
2586
|
storeHas = existingPlatforms.length > 0;
|
|
2140
2587
|
// 完整性校验:验证 existing 平台的文件完整性(与 classifyDependencies 同逻辑)
|
|
2141
2588
|
if (existingPlatforms.length > 0) {
|
|
2142
|
-
const { valid: verifiedPlatforms, corrupted } = await verifyExistingPlatforms(dep.libName, dep.commit, existingPlatforms);
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2589
|
+
const { valid: verifiedPlatforms, corrupted, repairRequired } = await verifyExistingPlatforms(dep.libName, dep.commit, existingPlatforms);
|
|
2590
|
+
existingPlatforms = verifiedPlatforms;
|
|
2591
|
+
repairRequiredPlatforms = repairRequired;
|
|
2592
|
+
if (corrupted.length > 0 || repairRequired.length > 0) {
|
|
2593
|
+
await executeLinkStoreRepair(dep, [...new Set([...corrupted, ...repairRequired])], { category: 'nested-platform-corruption' });
|
|
2594
|
+
}
|
|
2595
|
+
storeHas = existingPlatforms.length > 0;
|
|
2596
|
+
}
|
|
2597
|
+
const nestedLibKey = registry.getLibraryKey(dep.libName, dep.commit);
|
|
2598
|
+
const nestedLibrary = registry.getLibrary(nestedLibKey);
|
|
2599
|
+
if (nestedLibrary?.isGeneral === true &&
|
|
2600
|
+
!(await hasRequestedPlatformContent(dep, availablePlatforms, vars))) {
|
|
2601
|
+
const generalNeedsRepair = await prepareGeneralStoreRepair(dep, repairRequiredPlatforms);
|
|
2602
|
+
if (generalNeedsRepair) {
|
|
2603
|
+
storeHas = existingPlatforms.length > 0;
|
|
2151
2604
|
}
|
|
2152
2605
|
}
|
|
2153
|
-
|
|
2606
|
+
if (existingPlatforms.length > 0) {
|
|
2607
|
+
const sharedRepairPlatforms = await prepareSharedStoreRepair(dep, existingPlatforms, repairRequiredPlatforms);
|
|
2608
|
+
if (sharedRepairPlatforms.length > 0) {
|
|
2609
|
+
existingPlatforms = existingPlatforms.filter((platform) => !sharedRepairPlatforms.includes(platform));
|
|
2610
|
+
}
|
|
2611
|
+
storeHas = existingPlatforms.length > 0;
|
|
2612
|
+
}
|
|
2613
|
+
let isGeneral = await resolveLibraryGeneralState(dep, availablePlatforms, vars);
|
|
2154
2614
|
// 如果所有平台都已知不可用,跳过
|
|
2155
2615
|
if (availablePlatforms.length === 0 && knownUnavailable.length > 0 && !isGeneral) {
|
|
2156
2616
|
warn(`${indent} ${dep.libName} - 平台 [${knownUnavailable.join(', ')}] 已按规则跳过`);
|
|
@@ -2171,10 +2631,21 @@ async function linkNestedDependencies(dependencies, params) {
|
|
|
2171
2631
|
if (resolveResult.isGeneral) {
|
|
2172
2632
|
isGeneral = true;
|
|
2173
2633
|
}
|
|
2634
|
+
else if (resolveResult.absorbResult) {
|
|
2635
|
+
const absorbedPlatforms = resolveResult.absorbResult.platforms;
|
|
2636
|
+
const absorbedPlatformSet = new Set(absorbedPlatforms);
|
|
2637
|
+
const linkableAbsorbedPlatforms = [...new Set(availablePlatforms.flatMap((platform) => getRequestedPlatformTargets(platform, dep.sparse, vars)
|
|
2638
|
+
.filter((target) => absorbedPlatformSet.has(target))))];
|
|
2639
|
+
existingPlatforms = [...new Set([
|
|
2640
|
+
...existingPlatforms,
|
|
2641
|
+
...linkableAbsorbedPlatforms,
|
|
2642
|
+
])];
|
|
2643
|
+
debug(`${indent} ${dep.libName} - 本地吸收后平台回填: requested=${availablePlatforms.join(', ') || '无'}, absorbed=${absorbedPlatforms.join(', ') || '无'}, linkable=${existingPlatforms.join(', ') || '无'}`);
|
|
2644
|
+
}
|
|
2174
2645
|
if (resolveResult.absorbResult?.nestedLibraries.length) {
|
|
2175
2646
|
await registerNestedLibraries(resolveResult.absorbResult.nestedLibraries, projectHash);
|
|
2176
2647
|
}
|
|
2177
|
-
storeHas =
|
|
2648
|
+
storeHas = isGeneral || existingPlatforms.length > 0;
|
|
2178
2649
|
// 吸收为 General 后,检查是否实际需要平台内容
|
|
2179
2650
|
// 本地可能只有部分文件(如只有 _shared 内容),被误分类为 General
|
|
2180
2651
|
if (resolveResult.isGeneral && download) {
|
|
@@ -2229,7 +2700,7 @@ async function linkNestedDependencies(dependencies, params) {
|
|
|
2229
2700
|
let linkedPlatforms = isGeneral ? [GENERAL_PLATFORM] : [...existingPlatforms];
|
|
2230
2701
|
// 已链接,但需要检查是否需要补充缺失平台(与顶层依赖逻辑一致)
|
|
2231
2702
|
if (!isGeneral) {
|
|
2232
|
-
const supplementResult = await supplementMissingPlatforms(dep, platforms, registry, tx, { vars, useGitLightweightDownload: options.gitLightweightDownload });
|
|
2703
|
+
const supplementResult = await supplementMissingPlatforms(dep, platforms, registry, tx, { vars, useGitLightweightDownload: options.gitLightweightDownload, repairRequiredPlatforms });
|
|
2233
2704
|
// 注册嵌套依赖
|
|
2234
2705
|
if (supplementResult.nestedLibraries.length > 0) {
|
|
2235
2706
|
await registerNestedLibraries(supplementResult.nestedLibraries, projectHash);
|
|
@@ -2300,7 +2771,7 @@ async function linkNestedDependencies(dependencies, params) {
|
|
|
2300
2771
|
}
|
|
2301
2772
|
else {
|
|
2302
2773
|
// 平台库:先补充缺失平台(与顶层依赖逻辑一致)
|
|
2303
|
-
const supplementResult = await supplementMissingPlatforms(dep, platforms, registry, tx, { vars, useGitLightweightDownload: options.gitLightweightDownload });
|
|
2774
|
+
const supplementResult = await supplementMissingPlatforms(dep, platforms, registry, tx, { vars, useGitLightweightDownload: options.gitLightweightDownload, repairRequiredPlatforms });
|
|
2304
2775
|
// 注册嵌套依赖
|
|
2305
2776
|
if (supplementResult.nestedLibraries.length > 0) {
|
|
2306
2777
|
await registerNestedLibraries(supplementResult.nestedLibraries, projectHash);
|
|
@@ -2326,7 +2797,13 @@ async function linkNestedDependencies(dependencies, params) {
|
|
|
2326
2797
|
}
|
|
2327
2798
|
else if (download) {
|
|
2328
2799
|
// Store 没有,需要下载
|
|
2329
|
-
|
|
2800
|
+
const repairTargets = repairRequiredPlatforms.filter((platform) => availablePlatforms.includes(platform) || platform === GENERAL_PLATFORM);
|
|
2801
|
+
if (repairTargets.length > 0) {
|
|
2802
|
+
debug(`${indent} ${dep.libName} ${repairTargets.join(', ')} 本地缓存不完整,开始重新下载`);
|
|
2803
|
+
}
|
|
2804
|
+
else {
|
|
2805
|
+
info(`${indent} ${dep.libName} - 下载中...`);
|
|
2806
|
+
}
|
|
2330
2807
|
if (dryRun) {
|
|
2331
2808
|
info(`${indent} ${dep.libName} - 将下载`);
|
|
2332
2809
|
continue;
|
|
@@ -2384,7 +2861,12 @@ async function linkNestedDependencies(dependencies, params) {
|
|
|
2384
2861
|
platform: GENERAL_PLATFORM,
|
|
2385
2862
|
linkedPath: path.relative(projectRoot, localPath),
|
|
2386
2863
|
});
|
|
2387
|
-
|
|
2864
|
+
if (repairTargets.includes(GENERAL_PLATFORM)) {
|
|
2865
|
+
warn(`${indent} ${dep.libName} ${GENERAL_PLATFORM} 本地缓存不完整,已重新下载`);
|
|
2866
|
+
}
|
|
2867
|
+
else {
|
|
2868
|
+
success(`${indent} ${dep.libName} - 下载完成 (General)`);
|
|
2869
|
+
}
|
|
2388
2870
|
}
|
|
2389
2871
|
else if (assessment.downloadedRequested.length > 0) {
|
|
2390
2872
|
// 平台库处理
|
|
@@ -2413,7 +2895,13 @@ async function linkNestedDependencies(dependencies, params) {
|
|
|
2413
2895
|
});
|
|
2414
2896
|
warn(`${indent} ${dep.libName} 平台 [${newUnavailable.join(', ')}] 远程不存在,已记录`);
|
|
2415
2897
|
}
|
|
2416
|
-
|
|
2898
|
+
const repaired = assessment.downloadedRequested.filter((platform) => repairRequiredPlatforms.includes(platform));
|
|
2899
|
+
if (repaired.length > 0) {
|
|
2900
|
+
warn(`${indent} ${dep.libName} ${repaired.join(', ')} 本地缓存不完整,已重新下载`);
|
|
2901
|
+
}
|
|
2902
|
+
else {
|
|
2903
|
+
success(`${indent} ${dep.libName} - 下载完成 [${assessment.downloadedRequested.join(', ')}]`);
|
|
2904
|
+
}
|
|
2417
2905
|
}
|
|
2418
2906
|
else {
|
|
2419
2907
|
// 所有请求的平台都不可用,记录到 registry
|
|
@@ -2433,7 +2921,15 @@ async function linkNestedDependencies(dependencies, params) {
|
|
|
2433
2921
|
}
|
|
2434
2922
|
}
|
|
2435
2923
|
catch (err) {
|
|
2436
|
-
|
|
2924
|
+
if (repairTargets.length > 0) {
|
|
2925
|
+
error(`${indent} ${dep.libName} ${repairTargets.join(', ')} 本地缓存不完整,自动重新下载失败`);
|
|
2926
|
+
hint(`${indent} 可运行 td reset ${dep.libName} 后重试`);
|
|
2927
|
+
debug(`${indent} ${dep.libName} 下载失败: ${err.message}`);
|
|
2928
|
+
throw err;
|
|
2929
|
+
}
|
|
2930
|
+
else {
|
|
2931
|
+
warn(`${indent} ${dep.libName} - 下载失败: ${err.message}`);
|
|
2932
|
+
}
|
|
2437
2933
|
}
|
|
2438
2934
|
}
|
|
2439
2935
|
else {
|
|
@@ -2464,53 +2960,53 @@ function mergeDepLists(main, optional) {
|
|
|
2464
2960
|
return Array.from(depMap.values());
|
|
2465
2961
|
}
|
|
2466
2962
|
/**
|
|
2467
|
-
*
|
|
2468
|
-
*
|
|
2469
|
-
*
|
|
2470
|
-
* 没有 fileCount 的条目跳过校验(视为合法)。
|
|
2963
|
+
* 校验已有平台的完整性。
|
|
2964
|
+
* 只有带 fileCount 和 contentHash 的 StoreEntry 才被视为可验证缓存。
|
|
2965
|
+
* 缺失完整性记录的旧缓存不能证明自身完整,会被标记为需要重新下载。
|
|
2471
2966
|
*
|
|
2472
2967
|
* @param libName 库名
|
|
2473
2968
|
* @param commit commit hash
|
|
2474
2969
|
* @param existingPlatforms Store 中已有的平台列表
|
|
2475
|
-
* @returns { valid: string[]; corrupted: string[] }
|
|
2970
|
+
* @returns { valid: string[]; corrupted: string[]; repairRequired: string[] } 合法、损坏和需要重下的平台列表
|
|
2476
2971
|
*/
|
|
2477
2972
|
export async function verifyExistingPlatforms(libName, commit, existingPlatforms) {
|
|
2478
2973
|
const valid = [];
|
|
2479
2974
|
const corrupted = [];
|
|
2975
|
+
const repairRequired = [];
|
|
2480
2976
|
const registry = getRegistry();
|
|
2481
2977
|
for (const platform of existingPlatforms) {
|
|
2482
2978
|
const storeKey = registry.getStoreKey(libName, commit, platform);
|
|
2483
2979
|
const entry = registry.getStore(storeKey);
|
|
2484
|
-
|
|
2980
|
+
const visiblePlatform = platform === GENERAL_PLATFORM ? '_shared' : platform;
|
|
2485
2981
|
if (!entry) {
|
|
2486
|
-
|
|
2982
|
+
repairRequired.push(platform);
|
|
2983
|
+
debug(`${libName} (${commit.slice(0, 7)}) ${visiblePlatform}: Store 缺少 Registry 记录,准备重新下载`);
|
|
2487
2984
|
continue;
|
|
2488
2985
|
}
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
}
|
|
2498
|
-
valid.push(platform);
|
|
2986
|
+
if (entry.fileCount === 0) {
|
|
2987
|
+
corrupted.push(platform);
|
|
2988
|
+
warn(`${libName} (${commit.slice(0, 7)}) ${visiblePlatform}: Store 记录为空`);
|
|
2989
|
+
continue;
|
|
2990
|
+
}
|
|
2991
|
+
if (entry.fileCount == null || !entry.contentHash) {
|
|
2992
|
+
repairRequired.push(platform);
|
|
2993
|
+
debug(`${libName} (${commit.slice(0, 7)}) ${visiblePlatform}: Store 缺少完整性记录,准备重新下载`);
|
|
2499
2994
|
continue;
|
|
2500
2995
|
}
|
|
2501
|
-
const result = await store.
|
|
2996
|
+
const result = await store.fullVerify(libName, commit, platform, {
|
|
2502
2997
|
size: entry.size,
|
|
2503
2998
|
fileCount: entry.fileCount,
|
|
2999
|
+
contentHash: entry.contentHash,
|
|
2504
3000
|
});
|
|
2505
3001
|
if (result.valid) {
|
|
2506
3002
|
valid.push(platform);
|
|
2507
3003
|
}
|
|
2508
3004
|
else {
|
|
2509
3005
|
corrupted.push(platform);
|
|
2510
|
-
warn(`${libName} (${commit.slice(0, 7)}) ${
|
|
3006
|
+
warn(`${libName} (${commit.slice(0, 7)}) ${visiblePlatform}: 完整性校验失败 - ${result.reason}`);
|
|
2511
3007
|
}
|
|
2512
3008
|
}
|
|
2513
|
-
return { valid, corrupted };
|
|
3009
|
+
return { valid, corrupted, repairRequired };
|
|
2514
3010
|
}
|
|
2515
3011
|
/**
|
|
2516
3012
|
* 创建带完整性信息的 StoreEntry
|