ttsc 0.11.0 → 0.12.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 +25 -6
- package/cmd/ttsc/api_compile.go +101 -106
- package/cmd/ttsc/api_compile_branches_test.go +81 -0
- package/cmd/ttsc/api_transform.go +49 -54
- package/cmd/ttsc/api_transform_branches_test.go +41 -0
- package/cmd/ttsc/build.go +108 -109
- package/cmd/ttsc/build_branches_test.go +130 -0
- package/cmd/ttsc/helpers_test.go +47 -0
- package/cmd/ttsc/main.go +104 -103
- package/cmd/ttsc/main_alias_branches_test.go +29 -0
- package/cmd/ttscserver/main.go +104 -96
- package/cmd/ttscserver/run_lsp_prefers_tsgo_flag_test.go +42 -0
- package/cmd/ttscserver/run_lsp_reports_server_error_test.go +23 -25
- package/cmd/ttscserver/run_lsp_uses_tsgo_environment_test.go +44 -0
- package/cmd/utility-host/main.go +42 -0
- package/driver/lsp.go +60 -0
- package/driver/plugins.go +122 -0
- package/driver/program.go +346 -356
- package/driver/rewrite.go +336 -339
- package/{driver → internal/lspserver}/lsp_envelope.go +70 -64
- package/{driver → internal/lspserver}/lsp_frame.go +62 -62
- package/{driver → internal/lspserver}/lsp_plugin.go +58 -58
- package/internal/lspserver/lsp_proxy.go +391 -0
- package/internal/lspserver/lsp_server.go +231 -0
- package/lib/TtscCompiler.d.ts +3 -3
- package/lib/TtscCompiler.js +20 -6
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/resolveTsgo.d.ts +1 -0
- package/lib/compiler/internal/resolveTsgo.js +15 -4
- package/lib/compiler/internal/resolveTsgo.js.map +1 -1
- package/lib/compiler/internal/runBuild.js +12 -4
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +12 -33
- package/lib/compiler/internal/sharedHostHelpers.js +27 -70
- package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +12 -5
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -1
- package/lib/index.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +9 -4
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.d.ts +1 -0
- package/lib/launcher/internal/runTtscserver.js +38 -1
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.d.ts +3 -0
- package/lib/plugin/internal/buildSourcePlugin.js +444 -41
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.js +153 -34
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerContext.d.ts +2 -1
- package/lib/structures/ITtscPlugin.d.ts +8 -13
- package/lib/structures/ITtscPluginFactoryContext.d.ts +4 -5
- package/lib/structures/ITtscProjectPluginConfig.d.ts +2 -2
- package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +4 -2
- package/package.json +8 -8
- package/shim/lsp/shim.go +11 -17
- package/src/TtscCompiler.ts +20 -7
- package/src/compiler/internal/resolveTsgo.ts +18 -5
- package/src/compiler/internal/runBuild.ts +17 -4
- package/src/compiler/internal/sharedHostHelpers.ts +34 -67
- package/src/compiler/internal/transformProjectInMemory.ts +18 -5
- package/src/index.ts +0 -1
- package/src/launcher/internal/runTtsc.ts +9 -4
- package/src/launcher/internal/runTtscserver.ts +43 -1
- package/src/plugin/internal/buildSourcePlugin.ts +475 -41
- package/src/plugin/internal/loadProjectPlugins.ts +199 -39
- package/src/structures/ITtscCompilerContext.ts +2 -1
- package/src/structures/ITtscPlugin.ts +9 -13
- package/src/structures/ITtscPluginFactoryContext.ts +4 -5
- package/src/structures/ITtscProjectPluginConfig.ts +2 -2
- package/src/structures/internal/ITtscLoadedNativePlugin.ts +4 -2
- package/utility/host.go +267 -1296
- package/driver/lsp_proxy.go +0 -394
- package/driver/lsp_server.go +0 -197
- package/lib/defineTtscPlugin.d.ts +0 -18
- package/lib/defineTtscPlugin.js +0 -7
- package/lib/defineTtscPlugin.js.map +0 -1
- package/src/defineTtscPlugin.ts +0 -26
|
@@ -12,14 +12,15 @@ const TSGO_GO_MODULE_PATH = "github.com/microsoft/typescript-go";
|
|
|
12
12
|
const PRUNE_DIRS = new Set(["node_modules", ".git", ".ttsc"]);
|
|
13
13
|
const GENERATED_WORKSPACE_FILES = new Set(["go.work", "go.work.sum"]);
|
|
14
14
|
|
|
15
|
-
// Go
|
|
16
|
-
//
|
|
17
|
-
//
|
|
15
|
+
// Go build environment values that can change the produced binary or decide
|
|
16
|
+
// whether `go build` succeeds. Hashed into the plugin cache key so target,
|
|
17
|
+
// build-tag, cgo, FIPS, and external-link variants never collide.
|
|
18
18
|
const GO_BUILD_ENV_KEYS: readonly string[] = [
|
|
19
19
|
"GOOS",
|
|
20
20
|
"GOARCH",
|
|
21
21
|
"GOAMD64",
|
|
22
22
|
"GOARM",
|
|
23
|
+
"GOARM64",
|
|
23
24
|
"GO386",
|
|
24
25
|
"GOMIPS",
|
|
25
26
|
"GOMIPS64",
|
|
@@ -28,11 +29,73 @@ const GO_BUILD_ENV_KEYS: readonly string[] = [
|
|
|
28
29
|
"GOWASM",
|
|
29
30
|
"GOFLAGS",
|
|
30
31
|
"GOEXPERIMENT",
|
|
32
|
+
"GOFIPS140",
|
|
33
|
+
"GO_EXTLINK_ENABLED",
|
|
34
|
+
"GCCGO",
|
|
35
|
+
"GCCGOTOOLDIR",
|
|
31
36
|
"CGO_ENABLED",
|
|
37
|
+
"AR",
|
|
38
|
+
"CC",
|
|
39
|
+
"CXX",
|
|
40
|
+
"FC",
|
|
41
|
+
"PKG_CONFIG",
|
|
42
|
+
"CGO_CFLAGS",
|
|
43
|
+
"CGO_CFLAGS_ALLOW",
|
|
44
|
+
"CGO_CFLAGS_DISALLOW",
|
|
45
|
+
"CGO_CPPFLAGS",
|
|
46
|
+
"CGO_CPPFLAGS_ALLOW",
|
|
47
|
+
"CGO_CPPFLAGS_DISALLOW",
|
|
48
|
+
"CGO_CXXFLAGS",
|
|
49
|
+
"CGO_CXXFLAGS_ALLOW",
|
|
50
|
+
"CGO_CXXFLAGS_DISALLOW",
|
|
51
|
+
"CGO_FFLAGS",
|
|
52
|
+
"CGO_FFLAGS_ALLOW",
|
|
53
|
+
"CGO_FFLAGS_DISALLOW",
|
|
54
|
+
"CGO_LDFLAGS",
|
|
55
|
+
"CGO_LDFLAGS_ALLOW",
|
|
56
|
+
"CGO_LDFLAGS_DISALLOW",
|
|
32
57
|
"GOTOOLCHAIN",
|
|
58
|
+
"GOROOT",
|
|
59
|
+
];
|
|
60
|
+
const GO_BUILD_COMMAND_ENV_KEYS = new Set([
|
|
61
|
+
"AR",
|
|
62
|
+
"CC",
|
|
63
|
+
"CXX",
|
|
64
|
+
"FC",
|
|
65
|
+
"GCCGO",
|
|
66
|
+
"PKG_CONFIG",
|
|
67
|
+
]);
|
|
68
|
+
const EXTERNAL_GO_BUILD_ENV_KEYS: readonly string[] = [
|
|
69
|
+
"CPATH",
|
|
70
|
+
"C_INCLUDE_PATH",
|
|
71
|
+
"CPLUS_INCLUDE_PATH",
|
|
72
|
+
"DYLD_LIBRARY_PATH",
|
|
73
|
+
"INCLUDE",
|
|
74
|
+
"LD_LIBRARY_PATH",
|
|
75
|
+
"LIB",
|
|
76
|
+
"LIBRARY_PATH",
|
|
77
|
+
"LIBPATH",
|
|
78
|
+
"MACOSX_DEPLOYMENT_TARGET",
|
|
79
|
+
"OBJC_INCLUDE_PATH",
|
|
80
|
+
"PKG_CONFIG_ALLOW_SYSTEM_CFLAGS",
|
|
81
|
+
"PKG_CONFIG_ALLOW_SYSTEM_LIBS",
|
|
82
|
+
"PKG_CONFIG_LIBDIR",
|
|
83
|
+
"PKG_CONFIG_PATH",
|
|
84
|
+
"PKG_CONFIG_SYSROOT_DIR",
|
|
85
|
+
"PKG_CONFIG_TOP_BUILD_DIR",
|
|
86
|
+
"SDKROOT",
|
|
33
87
|
];
|
|
34
88
|
const CONTRIBUTIONS_FILE_NAME = "ttsc_contributions.go";
|
|
35
89
|
const CONTRIB_DIRNAME = "contrib";
|
|
90
|
+
const GLOBAL_CACHE_DIRNAME = "ttsc";
|
|
91
|
+
const PLUGIN_CACHE_DIRNAME = "plugins";
|
|
92
|
+
const CACHE_LAST_USED_FILE = ".last-used";
|
|
93
|
+
const CACHE_GC_MARKER_FILE = ".gc-last-run";
|
|
94
|
+
const GLOBAL_CACHE_GC_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
95
|
+
const GLOBAL_CACHE_ENTRY_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
96
|
+
const GLOBAL_CACHE_MAX_BYTES = 2 * 1024 * 1024 * 1024;
|
|
97
|
+
const GLOBAL_CACHE_TARGET_BYTES = Math.floor(GLOBAL_CACHE_MAX_BYTES * 0.8);
|
|
98
|
+
const GLOBAL_CACHE_PROTECTED_AGE_MS = 60 * 60 * 1000;
|
|
36
99
|
|
|
37
100
|
/** One contributor's resolved Go source plus its target sub-package name. */
|
|
38
101
|
export interface ITtscBuildContributor {
|
|
@@ -56,7 +119,7 @@ export function buildSourcePlugin(opts: {
|
|
|
56
119
|
tsgoVersion: string;
|
|
57
120
|
}): string {
|
|
58
121
|
const { dir, entry, source } = resolveSourceBuildTarget(opts);
|
|
59
|
-
const overlayDirs = opts.overlayDirs ?? findTtscOverlayDirs();
|
|
122
|
+
const overlayDirs = [...(opts.overlayDirs ?? findTtscOverlayDirs())].sort();
|
|
60
123
|
const contributors = opts.contributors ?? [];
|
|
61
124
|
const goBinary = resolveGoCompiler();
|
|
62
125
|
ensureExecutableGoToolchain(goBinary);
|
|
@@ -74,6 +137,7 @@ export function buildSourcePlugin(opts: {
|
|
|
74
137
|
const binaryName = process.platform === "win32" ? "plugin.exe" : "plugin";
|
|
75
138
|
const binaryPath = path.join(cacheDir, binaryName);
|
|
76
139
|
if (fs.existsSync(binaryPath)) {
|
|
140
|
+
touchCacheEntry(cacheDir);
|
|
77
141
|
return binaryPath;
|
|
78
142
|
}
|
|
79
143
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
@@ -111,6 +175,7 @@ export function buildSourcePlugin(opts: {
|
|
|
111
175
|
runGoBuild(scratchDir, entry, scratchBinaryName, opts.pluginName, goBinary);
|
|
112
176
|
const builtBinary = path.join(scratchDir, scratchBinaryName);
|
|
113
177
|
publishBuiltBinary(builtBinary, binaryPath);
|
|
178
|
+
touchCacheEntry(cacheDir);
|
|
114
179
|
return binaryPath;
|
|
115
180
|
} finally {
|
|
116
181
|
fs.rmSync(scratchDir, { recursive: true, force: true });
|
|
@@ -367,11 +432,22 @@ function writeGoWork(
|
|
|
367
432
|
): void {
|
|
368
433
|
const goModReader = createGoModReader(goBinary, pluginName);
|
|
369
434
|
validateSourceReplacements(scratchDir, useDirs, goModReader, pluginName);
|
|
435
|
+
const sourceInfo = goModReader.read(scratchDir);
|
|
436
|
+
const effectiveUseDirs =
|
|
437
|
+
sourceInfo.modulePath === TTSC_GO_MODULE_PATH
|
|
438
|
+
? useDirs.filter((dir) => {
|
|
439
|
+
const modulePath = goModReader.read(dir).modulePath;
|
|
440
|
+
return modulePath !== null && !isTtscManagedModulePath(modulePath);
|
|
441
|
+
})
|
|
442
|
+
: useDirs;
|
|
370
443
|
const useLines = ["\t."];
|
|
371
|
-
for (const dir of
|
|
444
|
+
for (const dir of effectiveUseDirs) {
|
|
372
445
|
useLines.push(`\t${dir.replace(/\\/g, "/")}`);
|
|
373
446
|
}
|
|
374
|
-
const replaceLines = sourceBuildWorkspaceReplacements(
|
|
447
|
+
const replaceLines = sourceBuildWorkspaceReplacements(
|
|
448
|
+
effectiveUseDirs,
|
|
449
|
+
goModReader,
|
|
450
|
+
);
|
|
375
451
|
const replaceBlock =
|
|
376
452
|
replaceLines.length === 0 ? "" : `\n\n${replaceLines.join("\n")}\n`;
|
|
377
453
|
const goWork = `go 1.26\n\nuse (\n${useLines.join("\n")}\n)${replaceBlock}`;
|
|
@@ -601,6 +677,7 @@ function goToolchainNotFoundMessage(pluginName: string): string {
|
|
|
601
677
|
|
|
602
678
|
function goBuildEnv(goBinary: string): NodeJS.ProcessEnv {
|
|
603
679
|
const env = { ...process.env };
|
|
680
|
+
env.GOWORK = "auto";
|
|
604
681
|
const goRoot = inferGoRoot(goBinary);
|
|
605
682
|
if (goRoot && !env.GOROOT) {
|
|
606
683
|
env.GOROOT = goRoot;
|
|
@@ -691,30 +768,63 @@ function walkForGoMod(dir: string, out: string[]): void {
|
|
|
691
768
|
}
|
|
692
769
|
}
|
|
693
770
|
|
|
694
|
-
function resolvePluginCacheRoot(
|
|
771
|
+
export function resolvePluginCacheRoot(
|
|
695
772
|
projectRoot: string,
|
|
696
773
|
cacheDir?: string,
|
|
697
774
|
): string {
|
|
698
775
|
if (cacheDir) {
|
|
699
|
-
return path.resolve(projectRoot, cacheDir,
|
|
776
|
+
return path.resolve(projectRoot, cacheDir, PLUGIN_CACHE_DIRNAME);
|
|
700
777
|
}
|
|
701
778
|
if (process.env.TTSC_CACHE_DIR) {
|
|
702
|
-
return path.resolve(process.env.TTSC_CACHE_DIR,
|
|
779
|
+
return path.resolve(process.env.TTSC_CACHE_DIR, PLUGIN_CACHE_DIRNAME);
|
|
703
780
|
}
|
|
704
|
-
const root =
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
return path.join(nodeModules, ".ttsc", "plugins");
|
|
708
|
-
}
|
|
709
|
-
return path.join(root, ".ttsc", "plugins");
|
|
781
|
+
const root = resolveGlobalPluginCacheRoot();
|
|
782
|
+
maybePruneGlobalPluginCache(root);
|
|
783
|
+
return root;
|
|
710
784
|
}
|
|
711
785
|
|
|
712
|
-
function
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
786
|
+
export function resolveGlobalPluginCacheRoot(): string {
|
|
787
|
+
return path.join(
|
|
788
|
+
resolveUserCacheRoot(),
|
|
789
|
+
GLOBAL_CACHE_DIRNAME,
|
|
790
|
+
PLUGIN_CACHE_DIRNAME,
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export function defaultPluginCacheCleanTargets(projectRoot: string): string[] {
|
|
795
|
+
return [
|
|
796
|
+
resolveGlobalPluginCacheRoot(),
|
|
797
|
+
path.join(projectRoot, "node_modules", ".ttsc"),
|
|
798
|
+
path.join(projectRoot, ".ttsc"),
|
|
799
|
+
];
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function resolveUserCacheRoot(): string {
|
|
803
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
804
|
+
if (xdg && path.isAbsolute(xdg)) {
|
|
805
|
+
return xdg;
|
|
806
|
+
}
|
|
807
|
+
if (process.platform === "win32") {
|
|
808
|
+
const local = process.env.LOCALAPPDATA;
|
|
809
|
+
if (local && path.isAbsolute(local)) {
|
|
810
|
+
return local;
|
|
811
|
+
}
|
|
812
|
+
const home = os.homedir();
|
|
813
|
+
if (home) {
|
|
814
|
+
return path.join(home, "AppData", "Local");
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (process.platform === "darwin") {
|
|
818
|
+
const home = os.homedir();
|
|
819
|
+
if (home) {
|
|
820
|
+
return path.join(home, "Library", "Caches");
|
|
821
|
+
}
|
|
717
822
|
}
|
|
823
|
+
const home = os.homedir();
|
|
824
|
+
if (home) {
|
|
825
|
+
return path.join(home, ".cache");
|
|
826
|
+
}
|
|
827
|
+
return path.join(os.tmpdir(), "ttsc-cache");
|
|
718
828
|
}
|
|
719
829
|
|
|
720
830
|
function resolveGoCompiler(): string {
|
|
@@ -785,17 +895,8 @@ export function computeCacheKey(inputs: {
|
|
|
785
895
|
if (inputs.goBinary !== undefined) {
|
|
786
896
|
hash.update(`go=${resolveGoCompilerIdentity(inputs.goBinary)}\n`);
|
|
787
897
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
// `GOOS=linux GOARCH=arm64` on a darwin host would write a Linux-arm64
|
|
791
|
-
// binary into the same cache slot as the native build, and the next
|
|
792
|
-
// native invocation would spawn an unrunnable artifact.
|
|
793
|
-
for (const key of GO_BUILD_ENV_KEYS) {
|
|
794
|
-
const value = process.env[key];
|
|
795
|
-
if (value !== undefined && value !== "") {
|
|
796
|
-
hash.update(`${key}=${value}\n`);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
898
|
+
hashGoBuildEnvironment(hash, inputs.goBinary, inputs.dir);
|
|
899
|
+
hashExternalGoBuildEnvironment(hash);
|
|
799
900
|
hashSourceDirectory(hash, "plugin", inputs.dir);
|
|
800
901
|
for (const [index, dir] of [...(inputs.overlayDirs ?? [])].sort().entries()) {
|
|
801
902
|
hashSourceDirectory(hash, `overlay:${index}`, dir);
|
|
@@ -879,22 +980,19 @@ function isHashableFile(name: string): boolean {
|
|
|
879
980
|
|
|
880
981
|
function resolveGoCompilerIdentity(goBinary: string): string {
|
|
881
982
|
const resolved = resolveExecutableIdentityPath(goBinary);
|
|
983
|
+
if (!fs.existsSync(resolved)) {
|
|
984
|
+
return "missing";
|
|
985
|
+
}
|
|
882
986
|
const version = spawnSync(goBinary, ["version"], {
|
|
883
987
|
encoding: "utf8",
|
|
884
988
|
windowsHide: true,
|
|
885
989
|
});
|
|
886
990
|
const versionText =
|
|
887
991
|
version.error !== undefined
|
|
888
|
-
? version.error.message
|
|
992
|
+
? ((version.error as NodeJS.ErrnoException).code ?? version.error.message)
|
|
889
993
|
: `${version.status ?? 0}:${version.stdout}${version.stderr}`;
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
const stat = fs.statSync(resolved);
|
|
893
|
-
statText = `${stat.size}:${stat.mtimeMs}`;
|
|
894
|
-
} catch {
|
|
895
|
-
statText = "missing";
|
|
896
|
-
}
|
|
897
|
-
return `${goBinary}:${resolved}:${statText}:${versionText}`;
|
|
994
|
+
const binaryHash = hashFile(resolved);
|
|
995
|
+
return `sha256:${binaryHash}:${versionText}`;
|
|
898
996
|
}
|
|
899
997
|
|
|
900
998
|
function resolveExecutableIdentityPath(binary: string): string {
|
|
@@ -904,7 +1002,7 @@ function resolveExecutableIdentityPath(binary: string): string {
|
|
|
904
1002
|
if (binary.includes(path.sep)) {
|
|
905
1003
|
return resolveRealPath(path.resolve(binary));
|
|
906
1004
|
}
|
|
907
|
-
for (const dir of (
|
|
1005
|
+
for (const dir of readPathEnvironment().split(path.delimiter)) {
|
|
908
1006
|
if (dir.length === 0) continue;
|
|
909
1007
|
const candidate = path.join(dir, binary);
|
|
910
1008
|
if (fs.existsSync(candidate)) {
|
|
@@ -920,6 +1018,10 @@ function resolveExecutableIdentityPath(binary: string): string {
|
|
|
920
1018
|
return binary;
|
|
921
1019
|
}
|
|
922
1020
|
|
|
1021
|
+
function readPathEnvironment(): string {
|
|
1022
|
+
return process.env.PATH ?? process.env.Path ?? "";
|
|
1023
|
+
}
|
|
1024
|
+
|
|
923
1025
|
function resolveRealPath(location: string): string {
|
|
924
1026
|
try {
|
|
925
1027
|
return fs.realpathSync(location);
|
|
@@ -927,3 +1029,335 @@ function resolveRealPath(location: string): string {
|
|
|
927
1029
|
return location;
|
|
928
1030
|
}
|
|
929
1031
|
}
|
|
1032
|
+
|
|
1033
|
+
function hashFile(file: string): string {
|
|
1034
|
+
const hash = crypto.createHash("sha256");
|
|
1035
|
+
hash.update(fs.readFileSync(file));
|
|
1036
|
+
return hash.digest("hex");
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function hashGoBuildEnvironment(
|
|
1040
|
+
hash: crypto.Hash,
|
|
1041
|
+
goBinary: string | undefined,
|
|
1042
|
+
cwd: string,
|
|
1043
|
+
): void {
|
|
1044
|
+
const values = resolveGoBuildEnvironment(goBinary, cwd);
|
|
1045
|
+
for (const key of GO_BUILD_ENV_KEYS) {
|
|
1046
|
+
const value = values.get(key);
|
|
1047
|
+
if (value !== undefined && value !== "") {
|
|
1048
|
+
hash.update(`${key}=${value}\n`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function resolveGoBuildEnvironment(
|
|
1054
|
+
goBinary: string | undefined,
|
|
1055
|
+
cwd: string,
|
|
1056
|
+
): Map<string, string> {
|
|
1057
|
+
const values = new Map<string, string>();
|
|
1058
|
+
if (goBinary !== undefined) {
|
|
1059
|
+
const result = spawnSync(goBinary, ["env", "-json", ...GO_BUILD_ENV_KEYS], {
|
|
1060
|
+
cwd,
|
|
1061
|
+
encoding: "utf8",
|
|
1062
|
+
env: goBuildEnv(goBinary),
|
|
1063
|
+
maxBuffer: 1024 * 1024,
|
|
1064
|
+
windowsHide: true,
|
|
1065
|
+
});
|
|
1066
|
+
if (result.error === undefined && result.status === 0) {
|
|
1067
|
+
try {
|
|
1068
|
+
const parsed = JSON.parse(result.stdout) as Record<string, unknown>;
|
|
1069
|
+
for (const key of GO_BUILD_ENV_KEYS) {
|
|
1070
|
+
const raw = parsed[key];
|
|
1071
|
+
if (typeof raw === "string" && raw !== "") {
|
|
1072
|
+
values.set(key, normalizeGoBuildEnvValue(key, raw));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
} catch {
|
|
1076
|
+
// Fall back to process.env below; a cache key is still better than
|
|
1077
|
+
// failing before `go build` can produce the actionable error.
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
for (const key of GO_BUILD_ENV_KEYS) {
|
|
1082
|
+
if (values.has(key)) continue;
|
|
1083
|
+
const value = process.env[key];
|
|
1084
|
+
if (value !== undefined && value !== "") {
|
|
1085
|
+
values.set(key, normalizeGoBuildEnvValue(key, value));
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return values;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function normalizeGoBuildEnvValue(key: string, value: string): string {
|
|
1092
|
+
if (key === "GOROOT") {
|
|
1093
|
+
return resolveGoRootCacheIdentity(value);
|
|
1094
|
+
}
|
|
1095
|
+
if (GO_BUILD_COMMAND_ENV_KEYS.has(key)) {
|
|
1096
|
+
return `${value}\0${resolveCommandCacheIdentity(value)}`;
|
|
1097
|
+
}
|
|
1098
|
+
return value;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function resolveCommandCacheIdentity(command: string): string {
|
|
1102
|
+
const executable = firstCommandToken(command);
|
|
1103
|
+
if (executable === null) {
|
|
1104
|
+
return "command:empty";
|
|
1105
|
+
}
|
|
1106
|
+
const resolved = resolveExecutableIdentityPath(executable);
|
|
1107
|
+
if (!fs.existsSync(resolved)) {
|
|
1108
|
+
return `command:missing:${executable}`;
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
return `command:sha256:${hashFile(resolved)}`;
|
|
1112
|
+
} catch {
|
|
1113
|
+
return `command:unreadable:${resolved}`;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function firstCommandToken(command: string): string | null {
|
|
1118
|
+
const trimmed = command.trim();
|
|
1119
|
+
if (trimmed === "") {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
const quote = trimmed[0];
|
|
1123
|
+
if (quote === "'" || quote === '"') {
|
|
1124
|
+
const end = trimmed.indexOf(quote, 1);
|
|
1125
|
+
return end === -1 ? trimmed.slice(1) : trimmed.slice(1, end);
|
|
1126
|
+
}
|
|
1127
|
+
return trimmed.split(/\s+/)[0] ?? null;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function hashExternalGoBuildEnvironment(hash: crypto.Hash): void {
|
|
1131
|
+
for (const key of EXTERNAL_GO_BUILD_ENV_KEYS) {
|
|
1132
|
+
const value = process.env[key];
|
|
1133
|
+
if (value !== undefined && value !== "") {
|
|
1134
|
+
hash.update(`${key}=${value}\n`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function resolveGoRootCacheIdentity(goRoot: string): string {
|
|
1140
|
+
const resolved = resolveRealPath(goRoot);
|
|
1141
|
+
if (!fs.existsSync(resolved)) {
|
|
1142
|
+
return `missing:${goRoot}`;
|
|
1143
|
+
}
|
|
1144
|
+
const hash = crypto.createHash("sha256");
|
|
1145
|
+
for (const file of collectGoRootIdentityFiles(resolved)) {
|
|
1146
|
+
const relative = path.relative(resolved, file).replace(/\\/g, "/");
|
|
1147
|
+
hash.update(`f=${relative}\n`);
|
|
1148
|
+
hash.update(fs.readFileSync(file));
|
|
1149
|
+
hash.update("\n");
|
|
1150
|
+
}
|
|
1151
|
+
return `sha256:${hash.digest("hex")}`;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function collectGoRootIdentityFiles(root: string): string[] {
|
|
1155
|
+
const out: string[] = [];
|
|
1156
|
+
walkGoRootIdentity(root, root, out);
|
|
1157
|
+
out.sort();
|
|
1158
|
+
return out;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function walkGoRootIdentity(root: string, dir: string, out: string[]): void {
|
|
1162
|
+
let entries: fs.Dirent[];
|
|
1163
|
+
try {
|
|
1164
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1165
|
+
} catch {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
for (const entry of entries) {
|
|
1169
|
+
const file = path.join(dir, entry.name);
|
|
1170
|
+
const rel = path.relative(root, file).replace(/\\/g, "/");
|
|
1171
|
+
if (entry.isDirectory()) {
|
|
1172
|
+
if (shouldHashGoRootPath(rel, true)) {
|
|
1173
|
+
walkGoRootIdentity(root, file, out);
|
|
1174
|
+
}
|
|
1175
|
+
} else if (entry.isFile() && shouldHashGoRootPath(rel, false)) {
|
|
1176
|
+
out.push(file);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function shouldHashGoRootPath(rel: string, isDir: boolean): boolean {
|
|
1182
|
+
if (rel === "") return true;
|
|
1183
|
+
const parts = rel.split("/");
|
|
1184
|
+
if (parts.includes(".git") || parts.includes("testdata")) return false;
|
|
1185
|
+
if (!isDir && rel.endsWith("_test.go")) return false;
|
|
1186
|
+
|
|
1187
|
+
const first = parts[0]!;
|
|
1188
|
+
if (parts.length === 1) {
|
|
1189
|
+
if (isDir) return ["bin", "pkg", "src", "lib"].includes(first);
|
|
1190
|
+
return ["VERSION", "go.env"].includes(first);
|
|
1191
|
+
}
|
|
1192
|
+
if (first === "bin") {
|
|
1193
|
+
if (isDir) return true;
|
|
1194
|
+
const base = path.basename(rel);
|
|
1195
|
+
return (
|
|
1196
|
+
base === "go" ||
|
|
1197
|
+
base === "go.exe" ||
|
|
1198
|
+
base === "gofmt" ||
|
|
1199
|
+
base === "gofmt.exe"
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
if (first === "pkg") {
|
|
1203
|
+
const second = parts[1]!;
|
|
1204
|
+
return second === "tool" || second === "include";
|
|
1205
|
+
}
|
|
1206
|
+
if (first === "src") {
|
|
1207
|
+
if (!isDir && parts.length === 2) {
|
|
1208
|
+
return ["go.mod", "go.sum"].includes(parts[1]!);
|
|
1209
|
+
}
|
|
1210
|
+
return parts[1] !== "cmd";
|
|
1211
|
+
}
|
|
1212
|
+
if (first === "lib") {
|
|
1213
|
+
return parts[1] === "time";
|
|
1214
|
+
}
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function touchCacheEntry(cacheDir: string): void {
|
|
1219
|
+
try {
|
|
1220
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
1221
|
+
fs.writeFileSync(
|
|
1222
|
+
path.join(cacheDir, CACHE_LAST_USED_FILE),
|
|
1223
|
+
`${Date.now()}\n`,
|
|
1224
|
+
);
|
|
1225
|
+
} catch {
|
|
1226
|
+
// Cache hits must not fail because metadata touch failed.
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function maybePruneGlobalPluginCache(root: string): void {
|
|
1231
|
+
try {
|
|
1232
|
+
fs.mkdirSync(root, { recursive: true });
|
|
1233
|
+
const marker = path.join(root, CACHE_GC_MARKER_FILE);
|
|
1234
|
+
const now = Date.now();
|
|
1235
|
+
const lastRun = readTimestamp(marker);
|
|
1236
|
+
if (lastRun !== null && now - lastRun < GLOBAL_CACHE_GC_INTERVAL_MS) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
fs.writeFileSync(marker, `${now}\n`);
|
|
1240
|
+
pruneGlobalPluginCache(root, now);
|
|
1241
|
+
} catch {
|
|
1242
|
+
// Global cache GC is opportunistic; builds still proceed when it fails.
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function pruneGlobalPluginCache(root: string, now: number): void {
|
|
1247
|
+
const entries = collectGlobalCacheEntries(root, now);
|
|
1248
|
+
for (const entry of entries) {
|
|
1249
|
+
if (now - entry.lastUsedAt <= GLOBAL_CACHE_ENTRY_MAX_AGE_MS) {
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
removeCacheEntry(entry);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const remaining = collectGlobalCacheEntries(root, now);
|
|
1256
|
+
let total = remaining.reduce((sum, entry) => sum + entry.size, 0);
|
|
1257
|
+
if (total <= GLOBAL_CACHE_MAX_BYTES) {
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
for (const entry of remaining.sort((a, b) => a.lastUsedAt - b.lastUsedAt)) {
|
|
1261
|
+
if (total <= GLOBAL_CACHE_TARGET_BYTES) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (now - entry.lastUsedAt <= GLOBAL_CACHE_PROTECTED_AGE_MS) {
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
removeCacheEntry(entry);
|
|
1268
|
+
total -= entry.size;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
interface GlobalCacheEntry {
|
|
1273
|
+
dir: string;
|
|
1274
|
+
lastUsedAt: number;
|
|
1275
|
+
size: number;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function collectGlobalCacheEntries(
|
|
1279
|
+
root: string,
|
|
1280
|
+
now: number,
|
|
1281
|
+
): GlobalCacheEntry[] {
|
|
1282
|
+
const entries: GlobalCacheEntry[] = [];
|
|
1283
|
+
let dirents: fs.Dirent[];
|
|
1284
|
+
try {
|
|
1285
|
+
dirents = fs.readdirSync(root, { withFileTypes: true });
|
|
1286
|
+
} catch {
|
|
1287
|
+
return entries;
|
|
1288
|
+
}
|
|
1289
|
+
for (const dirent of dirents) {
|
|
1290
|
+
if (!dirent.isDirectory()) {
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
const dir = path.join(root, dirent.name);
|
|
1294
|
+
const lastUsedAt = readCacheEntryLastUsedAt(dir, now);
|
|
1295
|
+
entries.push({
|
|
1296
|
+
dir,
|
|
1297
|
+
lastUsedAt,
|
|
1298
|
+
size: directorySize(dir),
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
return entries;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function readCacheEntryLastUsedAt(dir: string, now: number): number {
|
|
1305
|
+
const touched = readTimestamp(path.join(dir, CACHE_LAST_USED_FILE));
|
|
1306
|
+
if (touched !== null) {
|
|
1307
|
+
return touched;
|
|
1308
|
+
}
|
|
1309
|
+
for (const name of ["plugin", "plugin.exe"]) {
|
|
1310
|
+
try {
|
|
1311
|
+
return fs.statSync(path.join(dir, name)).mtimeMs;
|
|
1312
|
+
} catch {}
|
|
1313
|
+
}
|
|
1314
|
+
try {
|
|
1315
|
+
return fs.statSync(dir).mtimeMs;
|
|
1316
|
+
} catch {
|
|
1317
|
+
return now;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function readTimestamp(file: string): number | null {
|
|
1322
|
+
try {
|
|
1323
|
+
const text = fs.readFileSync(file, "utf8").trim();
|
|
1324
|
+
const value = Number(text);
|
|
1325
|
+
if (Number.isFinite(value)) {
|
|
1326
|
+
return value;
|
|
1327
|
+
}
|
|
1328
|
+
} catch {}
|
|
1329
|
+
try {
|
|
1330
|
+
return fs.statSync(file).mtimeMs;
|
|
1331
|
+
} catch {
|
|
1332
|
+
return null;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function directorySize(dir: string): number {
|
|
1337
|
+
let total = 0;
|
|
1338
|
+
let entries: fs.Dirent[];
|
|
1339
|
+
try {
|
|
1340
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1341
|
+
} catch {
|
|
1342
|
+
return total;
|
|
1343
|
+
}
|
|
1344
|
+
for (const entry of entries) {
|
|
1345
|
+
const file = path.join(dir, entry.name);
|
|
1346
|
+
try {
|
|
1347
|
+
if (entry.isDirectory()) {
|
|
1348
|
+
total += directorySize(file);
|
|
1349
|
+
} else if (entry.isFile()) {
|
|
1350
|
+
total += fs.statSync(file).size;
|
|
1351
|
+
}
|
|
1352
|
+
} catch {}
|
|
1353
|
+
}
|
|
1354
|
+
return total;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function removeCacheEntry(entry: GlobalCacheEntry): void {
|
|
1358
|
+
try {
|
|
1359
|
+
fs.rmSync(entry.dir, { recursive: true, force: true });
|
|
1360
|
+
} catch {
|
|
1361
|
+
// Windows may reject removal while a plugin binary is still running.
|
|
1362
|
+
}
|
|
1363
|
+
}
|