ttsc 0.8.1 → 0.10.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.
Files changed (88) hide show
  1. package/README.md +39 -68
  2. package/cmd/ttsc/api_compile.go +109 -115
  3. package/cmd/ttsc/api_transform.go +54 -51
  4. package/cmd/ttsc/main.go +103 -121
  5. package/driver/program.go +30 -2
  6. package/driver/rewrite.go +126 -9
  7. package/go.mod +2 -0
  8. package/lib/TtscCompiler.js +28 -46
  9. package/lib/TtscCompiler.js.map +1 -1
  10. package/lib/compiler/internal/buildNativeCompiler.js +8 -38
  11. package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
  12. package/lib/compiler/internal/compileProjectInMemory.js +36 -63
  13. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  14. package/lib/compiler/internal/project/readProjectConfig.js +108 -60
  15. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  16. package/lib/compiler/internal/project/resolveProjectConfig.js +15 -45
  17. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  18. package/lib/compiler/internal/resolveBinary.js +10 -40
  19. package/lib/compiler/internal/resolveBinary.js.map +1 -1
  20. package/lib/compiler/internal/resolveEmittedJavaScript.js +21 -48
  21. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  22. package/lib/compiler/internal/resolveTsgo.js +14 -44
  23. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  24. package/lib/compiler/internal/runBuild.d.ts +7 -0
  25. package/lib/compiler/internal/runBuild.js +47 -66
  26. package/lib/compiler/internal/runBuild.js.map +1 -1
  27. package/lib/compiler/internal/runSingleFileEmit.js +18 -48
  28. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  29. package/lib/compiler/internal/transformProjectInMemory.js +41 -75
  30. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  31. package/lib/launcher/internal/getCompilerVersionText.js +7 -37
  32. package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
  33. package/lib/launcher/internal/prepareExecution.js +48 -69
  34. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  35. package/lib/launcher/internal/runTtsc.js +53 -64
  36. package/lib/launcher/internal/runTtsc.js.map +1 -1
  37. package/lib/launcher/internal/runTtsx.js +328 -57
  38. package/lib/launcher/internal/runTtsx.js.map +1 -1
  39. package/lib/plugin/internal/buildSourcePlugin.d.ts +8 -0
  40. package/lib/plugin/internal/buildSourcePlugin.js +287 -139
  41. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  42. package/lib/plugin/internal/loadProjectPlugins.d.ts +2 -0
  43. package/lib/plugin/internal/loadProjectPlugins.js +197 -9
  44. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  45. package/lib/structures/ITtscCompilerContext.d.ts +16 -3
  46. package/lib/structures/ITtscCompilerResult.d.ts +4 -0
  47. package/lib/structures/ITtscCompilerTransformation.d.ts +2 -0
  48. package/lib/structures/ITtscPluginFactoryContext.d.ts +6 -3
  49. package/lib/structures/ITtscProjectPluginConfig.d.ts +8 -7
  50. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
  51. package/lib/structures/internal/ITtscProjectLocatorOptions.d.ts +2 -0
  52. package/lib/structures/internal/TtscCommonOptions.d.ts +2 -0
  53. package/package.json +9 -14
  54. package/shim/diagnosticwriter/lint.go +8 -0
  55. package/shim/printer/go.mod +5 -0
  56. package/shim/printer/shim.go +23 -0
  57. package/src/TtscCompiler.ts +20 -5
  58. package/src/compiler/internal/buildNativeCompiler.ts +2 -2
  59. package/src/compiler/internal/compileProjectInMemory.ts +21 -16
  60. package/src/compiler/internal/project/readProjectConfig.ts +134 -35
  61. package/src/compiler/internal/project/resolveProjectConfig.ts +2 -2
  62. package/src/compiler/internal/resolveBinary.ts +2 -2
  63. package/src/compiler/internal/resolveEmittedJavaScript.ts +11 -7
  64. package/src/compiler/internal/resolveTsgo.ts +2 -2
  65. package/src/compiler/internal/runBuild.ts +34 -21
  66. package/src/compiler/internal/runSingleFileEmit.ts +3 -3
  67. package/src/compiler/internal/transformProjectInMemory.ts +37 -37
  68. package/src/launcher/internal/getCompilerVersionText.ts +2 -2
  69. package/src/launcher/internal/prepareExecution.ts +14 -4
  70. package/src/launcher/internal/runTtsc.ts +30 -4
  71. package/src/launcher/internal/runTtsx.ts +377 -26
  72. package/src/plugin/internal/buildSourcePlugin.ts +321 -50
  73. package/src/plugin/internal/loadProjectPlugins.ts +266 -10
  74. package/src/structures/ITtscCompilerContext.ts +17 -3
  75. package/src/structures/ITtscCompilerResult.ts +5 -0
  76. package/src/structures/ITtscCompilerTransformation.ts +3 -0
  77. package/src/structures/ITtscPluginFactoryContext.ts +6 -3
  78. package/src/structures/ITtscProjectPluginConfig.ts +8 -7
  79. package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
  80. package/src/structures/internal/ITtscProjectLocatorOptions.ts +2 -0
  81. package/src/structures/internal/TtscCommonOptions.ts +2 -0
  82. package/utility/host.go +622 -20
  83. package/cmd/ttsc/main_test.go +0 -288
  84. package/test/compiler.test.ts +0 -422
  85. package/test/platform.test.ts +0 -12
  86. package/test/project.test.ts +0 -197
  87. package/test/source-build.test.ts +0 -45
  88. package/test/tsgo.test.ts +0 -69
