ttsc 0.26.2 → 0.28.0
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 +9 -0
- package/cmd/graphbench/main.go +26 -16
- package/cmd/graphdump/main.go +37 -12
- package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
- package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
- package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
- package/cmd/ttsc/api_transform.go +28 -3
- package/cmd/ttscgraph/auxiliary_identity_state_tracks_retargets_not_source_contents_test.go +85 -0
- package/cmd/ttscgraph/disk_realpath_other.go +16 -0
- package/cmd/ttscgraph/disk_realpath_windows.go +56 -0
- package/cmd/ttscgraph/dump.go +43 -11
- package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
- package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
- package/cmd/ttscgraph/serve.go +357 -60
- package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
- package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
- package/cmd/ttscgraph/serve_shards.go +31 -9
- package/driver/config_discovery.go +121 -0
- package/driver/emit_plugin.go +77 -11
- package/driver/graph.go +64 -11
- package/driver/input_observation.go +273 -0
- package/driver/input_observation_test.go +149 -0
- package/driver/plugins.go +506 -15
- package/driver/program.go +54 -1
- package/driver/resolution_candidates.go +13 -5
- package/driver/rewrite.go +45 -2
- package/driver/transform_dependencies.go +283 -0
- package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
- package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
- package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
- package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
- package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
- package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
- package/internal/graph/artifacts.go +230 -0
- package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
- package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
- package/internal/graph/build.go +35 -0
- package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
- package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
- package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
- package/internal/graph/doc_tags.go +416 -0
- package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
- package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
- package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
- package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
- package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
- package/internal/graph/dump.go +60 -13
- package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
- package/internal/graph/dump_paths.go +2 -2
- package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
- package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
- package/internal/graph/edges.go +1 -0
- package/internal/graph/graph.go +79 -4
- package/internal/graph/provenance.go +29 -2
- package/lib/TtscCompiler.d.ts +12 -11
- package/lib/TtscCompiler.js +44 -29
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/captureProcessOutput.js +2 -2
- package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.js +4 -3
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +9 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.js +3 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +1 -1
- package/lib/compiler/internal/runBuild.js +80 -34
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +2 -2
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +28 -0
- package/lib/compiler/internal/sharedHostHelpers.js +39 -0
- package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.js +12 -4
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -0
- package/lib/compiler/internal/transformProjectInMemory.js +239 -35
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/flags/schema.js +12 -5
- package/lib/flags/schema.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/internal/createCanonicalTempDirectory.d.ts +17 -0
- package/lib/internal/createCanonicalTempDirectory.js +42 -0
- package/lib/internal/createCanonicalTempDirectory.js.map +1 -0
- package/lib/internal/projectInputPathIdentity.d.ts +3 -0
- package/lib/internal/projectInputPathIdentity.js +11 -5
- package/lib/internal/projectInputPathIdentity.js.map +1 -1
- package/lib/internal/resolveNodeBinary.d.ts +14 -0
- package/lib/internal/resolveNodeBinary.js +150 -0
- package/lib/internal/resolveNodeBinary.js.map +1 -0
- package/lib/internal/resolveSafeCacheCleanupTargets.d.ts +15 -0
- package/lib/internal/resolveSafeCacheCleanupTargets.js +61 -0
- package/lib/internal/resolveSafeCacheCleanupTargets.js.map +1 -0
- package/lib/launcher/internal/prepareExecution.d.ts +3 -0
- package/lib/launcher/internal/prepareExecution.js +42 -14
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +25 -13
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +18 -3
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +2 -0
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +47 -3
- package/lib/launcher/internal/runtimeHooks.js +746 -64
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.d.ts +62 -8
- package/lib/plugin/internal/buildSourcePlugin.js +754 -72
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/capabilityResolutionCache.d.ts +76 -0
- package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
- package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
- package/lib/plugin/internal/loadProjectPlugins.d.ts +17 -5
- package/lib/plugin/internal/loadProjectPlugins.js +1339 -77
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/plugin/resolveCapabilityPlugins.d.ts +55 -0
- package/lib/plugin/resolveCapabilityPlugins.js +118 -0
- package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
- package/lib/register.d.ts +1 -0
- package/lib/register.js +46 -0
- package/lib/register.js.map +1 -0
- package/lib/structures/ITtscCompilerContext.d.ts +6 -3
- package/lib/structures/ITtscCompilerTransformation.d.ts +38 -0
- package/lib/structures/ITtscPlugin.d.ts +33 -0
- package/package.json +16 -9
- package/shim/ast/parent.go +16 -3
- package/shim/checker/enums_gen.go +11 -0
- package/shim/checker/go.mod +9 -0
- package/shim/checker/go.sum +22 -0
- package/shim/checker/shim.go +78 -0
- package/shim/compiler/emithook.go +10 -0
- package/shim/compiler/incremental.go +45 -8
- package/src/TtscCompiler.ts +55 -33
- package/src/compiler/internal/captureProcessOutput.ts +3 -2
- package/src/compiler/internal/compileProjectInMemory.ts +4 -3
- package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
- package/src/compiler/internal/resolveEmittedJavaScript.ts +6 -0
- package/src/compiler/internal/runBuild.ts +102 -39
- package/src/compiler/internal/runSingleFileEmit.ts +2 -2
- package/src/compiler/internal/sharedHostHelpers.ts +44 -0
- package/src/compiler/internal/startResidentTransform.ts +12 -3
- package/src/compiler/internal/transformProjectInMemory.ts +410 -39
- package/src/flags/schema.ts +12 -5
- package/src/index.ts +1 -0
- package/src/internal/createCanonicalTempDirectory.ts +53 -0
- package/src/internal/projectInputPathIdentity.ts +17 -4
- package/src/internal/resolveNodeBinary.ts +173 -0
- package/src/internal/resolveSafeCacheCleanupTargets.ts +85 -0
- package/src/launcher/internal/prepareExecution.ts +50 -14
- package/src/launcher/internal/runTtsc.ts +24 -13
- package/src/launcher/internal/runTtscserver.ts +18 -3
- package/src/launcher/internal/runTtsx.ts +2 -0
- package/src/launcher/internal/runtimeHooks.ts +934 -68
- package/src/plugin/internal/buildSourcePlugin.ts +984 -79
- package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
- package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
- package/src/plugin/resolveCapabilityPlugins.ts +171 -0
- package/src/register.ts +47 -0
- package/src/structures/ITtscCompilerContext.ts +6 -3
- package/src/structures/ITtscCompilerTransformation.ts +46 -0
- package/src/structures/ITtscPlugin.ts +36 -0
- package/tools/shim_audit/baseline.json +0 -2
- package/utility/host.go +18 -3
- package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +0 -5
- package/lib/internal/assertSafeExplicitCacheDirectory.js +0 -26
- package/lib/internal/assertSafeExplicitCacheDirectory.js.map +0 -1
- package/src/internal/assertSafeExplicitCacheDirectory.ts +0 -29
|
@@ -14,12 +14,16 @@ exports.formatGoWorkPath = formatGoWorkPath;
|
|
|
14
14
|
exports.autoQuoteGoModToken = autoQuoteGoModToken;
|
|
15
15
|
exports.spawnGoTool = spawnGoTool;
|
|
16
16
|
exports.windowsGoCommandArgs = windowsGoCommandArgs;
|
|
17
|
+
exports.ensureExecutableGoToolchain = ensureExecutableGoToolchain;
|
|
17
18
|
exports.resolvePluginCacheRoot = resolvePluginCacheRoot;
|
|
18
19
|
exports.resolveSourceBuildCachePaths = resolveSourceBuildCachePaths;
|
|
19
20
|
exports.resolveCleanTargets = resolveCleanTargets;
|
|
20
21
|
exports.legacyGlobalCacheTargets = legacyGlobalCacheTargets;
|
|
21
22
|
exports.isPathWithin = isPathWithin;
|
|
22
23
|
exports.computeCacheKey = computeCacheKey;
|
|
24
|
+
exports.prunePluginCacheRoot = prunePluginCacheRoot;
|
|
25
|
+
exports.pruneGoBuildCacheRoot = pruneGoBuildCacheRoot;
|
|
26
|
+
exports.withGoBuildCacheLease = withGoBuildCacheLease;
|
|
23
27
|
const node_child_process_1 = require("node:child_process");
|
|
24
28
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
25
29
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -27,8 +31,10 @@ const node_module_1 = require("node:module");
|
|
|
27
31
|
const node_os_1 = __importDefault(require("node:os"));
|
|
28
32
|
const node_path_1 = __importDefault(require("node:path"));
|
|
29
33
|
const node_url_1 = require("node:url");
|
|
34
|
+
const node_worker_threads_1 = require("node:worker_threads");
|
|
30
35
|
const captureProcessOutput_1 = require("../../compiler/internal/captureProcessOutput");
|
|
31
36
|
const paths_1 = require("../../compiler/internal/paths");
|
|
37
|
+
const createCanonicalTempDirectory_1 = require("../../internal/createCanonicalTempDirectory");
|
|
32
38
|
const GO_MOD_SEARCH_MAX_DEPTH = 3;
|
|
33
39
|
const TTSC_GO_MODULE_PATH = "github.com/samchon/ttsc/packages/ttsc";
|
|
34
40
|
const TSGO_GO_MODULE_PATH = "github.com/microsoft/typescript-go";
|
|
@@ -161,6 +167,29 @@ const PLUGIN_CACHE_ENTRY_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
|
161
167
|
const PLUGIN_CACHE_MAX_BYTES = 2 * 1024 * 1024 * 1024;
|
|
162
168
|
const PLUGIN_CACHE_TARGET_BYTES = Math.floor(PLUGIN_CACHE_MAX_BYTES * 0.8);
|
|
163
169
|
const PLUGIN_CACHE_PROTECTED_AGE_MS = 60 * 60 * 1000;
|
|
170
|
+
// Go's own object-cache trim is age-based and has no size ceiling. The default
|
|
171
|
+
// ttsc-owned cache therefore keeps up to 8 GiB and trims oldest objects toward
|
|
172
|
+
// 6 GiB. The newest target-sized set used within an hour remains protected so
|
|
173
|
+
// crossing the ceiling cannot immediately force another cold build.
|
|
174
|
+
const GO_BUILD_CACHE_GC_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
175
|
+
const GO_BUILD_CACHE_MAX_BYTES = 8 * 1024 * 1024 * 1024;
|
|
176
|
+
const GO_BUILD_CACHE_TARGET_BYTES = 6 * 1024 * 1024 * 1024;
|
|
177
|
+
const GO_BUILD_CACHE_PROTECTED_AGE_MS = 60 * 60 * 1000;
|
|
178
|
+
const GO_BUILD_CACHE_GC_MARKER_FILE = ".ttsc-gc";
|
|
179
|
+
const GO_BUILD_CACHE_LEASE_DIR = ".ttsc-build-leases";
|
|
180
|
+
const GO_BUILD_CACHE_MAINTENANCE_DIR = ".ttsc-maintenance";
|
|
181
|
+
const GO_BUILD_CACHE_COORDINATION_STALE_MS = 60 * 60 * 1000;
|
|
182
|
+
const GO_BUILD_CACHE_MAINTENANCE_STALE_MS = 60 * 1000;
|
|
183
|
+
const GO_BUILD_CACHE_COORDINATION_CLOCK_SKEW_MS = 5 * 60 * 1000;
|
|
184
|
+
const GO_BUILD_CACHE_COORDINATION_HEARTBEAT_MS = 5_000;
|
|
185
|
+
const GO_BUILD_CACHE_COORDINATION_POLL_MS = 25;
|
|
186
|
+
// Go amortizes cache-hit mtime writes over one hour. An object used inside
|
|
187
|
+
// ttsc's one-hour protection window may therefore still carry an mtime almost
|
|
188
|
+
// one additional hour old.
|
|
189
|
+
const GO_BUILD_CACHE_ACCESS_MTIME_GRANULARITY_MS = 60 * 60 * 1000;
|
|
190
|
+
const DEFAULT_SOURCE_BUILD_FILESYSTEM = Object.freeze({
|
|
191
|
+
readFile: (location) => node_fs_1.default.readFileSync(location),
|
|
192
|
+
});
|
|
164
193
|
/**
|
|
165
194
|
* Build one Go source plugin into a cached executable.
|
|
166
195
|
*
|
|
@@ -176,21 +205,30 @@ function buildSourcePlugin(opts) {
|
|
|
176
205
|
const { dir, entry, source } = resolveSourceBuildTarget(opts);
|
|
177
206
|
const overlayDirs = [...(opts.overlayDirs ?? findTtscOverlayDirs())].sort();
|
|
178
207
|
const contributors = opts.contributors ?? [];
|
|
179
|
-
const
|
|
180
|
-
|
|
208
|
+
const compiler = resolveGoCompiler(env);
|
|
209
|
+
const goBinary = resolveGoToolForBuild(compiler.binary, env, dir);
|
|
210
|
+
ensureExecutableGoToolchain(goBinary, compiler.bundled);
|
|
181
211
|
const key = computeCacheKey({
|
|
182
212
|
contributors,
|
|
183
213
|
dir,
|
|
184
214
|
entry,
|
|
185
215
|
env,
|
|
216
|
+
filesystem: opts.filesystem,
|
|
186
217
|
goBinary,
|
|
187
218
|
overlayDirs,
|
|
188
219
|
ttscVersion: opts.ttscVersion,
|
|
189
220
|
tsgoVersion: opts.tsgoVersion,
|
|
190
221
|
});
|
|
191
222
|
const paths = resolveSourceBuildCachePaths(opts.baseDir, opts.cacheDir, env);
|
|
192
|
-
|
|
193
|
-
const
|
|
223
|
+
const managePluginCache = !opts.cacheDir && !env.TTSC_CACHE_DIR;
|
|
224
|
+
const manageGoBuildCache = shouldManageSourceBuildCaches(paths, opts.cacheDir, env);
|
|
225
|
+
const pluginRoot = managePluginCache
|
|
226
|
+
? canonicalPluginCacheRoot(paths.pluginRoot)
|
|
227
|
+
: paths.pluginRoot;
|
|
228
|
+
maybePruneSourceBuildCaches({ ...paths, pluginRoot }, opts.cacheDir, env);
|
|
229
|
+
const cacheDir = managePluginCache
|
|
230
|
+
? canonicalPluginCacheEntry(pluginRoot, key)
|
|
231
|
+
: node_path_1.default.join(pluginRoot, key);
|
|
194
232
|
const binaryName = process.platform === "win32" ? "plugin.exe" : "plugin";
|
|
195
233
|
const binaryPath = node_path_1.default.join(cacheDir, binaryName);
|
|
196
234
|
if (node_fs_1.default.existsSync(binaryPath)) {
|
|
@@ -200,7 +238,7 @@ function buildSourcePlugin(opts) {
|
|
|
200
238
|
node_fs_1.default.mkdirSync(cacheDir, { recursive: true });
|
|
201
239
|
const label = opts.label ?? "source plugin";
|
|
202
240
|
const quiet = opts.quiet === true;
|
|
203
|
-
|
|
241
|
+
const built = buildUnderPluginLock(cacheDir, binaryPath, { label, pluginName: opts.pluginName, quiet }, () => compileSourcePlugin({
|
|
204
242
|
binaryPath,
|
|
205
243
|
cacheDir,
|
|
206
244
|
contributors,
|
|
@@ -208,14 +246,26 @@ function buildSourcePlugin(opts) {
|
|
|
208
246
|
entry,
|
|
209
247
|
env,
|
|
210
248
|
goBinary,
|
|
249
|
+
normalizeGoToolPermissions: compiler.bundled,
|
|
211
250
|
key,
|
|
212
251
|
label,
|
|
213
252
|
goBuildCacheRoot: paths.goBuildRoot,
|
|
253
|
+
manageGoBuildCache,
|
|
214
254
|
overlayDirs,
|
|
215
255
|
pluginName: opts.pluginName,
|
|
216
256
|
quiet,
|
|
217
257
|
source,
|
|
218
258
|
}));
|
|
259
|
+
if (managePluginCache) {
|
|
260
|
+
// The pre-build daily pass cannot account for the binary this cold build
|
|
261
|
+
// just published. Enforce the size policy after publication, once this
|
|
262
|
+
// process has released its per-key build lock.
|
|
263
|
+
prunePluginCacheRoot(pluginRoot, {
|
|
264
|
+
force: true,
|
|
265
|
+
protectedEntries: [cacheDir],
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return built;
|
|
219
269
|
}
|
|
220
270
|
/** Run the actual `go build` and publish the binary; assumes the lock is held. */
|
|
221
271
|
function compileSourcePlugin(opts) {
|
|
@@ -229,7 +279,7 @@ function compileSourcePlugin(opts) {
|
|
|
229
279
|
`(this runs once per cache key and can take several minutes on a cold Go cache) ` +
|
|
230
280
|
`See https://ttsc.dev/docs/ttsc/compile#plugin-cache to persist it across builds.\n`);
|
|
231
281
|
}
|
|
232
|
-
const scratchDir =
|
|
282
|
+
const scratchDir = (0, createCanonicalTempDirectory_1.createCanonicalTempDirectory)(`ttsc-plugin-${opts.key}-`);
|
|
233
283
|
try {
|
|
234
284
|
materializeScratchDir(opts.dir, scratchDir);
|
|
235
285
|
const goModReader = createGoModReader(opts.goBinary, opts.pluginName, opts.env);
|
|
@@ -244,7 +294,22 @@ function compileSourcePlugin(opts) {
|
|
|
244
294
|
}
|
|
245
295
|
writeGoWork(scratchDir, opts.overlayDirs, opts.goBinary, opts.pluginName, opts.env);
|
|
246
296
|
const scratchBinaryName = process.platform === "win32" ? ".ttsc-plugin.exe" : ".ttsc-plugin";
|
|
247
|
-
|
|
297
|
+
let attemptedGoBuildCacheRoot;
|
|
298
|
+
try {
|
|
299
|
+
withGoBuildCacheLease(opts.goBuildCacheRoot, opts.manageGoBuildCache, (goBuildCacheRoot) => {
|
|
300
|
+
attemptedGoBuildCacheRoot = goBuildCacheRoot;
|
|
301
|
+
runGoBuild(scratchDir, opts.entry, scratchBinaryName, opts.pluginName, opts.goBinary, goBuildCacheRoot, opts.env, opts.normalizeGoToolPermissions);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
if (opts.manageGoBuildCache && attemptedGoBuildCacheRoot !== undefined) {
|
|
306
|
+
// The daily pre-build pass cannot see objects the build is about to
|
|
307
|
+
// add, including objects left behind by a failed compile. Enforce the
|
|
308
|
+
// size policy after every actual cold-build attempt so churn cannot
|
|
309
|
+
// grow the cache unchecked behind a fresh daily marker.
|
|
310
|
+
pruneGoBuildCacheRoot(attemptedGoBuildCacheRoot, { force: true });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
248
313
|
const builtBinary = node_path_1.default.join(scratchDir, scratchBinaryName);
|
|
249
314
|
publishBuiltBinary(builtBinary, opts.binaryPath);
|
|
250
315
|
touchCacheEntry(opts.cacheDir);
|
|
@@ -431,6 +496,9 @@ function ensurePluginBuildLockProtocol(protocolDir) {
|
|
|
431
496
|
}
|
|
432
497
|
function isPluginBuildLockProtocolV2(lockDir) {
|
|
433
498
|
try {
|
|
499
|
+
const stats = node_fs_1.default.lstatSync(lockDir);
|
|
500
|
+
if (!stats.isDirectory() || stats.isSymbolicLink())
|
|
501
|
+
return false;
|
|
434
502
|
return (node_fs_1.default.readFileSync(node_path_1.default.join(lockDir, PLUGIN_BUILD_LOCK_PROTOCOL_FILE), "utf8") === PLUGIN_BUILD_LOCK_PROTOCOL);
|
|
435
503
|
}
|
|
436
504
|
catch {
|
|
@@ -794,7 +862,10 @@ function pluginBuildLockAgeMs(lockDir, now) {
|
|
|
794
862
|
}
|
|
795
863
|
function pluginBuildLockPathExists(lockDir) {
|
|
796
864
|
try {
|
|
797
|
-
node_fs_1.default.
|
|
865
|
+
const stats = node_fs_1.default.lstatSync(lockDir);
|
|
866
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
867
|
+
throw new Error(`ttsc: unsafe plugin build lock path: ${lockDir}`);
|
|
868
|
+
}
|
|
798
869
|
return true;
|
|
799
870
|
}
|
|
800
871
|
catch (error) {
|
|
@@ -1293,8 +1364,8 @@ function isTtscManagedModulePath(modulePath) {
|
|
|
1293
1364
|
modulePath === TSGO_GO_MODULE_PATH ||
|
|
1294
1365
|
modulePath.startsWith("github.com/microsoft/typescript-go/shim/"));
|
|
1295
1366
|
}
|
|
1296
|
-
function runGoBuild(cwd, entry, binaryName, pluginName, goBinary, goBuildCacheRoot, env) {
|
|
1297
|
-
ensureExecutableGoToolchain(goBinary);
|
|
1367
|
+
function runGoBuild(cwd, entry, binaryName, pluginName, goBinary, goBuildCacheRoot, env, normalizeGoToolPermissions) {
|
|
1368
|
+
ensureExecutableGoToolchain(goBinary, normalizeGoToolPermissions);
|
|
1298
1369
|
const result = spawnGoTool(goBinary, ["build", "-o", binaryName, entry], {
|
|
1299
1370
|
cwd,
|
|
1300
1371
|
encoding: "utf8",
|
|
@@ -1428,30 +1499,42 @@ function inferGoRoot(goBinary) {
|
|
|
1428
1499
|
const goRoot = node_path_1.default.dirname(binDir);
|
|
1429
1500
|
return node_fs_1.default.existsSync(node_path_1.default.join(goRoot, "src", "runtime")) ? goRoot : null;
|
|
1430
1501
|
}
|
|
1431
|
-
function ensureExecutableGoToolchain(goBinary) {
|
|
1502
|
+
function ensureExecutableGoToolchain(goBinary, normalizeBundledPermissions) {
|
|
1432
1503
|
if (process.platform === "win32")
|
|
1433
1504
|
return;
|
|
1434
1505
|
if (!node_path_1.default.isAbsolute(goBinary) || !node_fs_1.default.existsSync(goBinary))
|
|
1435
1506
|
return;
|
|
1436
1507
|
try {
|
|
1437
|
-
|
|
1508
|
+
ensureExecutableFile(goBinary, normalizeBundledPermissions);
|
|
1438
1509
|
const goRoot = inferGoRoot(goBinary);
|
|
1439
1510
|
if (!goRoot)
|
|
1440
1511
|
return;
|
|
1441
1512
|
const gofmt = node_path_1.default.join(node_path_1.default.dirname(goBinary), "gofmt");
|
|
1442
|
-
if (node_fs_1.default.existsSync(gofmt))
|
|
1443
|
-
|
|
1513
|
+
if (node_fs_1.default.existsSync(gofmt)) {
|
|
1514
|
+
ensureExecutableFile(gofmt, normalizeBundledPermissions);
|
|
1515
|
+
}
|
|
1444
1516
|
const toolDir = node_path_1.default.join(goRoot, "pkg", "tool");
|
|
1445
1517
|
if (!node_fs_1.default.existsSync(toolDir))
|
|
1446
1518
|
return;
|
|
1447
1519
|
for (const file of walkToolFiles(toolDir)) {
|
|
1448
|
-
|
|
1520
|
+
ensureExecutableFile(file, normalizeBundledPermissions);
|
|
1449
1521
|
}
|
|
1450
1522
|
}
|
|
1451
1523
|
catch {
|
|
1452
1524
|
// Let the subsequent go build spawn fail with the real OS error.
|
|
1453
1525
|
}
|
|
1454
1526
|
}
|
|
1527
|
+
/** Repair tool execution without widening an explicitly selected toolchain. */
|
|
1528
|
+
function ensureExecutableFile(file, normalizeBundledPermissions) {
|
|
1529
|
+
const mode = node_fs_1.default.statSync(file).mode & 0o7777;
|
|
1530
|
+
if (normalizeBundledPermissions) {
|
|
1531
|
+
if (mode !== 0o755)
|
|
1532
|
+
node_fs_1.default.chmodSync(file, 0o755);
|
|
1533
|
+
}
|
|
1534
|
+
else if ((mode & 0o111) === 0) {
|
|
1535
|
+
node_fs_1.default.chmodSync(file, mode | 0o100);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1455
1538
|
function walkToolFiles(dir) {
|
|
1456
1539
|
const out = [];
|
|
1457
1540
|
let entries;
|
|
@@ -1519,16 +1602,16 @@ function walkForGoMod(dir, out) {
|
|
|
1519
1602
|
*/
|
|
1520
1603
|
function resolvePluginCacheRoot(projectRoot, cacheDir, env = process.env) {
|
|
1521
1604
|
const paths = resolveSourceBuildCachePaths(projectRoot, cacheDir, env);
|
|
1522
|
-
|
|
1605
|
+
maybePruneSourceBuildCaches(paths, cacheDir, env);
|
|
1523
1606
|
return paths.pluginRoot;
|
|
1524
1607
|
}
|
|
1525
1608
|
/**
|
|
1526
1609
|
* Resolve all source-plugin build cache directories for one invocation.
|
|
1527
1610
|
*
|
|
1528
1611
|
* `pluginRoot` stores compiled plugin binaries; `goBuildRoot` is the Go object
|
|
1529
|
-
* cache passed as `GOCACHE` while ttsc builds those binaries.
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
1612
|
+
* cache passed as `GOCACHE` while ttsc builds those binaries. The default Go
|
|
1613
|
+
* cache lives under `root`; an explicit `TTSC_GO_CACHE_DIR` or `GOCACHE` keeps
|
|
1614
|
+
* its independently resolved location and ownership policy.
|
|
1532
1615
|
*/
|
|
1533
1616
|
function resolveSourceBuildCachePaths(projectRoot, cacheDir, env = process.env) {
|
|
1534
1617
|
const root = resolveSourceBuildCacheRoot(projectRoot, cacheDir, env);
|
|
@@ -1655,7 +1738,7 @@ function resolveGoBuildCacheRoot(root, projectRoot, env) {
|
|
|
1655
1738
|
source: "ttsc-cache",
|
|
1656
1739
|
};
|
|
1657
1740
|
}
|
|
1658
|
-
function
|
|
1741
|
+
function maybePruneSourceBuildCaches(paths, cacheDir, env = process.env) {
|
|
1659
1742
|
// GC only the default (workspace-local) location. When the user pins an
|
|
1660
1743
|
// explicit `cacheDir`/`TTSC_CACHE_DIR`, they own its lifetime, so ttsc must
|
|
1661
1744
|
// not delete entries out from under them. `env` is the effective instance
|
|
@@ -1663,16 +1746,23 @@ function maybePrunePluginCache(paths, cacheDir, env = process.env) {
|
|
|
1663
1746
|
// `context.env` is honored without leaning on the shared `process.env`.
|
|
1664
1747
|
if (!cacheDir && !env.TTSC_CACHE_DIR) {
|
|
1665
1748
|
prunePluginCacheRoot(paths.pluginRoot);
|
|
1749
|
+
if (paths.goBuildRootSource === "ttsc-cache") {
|
|
1750
|
+
pruneGoBuildCacheRoot(paths.goBuildRoot);
|
|
1751
|
+
}
|
|
1666
1752
|
}
|
|
1667
1753
|
}
|
|
1754
|
+
/** Report whether this invocation owns and automatically maintains both caches. */
|
|
1755
|
+
function shouldManageSourceBuildCaches(paths, cacheDir, env) {
|
|
1756
|
+
return (!cacheDir && !env.TTSC_CACHE_DIR && paths.goBuildRootSource === "ttsc-cache");
|
|
1757
|
+
}
|
|
1668
1758
|
/**
|
|
1669
1759
|
* Return every directory `ttsc clean` should remove for `projectRoot`.
|
|
1670
1760
|
*
|
|
1671
|
-
* Covers the resolved
|
|
1672
|
-
*
|
|
1673
|
-
*
|
|
1674
|
-
*
|
|
1675
|
-
*
|
|
1761
|
+
* Covers the resolved plugin-binary root, a safely named nested `go-build/`, a
|
|
1762
|
+
* ttsc-owned Go build cache that lives OUTSIDE that root (`TTSC_GO_CACHE_DIR`),
|
|
1763
|
+
* and the two legacy project-local caches. A user-provided `GOCACHE` is never
|
|
1764
|
+
* removed. Pure over `env`, so the CLI passes `process.env` and a programmatic
|
|
1765
|
+
* caller can pass an injected environment.
|
|
1676
1766
|
*/
|
|
1677
1767
|
function resolveCleanTargets(projectRoot, cacheDir, env = process.env) {
|
|
1678
1768
|
const paths = resolveSourceBuildCachePaths(projectRoot, cacheDir, env);
|
|
@@ -1737,24 +1827,29 @@ function isPathWithin(child, parent) {
|
|
|
1737
1827
|
}
|
|
1738
1828
|
function resolveGoCompiler(env = process.env) {
|
|
1739
1829
|
const explicit = env.TTSC_GO_BINARY;
|
|
1740
|
-
if (explicit && explicit.length > 0)
|
|
1741
|
-
return explicit;
|
|
1830
|
+
if (explicit && explicit.length > 0) {
|
|
1831
|
+
return { binary: explicit, bundled: false };
|
|
1832
|
+
}
|
|
1742
1833
|
try {
|
|
1743
|
-
return
|
|
1834
|
+
return {
|
|
1835
|
+
binary: (0, node_module_1.createRequire)(__filename).resolve(`@ttsc/${process.platform}-${process.arch}/bin/go/bin/${process.platform === "win32" ? "go.exe" : "go"}`),
|
|
1836
|
+
bundled: true,
|
|
1837
|
+
};
|
|
1744
1838
|
}
|
|
1745
1839
|
catch {
|
|
1746
1840
|
/* fall through */
|
|
1747
1841
|
}
|
|
1748
1842
|
const platformPackage = node_path_1.default.resolve(__dirname, "..", "..", "..", "..", `ttsc-${process.platform}-${process.arch}`, "bin", "go", "bin", process.platform === "win32" ? "go.exe" : "go");
|
|
1749
|
-
if (node_fs_1.default.existsSync(platformPackage))
|
|
1750
|
-
return platformPackage;
|
|
1843
|
+
if (node_fs_1.default.existsSync(platformPackage)) {
|
|
1844
|
+
return { binary: platformPackage, bundled: true };
|
|
1845
|
+
}
|
|
1751
1846
|
const local = node_path_1.default.resolve(__dirname, "..", "..", "..", "..", "native", "go", "bin", process.platform === "win32" ? "go.exe" : "go");
|
|
1752
1847
|
if (node_fs_1.default.existsSync(local))
|
|
1753
|
-
return local;
|
|
1848
|
+
return { binary: local, bundled: true };
|
|
1754
1849
|
const homeSdk = node_path_1.default.join(env.HOME ?? "", "go-sdk", "go", "bin", process.platform === "win32" ? "go.exe" : "go");
|
|
1755
1850
|
if (node_fs_1.default.existsSync(homeSdk))
|
|
1756
|
-
return homeSdk;
|
|
1757
|
-
return "go";
|
|
1851
|
+
return { binary: homeSdk, bundled: false };
|
|
1852
|
+
return { binary: "go", bundled: false };
|
|
1758
1853
|
}
|
|
1759
1854
|
/**
|
|
1760
1855
|
* Compute a deterministic SHA-256 cache key for a plugin build.
|
|
@@ -1769,6 +1864,9 @@ function resolveGoCompiler(env = process.env) {
|
|
|
1769
1864
|
*/
|
|
1770
1865
|
function computeCacheKey(inputs) {
|
|
1771
1866
|
const env = inputs.env ?? process.env;
|
|
1867
|
+
const filesystem = {
|
|
1868
|
+
readFile: inputs.filesystem?.readFile ?? DEFAULT_SOURCE_BUILD_FILESYSTEM.readFile,
|
|
1869
|
+
};
|
|
1772
1870
|
const goBinary = inputs.goBinary === undefined
|
|
1773
1871
|
? undefined
|
|
1774
1872
|
: resolveGoToolForBuild(inputs.goBinary, env, inputs.dir);
|
|
@@ -1780,7 +1878,7 @@ function computeCacheKey(inputs) {
|
|
|
1780
1878
|
if (goBinary !== undefined) {
|
|
1781
1879
|
hash.update(`go=${resolveGoCompilerIdentity(goBinary, env, inputs.dir)}\n`);
|
|
1782
1880
|
}
|
|
1783
|
-
hashGoBuildEnvironment(hash, goBinary, inputs.dir, env);
|
|
1881
|
+
hashGoBuildEnvironment(hash, goBinary, inputs.dir, env, filesystem);
|
|
1784
1882
|
hashExternalGoBuildEnvironment(hash, env);
|
|
1785
1883
|
hashSourceDirectory(hash, "plugin", inputs.dir);
|
|
1786
1884
|
for (const [index, dir] of [...(inputs.overlayDirs ?? [])].sort().entries()) {
|
|
@@ -1860,8 +1958,9 @@ function isHashableFile(name) {
|
|
|
1860
1958
|
// binary hashes for a value that does not change between plugins. The result
|
|
1861
1959
|
// is a pure function of the go binary's resolved real path plus its on-disk
|
|
1862
1960
|
// content; the memo key therefore mixes the resolved real path with a cheap
|
|
1863
|
-
// content signature (byte size
|
|
1864
|
-
// a long-lived host rewrites
|
|
1961
|
+
// content signature (filesystem identity, mode, byte size, and nanosecond
|
|
1962
|
+
// change/modify times). That signature changes if a long-lived host rewrites
|
|
1963
|
+
// or atomically replaces the binary between calls, so the memo
|
|
1865
1964
|
// re-derives the identity exactly as the un-memoized code would and the
|
|
1866
1965
|
// cache-key bytes stay byte-for-byte identical to today. The selected compiler
|
|
1867
1966
|
// path is shared by every build subprocess on Windows, while `go version` uses
|
|
@@ -1893,7 +1992,7 @@ function resolveGoCompilerIdentity(goBinary, env = process.env, cwd = process.cw
|
|
|
1893
1992
|
// stat-ed, so the rare unstattable case never serves a stale identity.
|
|
1894
1993
|
function goCompilerIdentityMemoKey(goBinary, resolved, env, cwd) {
|
|
1895
1994
|
try {
|
|
1896
|
-
const stat = node_fs_1.default.statSync(resolved);
|
|
1995
|
+
const stat = node_fs_1.default.statSync(resolved, { bigint: true });
|
|
1897
1996
|
const context = node_crypto_1.default.createHash("sha256");
|
|
1898
1997
|
context.update(cwd);
|
|
1899
1998
|
for (const [key, value] of Object.entries(env).sort(([left], [right]) => left === right ? 0 : left < right ? -1 : 1)) {
|
|
@@ -1901,7 +2000,17 @@ function goCompilerIdentityMemoKey(goBinary, resolved, env, cwd) {
|
|
|
1901
2000
|
continue;
|
|
1902
2001
|
context.update(`\0${key.length}:${key}${value.length}:${value}`);
|
|
1903
2002
|
}
|
|
1904
|
-
return
|
|
2003
|
+
return [
|
|
2004
|
+
goBinary,
|
|
2005
|
+
resolved,
|
|
2006
|
+
stat.dev,
|
|
2007
|
+
stat.ino,
|
|
2008
|
+
stat.mode,
|
|
2009
|
+
stat.size,
|
|
2010
|
+
stat.mtimeNs,
|
|
2011
|
+
stat.ctimeNs,
|
|
2012
|
+
context.digest("hex"),
|
|
2013
|
+
].join("\0");
|
|
1905
2014
|
}
|
|
1906
2015
|
catch {
|
|
1907
2016
|
return null;
|
|
@@ -2094,8 +2203,8 @@ function hashFile(file) {
|
|
|
2094
2203
|
hash.update(node_fs_1.default.readFileSync(file));
|
|
2095
2204
|
return hash.digest("hex");
|
|
2096
2205
|
}
|
|
2097
|
-
function hashGoBuildEnvironment(hash, goBinary, cwd, env) {
|
|
2098
|
-
const values = resolveGoBuildEnvironment(goBinary, cwd, env);
|
|
2206
|
+
function hashGoBuildEnvironment(hash, goBinary, cwd, env, filesystem) {
|
|
2207
|
+
const values = resolveGoBuildEnvironment(goBinary, cwd, env, filesystem);
|
|
2099
2208
|
for (const key of GO_BUILD_ENV_KEYS) {
|
|
2100
2209
|
const value = values.get(key);
|
|
2101
2210
|
if (value !== undefined && value !== "") {
|
|
@@ -2103,7 +2212,7 @@ function hashGoBuildEnvironment(hash, goBinary, cwd, env) {
|
|
|
2103
2212
|
}
|
|
2104
2213
|
}
|
|
2105
2214
|
}
|
|
2106
|
-
function resolveGoBuildEnvironment(goBinary, cwd, env) {
|
|
2215
|
+
function resolveGoBuildEnvironment(goBinary, cwd, env, filesystem) {
|
|
2107
2216
|
const values = new Map();
|
|
2108
2217
|
if (goBinary !== undefined) {
|
|
2109
2218
|
const result = spawnGoTool(goBinary, ["env", "-json", ...GO_BUILD_ENV_KEYS], {
|
|
@@ -2118,7 +2227,7 @@ function resolveGoBuildEnvironment(goBinary, cwd, env) {
|
|
|
2118
2227
|
for (const key of GO_BUILD_ENV_KEYS) {
|
|
2119
2228
|
const raw = parsed[key];
|
|
2120
2229
|
if (typeof raw === "string" && raw !== "") {
|
|
2121
|
-
values.set(key, normalizeGoBuildEnvValue(key, raw, env));
|
|
2230
|
+
values.set(key, normalizeGoBuildEnvValue(key, raw, env, filesystem));
|
|
2122
2231
|
}
|
|
2123
2232
|
}
|
|
2124
2233
|
}
|
|
@@ -2133,14 +2242,14 @@ function resolveGoBuildEnvironment(goBinary, cwd, env) {
|
|
|
2133
2242
|
continue;
|
|
2134
2243
|
const value = env[key];
|
|
2135
2244
|
if (value !== undefined && value !== "") {
|
|
2136
|
-
values.set(key, normalizeGoBuildEnvValue(key, value, env));
|
|
2245
|
+
values.set(key, normalizeGoBuildEnvValue(key, value, env, filesystem));
|
|
2137
2246
|
}
|
|
2138
2247
|
}
|
|
2139
2248
|
return values;
|
|
2140
2249
|
}
|
|
2141
|
-
function normalizeGoBuildEnvValue(key, value, env) {
|
|
2250
|
+
function normalizeGoBuildEnvValue(key, value, env, filesystem) {
|
|
2142
2251
|
if (key === "GOROOT") {
|
|
2143
|
-
return resolveGoRootCacheIdentity(value);
|
|
2252
|
+
return resolveGoRootCacheIdentity(value, filesystem);
|
|
2144
2253
|
}
|
|
2145
2254
|
if (GO_BUILD_COMMAND_ENV_KEYS.has(key)) {
|
|
2146
2255
|
return `${value}\0${resolveCommandCacheIdentity(value, env)}`;
|
|
@@ -2183,32 +2292,81 @@ function hashExternalGoBuildEnvironment(hash, env) {
|
|
|
2183
2292
|
}
|
|
2184
2293
|
}
|
|
2185
2294
|
}
|
|
2186
|
-
|
|
2295
|
+
// GOROOT is immutable during normal use but contributes roughly 140 MiB of
|
|
2296
|
+
// source/tool content to every plugin key. Retain only the final pathless
|
|
2297
|
+
// content identity, guarded by a fresh metadata manifest on every call. A
|
|
2298
|
+
// changed or incomplete manifest falls through to the historical full read.
|
|
2299
|
+
const goRootIdentityCache = new Map();
|
|
2300
|
+
function resolveGoRootCacheIdentity(goRoot, filesystem) {
|
|
2187
2301
|
const resolved = resolveRealPath(goRoot);
|
|
2188
2302
|
if (!node_fs_1.default.existsSync(resolved)) {
|
|
2189
2303
|
return `missing:${goRoot}`;
|
|
2190
2304
|
}
|
|
2305
|
+
const snapshot = collectGoRootIdentitySnapshot(resolved);
|
|
2306
|
+
if (snapshot.complete) {
|
|
2307
|
+
const cached = goRootIdentityCache.get(resolved);
|
|
2308
|
+
if (cached?.signature === snapshot.signature) {
|
|
2309
|
+
return cached.identity;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2191
2312
|
const hash = node_crypto_1.default.createHash("sha256");
|
|
2192
|
-
for (const file of
|
|
2313
|
+
for (const file of snapshot.files) {
|
|
2193
2314
|
const relative = node_path_1.default.relative(resolved, file).replace(/\\/g, "/");
|
|
2194
2315
|
hash.update(`f=${relative}\n`);
|
|
2195
|
-
hash.update(
|
|
2316
|
+
hash.update(filesystem.readFile(file));
|
|
2196
2317
|
hash.update("\n");
|
|
2197
2318
|
}
|
|
2198
|
-
|
|
2319
|
+
const identity = `sha256:${hash.digest("hex")}`;
|
|
2320
|
+
if (snapshot.complete) {
|
|
2321
|
+
goRootIdentityCache.set(resolved, {
|
|
2322
|
+
identity,
|
|
2323
|
+
signature: snapshot.signature,
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
return identity;
|
|
2199
2327
|
}
|
|
2200
|
-
function
|
|
2328
|
+
function collectGoRootIdentitySnapshot(root) {
|
|
2201
2329
|
const out = [];
|
|
2202
|
-
|
|
2330
|
+
const state = { complete: true };
|
|
2331
|
+
walkGoRootIdentity(root, root, out, state);
|
|
2203
2332
|
out.sort();
|
|
2204
|
-
|
|
2333
|
+
const signature = node_crypto_1.default.createHash("sha256");
|
|
2334
|
+
for (const file of out) {
|
|
2335
|
+
const relative = node_path_1.default.relative(root, file).replace(/\\/g, "/");
|
|
2336
|
+
try {
|
|
2337
|
+
const stats = node_fs_1.default.statSync(file, { bigint: true });
|
|
2338
|
+
if (!stats.isFile()) {
|
|
2339
|
+
state.complete = false;
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
signature.update([
|
|
2343
|
+
relative,
|
|
2344
|
+
stats.dev,
|
|
2345
|
+
stats.ino,
|
|
2346
|
+
stats.mode,
|
|
2347
|
+
stats.size,
|
|
2348
|
+
stats.mtimeNs,
|
|
2349
|
+
stats.ctimeNs,
|
|
2350
|
+
].join("\0"));
|
|
2351
|
+
signature.update("\n");
|
|
2352
|
+
}
|
|
2353
|
+
catch {
|
|
2354
|
+
state.complete = false;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
return {
|
|
2358
|
+
complete: state.complete,
|
|
2359
|
+
files: out,
|
|
2360
|
+
signature: signature.digest("hex"),
|
|
2361
|
+
};
|
|
2205
2362
|
}
|
|
2206
|
-
function walkGoRootIdentity(root, dir, out) {
|
|
2363
|
+
function walkGoRootIdentity(root, dir, out, state) {
|
|
2207
2364
|
let entries;
|
|
2208
2365
|
try {
|
|
2209
2366
|
entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
2210
2367
|
}
|
|
2211
2368
|
catch {
|
|
2369
|
+
state.complete = false;
|
|
2212
2370
|
return;
|
|
2213
2371
|
}
|
|
2214
2372
|
for (const entry of entries) {
|
|
@@ -2216,7 +2374,7 @@ function walkGoRootIdentity(root, dir, out) {
|
|
|
2216
2374
|
const rel = node_path_1.default.relative(root, file).replace(/\\/g, "/");
|
|
2217
2375
|
if (entry.isDirectory()) {
|
|
2218
2376
|
if (shouldHashGoRootPath(rel, true)) {
|
|
2219
|
-
walkGoRootIdentity(root, file, out);
|
|
2377
|
+
walkGoRootIdentity(root, file, out, state);
|
|
2220
2378
|
}
|
|
2221
2379
|
}
|
|
2222
2380
|
else if (entry.isFile() && shouldHashGoRootPath(rel, false)) {
|
|
@@ -2265,50 +2423,554 @@ function shouldHashGoRootPath(rel, isDir) {
|
|
|
2265
2423
|
function touchCacheEntry(cacheDir) {
|
|
2266
2424
|
try {
|
|
2267
2425
|
node_fs_1.default.mkdirSync(cacheDir, { recursive: true });
|
|
2268
|
-
|
|
2426
|
+
replaceCacheMetadataFile(node_path_1.default.join(cacheDir, CACHE_LAST_USED_FILE), `${Date.now()}\n`);
|
|
2269
2427
|
}
|
|
2270
2428
|
catch {
|
|
2271
2429
|
// Cache hits must not fail because metadata touch failed.
|
|
2272
2430
|
}
|
|
2273
2431
|
}
|
|
2274
|
-
function prunePluginCacheRoot(root) {
|
|
2432
|
+
function prunePluginCacheRoot(root, options = {}) {
|
|
2275
2433
|
try {
|
|
2276
|
-
|
|
2277
|
-
const marker = node_path_1.default.join(
|
|
2278
|
-
const now = Date.now();
|
|
2434
|
+
const cacheRoot = canonicalPluginCacheRoot(root);
|
|
2435
|
+
const marker = node_path_1.default.join(cacheRoot, CACHE_GC_MARKER_FILE);
|
|
2436
|
+
const now = options.now ?? Date.now();
|
|
2279
2437
|
const lastRun = readTimestamp(marker);
|
|
2280
|
-
if (
|
|
2438
|
+
if (options.force !== true &&
|
|
2439
|
+
lastRun !== null &&
|
|
2440
|
+
lastRun <= now &&
|
|
2441
|
+
now - lastRun < PLUGIN_CACHE_GC_INTERVAL_MS) {
|
|
2281
2442
|
return;
|
|
2282
2443
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2444
|
+
const remainingBytes = prunePluginCacheEntries(cacheRoot, {
|
|
2445
|
+
maxBytes: options.maxBytes ?? PLUGIN_CACHE_MAX_BYTES,
|
|
2446
|
+
now,
|
|
2447
|
+
protectedEntries: canonicalPluginCacheProtectedEntries(cacheRoot, options.protectedEntries ?? []),
|
|
2448
|
+
protectedAgeMs: options.protectedAgeMs ?? PLUGIN_CACHE_PROTECTED_AGE_MS,
|
|
2449
|
+
targetBytes: options.targetBytes ?? PLUGIN_CACHE_TARGET_BYTES,
|
|
2450
|
+
});
|
|
2451
|
+
const maxBytes = options.maxBytes ?? PLUGIN_CACHE_MAX_BYTES;
|
|
2452
|
+
const protectedAgeMs = options.protectedAgeMs ?? PLUGIN_CACHE_PROTECTED_AGE_MS;
|
|
2453
|
+
const markerTimestamp = remainingBytes > maxBytes
|
|
2454
|
+
? now - PLUGIN_CACHE_GC_INTERVAL_MS + protectedAgeMs
|
|
2455
|
+
: now;
|
|
2456
|
+
replaceCacheMetadataFile(marker, `${markerTimestamp}\n`);
|
|
2285
2457
|
}
|
|
2286
2458
|
catch {
|
|
2287
2459
|
// Plugin-cache GC is opportunistic; builds still proceed when it fails.
|
|
2288
2460
|
}
|
|
2289
2461
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2462
|
+
/** Resolve explicit GC exclusions without allowing an alias outside root. */
|
|
2463
|
+
function canonicalPluginCacheProtectedEntries(root, entries) {
|
|
2464
|
+
const protectedEntries = new Set();
|
|
2465
|
+
for (const entry of entries) {
|
|
2466
|
+
const candidate = node_path_1.default.resolve(entry);
|
|
2467
|
+
const stats = node_fs_1.default.lstatSync(candidate);
|
|
2468
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
2469
|
+
throw new Error(`ttsc: unsafe protected plugin cache entry: ${entry}`);
|
|
2470
|
+
}
|
|
2471
|
+
const physical = node_fs_1.default.realpathSync.native(candidate);
|
|
2472
|
+
if (node_path_1.default.dirname(physical) !== root) {
|
|
2473
|
+
throw new Error(`ttsc: protected plugin cache entry escaped root: ${entry}`);
|
|
2474
|
+
}
|
|
2475
|
+
protectedEntries.add(physical);
|
|
2476
|
+
}
|
|
2477
|
+
return protectedEntries;
|
|
2478
|
+
}
|
|
2479
|
+
/** Pin the default plugin cache to one ordinary physical directory. */
|
|
2480
|
+
function canonicalPluginCacheRoot(root) {
|
|
2481
|
+
node_fs_1.default.mkdirSync(root, { recursive: true });
|
|
2482
|
+
const physicalParent = node_fs_1.default.realpathSync.native(node_path_1.default.dirname(root));
|
|
2483
|
+
const stats = node_fs_1.default.lstatSync(root);
|
|
2484
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
2485
|
+
throw new Error(`ttsc: unsafe plugin cache root: ${root}`);
|
|
2486
|
+
}
|
|
2487
|
+
// If an ancestor alias is retargeted after this point, all later cache work
|
|
2488
|
+
// stays on the original physical directory rather than following it.
|
|
2489
|
+
const physicalRoot = node_fs_1.default.realpathSync.native(root);
|
|
2490
|
+
if (node_path_1.default.dirname(physicalRoot) !== physicalParent) {
|
|
2491
|
+
throw new Error(`ttsc: plugin cache root escaped its parent: ${root}`);
|
|
2492
|
+
}
|
|
2493
|
+
return physicalRoot;
|
|
2494
|
+
}
|
|
2495
|
+
/** Create one content-addressed cache entry without following a leaf alias. */
|
|
2496
|
+
function canonicalPluginCacheEntry(root, key) {
|
|
2497
|
+
const directory = node_path_1.default.join(root, key);
|
|
2498
|
+
node_fs_1.default.mkdirSync(directory, { recursive: true });
|
|
2499
|
+
const stats = node_fs_1.default.lstatSync(directory);
|
|
2500
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
2501
|
+
throw new Error(`ttsc: unsafe plugin cache entry: ${directory}`);
|
|
2502
|
+
}
|
|
2503
|
+
const physicalDirectory = node_fs_1.default.realpathSync.native(directory);
|
|
2504
|
+
if (node_path_1.default.dirname(physicalDirectory) !== root) {
|
|
2505
|
+
throw new Error(`ttsc: plugin cache entry escaped its root: ${directory}`);
|
|
2506
|
+
}
|
|
2507
|
+
return physicalDirectory;
|
|
2508
|
+
}
|
|
2509
|
+
function prunePluginCacheEntries(root, options) {
|
|
2510
|
+
const entries = collectPluginCacheEntries(root, options.now);
|
|
2292
2511
|
for (const entry of entries) {
|
|
2293
|
-
if (now - entry.lastUsedAt <= PLUGIN_CACHE_ENTRY_MAX_AGE_MS
|
|
2512
|
+
if (options.now - entry.lastUsedAt <= PLUGIN_CACHE_ENTRY_MAX_AGE_MS ||
|
|
2513
|
+
options.protectedEntries.has(entry.dir) ||
|
|
2514
|
+
pluginCacheEntryHasActiveBuild(entry, options.now)) {
|
|
2294
2515
|
continue;
|
|
2295
2516
|
}
|
|
2296
2517
|
removeCacheEntry(entry);
|
|
2297
2518
|
}
|
|
2298
|
-
const remaining = collectPluginCacheEntries(root, now);
|
|
2519
|
+
const remaining = collectPluginCacheEntries(root, options.now);
|
|
2299
2520
|
let total = remaining.reduce((sum, entry) => sum + entry.size, 0);
|
|
2300
|
-
if (total <=
|
|
2301
|
-
return;
|
|
2521
|
+
if (total <= options.maxBytes) {
|
|
2522
|
+
return total;
|
|
2523
|
+
}
|
|
2524
|
+
const protectedEntries = new Set(options.protectedEntries);
|
|
2525
|
+
let protectedBytes = 0;
|
|
2526
|
+
for (const entry of [...remaining].sort((a, b) => b.lastUsedAt - a.lastUsedAt)) {
|
|
2527
|
+
if (pluginCacheEntryHasActiveBuild(entry, options.now)) {
|
|
2528
|
+
protectedEntries.add(entry.dir);
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
2531
|
+
if (options.now - entry.lastUsedAt > options.protectedAgeMs)
|
|
2532
|
+
continue;
|
|
2533
|
+
if (protectedBytes >= options.targetBytes)
|
|
2534
|
+
continue;
|
|
2535
|
+
if (protectedBytes + entry.size > options.targetBytes)
|
|
2536
|
+
continue;
|
|
2537
|
+
protectedEntries.add(entry.dir);
|
|
2538
|
+
protectedBytes += entry.size;
|
|
2302
2539
|
}
|
|
2303
2540
|
for (const entry of remaining.sort((a, b) => a.lastUsedAt - b.lastUsedAt)) {
|
|
2304
|
-
if (total <=
|
|
2541
|
+
if (total <= options.targetBytes) {
|
|
2542
|
+
return total;
|
|
2543
|
+
}
|
|
2544
|
+
if (protectedEntries.has(entry.dir))
|
|
2545
|
+
continue;
|
|
2546
|
+
if (removeCacheEntry(entry))
|
|
2547
|
+
total -= entry.size;
|
|
2548
|
+
}
|
|
2549
|
+
return total;
|
|
2550
|
+
}
|
|
2551
|
+
/** Conservatively protect an entry while any build generation owns its key. */
|
|
2552
|
+
function pluginCacheEntryHasActiveBuild(entry, now) {
|
|
2553
|
+
const lockDir = `${entry.dir}.lock`;
|
|
2554
|
+
try {
|
|
2555
|
+
return inspectPluginBuildLock(lockDir, now).state === "active";
|
|
2556
|
+
}
|
|
2557
|
+
catch {
|
|
2558
|
+
// Malformed or unreadable coordination state cannot disprove ownership.
|
|
2559
|
+
return true;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
/**
|
|
2563
|
+
* Opportunistically bound one ttsc-owned Go object cache.
|
|
2564
|
+
*
|
|
2565
|
+
* A maintenance intent blocks new build leases. If any existing lease remains,
|
|
2566
|
+
* maintenance yields without touching the cache; the next invocation retries.
|
|
2567
|
+
* Only Go's two-hex object directories are scanned, so coordination metadata
|
|
2568
|
+
* and Go's own trim marker remain outside the size policy.
|
|
2569
|
+
*/
|
|
2570
|
+
function pruneGoBuildCacheRoot(root, options = {}) {
|
|
2571
|
+
let intent;
|
|
2572
|
+
try {
|
|
2573
|
+
const cacheRoot = canonicalGoBuildCacheRoot(root);
|
|
2574
|
+
const marker = node_path_1.default.join(cacheRoot, GO_BUILD_CACHE_GC_MARKER_FILE);
|
|
2575
|
+
const now = options.now ?? Date.now();
|
|
2576
|
+
const lastRun = readTimestamp(marker);
|
|
2577
|
+
if (options.force !== true &&
|
|
2578
|
+
lastRun !== null &&
|
|
2579
|
+
lastRun <= now &&
|
|
2580
|
+
now - lastRun < GO_BUILD_CACHE_GC_INTERVAL_MS) {
|
|
2305
2581
|
return;
|
|
2306
2582
|
}
|
|
2307
|
-
|
|
2583
|
+
intent = createGoBuildCacheCoordinationRecord(cacheRoot, GO_BUILD_CACHE_MAINTENANCE_DIR);
|
|
2584
|
+
if (!intent.startHeartbeat()) {
|
|
2585
|
+
// Maintenance is opportunistic. Without an independent heartbeat a
|
|
2586
|
+
// synchronous scan could look abandoned while it is still deleting, so
|
|
2587
|
+
// yield instead of weakening the build/maintenance exclusion.
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
if (collectLiveGoBuildCacheCoordinationRecords(cacheRoot, GO_BUILD_CACHE_LEASE_DIR, now).length !== 0) {
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
const remainingBytes = pruneGoBuildCacheEntries(cacheRoot, {
|
|
2594
|
+
maxBytes: options.maxBytes ?? GO_BUILD_CACHE_MAX_BYTES,
|
|
2595
|
+
now,
|
|
2596
|
+
protectedAgeMs: options.protectedAgeMs ?? GO_BUILD_CACHE_PROTECTED_AGE_MS,
|
|
2597
|
+
targetBytes: options.targetBytes ?? GO_BUILD_CACHE_TARGET_BYTES,
|
|
2598
|
+
});
|
|
2599
|
+
// If recent protection or a transient delete failure left the cache above
|
|
2600
|
+
// the ceiling, retry after the protection window instead of suppressing
|
|
2601
|
+
// every maintenance attempt for a full day.
|
|
2602
|
+
const maxBytes = options.maxBytes ?? GO_BUILD_CACHE_MAX_BYTES;
|
|
2603
|
+
const protectedAgeMs = options.protectedAgeMs ?? GO_BUILD_CACHE_PROTECTED_AGE_MS;
|
|
2604
|
+
const markerTimestamp = remainingBytes > maxBytes
|
|
2605
|
+
? now - GO_BUILD_CACHE_GC_INTERVAL_MS + protectedAgeMs
|
|
2606
|
+
: now;
|
|
2607
|
+
replaceCacheMetadataFile(marker, `${markerTimestamp}\n`);
|
|
2608
|
+
}
|
|
2609
|
+
catch {
|
|
2610
|
+
// Go-cache GC is opportunistic; builds still proceed when it fails.
|
|
2611
|
+
}
|
|
2612
|
+
finally {
|
|
2613
|
+
intent?.finish();
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Run one Go build under a cross-process cache lease when ttsc owns the cache.
|
|
2618
|
+
*
|
|
2619
|
+
* The lease is published before checking maintenance. A maintenance process
|
|
2620
|
+
* that arrived first keeps its intent visible, so this builder withdraws and
|
|
2621
|
+
* retries; one that arrives second sees the lease and yields. That ordering
|
|
2622
|
+
* closes the scan/start race without serializing independent Go builds.
|
|
2623
|
+
*/
|
|
2624
|
+
function withGoBuildCacheLease(root, managed, callback) {
|
|
2625
|
+
if (!managed) {
|
|
2626
|
+
return callback(root);
|
|
2627
|
+
}
|
|
2628
|
+
const cacheRoot = canonicalGoBuildCacheRoot(root);
|
|
2629
|
+
const started = Date.now();
|
|
2630
|
+
for (;;) {
|
|
2631
|
+
const lease = createGoBuildCacheCoordinationRecord(cacheRoot, GO_BUILD_CACHE_LEASE_DIR);
|
|
2632
|
+
const maintenance = collectLiveGoBuildCacheCoordinationRecords(cacheRoot, GO_BUILD_CACHE_MAINTENANCE_DIR, Date.now());
|
|
2633
|
+
if (maintenance.length === 0) {
|
|
2634
|
+
try {
|
|
2635
|
+
if (!lease.startHeartbeat()) {
|
|
2636
|
+
throw new Error(`ttsc: unable to start Go build cache lease heartbeat at ${cacheRoot}`);
|
|
2637
|
+
}
|
|
2638
|
+
return callback(cacheRoot);
|
|
2639
|
+
}
|
|
2640
|
+
finally {
|
|
2641
|
+
lease.finish();
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
lease.finish();
|
|
2645
|
+
if (Date.now() - started > PLUGIN_BUILD_LOCK_STEAL_MS) {
|
|
2646
|
+
throw new Error(`ttsc: timed out waiting for Go build cache maintenance at ${cacheRoot}`);
|
|
2647
|
+
}
|
|
2648
|
+
sleepSync(GO_BUILD_CACHE_COORDINATION_POLL_MS);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
/**
|
|
2652
|
+
* Create and pin the owned Go cache to an ordinary physical directory.
|
|
2653
|
+
*
|
|
2654
|
+
* The leaf may be user-controlled inside `node_modules/.cache`; accepting a
|
|
2655
|
+
* symlink or junction there would let LRU deletion escape into an arbitrary
|
|
2656
|
+
* two-hex directory. Returning the canonical spelling also keeps the build,
|
|
2657
|
+
* leases, and maintenance on the same directory if an ancestor alias moves.
|
|
2658
|
+
*/
|
|
2659
|
+
function canonicalGoBuildCacheRoot(root) {
|
|
2660
|
+
node_fs_1.default.mkdirSync(root, { recursive: true });
|
|
2661
|
+
const physicalParent = node_fs_1.default.realpathSync.native(node_path_1.default.dirname(root));
|
|
2662
|
+
const stats = node_fs_1.default.lstatSync(root);
|
|
2663
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
2664
|
+
throw new Error(`ttsc: unsafe Go build cache root: ${root}`);
|
|
2665
|
+
}
|
|
2666
|
+
const physicalRoot = node_fs_1.default.realpathSync.native(root);
|
|
2667
|
+
if (node_path_1.default.dirname(physicalRoot) !== physicalParent) {
|
|
2668
|
+
throw new Error(`ttsc: Go build cache root escaped its parent: ${root}`);
|
|
2669
|
+
}
|
|
2670
|
+
return physicalRoot;
|
|
2671
|
+
}
|
|
2672
|
+
/** Prune oldest Go cache objects to the requested deterministic size target. */
|
|
2673
|
+
function pruneGoBuildCacheEntries(root, options) {
|
|
2674
|
+
const entries = collectGoBuildCacheObjects(root);
|
|
2675
|
+
let total = entries.reduce((sum, entry) => sum + entry.size, 0);
|
|
2676
|
+
if (total <= options.maxBytes) {
|
|
2677
|
+
return total;
|
|
2678
|
+
}
|
|
2679
|
+
// Protect the newest recent objects only up to one target-sized cohort
|
|
2680
|
+
// reserve. Protecting every object younger than an hour would let repeated
|
|
2681
|
+
// 2.9 GiB cold builds grow without bound during that hour.
|
|
2682
|
+
const protectedFiles = new Set();
|
|
2683
|
+
let protectedBytes = 0;
|
|
2684
|
+
for (const entry of [...entries].sort((a, b) => b.lastUsedAt - a.lastUsedAt)) {
|
|
2685
|
+
if (options.now - entry.lastUsedAt >
|
|
2686
|
+
options.protectedAgeMs + GO_BUILD_CACHE_ACCESS_MTIME_GRANULARITY_MS) {
|
|
2308
2687
|
continue;
|
|
2309
2688
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2689
|
+
if (protectedBytes >= options.targetBytes) {
|
|
2690
|
+
break;
|
|
2691
|
+
}
|
|
2692
|
+
if (protectedBytes + entry.size > options.targetBytes) {
|
|
2693
|
+
break;
|
|
2694
|
+
}
|
|
2695
|
+
protectedFiles.add(entry.file);
|
|
2696
|
+
protectedBytes += entry.size;
|
|
2697
|
+
}
|
|
2698
|
+
for (const entry of entries.sort((a, b) => a.lastUsedAt - b.lastUsedAt)) {
|
|
2699
|
+
if (total <= options.targetBytes) {
|
|
2700
|
+
return total;
|
|
2701
|
+
}
|
|
2702
|
+
if (protectedFiles.has(entry.file)) {
|
|
2703
|
+
continue;
|
|
2704
|
+
}
|
|
2705
|
+
try {
|
|
2706
|
+
node_fs_1.default.rmSync(entry.file, { force: true });
|
|
2707
|
+
total -= entry.size;
|
|
2708
|
+
}
|
|
2709
|
+
catch {
|
|
2710
|
+
// A concurrent antivirus/indexer can transiently hold a file on Windows.
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
return total;
|
|
2714
|
+
}
|
|
2715
|
+
function collectGoBuildCacheObjects(root) {
|
|
2716
|
+
const output = [];
|
|
2717
|
+
let buckets;
|
|
2718
|
+
try {
|
|
2719
|
+
buckets = node_fs_1.default.readdirSync(root, { withFileTypes: true });
|
|
2720
|
+
}
|
|
2721
|
+
catch {
|
|
2722
|
+
return output;
|
|
2723
|
+
}
|
|
2724
|
+
for (const bucket of buckets) {
|
|
2725
|
+
if (!bucket.isDirectory() || !/^[0-9a-f]{2}$/.test(bucket.name)) {
|
|
2726
|
+
continue;
|
|
2727
|
+
}
|
|
2728
|
+
const directory = node_path_1.default.join(root, bucket.name);
|
|
2729
|
+
let files;
|
|
2730
|
+
try {
|
|
2731
|
+
files = node_fs_1.default.readdirSync(directory, { withFileTypes: true });
|
|
2732
|
+
}
|
|
2733
|
+
catch {
|
|
2734
|
+
continue;
|
|
2735
|
+
}
|
|
2736
|
+
for (const file of files) {
|
|
2737
|
+
if (!file.isFile()) {
|
|
2738
|
+
continue;
|
|
2739
|
+
}
|
|
2740
|
+
const absolute = node_path_1.default.join(directory, file.name);
|
|
2741
|
+
try {
|
|
2742
|
+
const stats = node_fs_1.default.statSync(absolute);
|
|
2743
|
+
output.push({
|
|
2744
|
+
file: absolute,
|
|
2745
|
+
lastUsedAt: stats.mtimeMs,
|
|
2746
|
+
size: stats.size,
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
catch { }
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
return output;
|
|
2753
|
+
}
|
|
2754
|
+
function createGoBuildCacheCoordinationRecord(root, directoryName) {
|
|
2755
|
+
const directory = goBuildCacheCoordinationDirectory(root, directoryName, true);
|
|
2756
|
+
const record = node_path_1.default.join(directory, `${process.pid}-${node_crypto_1.default.randomBytes(16).toString("hex")}.json`);
|
|
2757
|
+
const metadata = {
|
|
2758
|
+
directoryName,
|
|
2759
|
+
hostname: node_os_1.default.hostname(),
|
|
2760
|
+
pid: process.pid,
|
|
2761
|
+
startedAt: Date.now(),
|
|
2762
|
+
};
|
|
2763
|
+
writeGoBuildCacheCoordinationRecord(record, metadata, "active");
|
|
2764
|
+
let heartbeat;
|
|
2765
|
+
return {
|
|
2766
|
+
file: record,
|
|
2767
|
+
finish: () => {
|
|
2768
|
+
heartbeat?.stop();
|
|
2769
|
+
heartbeat = undefined;
|
|
2770
|
+
// A failed unlink must not leave a completed task looking active until
|
|
2771
|
+
// its stale timeout. Persist completion first; collectors discard it.
|
|
2772
|
+
try {
|
|
2773
|
+
writeGoBuildCacheCoordinationRecord(record, metadata, "complete");
|
|
2774
|
+
}
|
|
2775
|
+
catch { }
|
|
2776
|
+
try {
|
|
2777
|
+
node_fs_1.default.rmSync(record, { force: true });
|
|
2778
|
+
}
|
|
2779
|
+
catch { }
|
|
2780
|
+
},
|
|
2781
|
+
startHeartbeat: () => {
|
|
2782
|
+
heartbeat ??= startGoBuildCacheHeartbeat(record);
|
|
2783
|
+
return heartbeat !== undefined;
|
|
2784
|
+
},
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
function writeGoBuildCacheCoordinationRecord(file, metadata, status) {
|
|
2788
|
+
replaceCacheMetadataFile(file, `${JSON.stringify({ ...metadata, status, version: 1 })}\n`);
|
|
2789
|
+
}
|
|
2790
|
+
/** Refresh one synchronous cache task's record from a background worker. */
|
|
2791
|
+
function startGoBuildCacheHeartbeat(file) {
|
|
2792
|
+
const control = new SharedArrayBuffer(4);
|
|
2793
|
+
const state = new Int32Array(control);
|
|
2794
|
+
try {
|
|
2795
|
+
const worker = new node_worker_threads_1.Worker([
|
|
2796
|
+
'const fs = process.getBuiltinModule("node:fs");',
|
|
2797
|
+
'const { workerData } = process.getBuiltinModule("node:worker_threads");',
|
|
2798
|
+
"const state = new Int32Array(workerData.control);",
|
|
2799
|
+
"for (;;) {",
|
|
2800
|
+
" const result = Atomics.wait(state, 0, 0, workerData.interval);",
|
|
2801
|
+
' if (result !== "timed-out" || Atomics.load(state, 0) !== 0) break;',
|
|
2802
|
+
" try {",
|
|
2803
|
+
" const now = new Date();",
|
|
2804
|
+
" fs.utimesSync(workerData.file, now, now);",
|
|
2805
|
+
" } catch {}",
|
|
2806
|
+
"}",
|
|
2807
|
+
].join("\n"), {
|
|
2808
|
+
eval: true,
|
|
2809
|
+
workerData: {
|
|
2810
|
+
control,
|
|
2811
|
+
file,
|
|
2812
|
+
interval: GO_BUILD_CACHE_COORDINATION_HEARTBEAT_MS,
|
|
2813
|
+
},
|
|
2814
|
+
});
|
|
2815
|
+
worker.unref();
|
|
2816
|
+
return {
|
|
2817
|
+
stop: () => {
|
|
2818
|
+
Atomics.store(state, 0, 1);
|
|
2819
|
+
Atomics.notify(state, 0);
|
|
2820
|
+
void worker.terminate();
|
|
2821
|
+
},
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
catch { }
|
|
2825
|
+
// Node's permission model can deny Worker construction while still allowing
|
|
2826
|
+
// the child process required for `go build`. An IPC-bound helper provides
|
|
2827
|
+
// the same independent heartbeat and exits automatically if its parent dies.
|
|
2828
|
+
try {
|
|
2829
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [
|
|
2830
|
+
"-e",
|
|
2831
|
+
[
|
|
2832
|
+
'const fs = require("node:fs");',
|
|
2833
|
+
"const file = process.argv[1];",
|
|
2834
|
+
"const interval = Number(process.argv[2]);",
|
|
2835
|
+
"const timer = setInterval(() => {",
|
|
2836
|
+
" try {",
|
|
2837
|
+
" const now = new Date();",
|
|
2838
|
+
" fs.utimesSync(file, now, now);",
|
|
2839
|
+
" } catch {}",
|
|
2840
|
+
"}, interval);",
|
|
2841
|
+
'process.on("disconnect", () => {',
|
|
2842
|
+
" clearInterval(timer);",
|
|
2843
|
+
" process.exit(0);",
|
|
2844
|
+
"});",
|
|
2845
|
+
].join("\n"),
|
|
2846
|
+
file,
|
|
2847
|
+
String(GO_BUILD_CACHE_COORDINATION_HEARTBEAT_MS),
|
|
2848
|
+
], {
|
|
2849
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
2850
|
+
windowsHide: true,
|
|
2851
|
+
});
|
|
2852
|
+
child.unref();
|
|
2853
|
+
child.channel?.unref();
|
|
2854
|
+
return {
|
|
2855
|
+
stop: () => {
|
|
2856
|
+
if (child.connected)
|
|
2857
|
+
child.disconnect();
|
|
2858
|
+
child.kill();
|
|
2859
|
+
},
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
catch {
|
|
2863
|
+
return undefined;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
function collectLiveGoBuildCacheCoordinationRecords(root, directoryName, now) {
|
|
2867
|
+
const directory = goBuildCacheCoordinationDirectory(root, directoryName, false);
|
|
2868
|
+
if (directory === undefined)
|
|
2869
|
+
return [];
|
|
2870
|
+
let records;
|
|
2871
|
+
try {
|
|
2872
|
+
records = node_fs_1.default.readdirSync(directory, { withFileTypes: true });
|
|
2873
|
+
}
|
|
2874
|
+
catch (error) {
|
|
2875
|
+
if (error.code === "ENOENT")
|
|
2876
|
+
return [];
|
|
2877
|
+
throw error;
|
|
2878
|
+
}
|
|
2879
|
+
const live = [];
|
|
2880
|
+
for (const record of records) {
|
|
2881
|
+
if (!record.isFile()) {
|
|
2882
|
+
continue;
|
|
2883
|
+
}
|
|
2884
|
+
const file = node_path_1.default.join(directory, record.name);
|
|
2885
|
+
if (goBuildCacheCoordinationRecordIsLive(file, directoryName, now)) {
|
|
2886
|
+
live.push(file);
|
|
2887
|
+
continue;
|
|
2888
|
+
}
|
|
2889
|
+
try {
|
|
2890
|
+
node_fs_1.default.rmSync(file, { force: true });
|
|
2891
|
+
}
|
|
2892
|
+
catch { }
|
|
2893
|
+
}
|
|
2894
|
+
return live;
|
|
2895
|
+
}
|
|
2896
|
+
/**
|
|
2897
|
+
* Resolve one private coordination directory without following a project-
|
|
2898
|
+
* supplied symlink or junction outside the owned Go cache.
|
|
2899
|
+
*/
|
|
2900
|
+
function goBuildCacheCoordinationDirectory(root, directoryName, create) {
|
|
2901
|
+
if (create)
|
|
2902
|
+
node_fs_1.default.mkdirSync(root, { recursive: true });
|
|
2903
|
+
const directory = node_path_1.default.join(root, directoryName);
|
|
2904
|
+
if (create) {
|
|
2905
|
+
try {
|
|
2906
|
+
node_fs_1.default.mkdirSync(directory);
|
|
2907
|
+
}
|
|
2908
|
+
catch (error) {
|
|
2909
|
+
if (error.code !== "EEXIST")
|
|
2910
|
+
throw error;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
let stats;
|
|
2914
|
+
try {
|
|
2915
|
+
stats = node_fs_1.default.lstatSync(directory);
|
|
2916
|
+
}
|
|
2917
|
+
catch (error) {
|
|
2918
|
+
if (!create && error.code === "ENOENT") {
|
|
2919
|
+
return undefined;
|
|
2920
|
+
}
|
|
2921
|
+
throw error;
|
|
2922
|
+
}
|
|
2923
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
2924
|
+
throw new Error(`ttsc: unsafe Go build cache coordination directory: ${directory}`);
|
|
2925
|
+
}
|
|
2926
|
+
const physicalRoot = node_fs_1.default.realpathSync.native(root);
|
|
2927
|
+
const physicalDirectory = node_fs_1.default.realpathSync.native(directory);
|
|
2928
|
+
if (node_path_1.default.dirname(physicalDirectory) !== physicalRoot) {
|
|
2929
|
+
throw new Error(`ttsc: Go build cache coordination directory escaped its root: ${directory}`);
|
|
2930
|
+
}
|
|
2931
|
+
return physicalDirectory;
|
|
2932
|
+
}
|
|
2933
|
+
function goBuildCacheCoordinationRecordIsLive(file, directoryName, now) {
|
|
2934
|
+
let contents;
|
|
2935
|
+
try {
|
|
2936
|
+
contents = node_fs_1.default.readFileSync(file, "utf8");
|
|
2937
|
+
const parsed = JSON.parse(contents);
|
|
2938
|
+
if (parsed.status === "complete")
|
|
2939
|
+
return false;
|
|
2940
|
+
// Parse valid records for forward compatibility, but do not equate a PID's
|
|
2941
|
+
// lifetime with one task. A failed unlink can leave a record owned by a
|
|
2942
|
+
// still-running Vite process, while a dead Node parent can leave its
|
|
2943
|
+
// spawnSync Go child alive. The heartbeat/grace below models the task.
|
|
2944
|
+
void parsed;
|
|
2945
|
+
}
|
|
2946
|
+
catch { }
|
|
2947
|
+
try {
|
|
2948
|
+
const age = now - node_fs_1.default.statSync(file).mtimeMs;
|
|
2949
|
+
if (age < -GO_BUILD_CACHE_COORDINATION_CLOCK_SKEW_MS) {
|
|
2950
|
+
// A restored cache can carry a far-future timestamp, but the same state
|
|
2951
|
+
// also occurs when the system clock moves backward during a real build.
|
|
2952
|
+
// Rebase the record and grant one ordinary grace period; an active
|
|
2953
|
+
// heartbeat keeps refreshing it, while an orphan then expires normally.
|
|
2954
|
+
// Treat a failed rebase as live too: the safe failure mode is to defer
|
|
2955
|
+
// opportunistic maintenance, never to delete under a possibly live Go.
|
|
2956
|
+
// Replace the directory entry rather than changing its inode in place.
|
|
2957
|
+
// A restored or user-modified cache can contain a hard-linked record;
|
|
2958
|
+
// utimesSync(file) would then mutate metadata outside the owned cache.
|
|
2959
|
+
if (contents !== undefined) {
|
|
2960
|
+
try {
|
|
2961
|
+
replaceCacheMetadataFile(file, contents);
|
|
2962
|
+
}
|
|
2963
|
+
catch { }
|
|
2964
|
+
}
|
|
2965
|
+
return true;
|
|
2966
|
+
}
|
|
2967
|
+
const staleMs = directoryName === GO_BUILD_CACHE_MAINTENANCE_DIR
|
|
2968
|
+
? GO_BUILD_CACHE_MAINTENANCE_STALE_MS
|
|
2969
|
+
: GO_BUILD_CACHE_COORDINATION_STALE_MS;
|
|
2970
|
+
return age <= staleMs;
|
|
2971
|
+
}
|
|
2972
|
+
catch (error) {
|
|
2973
|
+
return error.code !== "ENOENT";
|
|
2312
2974
|
}
|
|
2313
2975
|
}
|
|
2314
2976
|
function collectPluginCacheEntries(root, now) {
|
|
@@ -2370,6 +3032,24 @@ function readTimestamp(file) {
|
|
|
2370
3032
|
return null;
|
|
2371
3033
|
}
|
|
2372
3034
|
}
|
|
3035
|
+
/** Replace cache metadata without following a pre-existing link or hard link. */
|
|
3036
|
+
function replaceCacheMetadataFile(file, contents) {
|
|
3037
|
+
const temporary = node_path_1.default.join(node_path_1.default.dirname(file), `.${node_path_1.default.basename(file)}.${process.pid}-${node_crypto_1.default
|
|
3038
|
+
.randomBytes(16)
|
|
3039
|
+
.toString("hex")}.tmp`);
|
|
3040
|
+
try {
|
|
3041
|
+
node_fs_1.default.writeFileSync(temporary, contents, { encoding: "utf8", flag: "wx" });
|
|
3042
|
+
// rename replaces the directory entry itself. Unlike writeFile(file), it
|
|
3043
|
+
// cannot follow a symlink or mutate another hard link to the old inode.
|
|
3044
|
+
node_fs_1.default.renameSync(temporary, file);
|
|
3045
|
+
}
|
|
3046
|
+
finally {
|
|
3047
|
+
try {
|
|
3048
|
+
node_fs_1.default.rmSync(temporary, { force: true });
|
|
3049
|
+
}
|
|
3050
|
+
catch { }
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
2373
3053
|
function directorySize(dir) {
|
|
2374
3054
|
let total = 0;
|
|
2375
3055
|
let entries;
|
|
@@ -2396,9 +3076,11 @@ function directorySize(dir) {
|
|
|
2396
3076
|
function removeCacheEntry(entry) {
|
|
2397
3077
|
try {
|
|
2398
3078
|
node_fs_1.default.rmSync(entry.dir, { recursive: true, force: true });
|
|
3079
|
+
return !node_fs_1.default.existsSync(entry.dir);
|
|
2399
3080
|
}
|
|
2400
3081
|
catch {
|
|
2401
3082
|
// Windows may reject removal while a plugin binary is still running.
|
|
3083
|
+
return false;
|
|
2402
3084
|
}
|
|
2403
3085
|
}
|
|
2404
3086
|
//# sourceMappingURL=buildSourcePlugin.js.map
|