ttsc 0.18.3 → 0.19.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 +5 -6
- package/cmd/graphbench/main.go +1 -1
- package/cmd/platform/main.go +1 -48
- package/cmd/ttsc/build_branches_test.go +5 -1
- package/cmd/ttsc/main.go +0 -50
- package/cmd/ttsc/main_alias_branches_test.go +6 -12
- package/cmd/ttscgraph/dump.go +13 -3
- package/cmd/ttscgraph/main.go +18 -12
- package/cmd/ttscgraph/run_help_version_and_bad_flags_exit_codes_test.go +23 -5
- package/cmd/ttscgraph/serve.go +863 -0
- package/cmd/ttscgraph/serve_protocol_rejects_invalid_request_line_test.go +43 -0
- package/cmd/ttscgraph/serve_protocol_retries_session_creation_test.go +83 -0
- package/cmd/ttscgraph/serve_protocol_reuses_session_test.go +35 -0
- package/cmd/ttscgraph/serve_session_applies_source_edit_incrementally_test.go +55 -0
- package/cmd/ttscgraph/serve_session_fails_closed_on_deleted_config_test.go +68 -0
- package/cmd/ttscgraph/serve_session_fails_closed_on_invalid_config_test.go +50 -0
- package/cmd/ttscgraph/serve_session_keeps_missing_literal_root_unchanged_test.go +46 -0
- package/cmd/ttscgraph/serve_session_rebuilds_import_graph_change_test.go +60 -0
- package/cmd/ttscgraph/serve_session_reloads_created_literal_root_test.go +44 -0
- package/cmd/ttscgraph/serve_session_reloads_extended_config_change_test.go +45 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_conditional_export_test.go +60 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_config_types_test.go +44 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_excluded_import_test.go +62 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_package_export_test.go +46 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_package_import_test.go +46 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_path_alias_test.go +47 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_reference_path_test.go +42 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_root_dirs_target_test.go +46 -0
- package/cmd/ttscgraph/serve_session_reloads_newly_resolved_type_reference_test.go +42 -0
- package/cmd/ttscgraph/serve_session_reloads_root_file_set_test.go +52 -0
- package/cmd/ttscgraph/serve_session_reloads_source_deleted_during_load_test.go +60 -0
- package/cmd/ttscgraph/serve_session_reloads_transitive_project_reference_test.go +55 -0
- package/cmd/ttscgraph/serve_session_reloads_when_program_plugin_linked_test.go +53 -0
- package/cmd/ttscgraph/serve_session_reuses_unchanged_preamble_source_test.go +45 -0
- package/cmd/ttscgraph/serve_session_reuses_unchanged_snapshot_test.go +52 -0
- package/cmd/ttscgraph/serve_session_revisits_source_edited_during_load_test.go +61 -0
- package/cmd/ttscgraph/serve_session_tolerates_os_invalid_resolution_candidate_test.go +56 -0
- package/cmd/ttscgraph/serve_session_tracks_all_module_specifier_forms_test.go +41 -0
- package/cmd/ttscgraph/serve_session_tracks_diamond_project_references_once_test.go +61 -0
- package/cmd/ttscserver/main.go +5 -3
- package/driver/lsp.go +13 -1
- package/driver/plugins.go +56 -5
- package/driver/program.go +25 -10
- package/driver/rewrite.go +381 -21
- package/driver/session.go +11 -3
- package/go.mod +1 -0
- package/internal/graph/build.go +297 -3
- package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +16 -2
- package/internal/graph/diagnostics.go +5 -4
- package/internal/graph/dump.go +60 -4
- package/internal/graph/dump_is_a_function_of_the_source_test.go +71 -0
- package/internal/graph/dump_serializes_the_full_graph_test.go +16 -4
- package/internal/graph/edges.go +122 -11
- package/internal/graph/exports.go +74 -17
- package/internal/graph/graph.go +33 -1
- package/internal/graph/node_identity_is_position_invariant_test.go +4 -2
- package/internal/graph/resolve.go +24 -0
- package/internal/graphsymbols/provider.go +50 -14
- package/internal/lspserver/lsp_native_plugin_source.go +77 -23
- package/internal/lspserver/lsp_plugin.go +19 -2
- package/internal/lspserver/lsp_proxy.go +244 -52
- package/internal/lspserver/lsp_server.go +35 -27
- package/internal/lspserver/lsp_symbols.go +25 -16
- package/lib/TtscCompiler.d.ts +13 -2
- package/lib/TtscCompiler.js +30 -20
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/TtscService.js +9 -7
- package/lib/TtscService.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.js +15 -1
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +3 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js +14 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -0
- package/lib/compiler/internal/project/readProjectConfig.js +53 -4
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/project/resolveProjectConfig.d.ts +9 -2
- package/lib/compiler/internal/project/resolveProjectConfig.js +45 -16
- package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentTransformProcess.d.ts +16 -5
- package/lib/compiler/internal/residentTransformProcess.js +68 -20
- package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +18 -0
- package/lib/compiler/internal/runBuild.js +220 -24
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +7 -4
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +16 -0
- package/lib/compiler/internal/sharedHostHelpers.js +23 -0
- package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.js +18 -1
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +34 -8
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/flags/parser.d.ts +17 -0
- package/lib/flags/parser.js +11 -1
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.js +7 -0
- package/lib/flags/schema.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.js +21 -2
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +13 -53
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +11 -0
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +27 -11
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +81 -0
- package/lib/launcher/internal/runtimeHooks.js +632 -43
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/launcher/internal/servedSourceMap.d.ts +25 -0
- package/lib/launcher/internal/servedSourceMap.js +164 -0
- package/lib/launcher/internal/servedSourceMap.js.map +1 -0
- package/lib/plugin/internal/buildSourcePlugin.d.ts +146 -2
- package/lib/plugin/internal/buildSourcePlugin.js +651 -113
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +7 -0
- package/lib/plugin/internal/loadProjectPlugins.js +33 -10
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerContext.d.ts +16 -2
- package/lib/structures/ITtscCompilerResult.d.ts +8 -5
- package/lib/structures/ITtscPluginCapabilities.d.ts +8 -0
- package/lib/structures/ITtscPluginFactoryContext.d.ts +14 -0
- package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +3 -0
- package/lib/structures/internal/ITtscProjectIdentity.d.ts +17 -0
- package/lib/structures/internal/ITtscProjectIdentity.js +3 -0
- package/lib/structures/internal/ITtscProjectIdentity.js.map +1 -0
- package/lib/structures/internal/TtscCommonOptions.d.ts +8 -0
- package/package.json +11 -8
- package/shim/ast/lint.go +10 -0
- package/shim/ast/shim.go +44 -0
- package/shim/checker/enums_gen.go +19 -0
- package/shim/checker/shim.go +130 -0
- package/shim/diagnosticwriter/lint.go +30 -3
- package/shim/printer/shim.go +22 -4
- package/shim/scanner/regexp.go +27 -0
- package/src/TtscCompiler.ts +29 -22
- package/src/TtscService.ts +15 -7
- package/src/compiler/internal/compileProjectInMemory.ts +18 -1
- package/src/compiler/internal/project/createNativeProjectContextArgs.ts +15 -0
- package/src/compiler/internal/project/readProjectConfig.ts +61 -5
- package/src/compiler/internal/project/resolveProjectConfig.ts +51 -21
- package/src/compiler/internal/residentTransformProcess.ts +95 -21
- package/src/compiler/internal/runBuild.ts +313 -32
- package/src/compiler/internal/runSingleFileEmit.ts +7 -4
- package/src/compiler/internal/sharedHostHelpers.ts +24 -0
- package/src/compiler/internal/startResidentTransform.ts +21 -1
- package/src/compiler/internal/transformProjectInMemory.ts +49 -6
- package/src/flags/parser.ts +28 -1
- package/src/flags/schema.ts +8 -0
- package/src/launcher/internal/prepareExecution.ts +22 -2
- package/src/launcher/internal/runTtsc.ts +13 -61
- package/src/launcher/internal/runTtscserver.ts +14 -0
- package/src/launcher/internal/runTtsx.ts +28 -13
- package/src/launcher/internal/runtimeHooks.ts +771 -53
- package/src/launcher/internal/servedSourceMap.ts +183 -0
- package/src/plugin/internal/buildSourcePlugin.ts +859 -122
- package/src/plugin/internal/loadProjectPlugins.ts +36 -6
- package/src/structures/ITtscCompilerContext.ts +17 -2
- package/src/structures/ITtscCompilerResult.ts +8 -5
- package/src/structures/ITtscPluginCapabilities.ts +9 -0
- package/src/structures/ITtscPluginFactoryContext.ts +15 -0
- package/src/structures/internal/ITtscParsedProjectConfig.ts +3 -0
- package/src/structures/internal/ITtscProjectIdentity.ts +17 -0
- package/src/structures/internal/TtscCommonOptions.ts +8 -0
- package/tools/shim_audit/baseline.json +16 -7
- package/tools/shim_audit/main.go +777 -71
- package/tools/shim_audit/main_test.go +28 -1
- package/tools/shim_audit/producer_exemptions_require_rationale_test.go +42 -0
- package/tools/shim_audit/producer_surface_includes_exposed_methods_test.go +154 -0
- package/tools/shim_audit/producer_surface_rejects_unproducible_reference_test.go +142 -0
- package/utility/host.go +46 -20
- package/utility/serve.go +1 -1
- package/shim/checker/signature_api_completeness.go +0 -58
|
@@ -4,6 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.buildSourcePlugin = buildSourcePlugin;
|
|
7
|
+
exports.acquirePluginBuildLock = acquirePluginBuildLock;
|
|
8
|
+
exports.releasePluginBuildLock = releasePluginBuildLock;
|
|
9
|
+
exports.reclaimPluginBuildLock = reclaimPluginBuildLock;
|
|
10
|
+
exports.waitForPluginBinary = waitForPluginBinary;
|
|
11
|
+
exports.inspectPluginBuildLock = inspectPluginBuildLock;
|
|
12
|
+
exports.formatDuration = formatDuration;
|
|
13
|
+
exports.formatGoWorkPath = formatGoWorkPath;
|
|
14
|
+
exports.autoQuoteGoModToken = autoQuoteGoModToken;
|
|
7
15
|
exports.resolvePluginCacheRoot = resolvePluginCacheRoot;
|
|
8
16
|
exports.resolveSourceBuildCachePaths = resolveSourceBuildCachePaths;
|
|
9
17
|
exports.resolveCleanTargets = resolveCleanTargets;
|
|
@@ -111,6 +119,14 @@ const PLUGIN_BUILD_LOCK_POLL_MS = 50;
|
|
|
111
119
|
const PLUGIN_BUILD_LOCK_LEGACY_STALE_MS = 30_000;
|
|
112
120
|
const PLUGIN_BUILD_LOCK_STATUS_MS = 30_000;
|
|
113
121
|
const PLUGIN_BUILD_LOCK_OWNER_FILE = "owner.json";
|
|
122
|
+
const PLUGIN_BUILD_LOCK_PROTOCOL_FILE = "protocol-v2";
|
|
123
|
+
const PLUGIN_BUILD_LOCK_GENERATION_FILE = "generation";
|
|
124
|
+
const PLUGIN_BUILD_LOCK_LEGACY_FENCE_DIR = "legacy-generation";
|
|
125
|
+
const PLUGIN_BUILD_LOCK_LEGACY_FENCE_RECORD = "fence.json";
|
|
126
|
+
const PLUGIN_BUILD_LOCK_CURRENT_DIR = "current";
|
|
127
|
+
const PLUGIN_BUILD_LOCK_RETIRED_DIR = "retired";
|
|
128
|
+
const PLUGIN_BUILD_LOCK_V2_SUFFIX = ".v2";
|
|
129
|
+
const PLUGIN_BUILD_LOCK_PROTOCOL = "ttsc-plugin-build-lock-v2\n";
|
|
114
130
|
// The default cache lives INSIDE the workspace, at
|
|
115
131
|
// `<workspaceRoot>/node_modules/.cache/ttsc`, so `rm -rf node_modules` (or
|
|
116
132
|
// deleting the repo) reclaims every compiled plugin binary and Go object file.
|
|
@@ -141,24 +157,35 @@ const PLUGIN_CACHE_ENTRY_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
|
141
157
|
const PLUGIN_CACHE_MAX_BYTES = 2 * 1024 * 1024 * 1024;
|
|
142
158
|
const PLUGIN_CACHE_TARGET_BYTES = Math.floor(PLUGIN_CACHE_MAX_BYTES * 0.8);
|
|
143
159
|
const PLUGIN_CACHE_PROTECTED_AGE_MS = 60 * 60 * 1000;
|
|
144
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* Build one Go source plugin into a cached executable.
|
|
162
|
+
*
|
|
163
|
+
* `opts.env` is the effective environment for this build — the caller merges `{
|
|
164
|
+
* ...process.env, ...context.env }` so a programmatic `TtscCompiler` instance
|
|
165
|
+
* can pin its own Go toolchain (`TTSC_GO_BINARY`), Go build cache
|
|
166
|
+
* (`TTSC_GO_CACHE_DIR`), and Go build variables (`GOFLAGS`, `CGO_*`, …) without
|
|
167
|
+
* mutating the shared `process.env`. CLI callers omit it and inherit
|
|
168
|
+
* `process.env`, so ambient behavior is unchanged.
|
|
169
|
+
*/
|
|
145
170
|
function buildSourcePlugin(opts) {
|
|
171
|
+
const env = opts.env ?? process.env;
|
|
146
172
|
const { dir, entry, source } = resolveSourceBuildTarget(opts);
|
|
147
173
|
const overlayDirs = [...(opts.overlayDirs ?? findTtscOverlayDirs())].sort();
|
|
148
174
|
const contributors = opts.contributors ?? [];
|
|
149
|
-
const goBinary = resolveGoCompiler();
|
|
175
|
+
const goBinary = resolveGoCompiler(env);
|
|
150
176
|
ensureExecutableGoToolchain(goBinary);
|
|
151
177
|
const key = computeCacheKey({
|
|
152
178
|
contributors,
|
|
153
179
|
dir,
|
|
154
180
|
entry,
|
|
181
|
+
env,
|
|
155
182
|
goBinary,
|
|
156
183
|
overlayDirs,
|
|
157
184
|
ttscVersion: opts.ttscVersion,
|
|
158
185
|
tsgoVersion: opts.tsgoVersion,
|
|
159
186
|
});
|
|
160
|
-
const paths = resolveSourceBuildCachePaths(opts.baseDir, opts.cacheDir);
|
|
161
|
-
maybePrunePluginCache(paths, opts.cacheDir);
|
|
187
|
+
const paths = resolveSourceBuildCachePaths(opts.baseDir, opts.cacheDir, env);
|
|
188
|
+
maybePrunePluginCache(paths, opts.cacheDir, env);
|
|
162
189
|
const cacheDir = node_path_1.default.join(paths.pluginRoot, key);
|
|
163
190
|
const binaryName = process.platform === "win32" ? "plugin.exe" : "plugin";
|
|
164
191
|
const binaryPath = node_path_1.default.join(cacheDir, binaryName);
|
|
@@ -175,6 +202,7 @@ function buildSourcePlugin(opts) {
|
|
|
175
202
|
contributors,
|
|
176
203
|
dir,
|
|
177
204
|
entry,
|
|
205
|
+
env,
|
|
178
206
|
goBinary,
|
|
179
207
|
key,
|
|
180
208
|
label,
|
|
@@ -199,7 +227,7 @@ function compileSourcePlugin(opts) {
|
|
|
199
227
|
const scratchDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), `ttsc-plugin-${opts.key}-`));
|
|
200
228
|
try {
|
|
201
229
|
materializeScratchDir(opts.dir, scratchDir);
|
|
202
|
-
const goModReader = createGoModReader(opts.goBinary, opts.pluginName);
|
|
230
|
+
const goModReader = createGoModReader(opts.goBinary, opts.pluginName, opts.env);
|
|
203
231
|
if (opts.contributors.length > 0) {
|
|
204
232
|
mergeContributors({
|
|
205
233
|
contributors: opts.contributors,
|
|
@@ -209,9 +237,9 @@ function compileSourcePlugin(opts) {
|
|
|
209
237
|
scratchDir,
|
|
210
238
|
});
|
|
211
239
|
}
|
|
212
|
-
writeGoWork(scratchDir, opts.overlayDirs, opts.goBinary, opts.pluginName);
|
|
240
|
+
writeGoWork(scratchDir, opts.overlayDirs, opts.goBinary, opts.pluginName, opts.env);
|
|
213
241
|
const scratchBinaryName = process.platform === "win32" ? ".ttsc-plugin.exe" : ".ttsc-plugin";
|
|
214
|
-
runGoBuild(scratchDir, opts.entry, scratchBinaryName, opts.pluginName, opts.goBinary, opts.goBuildCacheRoot);
|
|
242
|
+
runGoBuild(scratchDir, opts.entry, scratchBinaryName, opts.pluginName, opts.goBinary, opts.goBuildCacheRoot, opts.env);
|
|
215
243
|
const builtBinary = node_path_1.default.join(scratchDir, scratchBinaryName);
|
|
216
244
|
publishBuiltBinary(builtBinary, opts.binaryPath);
|
|
217
245
|
touchCacheEntry(opts.cacheDir);
|
|
@@ -226,17 +254,28 @@ function compileSourcePlugin(opts) {
|
|
|
226
254
|
* cache key, so concurrent fan-out (parallel suites, a benchmark, a worker
|
|
227
255
|
* pool) runs the `go build` once instead of once per process.
|
|
228
256
|
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
257
|
+
* `<cacheDir>.lock.v2` is a persistent coordination directory. The adjacent
|
|
258
|
+
* `<cacheDir>.lock` path remains reserved for legacy holders and is never
|
|
259
|
+
* reused for a v2 generation: an old holder or stale legacy reclaimer can
|
|
260
|
+
* therefore remove only the legacy path, never a v2 successor. A contender
|
|
261
|
+
* writes a non-empty candidate and atomically renames it to `current`; only one
|
|
262
|
+
* rename wins. The winner builds and publishes while every loser polls and
|
|
263
|
+
* reuses the resulting binary. A loser distinguishes two ways a generation
|
|
264
|
+
* stops blocking:
|
|
265
|
+
*
|
|
266
|
+
* - `released`: the holder retired `current` itself — it published, or its build
|
|
267
|
+
* threw and its `finally` freed the key. The loser simply retries the
|
|
268
|
+
* ordinary acquisition; nothing is stale and nothing is reported.
|
|
269
|
+
* - `abandoned`: `current` still exists but its owner is provably dead, it is an
|
|
270
|
+
* old metadata-less legacy lock, or the wait budget
|
|
271
|
+
* (`PLUGIN_BUILD_LOCK_STEAL_MS`) expired. Only then does the loser report and
|
|
272
|
+
* retire precisely that generation before retrying.
|
|
235
273
|
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
274
|
+
* Retired generations remain as non-empty tombstones. Release and reclaim both
|
|
275
|
+
* rename `current` to the observed generation's deterministic tombstone path.
|
|
276
|
+
* Once generation A is retired, a stale observer or old finalizer for A cannot
|
|
277
|
+
* rename successor B there because replacing the non-empty tombstone fails
|
|
278
|
+
* atomically. `publishBuiltBinary`'s atomic rename remains defense in depth.
|
|
240
279
|
*/
|
|
241
280
|
function buildUnderPluginLock(cacheDir, binaryPath, lockInfo, build) {
|
|
242
281
|
const lockDir = `${cacheDir}.lock`;
|
|
@@ -245,34 +284,39 @@ function buildUnderPluginLock(cacheDir, binaryPath, lockInfo, build) {
|
|
|
245
284
|
touchCacheEntry(cacheDir);
|
|
246
285
|
return binaryPath;
|
|
247
286
|
}
|
|
287
|
+
let lease;
|
|
248
288
|
try {
|
|
249
|
-
|
|
250
|
-
// not throw EEXIST, defeating the lock. The parent `cacheDir` already
|
|
251
|
-
// exists, so a single-level mkdir is all that is needed and its EEXIST
|
|
252
|
-
// is exactly the "someone else holds the lock" signal.
|
|
253
|
-
node_fs_1.default.mkdirSync(lockDir);
|
|
254
|
-
writePluginBuildLockOwner(lockDir);
|
|
289
|
+
lease = acquirePluginBuildLock(lockDir);
|
|
255
290
|
}
|
|
256
|
-
catch
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
291
|
+
catch {
|
|
292
|
+
// An unusable coordination directory must not silently skip the build.
|
|
293
|
+
// Atomic publication still preserves binary integrity.
|
|
294
|
+
return build();
|
|
295
|
+
}
|
|
296
|
+
if (lease === null) {
|
|
263
297
|
const waited = waitForPluginBinary({
|
|
264
298
|
binaryPath,
|
|
265
299
|
lockDir,
|
|
266
300
|
lockInfo,
|
|
267
301
|
timeoutMs: PLUGIN_BUILD_LOCK_STEAL_MS,
|
|
268
302
|
});
|
|
269
|
-
if (waited.published) {
|
|
303
|
+
if (waited.outcome === "published") {
|
|
270
304
|
touchCacheEntry(cacheDir);
|
|
271
305
|
return binaryPath;
|
|
272
306
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
307
|
+
if (waited.outcome === "abandoned") {
|
|
308
|
+
// Retire only the generation that produced this observation. Losing
|
|
309
|
+
// the rename race means another waiter (or the holder's normal
|
|
310
|
+
// finalizer) already made progress, so do not report a stale result as
|
|
311
|
+
// an abandonment.
|
|
312
|
+
if (reclaimPluginBuildLock(lockDir, waited.fence)) {
|
|
313
|
+
reportPluginLockSteal(lockDir, binaryPath, lockInfo, waited.reason);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// "released" needs no repair: the holder freed the key normally (its
|
|
317
|
+
// build published or failed), so retry the ordinary atomic acquisition.
|
|
318
|
+
// Reporting a steal or force-removing the path here would misclassify a
|
|
319
|
+
// routine handoff as abandonment (issue #421).
|
|
276
320
|
continue;
|
|
277
321
|
}
|
|
278
322
|
try {
|
|
@@ -284,30 +328,203 @@ function buildUnderPluginLock(cacheDir, binaryPath, lockInfo, build) {
|
|
|
284
328
|
return build();
|
|
285
329
|
}
|
|
286
330
|
finally {
|
|
287
|
-
|
|
331
|
+
releasePluginBuildLock(lockDir, lease);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Atomically acquire the current generation in a v2 coordination directory.
|
|
337
|
+
*
|
|
338
|
+
* A non-empty candidate is renamed to `current`. Directory rename cannot
|
|
339
|
+
* replace a non-empty `current`, so exactly one contender wins without an
|
|
340
|
+
* empty-owner publication window. `null` means either another v2 holder won or
|
|
341
|
+
* the path is a legacy lock that must be observed before it can be reclaimed.
|
|
342
|
+
*
|
|
343
|
+
* Exported for deterministic multi-process tests.
|
|
344
|
+
*/
|
|
345
|
+
function acquirePluginBuildLock(lockDir) {
|
|
346
|
+
// A legacy holder owns the old path. Never publish v2 ownership into that
|
|
347
|
+
// deletable namespace; wait until the legacy generation is released or
|
|
348
|
+
// reclaimed, then use the orthogonal persistent v2 directory.
|
|
349
|
+
if (pluginBuildLockPathExists(lockDir)) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
const protocolDir = pluginBuildLockProtocolDir(lockDir);
|
|
353
|
+
ensurePluginBuildLockProtocol(protocolDir);
|
|
354
|
+
// Close the initialization window as far as the legacy protocol permits. A
|
|
355
|
+
// legacy holder that appeared while v2 was initialized still blocks this
|
|
356
|
+
// acquisition. (A legacy executable cannot provide a true cross-path CAS.)
|
|
357
|
+
if (pluginBuildLockPathExists(lockDir)) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
const generation = node_crypto_1.default.randomBytes(16).toString("hex");
|
|
361
|
+
const candidateDir = node_path_1.default.join(protocolDir, `candidate-${generation}`);
|
|
362
|
+
node_fs_1.default.mkdirSync(candidateDir);
|
|
363
|
+
try {
|
|
364
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(candidateDir, PLUGIN_BUILD_LOCK_GENERATION_FILE), `${generation}\n`, { encoding: "utf8", flag: "wx" });
|
|
365
|
+
writePluginBuildLockOwner(candidateDir, generation);
|
|
366
|
+
try {
|
|
367
|
+
node_fs_1.default.renameSync(candidateDir, node_path_1.default.join(protocolDir, PLUGIN_BUILD_LOCK_CURRENT_DIR));
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
if (isMissingPathError(error) ||
|
|
371
|
+
isRenameDestinationOccupied(error, node_path_1.default.join(protocolDir, PLUGIN_BUILD_LOCK_CURRENT_DIR))) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
return { protocol: "v2", generation };
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
// The candidate name contains this process's random generation and can
|
|
380
|
+
// never alias `current` or another contender's candidate.
|
|
381
|
+
node_fs_1.default.rmSync(candidateDir, { force: true, recursive: true });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** Retire a held generation during the holder's `finally`. */
|
|
385
|
+
function releasePluginBuildLock(lockDir, lease) {
|
|
386
|
+
return retireV2PluginBuildLock(pluginBuildLockProtocolDir(lockDir), lease.generation);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Retire exactly the generation carried by an abandoned observation.
|
|
390
|
+
*
|
|
391
|
+
* Exported for deterministic multi-process tests.
|
|
392
|
+
*/
|
|
393
|
+
function reclaimPluginBuildLock(lockDir, fence) {
|
|
394
|
+
if (fence.protocol === "v2") {
|
|
395
|
+
return retireV2PluginBuildLock(pluginBuildLockProtocolDir(lockDir), fence.generation);
|
|
396
|
+
}
|
|
397
|
+
return retireLegacyPluginBuildLock(lockDir, fence.generation);
|
|
398
|
+
}
|
|
399
|
+
function pluginBuildLockProtocolDir(lockDir) {
|
|
400
|
+
return `${lockDir}${PLUGIN_BUILD_LOCK_V2_SUFFIX}`;
|
|
401
|
+
}
|
|
402
|
+
function ensurePluginBuildLockProtocol(protocolDir) {
|
|
403
|
+
if (isPluginBuildLockProtocolV2(protocolDir)) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const generation = node_crypto_1.default.randomBytes(16).toString("hex");
|
|
407
|
+
const candidateDir = `${protocolDir}.candidate-${generation}`;
|
|
408
|
+
node_fs_1.default.mkdirSync(candidateDir);
|
|
409
|
+
try {
|
|
410
|
+
node_fs_1.default.mkdirSync(node_path_1.default.join(candidateDir, PLUGIN_BUILD_LOCK_RETIRED_DIR));
|
|
411
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(candidateDir, PLUGIN_BUILD_LOCK_PROTOCOL_FILE), PLUGIN_BUILD_LOCK_PROTOCOL, { encoding: "utf8", flag: "wx" });
|
|
412
|
+
try {
|
|
413
|
+
node_fs_1.default.renameSync(candidateDir, protocolDir);
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
if (isRenameDestinationOccupied(error, protocolDir) &&
|
|
417
|
+
isPluginBuildLockProtocolV2(protocolDir)) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
node_fs_1.default.rmSync(candidateDir, { force: true, recursive: true });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function isPluginBuildLockProtocolV2(lockDir) {
|
|
428
|
+
try {
|
|
429
|
+
return (node_fs_1.default.readFileSync(node_path_1.default.join(lockDir, PLUGIN_BUILD_LOCK_PROTOCOL_FILE), "utf8") === PLUGIN_BUILD_LOCK_PROTOCOL);
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function retireV2PluginBuildLock(lockDir, generation) {
|
|
436
|
+
if (!isPluginBuildLockGeneration(generation))
|
|
437
|
+
return false;
|
|
438
|
+
const retiredDir = node_path_1.default.join(lockDir, PLUGIN_BUILD_LOCK_RETIRED_DIR);
|
|
439
|
+
try {
|
|
440
|
+
node_fs_1.default.mkdirSync(retiredDir);
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
if (error.code !== "EEXIST") {
|
|
444
|
+
if (isMissingPathError(error))
|
|
445
|
+
return false;
|
|
446
|
+
throw error;
|
|
288
447
|
}
|
|
289
448
|
}
|
|
449
|
+
const destination = node_path_1.default.join(retiredDir, generation);
|
|
450
|
+
try {
|
|
451
|
+
node_fs_1.default.renameSync(node_path_1.default.join(lockDir, PLUGIN_BUILD_LOCK_CURRENT_DIR), destination);
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
if (isMissingPathError(error) ||
|
|
456
|
+
isRenameDestinationOccupied(error, destination)) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function retireLegacyPluginBuildLock(lockDir, generation) {
|
|
463
|
+
if (!isPluginBuildLockGeneration(generation))
|
|
464
|
+
return false;
|
|
465
|
+
const captured = readLegacyPluginBuildLockFence(node_path_1.default.join(lockDir, PLUGIN_BUILD_LOCK_LEGACY_FENCE_DIR));
|
|
466
|
+
if (captured?.fence.generation !== generation) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
const destination = `${lockDir}.retired-${generation}`;
|
|
470
|
+
try {
|
|
471
|
+
node_fs_1.default.renameSync(lockDir, destination);
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
if (isMissingPathError(error) ||
|
|
476
|
+
isRenameDestinationOccupied(error, destination)) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function isMissingPathError(error) {
|
|
483
|
+
const code = error.code;
|
|
484
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
290
485
|
}
|
|
291
|
-
|
|
486
|
+
function isRenameDestinationOccupied(error, destination) {
|
|
487
|
+
const code = error.code;
|
|
488
|
+
if (code === "EEXIST" || code === "ENOTEMPTY") {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
return (code === "EACCES" || code === "EPERM") && node_fs_1.default.existsSync(destination);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Poll for the locked builder to publish its binary, up to `timeoutMs`.
|
|
495
|
+
*
|
|
496
|
+
* Exported for unit tests.
|
|
497
|
+
*/
|
|
292
498
|
function waitForPluginBinary(opts) {
|
|
293
499
|
const startedAt = Date.now();
|
|
294
500
|
let nextStatusAt = startedAt + PLUGIN_BUILD_LOCK_STATUS_MS;
|
|
295
501
|
for (;;) {
|
|
296
502
|
if (node_fs_1.default.existsSync(opts.binaryPath)) {
|
|
297
|
-
return {
|
|
503
|
+
return { outcome: "published" };
|
|
298
504
|
}
|
|
299
505
|
const now = Date.now();
|
|
300
|
-
const
|
|
301
|
-
if (
|
|
506
|
+
const lock = inspectPluginBuildLock(opts.lockDir, now);
|
|
507
|
+
if (lock.state === "released") {
|
|
508
|
+
// The holder retired its generation between the binary check above and
|
|
509
|
+
// this observation. That is a normal release, not abandonment: prefer the
|
|
510
|
+
// binary when it landed inside that window, otherwise hand the free key
|
|
511
|
+
// back to the caller.
|
|
512
|
+
return node_fs_1.default.existsSync(opts.binaryPath)
|
|
513
|
+
? { outcome: "published" }
|
|
514
|
+
: { outcome: "released" };
|
|
515
|
+
}
|
|
516
|
+
if (lock.state === "abandoned") {
|
|
302
517
|
return {
|
|
303
|
-
|
|
304
|
-
reason:
|
|
518
|
+
outcome: "abandoned",
|
|
519
|
+
reason: lock.reason,
|
|
520
|
+
fence: lock.fence,
|
|
305
521
|
};
|
|
306
522
|
}
|
|
307
523
|
if (now - startedAt > opts.timeoutMs) {
|
|
308
524
|
return {
|
|
309
|
-
|
|
525
|
+
outcome: "abandoned",
|
|
310
526
|
reason: `timed out after ${formatDuration(now - startedAt)}`,
|
|
527
|
+
fence: lock.fence,
|
|
311
528
|
};
|
|
312
529
|
}
|
|
313
530
|
if (!opts.lockInfo.quiet && now >= nextStatusAt) {
|
|
@@ -316,55 +533,221 @@ function waitForPluginBinary(opts) {
|
|
|
316
533
|
elapsedMs: now - startedAt,
|
|
317
534
|
lockDir: opts.lockDir,
|
|
318
535
|
lockInfo: opts.lockInfo,
|
|
319
|
-
owner:
|
|
536
|
+
owner: lock.owner,
|
|
320
537
|
});
|
|
321
538
|
nextStatusAt = now + PLUGIN_BUILD_LOCK_STATUS_MS;
|
|
322
539
|
}
|
|
323
540
|
sleepSync(PLUGIN_BUILD_LOCK_POLL_MS);
|
|
324
541
|
}
|
|
325
542
|
}
|
|
326
|
-
function writePluginBuildLockOwner(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
543
|
+
function writePluginBuildLockOwner(generationDir, generation) {
|
|
544
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(generationDir, PLUGIN_BUILD_LOCK_OWNER_FILE), `${JSON.stringify({
|
|
545
|
+
generation,
|
|
546
|
+
hostname: node_os_1.default.hostname(),
|
|
547
|
+
pid: process.pid,
|
|
548
|
+
startedAt: new Date().toISOString(),
|
|
549
|
+
}, null, 2)}\n`, "utf8");
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Classify the current state of a plugin build lock directory.
|
|
553
|
+
*
|
|
554
|
+
* Exported for unit tests.
|
|
555
|
+
*/
|
|
556
|
+
function inspectPluginBuildLock(lockDir, now) {
|
|
557
|
+
const protocolDir = pluginBuildLockProtocolDir(lockDir);
|
|
558
|
+
for (;;) {
|
|
559
|
+
if (isPluginBuildLockProtocolV2(protocolDir)) {
|
|
560
|
+
const v2 = inspectV2PluginBuildLock(protocolDir, now);
|
|
561
|
+
if (v2.state !== "released") {
|
|
562
|
+
return v2;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const legacy = captureLegacyPluginBuildLockFence(lockDir);
|
|
566
|
+
if (legacy !== null) {
|
|
567
|
+
return inspectLegacyPluginBuildLock(lockDir, now, legacy);
|
|
568
|
+
}
|
|
569
|
+
if (pluginBuildLockAgeMs(lockDir, now) === null) {
|
|
570
|
+
return { state: "released" };
|
|
571
|
+
}
|
|
572
|
+
// The path changed while its legacy fence was being captured. Re-observe
|
|
573
|
+
// the replacement rather than attaching the old state to a new owner.
|
|
333
574
|
}
|
|
334
|
-
|
|
335
|
-
|
|
575
|
+
}
|
|
576
|
+
function inspectV2PluginBuildLock(lockDir, now) {
|
|
577
|
+
const generationDir = node_path_1.default.join(lockDir, PLUGIN_BUILD_LOCK_CURRENT_DIR);
|
|
578
|
+
const generation = readPluginBuildLockGeneration(generationDir);
|
|
579
|
+
if (generation === null) {
|
|
580
|
+
if (pluginBuildLockAgeMs(generationDir, now) === null) {
|
|
581
|
+
return { state: "released" };
|
|
582
|
+
}
|
|
583
|
+
throw new Error(`ttsc plugin build lock has no valid ${PLUGIN_BUILD_LOCK_GENERATION_FILE}: ${generationDir}`);
|
|
336
584
|
}
|
|
585
|
+
const fence = { protocol: "v2", generation };
|
|
586
|
+
const owner = readPluginBuildLockOwner(generationDir);
|
|
587
|
+
if (owner !== null) {
|
|
588
|
+
const label = describePluginBuildLockOwner(owner);
|
|
589
|
+
if (isLocalHostName(owner.hostname) && !isProcessAlive(owner.pid)) {
|
|
590
|
+
return {
|
|
591
|
+
state: "abandoned",
|
|
592
|
+
reason: `${label} is no longer running`,
|
|
593
|
+
fence,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
state: "active",
|
|
598
|
+
owner: label,
|
|
599
|
+
fence,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const ageMs = pluginBuildLockAgeMs(generationDir, now);
|
|
603
|
+
if (ageMs === null) {
|
|
604
|
+
return { state: "released" };
|
|
605
|
+
}
|
|
606
|
+
if (ageMs > PLUGIN_BUILD_LOCK_LEGACY_STALE_MS) {
|
|
607
|
+
return {
|
|
608
|
+
state: "abandoned",
|
|
609
|
+
reason: `lock generation has no ${PLUGIN_BUILD_LOCK_OWNER_FILE} and is ` +
|
|
610
|
+
`${formatDuration(ageMs)} old`,
|
|
611
|
+
fence,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
state: "active",
|
|
616
|
+
owner: `lock generation with no ${PLUGIN_BUILD_LOCK_OWNER_FILE}`,
|
|
617
|
+
fence,
|
|
618
|
+
};
|
|
337
619
|
}
|
|
338
|
-
function
|
|
620
|
+
function inspectLegacyPluginBuildLock(lockDir, now, legacy) {
|
|
339
621
|
const owner = readPluginBuildLockOwner(lockDir);
|
|
340
622
|
if (owner !== null) {
|
|
341
623
|
const label = describePluginBuildLockOwner(owner);
|
|
342
624
|
if (isLocalHostName(owner.hostname) && !isProcessAlive(owner.pid)) {
|
|
343
625
|
return {
|
|
344
|
-
|
|
345
|
-
owner: label,
|
|
626
|
+
state: "abandoned",
|
|
346
627
|
reason: `${label} is no longer running`,
|
|
628
|
+
fence: legacy.fence,
|
|
347
629
|
};
|
|
348
630
|
}
|
|
349
631
|
return {
|
|
350
|
-
|
|
632
|
+
state: "active",
|
|
351
633
|
owner: label,
|
|
634
|
+
fence: legacy.fence,
|
|
352
635
|
};
|
|
353
636
|
}
|
|
354
|
-
const ageMs =
|
|
637
|
+
const ageMs = Math.max(0, now - legacy.legacyMtimeMs);
|
|
355
638
|
if (ageMs > PLUGIN_BUILD_LOCK_LEGACY_STALE_MS) {
|
|
356
639
|
return {
|
|
357
|
-
|
|
358
|
-
owner: `legacy lock with no ${PLUGIN_BUILD_LOCK_OWNER_FILE}`,
|
|
640
|
+
state: "abandoned",
|
|
359
641
|
reason: `legacy lock has no ${PLUGIN_BUILD_LOCK_OWNER_FILE} and is ` +
|
|
360
642
|
`${formatDuration(ageMs)} old`,
|
|
643
|
+
fence: legacy.fence,
|
|
361
644
|
};
|
|
362
645
|
}
|
|
363
646
|
return {
|
|
364
|
-
|
|
647
|
+
state: "active",
|
|
365
648
|
owner: `legacy lock with no ${PLUGIN_BUILD_LOCK_OWNER_FILE}`,
|
|
649
|
+
fence: legacy.fence,
|
|
366
650
|
};
|
|
367
651
|
}
|
|
652
|
+
function captureLegacyPluginBuildLockFence(lockDir) {
|
|
653
|
+
if (isPluginBuildLockProtocolV2(lockDir)) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
let legacyMtimeMs;
|
|
657
|
+
try {
|
|
658
|
+
legacyMtimeMs = node_fs_1.default.statSync(lockDir).mtimeMs;
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
if (isMissingPathError(error))
|
|
662
|
+
return null;
|
|
663
|
+
throw error;
|
|
664
|
+
}
|
|
665
|
+
const fenceDir = node_path_1.default.join(lockDir, PLUGIN_BUILD_LOCK_LEGACY_FENCE_DIR);
|
|
666
|
+
let captured = readLegacyPluginBuildLockFence(fenceDir);
|
|
667
|
+
if (captured === null) {
|
|
668
|
+
const generation = node_crypto_1.default.randomBytes(16).toString("hex");
|
|
669
|
+
// Keep candidates beside the legacy lock. Creating one inside `lockDir`
|
|
670
|
+
// would advance its mtime before a contender publishes the shared fence;
|
|
671
|
+
// a concurrent contender could then record an old lock as freshly created.
|
|
672
|
+
const candidateDir = `${lockDir}.legacy-candidate-${generation}`;
|
|
673
|
+
try {
|
|
674
|
+
node_fs_1.default.mkdirSync(candidateDir);
|
|
675
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(candidateDir, PLUGIN_BUILD_LOCK_LEGACY_FENCE_RECORD), `${JSON.stringify({ generation, legacyMtimeMs })}\n`, "utf8");
|
|
676
|
+
try {
|
|
677
|
+
node_fs_1.default.renameSync(candidateDir, fenceDir);
|
|
678
|
+
captured = {
|
|
679
|
+
fence: { protocol: "legacy", generation },
|
|
680
|
+
legacyMtimeMs,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
if (isRenameDestinationOccupied(error, fenceDir)) {
|
|
685
|
+
captured = readLegacyPluginBuildLockFence(fenceDir);
|
|
686
|
+
}
|
|
687
|
+
else if (isMissingPathError(error)) {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
if (isMissingPathError(error)) {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
throw error;
|
|
700
|
+
}
|
|
701
|
+
finally {
|
|
702
|
+
node_fs_1.default.rmSync(candidateDir, { force: true, recursive: true });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (captured === null) {
|
|
706
|
+
throw new Error(`invalid legacy plugin build lock fence: ${fenceDir}`);
|
|
707
|
+
}
|
|
708
|
+
// A stale observer can resume after the legacy holder released or another
|
|
709
|
+
// process retired the path. Confirm both the legacy layout and token after
|
|
710
|
+
// publication; v2 ownership is kept in the orthogonal sibling directory.
|
|
711
|
+
const confirmed = readLegacyPluginBuildLockFence(fenceDir);
|
|
712
|
+
if (isPluginBuildLockProtocolV2(lockDir) ||
|
|
713
|
+
confirmed === null ||
|
|
714
|
+
confirmed.fence.generation !== captured.fence.generation) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
return captured;
|
|
718
|
+
}
|
|
719
|
+
function readLegacyPluginBuildLockFence(fenceDir) {
|
|
720
|
+
try {
|
|
721
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(fenceDir, PLUGIN_BUILD_LOCK_LEGACY_FENCE_RECORD), "utf8"));
|
|
722
|
+
if (!isPluginBuildLockGeneration(parsed.generation) ||
|
|
723
|
+
typeof parsed.legacyMtimeMs !== "number" ||
|
|
724
|
+
!Number.isFinite(parsed.legacyMtimeMs) ||
|
|
725
|
+
parsed.legacyMtimeMs < 0) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
return {
|
|
729
|
+
fence: { protocol: "legacy", generation: parsed.generation },
|
|
730
|
+
legacyMtimeMs: parsed.legacyMtimeMs,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function readPluginBuildLockGeneration(generationDir) {
|
|
738
|
+
try {
|
|
739
|
+
const generation = node_fs_1.default
|
|
740
|
+
.readFileSync(node_path_1.default.join(generationDir, PLUGIN_BUILD_LOCK_GENERATION_FILE), "utf8")
|
|
741
|
+
.trim();
|
|
742
|
+
return isPluginBuildLockGeneration(generation) ? generation : null;
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function isPluginBuildLockGeneration(value) {
|
|
749
|
+
return typeof value === "string" && /^[0-9a-f]{32}$/.test(value);
|
|
750
|
+
}
|
|
368
751
|
function readPluginBuildLockOwner(lockDir) {
|
|
369
752
|
try {
|
|
370
753
|
const parsed = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(lockDir, PLUGIN_BUILD_LOCK_OWNER_FILE), "utf8"));
|
|
@@ -384,12 +767,35 @@ function readPluginBuildLockOwner(lockDir) {
|
|
|
384
767
|
return null;
|
|
385
768
|
}
|
|
386
769
|
}
|
|
770
|
+
/**
|
|
771
|
+
* Age of an observed lock directory, or `null` when it no longer exists. The
|
|
772
|
+
* holder may have retired it between the caller's checks. "Missing" is a
|
|
773
|
+
* observation, never encoded as a numeric age: the previous
|
|
774
|
+
* `Number.POSITIVE_INFINITY` encoding made a just-released lock look like an
|
|
775
|
+
* infinitely old abandoned legacy lock (issue #421).
|
|
776
|
+
*
|
|
777
|
+
* A stat failure that does not prove absence (e.g. `EPERM`) clamps to age 0:
|
|
778
|
+
* the lock is treated as fresh so a waiter never steals on ambiguous evidence,
|
|
779
|
+
* while the caller's wait budget still bounds the stall.
|
|
780
|
+
*/
|
|
387
781
|
function pluginBuildLockAgeMs(lockDir, now) {
|
|
388
782
|
try {
|
|
389
783
|
return Math.max(0, now - node_fs_1.default.statSync(lockDir).mtimeMs);
|
|
390
784
|
}
|
|
391
|
-
catch {
|
|
392
|
-
|
|
785
|
+
catch (error) {
|
|
786
|
+
const code = error.code;
|
|
787
|
+
return code === "ENOENT" || code === "ENOTDIR" ? null : 0;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function pluginBuildLockPathExists(lockDir) {
|
|
791
|
+
try {
|
|
792
|
+
node_fs_1.default.statSync(lockDir);
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
if (isMissingPathError(error))
|
|
797
|
+
return false;
|
|
798
|
+
throw error;
|
|
393
799
|
}
|
|
394
800
|
}
|
|
395
801
|
function isLocalHostName(hostname) {
|
|
@@ -416,11 +822,23 @@ function reportPluginLockWait(opts) {
|
|
|
416
822
|
function reportPluginLockSteal(lockDir, binaryPath, lockInfo, reason) {
|
|
417
823
|
if (lockInfo.quiet)
|
|
418
824
|
return;
|
|
419
|
-
const suffix = reason === undefined ? "" : ` (${reason})`;
|
|
420
825
|
process.stderr.write(`ttsc: reclaiming abandoned ${lockInfo.label} "${lockInfo.pluginName}" ` +
|
|
421
|
-
`cache lock at ${lockDir}; binary=${binaryPath}${
|
|
826
|
+
`cache lock at ${lockDir}; binary=${binaryPath} (${reason})\n`);
|
|
422
827
|
}
|
|
828
|
+
/**
|
|
829
|
+
* Render a millisecond duration for lock diagnostics (`137ms`, `42s`, `9m 3s`).
|
|
830
|
+
*
|
|
831
|
+
* Total over every number: no caller produces a non-finite duration anymore
|
|
832
|
+
* (the lock state machine reports "released" instead of an Infinity age), but
|
|
833
|
+
* as defense in depth a non-finite input renders as `an unknown time` so no
|
|
834
|
+
* public diagnostic can ever print `Infinitym NaNs` again (issue #421).
|
|
835
|
+
*
|
|
836
|
+
* Exported for unit tests.
|
|
837
|
+
*/
|
|
423
838
|
function formatDuration(ms) {
|
|
839
|
+
if (!Number.isFinite(ms)) {
|
|
840
|
+
return "an unknown time";
|
|
841
|
+
}
|
|
424
842
|
if (ms < 1_000) {
|
|
425
843
|
return `${Math.max(0, Math.round(ms))}ms`;
|
|
426
844
|
}
|
|
@@ -628,8 +1046,8 @@ function materializeScratchDir(source, scratch) {
|
|
|
628
1046
|
},
|
|
629
1047
|
});
|
|
630
1048
|
}
|
|
631
|
-
function writeGoWork(scratchDir, useDirs, goBinary, pluginName) {
|
|
632
|
-
const goModReader = createGoModReader(goBinary, pluginName);
|
|
1049
|
+
function writeGoWork(scratchDir, useDirs, goBinary, pluginName, env) {
|
|
1050
|
+
const goModReader = createGoModReader(goBinary, pluginName, env);
|
|
633
1051
|
validateSourceReplacements(scratchDir, useDirs, goModReader, pluginName);
|
|
634
1052
|
const sourceInfo = goModReader.read(scratchDir);
|
|
635
1053
|
const effectiveUseDirs = sourceInfo.modulePath === TTSC_GO_MODULE_PATH
|
|
@@ -640,7 +1058,7 @@ function writeGoWork(scratchDir, useDirs, goBinary, pluginName) {
|
|
|
640
1058
|
: useDirs;
|
|
641
1059
|
const useLines = ["\t."];
|
|
642
1060
|
for (const dir of effectiveUseDirs) {
|
|
643
|
-
useLines.push(`\t${dir
|
|
1061
|
+
useLines.push(`\t${formatGoWorkPath(dir)}`);
|
|
644
1062
|
}
|
|
645
1063
|
const replaceLines = sourceBuildWorkspaceReplacements(effectiveUseDirs, goModReader);
|
|
646
1064
|
const replaceBlock = replaceLines.length === 0 ? "" : `\n\n${replaceLines.join("\n")}\n`;
|
|
@@ -672,10 +1090,125 @@ function sourceBuildWorkspaceReplacements(useDirs, goModReader) {
|
|
|
672
1090
|
return [];
|
|
673
1091
|
}
|
|
674
1092
|
return [
|
|
675
|
-
`replace ${TTSC_GO_MODULE_PATH} v0.0.0 => ${ttscRoot
|
|
1093
|
+
`replace ${TTSC_GO_MODULE_PATH} v0.0.0 => ${formatGoWorkPath(ttscRoot)}`,
|
|
676
1094
|
];
|
|
677
1095
|
}
|
|
678
|
-
|
|
1096
|
+
/**
|
|
1097
|
+
* Format an absolute filesystem path as a single `go.work`/`go.mod` token.
|
|
1098
|
+
*
|
|
1099
|
+
* The modfile grammar shared by `go.mod` and `go.work` (parsed by
|
|
1100
|
+
* `golang.org/x/mod/modfile`) is whitespace-tokenized, so a `use`/`replace`
|
|
1101
|
+
* path that contains a space — a home or project directory such as `/Users/John
|
|
1102
|
+
* Smith/...` or `C:\Users\John Smith\...` — must be emitted as a quoted string
|
|
1103
|
+
* or `go` cannot parse the generated `go.work`. Normalize Windows separators to
|
|
1104
|
+
* `/` (the workspace convention) and then delegate to
|
|
1105
|
+
* {@link autoQuoteGoModToken}, which mirrors `modfile.AutoQuote`.
|
|
1106
|
+
*
|
|
1107
|
+
* Exported for unit tests.
|
|
1108
|
+
*/
|
|
1109
|
+
function formatGoWorkPath(p) {
|
|
1110
|
+
return autoQuoteGoModToken(p.replace(/\\/g, "/"));
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Quote `token` for a `go.mod`/`go.work` line exactly as
|
|
1114
|
+
* `golang.org/x/mod/modfile`'s `AutoQuote` does: return it unchanged when it is
|
|
1115
|
+
* already a clean bare token, otherwise return its Go double-quoted form so the
|
|
1116
|
+
* value round-trips through the modfile lexer. A space-free path is therefore
|
|
1117
|
+
* emitted byte-for-byte as before; only tokens that would otherwise
|
|
1118
|
+
* mis-tokenize are quoted.
|
|
1119
|
+
*
|
|
1120
|
+
* Exported for unit tests.
|
|
1121
|
+
*/
|
|
1122
|
+
function autoQuoteGoModToken(token) {
|
|
1123
|
+
return mustQuoteGoModToken(token) ? goQuoteString(token) : token;
|
|
1124
|
+
}
|
|
1125
|
+
// Mirror `modfile.MustQuote`: report whether `s` must be quoted to appear as a
|
|
1126
|
+
// single token on a modfile line.
|
|
1127
|
+
function mustQuoteGoModToken(s) {
|
|
1128
|
+
for (const ch of s) {
|
|
1129
|
+
if (ch === " " || ch === '"' || ch === "'" || ch === "`") {
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
if (ch === "(" ||
|
|
1133
|
+
ch === ")" ||
|
|
1134
|
+
ch === "[" ||
|
|
1135
|
+
ch === "]" ||
|
|
1136
|
+
ch === "{" ||
|
|
1137
|
+
ch === "}" ||
|
|
1138
|
+
ch === ",") {
|
|
1139
|
+
// Go tests `len(s) > 1` (byte length): a lone bracket/comma is a legal
|
|
1140
|
+
// bare token, but one embedded in a longer token forces quoting.
|
|
1141
|
+
if (Buffer.byteLength(s, "utf8") > 1) {
|
|
1142
|
+
return true;
|
|
1143
|
+
}
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
if (!isGoGraphic(ch)) {
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return s === "" || s === "//" || s === "/*";
|
|
1151
|
+
}
|
|
1152
|
+
// Mirror `strconv.Quote`: wrap in double quotes, backslash-escape `"` and `\`,
|
|
1153
|
+
// emit Go-printable runes verbatim (including the ASCII space and printable
|
|
1154
|
+
// Unicode), and escape everything else with Go's `\a\b\f\n\r\t\v` / `\xNN` /
|
|
1155
|
+
// `\uNNNN` / `\UNNNNNNNN` forms so the token round-trips through
|
|
1156
|
+
// `strconv.Unquote` in the modfile lexer.
|
|
1157
|
+
function goQuoteString(s) {
|
|
1158
|
+
let out = '"';
|
|
1159
|
+
for (const ch of s) {
|
|
1160
|
+
if (ch === '"' || ch === "\\") {
|
|
1161
|
+
out += `\\${ch}`;
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
if (isGoPrintable(ch)) {
|
|
1165
|
+
out += ch;
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
out += escapeGoRune(ch);
|
|
1169
|
+
}
|
|
1170
|
+
return `${out}"`;
|
|
1171
|
+
}
|
|
1172
|
+
function escapeGoRune(ch) {
|
|
1173
|
+
switch (ch) {
|
|
1174
|
+
case "\x07":
|
|
1175
|
+
return "\\a";
|
|
1176
|
+
case "\b":
|
|
1177
|
+
return "\\b";
|
|
1178
|
+
case "\f":
|
|
1179
|
+
return "\\f";
|
|
1180
|
+
case "\n":
|
|
1181
|
+
return "\\n";
|
|
1182
|
+
case "\r":
|
|
1183
|
+
return "\\r";
|
|
1184
|
+
case "\t":
|
|
1185
|
+
return "\\t";
|
|
1186
|
+
case "\v":
|
|
1187
|
+
return "\\v";
|
|
1188
|
+
default: {
|
|
1189
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
1190
|
+
if (cp < 0x20 || cp === 0x7f) {
|
|
1191
|
+
return `\\x${cp.toString(16).padStart(2, "0")}`;
|
|
1192
|
+
}
|
|
1193
|
+
if (cp < 0x10000) {
|
|
1194
|
+
return `\\u${cp.toString(16).padStart(4, "0")}`;
|
|
1195
|
+
}
|
|
1196
|
+
return `\\U${cp.toString(16).padStart(8, "0")}`;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
// `unicode.IsGraphic`: categories L, M, N, P, S, Zs (all spacing).
|
|
1201
|
+
const GO_GRAPHIC_RE = /^[\p{L}\p{M}\p{N}\p{P}\p{S}\p{Zs}]$/u;
|
|
1202
|
+
function isGoGraphic(ch) {
|
|
1203
|
+
return GO_GRAPHIC_RE.test(ch);
|
|
1204
|
+
}
|
|
1205
|
+
// `strconv.IsPrint`: the graphic categories except that the ONLY spacing
|
|
1206
|
+
// character is the ASCII space (U+0020); other Unicode spaces are escaped.
|
|
1207
|
+
const GO_PRINTABLE_RE = /^[\p{L}\p{M}\p{N}\p{P}\p{S}]$/u;
|
|
1208
|
+
function isGoPrintable(ch) {
|
|
1209
|
+
return ch === " " || GO_PRINTABLE_RE.test(ch);
|
|
1210
|
+
}
|
|
1211
|
+
function createGoModReader(goBinary, pluginName, env) {
|
|
679
1212
|
const cache = new Map();
|
|
680
1213
|
return {
|
|
681
1214
|
read(dir) {
|
|
@@ -684,20 +1217,20 @@ function createGoModReader(goBinary, pluginName) {
|
|
|
684
1217
|
if (cached !== undefined) {
|
|
685
1218
|
return cached;
|
|
686
1219
|
}
|
|
687
|
-
const info = readGoModInfo(resolved, goBinary, pluginName);
|
|
1220
|
+
const info = readGoModInfo(resolved, goBinary, pluginName, env);
|
|
688
1221
|
cache.set(resolved, info);
|
|
689
1222
|
return info;
|
|
690
1223
|
},
|
|
691
1224
|
};
|
|
692
1225
|
}
|
|
693
|
-
function readGoModInfo(dir, goBinary, pluginName) {
|
|
1226
|
+
function readGoModInfo(dir, goBinary, pluginName, env) {
|
|
694
1227
|
if (!node_fs_1.default.existsSync(node_path_1.default.join(dir, "go.mod"))) {
|
|
695
1228
|
return emptyGoModInfo();
|
|
696
1229
|
}
|
|
697
1230
|
const result = spawnGoTool(goBinary, ["mod", "edit", "-json"], {
|
|
698
1231
|
cwd: dir,
|
|
699
1232
|
encoding: "utf8",
|
|
700
|
-
env: goBuildEnv(goBinary),
|
|
1233
|
+
env: goBuildEnv(goBinary, undefined, env),
|
|
701
1234
|
maxBuffer: 1024 * 1024 * 16,
|
|
702
1235
|
windowsHide: true,
|
|
703
1236
|
});
|
|
@@ -754,12 +1287,12 @@ function isTtscManagedModulePath(modulePath) {
|
|
|
754
1287
|
modulePath === TSGO_GO_MODULE_PATH ||
|
|
755
1288
|
modulePath.startsWith("github.com/microsoft/typescript-go/shim/"));
|
|
756
1289
|
}
|
|
757
|
-
function runGoBuild(cwd, entry, binaryName, pluginName, goBinary, goBuildCacheRoot) {
|
|
1290
|
+
function runGoBuild(cwd, entry, binaryName, pluginName, goBinary, goBuildCacheRoot, env) {
|
|
758
1291
|
ensureExecutableGoToolchain(goBinary);
|
|
759
1292
|
const result = spawnGoTool(goBinary, ["build", "-o", binaryName, entry], {
|
|
760
1293
|
cwd,
|
|
761
1294
|
encoding: "utf8",
|
|
762
|
-
env: goBuildEnv(goBinary, goBuildCacheRoot),
|
|
1295
|
+
env: goBuildEnv(goBinary, goBuildCacheRoot, env),
|
|
763
1296
|
maxBuffer: 1024 * 1024 * 64,
|
|
764
1297
|
windowsHide: true,
|
|
765
1298
|
});
|
|
@@ -792,8 +1325,8 @@ function spawnGoTool(goBinary, args, options) {
|
|
|
792
1325
|
function shouldSpawnGoToolThroughShell(goBinary) {
|
|
793
1326
|
return process.platform === "win32" && /\.(?:bat|cmd)$/i.test(goBinary);
|
|
794
1327
|
}
|
|
795
|
-
function goBuildEnv(goBinary, goBuildCacheRoot) {
|
|
796
|
-
const env = { ...
|
|
1328
|
+
function goBuildEnv(goBinary, goBuildCacheRoot, effectiveEnv = process.env) {
|
|
1329
|
+
const env = { ...effectiveEnv };
|
|
797
1330
|
env.GOWORK = "auto";
|
|
798
1331
|
// Only the actual `go build` needs ttsc's GOCACHE; read-only metadata spawns
|
|
799
1332
|
// (`go mod edit`, `go env`, `go version`) call goBuildEnv with no cache root
|
|
@@ -906,9 +1439,9 @@ function walkForGoMod(dir, out) {
|
|
|
906
1439
|
* callers (and tests) that only need the plugin-binary root. Triggers the
|
|
907
1440
|
* opportunistic project-cache GC as a side effect for the default location.
|
|
908
1441
|
*/
|
|
909
|
-
function resolvePluginCacheRoot(projectRoot, cacheDir) {
|
|
910
|
-
const paths = resolveSourceBuildCachePaths(projectRoot, cacheDir);
|
|
911
|
-
maybePrunePluginCache(paths, cacheDir);
|
|
1442
|
+
function resolvePluginCacheRoot(projectRoot, cacheDir, env = process.env) {
|
|
1443
|
+
const paths = resolveSourceBuildCachePaths(projectRoot, cacheDir, env);
|
|
1444
|
+
maybePrunePluginCache(paths, cacheDir, env);
|
|
912
1445
|
return paths.pluginRoot;
|
|
913
1446
|
}
|
|
914
1447
|
/**
|
|
@@ -1044,11 +1577,13 @@ function resolveGoBuildCacheRoot(root, projectRoot, env) {
|
|
|
1044
1577
|
source: "ttsc-cache",
|
|
1045
1578
|
};
|
|
1046
1579
|
}
|
|
1047
|
-
function maybePrunePluginCache(paths, cacheDir) {
|
|
1580
|
+
function maybePrunePluginCache(paths, cacheDir, env = process.env) {
|
|
1048
1581
|
// GC only the default (workspace-local) location. When the user pins an
|
|
1049
1582
|
// explicit `cacheDir`/`TTSC_CACHE_DIR`, they own its lifetime, so ttsc must
|
|
1050
|
-
// not delete entries out from under them.
|
|
1051
|
-
|
|
1583
|
+
// not delete entries out from under them. `env` is the effective instance
|
|
1584
|
+
// environment so a programmatic caller that pins `TTSC_CACHE_DIR` only in
|
|
1585
|
+
// `context.env` is honored without leaning on the shared `process.env`.
|
|
1586
|
+
if (!cacheDir && !env.TTSC_CACHE_DIR) {
|
|
1052
1587
|
prunePluginCacheRoot(paths.pluginRoot);
|
|
1053
1588
|
}
|
|
1054
1589
|
}
|
|
@@ -1122,8 +1657,8 @@ function isPathWithin(child, parent) {
|
|
|
1122
1657
|
return (rel === "" ||
|
|
1123
1658
|
(rel !== ".." && !rel.startsWith(`..${node_path_1.default.sep}`) && !node_path_1.default.isAbsolute(rel)));
|
|
1124
1659
|
}
|
|
1125
|
-
function resolveGoCompiler() {
|
|
1126
|
-
const explicit =
|
|
1660
|
+
function resolveGoCompiler(env = process.env) {
|
|
1661
|
+
const explicit = env.TTSC_GO_BINARY;
|
|
1127
1662
|
if (explicit && explicit.length > 0)
|
|
1128
1663
|
return explicit;
|
|
1129
1664
|
try {
|
|
@@ -1138,7 +1673,7 @@ function resolveGoCompiler() {
|
|
|
1138
1673
|
const local = node_path_1.default.resolve(__dirname, "..", "..", "..", "..", "native", "go", "bin", process.platform === "win32" ? "go.exe" : "go");
|
|
1139
1674
|
if (node_fs_1.default.existsSync(local))
|
|
1140
1675
|
return local;
|
|
1141
|
-
const homeSdk = node_path_1.default.join(
|
|
1676
|
+
const homeSdk = node_path_1.default.join(env.HOME ?? "", "go-sdk", "go", "bin", process.platform === "win32" ? "go.exe" : "go");
|
|
1142
1677
|
if (node_fs_1.default.existsSync(homeSdk))
|
|
1143
1678
|
return homeSdk;
|
|
1144
1679
|
return "go";
|
|
@@ -1155,16 +1690,17 @@ function resolveGoCompiler() {
|
|
|
1155
1690
|
* Exposed for testing and for the `ttsc cache` CLI command.
|
|
1156
1691
|
*/
|
|
1157
1692
|
function computeCacheKey(inputs) {
|
|
1693
|
+
const env = inputs.env ?? process.env;
|
|
1158
1694
|
const hash = node_crypto_1.default.createHash("sha256");
|
|
1159
1695
|
hash.update(`ttsc=${inputs.ttscVersion}\n`);
|
|
1160
1696
|
hash.update(`tsgo=${inputs.tsgoVersion}\n`);
|
|
1161
1697
|
hash.update(`platform=${process.platform}/${process.arch}\n`);
|
|
1162
1698
|
hash.update(`entry=${inputs.entry}\n`);
|
|
1163
1699
|
if (inputs.goBinary !== undefined) {
|
|
1164
|
-
hash.update(`go=${resolveGoCompilerIdentity(inputs.goBinary)}\n`);
|
|
1700
|
+
hash.update(`go=${resolveGoCompilerIdentity(inputs.goBinary, env)}\n`);
|
|
1165
1701
|
}
|
|
1166
|
-
hashGoBuildEnvironment(hash, inputs.goBinary, inputs.dir);
|
|
1167
|
-
hashExternalGoBuildEnvironment(hash);
|
|
1702
|
+
hashGoBuildEnvironment(hash, inputs.goBinary, inputs.dir, env);
|
|
1703
|
+
hashExternalGoBuildEnvironment(hash, env);
|
|
1168
1704
|
hashSourceDirectory(hash, "plugin", inputs.dir);
|
|
1169
1705
|
for (const [index, dir] of [...(inputs.overlayDirs ?? [])].sort().entries()) {
|
|
1170
1706
|
hashSourceDirectory(hash, `overlay:${index}`, dir);
|
|
@@ -1249,8 +1785,8 @@ function isHashableFile(name) {
|
|
|
1249
1785
|
// cache-key bytes stay byte-for-byte identical to today. `go version` reads no
|
|
1250
1786
|
// cwd/custom env (the spawn passes neither), so cwd is not part of the key.
|
|
1251
1787
|
const goCompilerIdentityCache = new Map();
|
|
1252
|
-
function resolveGoCompilerIdentity(goBinary) {
|
|
1253
|
-
const resolved = resolveExecutableIdentityPath(goBinary);
|
|
1788
|
+
function resolveGoCompilerIdentity(goBinary, env = process.env) {
|
|
1789
|
+
const resolved = resolveExecutableIdentityPath(goBinary, env);
|
|
1254
1790
|
const memoKey = goCompilerIdentityMemoKey(goBinary, resolved);
|
|
1255
1791
|
if (memoKey !== null) {
|
|
1256
1792
|
const cached = goCompilerIdentityCache.get(memoKey);
|
|
@@ -1290,14 +1826,14 @@ function computeGoCompilerIdentity(goBinary, resolved) {
|
|
|
1290
1826
|
const binaryHash = hashFile(resolved);
|
|
1291
1827
|
return `sha256:${binaryHash}:${versionText}`;
|
|
1292
1828
|
}
|
|
1293
|
-
function resolveExecutableIdentityPath(binary) {
|
|
1829
|
+
function resolveExecutableIdentityPath(binary, env = process.env) {
|
|
1294
1830
|
if (node_path_1.default.isAbsolute(binary)) {
|
|
1295
1831
|
return resolveRealPath(binary);
|
|
1296
1832
|
}
|
|
1297
1833
|
if (binary.includes(node_path_1.default.sep)) {
|
|
1298
1834
|
return resolveRealPath(node_path_1.default.resolve(binary));
|
|
1299
1835
|
}
|
|
1300
|
-
for (const dir of readPathEnvironment().split(node_path_1.default.delimiter)) {
|
|
1836
|
+
for (const dir of readPathEnvironment(env).split(node_path_1.default.delimiter)) {
|
|
1301
1837
|
if (dir.length === 0)
|
|
1302
1838
|
continue;
|
|
1303
1839
|
const candidate = node_path_1.default.join(dir, binary);
|
|
@@ -1309,7 +1845,7 @@ function resolveExecutableIdentityPath(binary) {
|
|
|
1309
1845
|
// a `.cmd`/`.bat` wrapper resolves to its real file and is hashed into the
|
|
1310
1846
|
// cache key. Otherwise the wrapper reads as "missing" and a change to it
|
|
1311
1847
|
// would not invalidate the cached plugin binary.
|
|
1312
|
-
for (const ext of windowsExecutableExtensions()) {
|
|
1848
|
+
for (const ext of windowsExecutableExtensions(env)) {
|
|
1313
1849
|
const executable = `${candidate}${ext}`;
|
|
1314
1850
|
if (node_fs_1.default.existsSync(executable)) {
|
|
1315
1851
|
return resolveRealPath(executable);
|
|
@@ -1319,16 +1855,16 @@ function resolveExecutableIdentityPath(binary) {
|
|
|
1319
1855
|
}
|
|
1320
1856
|
return binary;
|
|
1321
1857
|
}
|
|
1322
|
-
function windowsExecutableExtensions() {
|
|
1323
|
-
const pathext =
|
|
1858
|
+
function windowsExecutableExtensions(env = process.env) {
|
|
1859
|
+
const pathext = env.PATHEXT;
|
|
1324
1860
|
const raw = pathext && pathext.length > 0 ? pathext : ".COM;.EXE;.BAT;.CMD";
|
|
1325
1861
|
return raw
|
|
1326
1862
|
.split(";")
|
|
1327
1863
|
.map((ext) => ext.trim().toLowerCase())
|
|
1328
1864
|
.filter((ext) => ext.length > 0);
|
|
1329
1865
|
}
|
|
1330
|
-
function readPathEnvironment() {
|
|
1331
|
-
return
|
|
1866
|
+
function readPathEnvironment(env = process.env) {
|
|
1867
|
+
return env.PATH ?? env.Path ?? "";
|
|
1332
1868
|
}
|
|
1333
1869
|
function resolveRealPath(location) {
|
|
1334
1870
|
try {
|
|
@@ -1343,8 +1879,8 @@ function hashFile(file) {
|
|
|
1343
1879
|
hash.update(node_fs_1.default.readFileSync(file));
|
|
1344
1880
|
return hash.digest("hex");
|
|
1345
1881
|
}
|
|
1346
|
-
function hashGoBuildEnvironment(hash, goBinary, cwd) {
|
|
1347
|
-
const values = resolveGoBuildEnvironment(goBinary, cwd);
|
|
1882
|
+
function hashGoBuildEnvironment(hash, goBinary, cwd, env) {
|
|
1883
|
+
const values = resolveGoBuildEnvironment(goBinary, cwd, env);
|
|
1348
1884
|
for (const key of GO_BUILD_ENV_KEYS) {
|
|
1349
1885
|
const value = values.get(key);
|
|
1350
1886
|
if (value !== undefined && value !== "") {
|
|
@@ -1352,13 +1888,13 @@ function hashGoBuildEnvironment(hash, goBinary, cwd) {
|
|
|
1352
1888
|
}
|
|
1353
1889
|
}
|
|
1354
1890
|
}
|
|
1355
|
-
function resolveGoBuildEnvironment(goBinary, cwd) {
|
|
1891
|
+
function resolveGoBuildEnvironment(goBinary, cwd, env) {
|
|
1356
1892
|
const values = new Map();
|
|
1357
1893
|
if (goBinary !== undefined) {
|
|
1358
1894
|
const result = spawnGoTool(goBinary, ["env", "-json", ...GO_BUILD_ENV_KEYS], {
|
|
1359
1895
|
cwd,
|
|
1360
1896
|
encoding: "utf8",
|
|
1361
|
-
env: goBuildEnv(goBinary),
|
|
1897
|
+
env: goBuildEnv(goBinary, undefined, env),
|
|
1362
1898
|
maxBuffer: 1024 * 1024,
|
|
1363
1899
|
windowsHide: true,
|
|
1364
1900
|
});
|
|
@@ -1368,41 +1904,41 @@ function resolveGoBuildEnvironment(goBinary, cwd) {
|
|
|
1368
1904
|
for (const key of GO_BUILD_ENV_KEYS) {
|
|
1369
1905
|
const raw = parsed[key];
|
|
1370
1906
|
if (typeof raw === "string" && raw !== "") {
|
|
1371
|
-
values.set(key, normalizeGoBuildEnvValue(key, raw));
|
|
1907
|
+
values.set(key, normalizeGoBuildEnvValue(key, raw, env));
|
|
1372
1908
|
}
|
|
1373
1909
|
}
|
|
1374
1910
|
}
|
|
1375
1911
|
catch {
|
|
1376
|
-
// Fall back to
|
|
1377
|
-
// failing before `go build` can produce the actionable error.
|
|
1912
|
+
// Fall back to the effective env below; a cache key is still better
|
|
1913
|
+
// than failing before `go build` can produce the actionable error.
|
|
1378
1914
|
}
|
|
1379
1915
|
}
|
|
1380
1916
|
}
|
|
1381
1917
|
for (const key of GO_BUILD_ENV_KEYS) {
|
|
1382
1918
|
if (values.has(key))
|
|
1383
1919
|
continue;
|
|
1384
|
-
const value =
|
|
1920
|
+
const value = env[key];
|
|
1385
1921
|
if (value !== undefined && value !== "") {
|
|
1386
|
-
values.set(key, normalizeGoBuildEnvValue(key, value));
|
|
1922
|
+
values.set(key, normalizeGoBuildEnvValue(key, value, env));
|
|
1387
1923
|
}
|
|
1388
1924
|
}
|
|
1389
1925
|
return values;
|
|
1390
1926
|
}
|
|
1391
|
-
function normalizeGoBuildEnvValue(key, value) {
|
|
1927
|
+
function normalizeGoBuildEnvValue(key, value, env) {
|
|
1392
1928
|
if (key === "GOROOT") {
|
|
1393
1929
|
return resolveGoRootCacheIdentity(value);
|
|
1394
1930
|
}
|
|
1395
1931
|
if (GO_BUILD_COMMAND_ENV_KEYS.has(key)) {
|
|
1396
|
-
return `${value}\0${resolveCommandCacheIdentity(value)}`;
|
|
1932
|
+
return `${value}\0${resolveCommandCacheIdentity(value, env)}`;
|
|
1397
1933
|
}
|
|
1398
1934
|
return value;
|
|
1399
1935
|
}
|
|
1400
|
-
function resolveCommandCacheIdentity(command) {
|
|
1936
|
+
function resolveCommandCacheIdentity(command, env) {
|
|
1401
1937
|
const executable = firstCommandToken(command);
|
|
1402
1938
|
if (executable === null) {
|
|
1403
1939
|
return "command:empty";
|
|
1404
1940
|
}
|
|
1405
|
-
const resolved = resolveExecutableIdentityPath(executable);
|
|
1941
|
+
const resolved = resolveExecutableIdentityPath(executable, env);
|
|
1406
1942
|
if (!node_fs_1.default.existsSync(resolved)) {
|
|
1407
1943
|
return `command:missing:${executable}`;
|
|
1408
1944
|
}
|
|
@@ -1425,9 +1961,9 @@ function firstCommandToken(command) {
|
|
|
1425
1961
|
}
|
|
1426
1962
|
return trimmed.split(/\s+/)[0] ?? null;
|
|
1427
1963
|
}
|
|
1428
|
-
function hashExternalGoBuildEnvironment(hash) {
|
|
1964
|
+
function hashExternalGoBuildEnvironment(hash, env) {
|
|
1429
1965
|
for (const key of EXTERNAL_GO_BUILD_ENV_KEYS) {
|
|
1430
|
-
const value =
|
|
1966
|
+
const value = env[key];
|
|
1431
1967
|
if (value !== undefined && value !== "") {
|
|
1432
1968
|
hash.update(`${key}=${value}\n`);
|
|
1433
1969
|
}
|
|
@@ -1571,7 +2107,9 @@ function collectPluginCacheEntries(root, now) {
|
|
|
1571
2107
|
return entries;
|
|
1572
2108
|
}
|
|
1573
2109
|
for (const dirent of dirents) {
|
|
1574
|
-
if (!dirent.isDirectory()
|
|
2110
|
+
if (!dirent.isDirectory() ||
|
|
2111
|
+
dirent.name.endsWith(".lock") ||
|
|
2112
|
+
dirent.name.includes(".lock.")) {
|
|
1575
2113
|
continue;
|
|
1576
2114
|
}
|
|
1577
2115
|
const dir = node_path_1.default.join(root, dirent.name);
|