@@ -1,19 +1,16 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import * as crypto from "node:crypto";
3
- import * as fs from "node:fs";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
4
  import { createRequire } from "node:module";
5
- import * as path from "node:path";
5
+ import os from "node:os";
6
+ import path from "node:path";
6
7
 
7
8
  const GO_MOD_SEARCH_MAX_DEPTH = 3;
9
+ const TTSC_GO_MODULE_PATH = "github.com/samchon/ttsc/packages/ttsc";
10
+ const TSGO_GO_MODULE_PATH = "github.com/microsoft/typescript-go";
8
11
 
9
- const SKIP_DIRS = new Set([
10
- "node_modules",
11
- ".git",
12
- "dist",
13
- "build",
14
- "vendor",
15
- "lib",
16
- ]);
12
+ const PRUNE_DIRS = new Set(["node_modules", ".git", ".ttsc"]);
13
+ const GENERATED_WORKSPACE_FILES = new Set(["go.work", "go.work.sum"]);
17
14
 
18
15
  /** Build one Go source plugin into a cached executable. */
19
16
  export function buildSourcePlugin(opts: {
@@ -28,9 +25,14 @@ export function buildSourcePlugin(opts: {
28
25
  tsgoVersion: string;
29
26
  }): string {
30
27
  const { dir, entry, source } = resolveSourceBuildTarget(opts);
28
+ const overlayDirs = opts.overlayDirs ?? findTtscOverlayDirs();
29
+ const goBinary = resolveGoCompiler();
30
+ ensureExecutableGoToolchain(goBinary);
31
31
  const key = computeCacheKey({
32
32
  dir,
33
33
  entry,
34
+ goBinary,
35
+ overlayDirs,
34
36
  ttscVersion: opts.ttscVersion,
35
37
  tsgoVersion: opts.tsgoVersion,
36
38
  });
@@ -49,27 +51,46 @@ export function buildSourcePlugin(opts: {
49
51
  );
50
52
  }
51
53
 
52
- const scratchDir = path.join(
53
- root,
54
- `scratch-${key}-${process.pid}-${Date.now()}`,
54
+ const scratchDir = fs.mkdtempSync(
55
+ path.join(os.tmpdir(), `ttsc-plugin-${key}-`),
55
56
  );
56
57
  try {
57
58
  materializeScratchDir(dir, scratchDir);
58
- writeGoWork(scratchDir, opts.overlayDirs ?? findTtscOverlayDirs());
59
+ writeGoWork(scratchDir, overlayDirs, goBinary, opts.pluginName);
59
60
  const scratchBinaryName =
60
61
  process.platform === "win32" ? ".ttsc-plugin.exe" : ".ttsc-plugin";
61
- runGoBuild(scratchDir, entry, scratchBinaryName, opts.pluginName);
62
+ runGoBuild(scratchDir, entry, scratchBinaryName, opts.pluginName, goBinary);
62
63
  const builtBinary = path.join(scratchDir, scratchBinaryName);
63
- fs.renameSync(builtBinary, binaryPath);
64
- if (process.platform !== "win32") {
65
- fs.chmodSync(binaryPath, 0o755);
66
- }
64
+ publishBuiltBinary(builtBinary, binaryPath);
67
65
  return binaryPath;
68
66
  } finally {
69
67
  fs.rmSync(scratchDir, { recursive: true, force: true });
70
68
  }
71
69
  }
72
70
 
71
+ function publishBuiltBinary(builtBinary: string, binaryPath: string): void {
72
+ const pending = `${binaryPath}.${process.pid}.${Date.now()}-${Math.random()
73
+ .toString(16)
74
+ .slice(2)}.tmp`;
75
+ fs.copyFileSync(builtBinary, pending);
76
+ if (process.platform !== "win32") {
77
+ fs.chmodSync(pending, 0o755);
78
+ }
79
+ try {
80
+ fs.renameSync(pending, binaryPath);
81
+ } catch (error) {
82
+ fs.rmSync(pending, { force: true });
83
+ const code = (error as NodeJS.ErrnoException).code;
84
+ if (
85
+ (code === "EEXIST" || code === "EPERM" || code === "EACCES") &&
86
+ fs.existsSync(binaryPath)
87
+ ) {
88
+ return;
89
+ }
90
+ throw error;
91
+ }
92
+ }
93
+
73
94
  function resolveSourceBuildTarget(opts: {
74
95
  source: string;
75
96
  pluginName: string;
@@ -136,52 +157,221 @@ function materializeScratchDir(source: string, scratch: string): void {
136
157
  recursive: true,
137
158
  filter: (src) => {
138
159
  const base = path.basename(src);
139
- if (SKIP_DIRS.has(base)) return false;
140
- if (base === "go.work" || base === "go.work.sum") return false;
160
+ if (shouldPruneDirectory(base)) return false;
161
+ if (shouldOmitSourceFile(base)) return false;
141
162
  return true;
142
163
  },
143
164
  });
144
165
  }
145
166
 
146
- function writeGoWork(scratchDir: string, useDirs: readonly string[]): void {
167
+ function writeGoWork(
168
+ scratchDir: string,
169
+ useDirs: readonly string[],
170
+ goBinary: string,
171
+ pluginName: string,
172
+ ): void {
173
+ const goModReader = createGoModReader(goBinary, pluginName);
174
+ validateSourceReplacements(
175
+ scratchDir,
176
+ useDirs,
177
+ goModReader,
178
+ pluginName,
179
+ );
147
180
  const useLines = ["\t."];
148
181
  for (const dir of useDirs) {
149
182
  useLines.push(`\t${dir.replace(/\\/g, "/")}`);
150
183
  }
151
- const replaceLines = sourceBuildWorkspaceReplacements(useDirs);
184
+ const replaceLines = sourceBuildWorkspaceReplacements(
185
+ useDirs,
186
+ goModReader,
187
+ );
152
188
  const replaceBlock =
153
189
  replaceLines.length === 0 ? "" : `\n\n${replaceLines.join("\n")}\n`;
154
190
  const goWork = `go 1.26\n\nuse (\n${useLines.join("\n")}\n)${replaceBlock}`;
155
191
  fs.writeFileSync(path.join(scratchDir, "go.work"), goWork, "utf8");
156
192
  }
157
193
 
194
+ function validateSourceReplacements(
195
+ scratchDir: string,
196
+ useDirs: readonly string[],
197
+ goModReader: GoModReader,
198
+ pluginName: string,
199
+ ): void {
200
+ const sourceInfo = goModReader.read(scratchDir);
201
+ if (sourceInfo.modulePath === TTSC_GO_MODULE_PATH) {
202
+ return;
203
+ }
204
+ const sourceReplacements = sourceInfo.replacements;
205
+ if (sourceReplacements.length === 0) {
206
+ return;
207
+ }
208
+ const overlayModules = collectOverlayModulePaths(useDirs, goModReader);
209
+ for (const replacement of sourceReplacements) {
210
+ if (
211
+ isTtscManagedModulePath(replacement.modulePath) ||
212
+ overlayModules.has(replacement.modulePath)
213
+ ) {
214
+ throw new Error(
215
+ `ttsc: plugin "${pluginName}" go.mod replaces ttsc-managed module ` +
216
+ `${JSON.stringify(replacement.modulePath)}. Remove this replace directive; ` +
217
+ `ttsc supplies its own compiler and shim modules while building source plugins.`,
218
+ );
219
+ }
220
+ }
221
+ }
222
+
158
223
  function sourceBuildWorkspaceReplacements(
159
224
  useDirs: readonly string[],
225
+ goModReader: GoModReader,
160
226
  ): string[] {
161
- const ttscRoot = useDirs.find((dir) =>
162
- hasModulePath(dir, "github.com/samchon/ttsc/packages/ttsc"),
227
+ const ttscRoot = useDirs.find(
228
+ (dir) => goModReader.read(dir).modulePath === TTSC_GO_MODULE_PATH,
163
229
  );
164
230
  if (!ttscRoot) {
165
231
  return [];
166
232
  }
167
233
  return [
168
- `replace github.com/samchon/ttsc/packages/ttsc v0.0.0 => ${ttscRoot.replace(/\\/g, "/")}`,
234
+ `replace ${TTSC_GO_MODULE_PATH} v0.0.0 => ${ttscRoot.replace(/\\/g, "/")}`,
169
235
  ];
170
236
  }
171
237
 
172
- function hasModulePath(dir: string, modulePath: string): boolean {
238
+ interface GoModReplacement {
239
+ readonly modulePath: string;
240
+ }
241
+
242
+ interface GoModInfo {
243
+ readonly modulePath: string | null;
244
+ readonly replacements: readonly GoModReplacement[];
245
+ }
246
+
247
+ interface GoModReader {
248
+ read(dir: string): GoModInfo;
249
+ }
250
+
251
+ interface GoModJson {
252
+ readonly Module?: {
253
+ readonly Path?: string;
254
+ };
255
+ readonly Require?: readonly {
256
+ readonly Path?: string;
257
+ readonly Version?: string;
258
+ }[];
259
+ readonly Replace?: readonly {
260
+ readonly Old?: {
261
+ readonly Path?: string;
262
+ readonly Version?: string;
263
+ };
264
+ readonly New?: {
265
+ readonly Path?: string;
266
+ readonly Version?: string;
267
+ };
268
+ }[];
269
+ }
270
+
271
+ function createGoModReader(goBinary: string, pluginName: string): GoModReader {
272
+ const cache = new Map<string, GoModInfo>();
273
+ return {
274
+ read(dir) {
275
+ const resolved = path.resolve(dir);
276
+ const cached = cache.get(resolved);
277
+ if (cached !== undefined) {
278
+ return cached;
279
+ }
280
+ const info = readGoModInfo(resolved, goBinary, pluginName);
281
+ cache.set(resolved, info);
282
+ return info;
283
+ },
284
+ };
285
+ }
286
+
287
+ function readGoModInfo(
288
+ dir: string,
289
+ goBinary: string,
290
+ pluginName: string,
291
+ ): GoModInfo {
292
+ if (!fs.existsSync(path.join(dir, "go.mod"))) {
293
+ return emptyGoModInfo();
294
+ }
295
+
296
+ const result = spawnSync(goBinary, ["mod", "edit", "-json"], {
297
+ cwd: dir,
298
+ encoding: "utf8",
299
+ env: goBuildEnv(goBinary),
300
+ maxBuffer: 1024 * 1024 * 16,
301
+ windowsHide: true,
302
+ });
303
+ if (result.error) {
304
+ if ((result.error as NodeJS.ErrnoException).code === "ENOENT") {
305
+ throw new Error(goToolchainNotFoundMessage(pluginName));
306
+ }
307
+ throw new Error(
308
+ `ttsc: reading go.mod for plugin "${pluginName}" failed to spawn ${goBinary}: ${result.error.message}`,
309
+ );
310
+ }
311
+ if (result.status !== 0) {
312
+ throw new Error(
313
+ `ttsc: reading go.mod for plugin "${pluginName}" failed:\n${result.stderr || result.stdout}`,
314
+ );
315
+ }
316
+
317
+ let json: GoModJson;
173
318
  try {
174
- const goMod = fs.readFileSync(path.join(dir, "go.mod"), "utf8");
175
- return new RegExp(`^module\\s+${escapeRegExp(modulePath)}\\s*$`, "m").test(
176
- goMod,
319
+ json = JSON.parse(result.stdout) as GoModJson;
320
+ } catch (error) {
321
+ throw new Error(
322
+ `ttsc: reading go.mod for plugin "${pluginName}" returned invalid JSON: ${
323
+ error instanceof Error ? error.message : String(error)
324
+ }`,
177
325
  );
178
- } catch {
179
- return false;
180
326
  }
327
+
328
+ return {
329
+ modulePath: json.Module?.Path ?? null,
330
+ replacements: (json.Replace ?? [])
331
+ .map(jsonReplacementToGoModReplacement)
332
+ .filter((replacement) => replacement !== null),
333
+ };
334
+ }
335
+
336
+ function emptyGoModInfo(): GoModInfo {
337
+ return {
338
+ modulePath: null,
339
+ replacements: [],
340
+ };
341
+ }
342
+
343
+ function jsonReplacementToGoModReplacement(
344
+ replacement: NonNullable<GoModJson["Replace"]>[number],
345
+ ): GoModReplacement | null {
346
+ const modulePath = replacement.Old?.Path;
347
+ if (modulePath === undefined) {
348
+ return null;
349
+ }
350
+ return {
351
+ modulePath,
352
+ };
353
+ }
354
+
355
+ function collectOverlayModulePaths(
356
+ dirs: readonly string[],
357
+ goModReader: GoModReader,
358
+ ): Set<string> {
359
+ const out = new Set<string>();
360
+ for (const dir of dirs) {
361
+ const modulePath = goModReader.read(dir).modulePath;
362
+ if (modulePath !== null) {
363
+ out.add(modulePath);
364
+ }
365
+ }
366
+ return out;
181
367
  }
182
368
 
183
- function escapeRegExp(value: string): string {
184
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
369
+ function isTtscManagedModulePath(modulePath: string): boolean {
370
+ return (
371
+ modulePath === TTSC_GO_MODULE_PATH ||
372
+ modulePath === TSGO_GO_MODULE_PATH ||
373
+ modulePath.startsWith("github.com/microsoft/typescript-go/shim/")
374
+ );
185
375
  }
186
376
 
187
377
  function runGoBuild(
@@ -189,8 +379,8 @@ function runGoBuild(
189
379
  entry: string,
190
380
  binaryName: string,
191
381
  pluginName: string,
382
+ goBinary: string,
192
383
  ): void {
193
- const goBinary = resolveGoCompiler();
194
384
  ensureExecutableGoToolchain(goBinary);
195
385
  const result = spawnSync(goBinary, ["build", "-o", binaryName, entry], {
196
386
  cwd,
@@ -201,11 +391,7 @@ function runGoBuild(
201
391
  });
202
392
  if (result.error) {
203
393
  if ((result.error as NodeJS.ErrnoException).code === "ENOENT") {
204
- throw new Error(
205
- `ttsc: building plugin "${pluginName}" failed because the Go toolchain was not found. ` +
206
- `Reinstall ttsc with optional dependencies so the bundled Go compiler is present, ` +
207
- `or set TTSC_GO_BINARY to an absolute path.`,
208
- );
394
+ throw new Error(goToolchainNotFoundMessage(pluginName));
209
395
  }
210
396
  throw new Error(
211
397
  `ttsc: building plugin "${pluginName}" failed to spawn ${goBinary}: ${result.error.message}`,
@@ -218,6 +404,14 @@ function runGoBuild(
218
404
  }
219
405
  }
220
406
 
407
+ function goToolchainNotFoundMessage(pluginName: string): string {
408
+ return (
409
+ `ttsc: building plugin "${pluginName}" failed because the Go toolchain was not found. ` +
410
+ `Reinstall ttsc with optional dependencies so the bundled Go compiler is present, ` +
411
+ `or set TTSC_GO_BINARY to an absolute path.`
412
+ );
413
+ }
414
+
221
415
  function goBuildEnv(goBinary: string): NodeJS.ProcessEnv {
222
416
  const env = { ...process.env };
223
417
  const goRoot = inferGoRoot(goBinary);
@@ -305,7 +499,7 @@ function walkForGoMod(dir: string, out: string[]): void {
305
499
  }
306
500
  for (const entry of entries) {
307
501
  if (!entry.isDirectory()) continue;
308
- if (SKIP_DIRS.has(entry.name)) continue;
502
+ if (shouldPruneDirectory(entry.name)) continue;
309
503
  walkForGoMod(path.join(dir, entry.name), out);
310
504
  }
311
505
  }
@@ -315,7 +509,7 @@ function resolvePluginCacheRoot(
315
509
  cacheDir?: string,
316
510
  ): string {
317
511
  if (cacheDir) {
318
- return path.resolve(cacheDir, "plugins");
512
+ return path.resolve(projectRoot, cacheDir, "plugins");
319
513
  }
320
514
  if (process.env.TTSC_CACHE_DIR) {
321
515
  return path.resolve(process.env.TTSC_CACHE_DIR, "plugins");
@@ -387,9 +581,11 @@ function resolveGoCompiler(): string {
387
581
  return "go";
388
582
  }
389
583
 
390
- function computeCacheKey(inputs: {
584
+ export function computeCacheKey(inputs: {
391
585
  dir: string;
392
586
  entry: string;
587
+ goBinary?: string;
588
+ overlayDirs?: readonly string[];
393
589
  ttscVersion: string;
394
590
  tsgoVersion: string;
395
591
  }): string {
@@ -398,12 +594,28 @@ function computeCacheKey(inputs: {
398
594
  hash.update(`tsgo=${inputs.tsgoVersion}\n`);
399
595
  hash.update(`platform=${process.platform}/${process.arch}\n`);
400
596
  hash.update(`entry=${inputs.entry}\n`);
401
- for (const file of collectSourceFiles(inputs.dir)) {
402
- const rel = path.relative(inputs.dir, file).replace(/\\/g, "/");
597
+ if (inputs.goBinary !== undefined) {
598
+ hash.update(`go=${resolveGoCompilerIdentity(inputs.goBinary)}\n`);
599
+ }
600
+ hashSourceDirectory(hash, "plugin", inputs.dir);
601
+ for (const [index, dir] of [...(inputs.overlayDirs ?? [])].sort().entries()) {
602
+ hashSourceDirectory(hash, `overlay:${index}`, dir);
603
+ }
604
+ return hash.digest("hex").slice(0, 32);
605
+ }
606
+
607
+ function hashSourceDirectory(
608
+ hash: crypto.Hash,
609
+ label: string,
610
+ root: string,
611
+ ): void {
612
+ hash.update(`dir=${label}\n`);
613
+ for (const file of collectSourceFiles(root)) {
614
+ const rel = path.relative(root, file).replace(/\\/g, "/");
403
615
  hash.update(`f=${rel}\n`);
404
616
  hash.update(fs.readFileSync(file));
617
+ hash.update("\n");
405
618
  }
406
- return hash.digest("hex").slice(0, 32);
407
619
  }
408
620
 
409
621
  function collectSourceFiles(root: string): string[] {
@@ -421,19 +633,78 @@ function walk(dir: string, out: string[]): void {
421
633
  return;
422
634
  }
423
635
  for (const entry of entries) {
424
- if (SKIP_DIRS.has(entry.name)) continue;
425
636
  const full = path.join(dir, entry.name);
426
637
  if (entry.isDirectory()) {
638
+ if (shouldPruneDirectory(entry.name)) continue;
427
639
  walk(full, out);
428
640
  continue;
429
641
  }
430
642
  if (!entry.isFile()) continue;
643
+ if (shouldOmitSourceFile(entry.name)) continue;
431
644
  if (!isHashableFile(entry.name)) continue;
432
645
  out.push(full);
433
646
  }
434
647
  }
435
648
 
649
+ function shouldPruneDirectory(name: string): boolean {
650
+ return PRUNE_DIRS.has(name);
651
+ }
652
+
653
+ function shouldOmitSourceFile(name: string): boolean {
654
+ return GENERATED_WORKSPACE_FILES.has(name);
655
+ }
656
+
436
657
  function isHashableFile(name: string): boolean {
437
- if (name === "go.mod" || name === "go.sum" || name === "go.work") return true;
438
- return /\.(?:go|s|c|h|cpp|hpp)$/i.test(name);
658
+ return !name.endsWith("~");
659
+ }
660
+
661
+ function resolveGoCompilerIdentity(goBinary: string): string {
662
+ const resolved = resolveExecutableIdentityPath(goBinary);
663
+ const version = spawnSync(goBinary, ["version"], {
664
+ encoding: "utf8",
665
+ windowsHide: true,
666
+ });
667
+ const versionText =
668
+ version.error !== undefined
669
+ ? version.error.message
670
+ : `${version.status ?? 0}:${version.stdout}${version.stderr}`;
671
+ let statText = "";
672
+ try {
673
+ const stat = fs.statSync(resolved);
674
+ statText = `${stat.size}:${stat.mtimeMs}`;
675
+ } catch {
676
+ statText = "missing";
677
+ }
678
+ return `${goBinary}:${resolved}:${statText}:${versionText}`;
679
+ }
680
+
681
+ function resolveExecutableIdentityPath(binary: string): string {
682
+ if (path.isAbsolute(binary)) {
683
+ return resolveRealPath(binary);
684
+ }
685
+ if (binary.includes(path.sep)) {
686
+ return resolveRealPath(path.resolve(binary));
687
+ }
688
+ for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
689
+ if (dir.length === 0) continue;
690
+ const candidate = path.join(dir, binary);
691
+ if (fs.existsSync(candidate)) {
692
+ return resolveRealPath(candidate);
693
+ }
694
+ if (process.platform === "win32") {
695
+ const executable = `${candidate}.exe`;
696
+ if (fs.existsSync(executable)) {
697
+ return resolveRealPath(executable);
698
+ }
699
+ }
700
+ }
701
+ return binary;
702
+ }
703
+
704
+ function resolveRealPath(location: string): string {
705
+ try {
706
+ return fs.realpathSync(location);
707
+ } catch {
708
+ return location;
709
+ }
439
710
  }