ttsc 0.32.1 → 0.33.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/cmd/ttsc-cache/account.go +94 -0
- package/cmd/ttsc-cache/interruption_test.go +176 -0
- package/cmd/ttsc-cache/main.go +252 -0
- package/cmd/ttsc-cache/merge.go +324 -0
- package/cmd/ttsc-cache/merge_test.go +161 -0
- package/cmd/ttsc-cache/prepare.go +330 -0
- package/cmd/ttsc-cache/prepare_test.go +456 -0
- package/cmd/ttsc-cache/protocol_test.go +119 -0
- package/cmd/ttsc-cache/publisher_seal_test.go +135 -0
- package/cmd/ttsc-cache/seal.go +427 -0
- package/cmd/ttsc-cache/stock_go_failure_test.go +94 -0
- package/cmd/ttsc-cache/stock_go_test.go +163 -0
- package/cmd/ttsc-cache/store.go +249 -0
- package/cmd/ttsc-cache/store_test.go +104 -0
- package/cmd/ttsc-cache/tuple.go +107 -0
- package/cmd/ttsc-cache/tuple_test.go +62 -0
- package/cmd/ttsc-cache/usage.go +73 -0
- package/driver/configloaderscratch/canonical_directory_other.go +34 -0
- package/driver/configloaderscratch/canonical_directory_windows.go +60 -0
- package/driver/configloaderscratch/identity_temp_base.go +81 -0
- package/driver/configloaderscratch/identity_temp_base_posix_test.go +124 -0
- package/driver/configloaderscratch/identity_temp_base_windows_test.go +156 -0
- package/driver/emit_provenance.go +7 -5
- package/driver/resolutioninputs/recorder.cjs +16 -4
- package/driver/rewrite.go +25 -16
- package/driver/write_file_if_changed.go +69 -0
- package/driver/write_file_if_changed_test.go +148 -0
- package/internal/packageartifacts/manifest.go +288 -0
- package/internal/packageartifacts/manifest_test.go +127 -0
- package/lib/compiler/internal/build/BuildExecution.d.ts +11 -7
- package/lib/compiler/internal/build/BuildExecution.js +22 -8
- package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
- package/lib/compiler/internal/build/ResidentCheckWatchSession.d.ts +10 -10
- package/lib/compiler/internal/build/ResidentCheckWatchSession.js +19 -14
- package/lib/compiler/internal/build/ResidentCheckWatchSession.js.map +1 -1
- package/lib/compiler/internal/build/RunBuildOptions.d.ts +8 -1
- package/lib/compiler/internal/build/TsgoArguments.d.ts +16 -0
- package/lib/compiler/internal/build/TsgoArguments.js +19 -0
- package/lib/compiler/internal/build/TsgoArguments.js.map +1 -1
- package/lib/compiler/internal/captureProcessOutput.d.ts +16 -13
- package/lib/compiler/internal/captureProcessOutput.js +22 -14
- package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.js +3 -1
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +5 -1
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/launcher/internal/runtime/PluginDescriptorInputReport.d.ts +43 -0
- package/lib/launcher/internal/runtime/PluginDescriptorInputReport.js +106 -0
- package/lib/launcher/internal/runtime/PluginDescriptorInputReport.js.map +1 -0
- package/lib/launcher/internal/runtime/installRuntimeHooks.js +15 -29
- package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
- package/lib/launcher/internal/ttscserver/runTtscserver.js +1 -0
- package/lib/launcher/internal/ttscserver/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/watch/WatchTopology.js +5 -4
- package/lib/launcher/internal/watch/WatchTopology.js.map +1 -1
- package/lib/plugin/internal/CapabilityResolutionFormat.js +4 -3
- package/lib/plugin/internal/CapabilityResolutionFormat.js.map +1 -1
- package/lib/plugin/internal/load/COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE.js +6 -0
- package/lib/plugin/internal/load/COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE.js.map +1 -1
- package/lib/plugin/internal/load/PLUGIN_DESCRIPTOR_SHIM_SOURCE.js +6 -0
- package/lib/plugin/internal/load/PLUGIN_DESCRIPTOR_SHIM_SOURCE.js.map +1 -1
- package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js +3 -3
- package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js.map +1 -1
- package/lib/plugin/internal/load/descriptorRecordEnvironment.d.ts +15 -0
- package/lib/plugin/internal/load/descriptorRecordEnvironment.js +24 -0
- package/lib/plugin/internal/load/descriptorRecordEnvironment.js.map +1 -0
- package/lib/plugin/internal/load/loadProjectPlugins.js +28 -26
- package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
- package/lib/plugin/internal/source/SourceBuildCacheLayout.d.ts +2 -0
- package/lib/plugin/internal/source/SourceBuildCacheLayout.js +2 -0
- package/lib/plugin/internal/source/SourceBuildCacheLayout.js.map +1 -1
- package/lib/plugin/internal/source/SourcePackageArtifacts.d.ts +145 -0
- package/lib/plugin/internal/source/SourcePackageArtifacts.js +612 -0
- package/lib/plugin/internal/source/SourcePackageArtifacts.js.map +1 -0
- package/lib/plugin/internal/source/buildSourcePlugin.js +54 -4
- package/lib/plugin/internal/source/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/source/prunePackageArtifactRoot.d.ts +19 -0
- package/lib/plugin/internal/source/prunePackageArtifactRoot.js +95 -0
- package/lib/plugin/internal/source/prunePackageArtifactRoot.js.map +1 -0
- package/lib/plugin/internal/source/resolveCleanTargets.js +1 -1
- package/lib/plugin/internal/source/resolveCleanTargets.js.map +1 -1
- package/lib/plugin/internal/source/resolveSourceBuildCachePaths.js +9 -4
- package/lib/plugin/internal/source/resolveSourceBuildCachePaths.js.map +1 -1
- package/lib/plugin/internal/source/selectedRecordIdentityTempParent.d.ts +18 -0
- package/lib/plugin/internal/source/selectedRecordIdentityTempParent.js +46 -0
- package/lib/plugin/internal/source/selectedRecordIdentityTempParent.js.map +1 -0
- package/package.json +8 -8
- package/src/compiler/internal/build/BuildExecution.ts +30 -10
- package/src/compiler/internal/build/ResidentCheckWatchSession.ts +23 -22
- package/src/compiler/internal/build/RunBuildOptions.ts +9 -1
- package/src/compiler/internal/build/TsgoArguments.ts +22 -0
- package/src/compiler/internal/captureProcessOutput.ts +28 -14
- package/src/compiler/internal/startResidentTransform.ts +7 -1
- package/src/compiler/internal/transformProjectInMemory.ts +9 -1
- package/src/launcher/internal/runtime/PluginDescriptorInputReport.ts +100 -0
- package/src/launcher/internal/runtime/installRuntimeHooks.ts +57 -55
- package/src/launcher/internal/ttscserver/runTtscserver.ts +5 -0
- package/src/launcher/internal/watch/WatchTopology.ts +5 -4
- package/src/plugin/internal/CapabilityResolutionFormat.ts +4 -3
- package/src/plugin/internal/load/COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE.ts +4 -0
- package/src/plugin/internal/load/PLUGIN_DESCRIPTOR_SHIM_SOURCE.ts +4 -0
- package/src/plugin/internal/load/PluginDescriptorEvaluationCache.ts +3 -3
- package/src/plugin/internal/load/descriptorRecordEnvironment.ts +24 -0
- package/src/plugin/internal/load/loadProjectPlugins.ts +45 -29
- package/src/plugin/internal/source/SourceBuildCacheLayout.ts +3 -0
- package/src/plugin/internal/source/SourcePackageArtifacts.ts +787 -0
- package/src/plugin/internal/source/buildSourcePlugin.ts +82 -12
- package/src/plugin/internal/source/prunePackageArtifactRoot.ts +100 -0
- package/src/plugin/internal/source/resolveCleanTargets.ts +1 -0
- package/src/plugin/internal/source/resolveSourceBuildCachePaths.ts +11 -4
- package/src/plugin/internal/source/selectedRecordIdentityTempParent.ts +40 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import util from "node:util";
|
|
6
|
+
|
|
7
|
+
import { SidecarEnvironment } from "../../../compiler/internal/sharedHost/SidecarEnvironment";
|
|
8
|
+
import { E2ETrace } from "../../../internal/E2ETrace";
|
|
9
|
+
import { OwnedSynchronousProcess } from "../../../internal/OwnedSynchronousProcess";
|
|
10
|
+
import { SourceNativeRetirement } from "../../../internal/SourceNativeRetirement";
|
|
11
|
+
import { SourceBuildCacheLayout } from "./SourceBuildCacheLayout";
|
|
12
|
+
import { spawnGoTool } from "./spawnGoTool";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Select and prepare a local publisher package cache for an actual cold Go
|
|
16
|
+
* build.
|
|
17
|
+
*
|
|
18
|
+
* The release package may pin an authenticated manifest digest. Explicit local
|
|
19
|
+
* experiments may instead supply both a manifest path and its separately
|
|
20
|
+
* trusted digest. Every candidate must match the selected stock-Go tuple, and
|
|
21
|
+
* the Go client still decides hits from its complete ActionIDs. Preparation is
|
|
22
|
+
* optional; a launched helper failure remains a terminal Go build failure.
|
|
23
|
+
*
|
|
24
|
+
* @evidence contracts/common.md#principled-implementation The selected release or explicit operator digest authenticates manifest bytes, a stock-Go tuple probe limits coverage to effective CGO=0, and Go's GOCACHEPROG retains final action authority.
|
|
25
|
+
* @evidence contracts/common.md#clear-and-simple-design One cold-build selector owns opt-in, trust, tuple, preparation and program quoting, returning an environment copy only after complete admission.
|
|
26
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts An absent pack, user cache program, unsupported Go tuple or failed preparation selects ordinary Go before launch; an active helper error is never replayed as an apparent cache miss.
|
|
27
|
+
* @evidence contracts/common.md#meaningful-documentation The comment distinguishes optional preparation from terminal active-helper failure and explains both trust sources.
|
|
28
|
+
* @evidence contracts/portability.md#os-neutral-implementation Native path and platform package resolution choose the host helper; Go's quoted argument grammar is applied without shell expansion on Windows or POSIX.
|
|
29
|
+
* @evidence contracts/performance.md#efficient-algorithms Only cold builds read at most 16 MiB of manifest bytes, hash the bounded value, probe the Go tool once, and request at most eight bounded packs; exact executable hits never call this selector.
|
|
30
|
+
* @evidenceExclude contracts/performance.md#reuse-equivalent-work Shared local pack admission is performed by the helper; this selector retains no memo that could outlive a changed effective environment.
|
|
31
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources The existing native task owns the bounded helper subprocess and guards its artifact root; the helper owns downloaded staging, while this call retains only bounded JSON and output buffers.
|
|
32
|
+
*/
|
|
33
|
+
export namespace SourcePackageArtifacts {
|
|
34
|
+
/**
|
|
35
|
+
* Record a cold build's real materialized Go package population for sealing.
|
|
36
|
+
* The operator owns the explicit output root and the records retain local
|
|
37
|
+
* paths until a release publisher consumes and removes them.
|
|
38
|
+
*
|
|
39
|
+
* @evidence contracts/common.md#principled-implementation Only an explicit absolute publisher root and an unused local cache program admit recording; Go list and the subsequent build inspect the same materialized workspace and effective environment.
|
|
40
|
+
* @evidence contracts/common.md#clear-and-simple-design One record directory holds Go package receipts, the action graph and its private overlay, while ordinary builds receive null.
|
|
41
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Go computes and records its own actions; this step never hashes package names into cache identities or replaces an existing caller program.
|
|
42
|
+
* @evidence contracts/common.md#meaningful-documentation The comment names the explicit authority, sensitive local receipts and who cleans them after publication.
|
|
43
|
+
* @evidence contracts/portability.md#os-neutral-implementation Native path construction and the shared Go command owner handle host quoting, working directories and native process completion.
|
|
44
|
+
* @evidence contracts/performance.md#efficient-algorithms Only explicit publisher builds collect at most 256 MiB of Go package receipts before the actual build; ordinary and exact-executable paths do not run this mode.
|
|
45
|
+
* @evidenceExclude contracts/performance.md#reuse-equivalent-work Publisher output is exclusive for one cold attempt and cannot be reused as evidence for a different materialized workspace.
|
|
46
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources The native scope guards the record directory, a byte cap bounds the saved Go output, and the operator owns its eventual deletion after sealing.
|
|
47
|
+
*/
|
|
48
|
+
export function record(opts: {
|
|
49
|
+
cwd: string;
|
|
50
|
+
entry: string;
|
|
51
|
+
env: NodeJS.ProcessEnv;
|
|
52
|
+
goBinary: string;
|
|
53
|
+
key: string;
|
|
54
|
+
pluginName: string;
|
|
55
|
+
ttscVersion: string;
|
|
56
|
+
}): {
|
|
57
|
+
env: NodeJS.ProcessEnv;
|
|
58
|
+
actionGraph: string;
|
|
59
|
+
sealArgs: string[];
|
|
60
|
+
helper: string;
|
|
61
|
+
outputDir: string;
|
|
62
|
+
} | null {
|
|
63
|
+
const root = SidecarEnvironment.read(
|
|
64
|
+
opts.env,
|
|
65
|
+
"TTSC_PACKAGE_ARTIFACT_RECORD_DIR",
|
|
66
|
+
);
|
|
67
|
+
if (root === undefined) return null;
|
|
68
|
+
if (!path.isAbsolute(root))
|
|
69
|
+
throw new Error("ttsc: publisher record directory must be absolute");
|
|
70
|
+
if (SidecarEnvironment.read(opts.env, "GOCACHEPROG"))
|
|
71
|
+
throw new Error(
|
|
72
|
+
"ttsc: publisher recording requires an unused GOCACHEPROG",
|
|
73
|
+
);
|
|
74
|
+
const helper = resolveHelper(opts.env);
|
|
75
|
+
if (helper === null)
|
|
76
|
+
throw new Error(
|
|
77
|
+
"ttsc: publisher recording requires a prebuilt cache helper",
|
|
78
|
+
);
|
|
79
|
+
const sourceCommit = SidecarEnvironment.read(
|
|
80
|
+
opts.env,
|
|
81
|
+
"TTSC_PACKAGE_ARTIFACT_SOURCE_COMMIT",
|
|
82
|
+
);
|
|
83
|
+
const packURLBase = SidecarEnvironment.read(
|
|
84
|
+
opts.env,
|
|
85
|
+
"TTSC_PACKAGE_ARTIFACT_PACK_URL_BASE",
|
|
86
|
+
);
|
|
87
|
+
const packName = SidecarEnvironment.read(
|
|
88
|
+
opts.env,
|
|
89
|
+
"TTSC_PACKAGE_ARTIFACT_PACK_NAME",
|
|
90
|
+
);
|
|
91
|
+
if (
|
|
92
|
+
!sourceCommit ||
|
|
93
|
+
!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(sourceCommit) ||
|
|
94
|
+
!packURLBase ||
|
|
95
|
+
!packName ||
|
|
96
|
+
!/^[a-z0-9-]{1,80}$/.test(packName)
|
|
97
|
+
)
|
|
98
|
+
throw new Error(
|
|
99
|
+
"ttsc: publisher recording needs a source commit, pack URL base and safe pack name",
|
|
100
|
+
);
|
|
101
|
+
const identity = crypto
|
|
102
|
+
.createHash("sha256")
|
|
103
|
+
.update(`${opts.pluginName}\0${opts.key}`)
|
|
104
|
+
.digest("hex");
|
|
105
|
+
fs.mkdirSync(root, { recursive: true });
|
|
106
|
+
const directory = path.join(root, identity);
|
|
107
|
+
fs.mkdirSync(directory);
|
|
108
|
+
SourceNativeRetirement.registerSharedRoot(directory);
|
|
109
|
+
const packages = path.join(directory, "packages.json");
|
|
110
|
+
const actionGraph = path.join(directory, "actiongraph.json");
|
|
111
|
+
const listed = spawnGoTool(
|
|
112
|
+
opts.goBinary,
|
|
113
|
+
["list", "-deps", "-json", opts.entry],
|
|
114
|
+
{
|
|
115
|
+
cwd: opts.cwd,
|
|
116
|
+
encoding: "utf8",
|
|
117
|
+
env: opts.env,
|
|
118
|
+
windowsHide: true,
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
OwnedSynchronousProcess.checkpoint();
|
|
122
|
+
if (listed.error || listed.status !== 0)
|
|
123
|
+
throw new Error(
|
|
124
|
+
`ttsc: publisher package receipts failed: ${listed.stderr || listed.error}`,
|
|
125
|
+
);
|
|
126
|
+
if (Buffer.byteLength(listed.stdout, "utf8") > 256 << 20)
|
|
127
|
+
throw new Error("ttsc: publisher package receipts exceed 256 MiB");
|
|
128
|
+
fs.writeFileSync(packages, listed.stdout, { encoding: "utf8", flag: "wx" });
|
|
129
|
+
const program = [helper, "-root", path.join(directory, "overlay")]
|
|
130
|
+
.map(quoteGoArgument)
|
|
131
|
+
.join(" ");
|
|
132
|
+
process.stderr.write(
|
|
133
|
+
`ttsc: recording Go package actions in ${directory}\n`,
|
|
134
|
+
);
|
|
135
|
+
const outputDir = path.join(directory, "sealed");
|
|
136
|
+
return {
|
|
137
|
+
env: { ...opts.env, GOCACHEPROG: program },
|
|
138
|
+
actionGraph,
|
|
139
|
+
helper,
|
|
140
|
+
outputDir,
|
|
141
|
+
sealArgs: [
|
|
142
|
+
"-seal",
|
|
143
|
+
"-root",
|
|
144
|
+
path.join(directory, "overlay"),
|
|
145
|
+
"-go",
|
|
146
|
+
opts.goBinary,
|
|
147
|
+
"-actiongraph",
|
|
148
|
+
actionGraph,
|
|
149
|
+
"-packages",
|
|
150
|
+
packages,
|
|
151
|
+
"-release",
|
|
152
|
+
opts.ttscVersion,
|
|
153
|
+
"-source-commit",
|
|
154
|
+
sourceCommit,
|
|
155
|
+
"-pack-url-base",
|
|
156
|
+
packURLBase,
|
|
157
|
+
"-pack-name",
|
|
158
|
+
packName,
|
|
159
|
+
"-out",
|
|
160
|
+
outputDir,
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Seal a successful publisher build before its materialized Go sources
|
|
167
|
+
* retire. The explicit record owner keeps the resulting immutable pack and
|
|
168
|
+
* digest.
|
|
169
|
+
*
|
|
170
|
+
* @evidence contracts/common.md#principled-implementation Sealing runs while the exact source workspace and Go receipts still exist, so source diagnostics come from the successful build rather than a reconstructed approximation.
|
|
171
|
+
* @evidence contracts/common.md#clear-and-simple-design The recorded plan owns one helper invocation and one bounded release receipt.
|
|
172
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Publisher failure is terminal for this explicit recording attempt; it cannot publish an unsealed executable while claiming artifact evidence.
|
|
173
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the materialized-source lifetime requirement and output owner.
|
|
174
|
+
* @evidence contracts/portability.md#os-neutral-implementation The prebuilt native helper receives separate arguments through the owned synchronous process seam.
|
|
175
|
+
* @evidence contracts/performance.md#efficient-algorithms Sealing runs only after an explicitly recorded cold build and streams package bodies through the native helper.
|
|
176
|
+
* @evidenceExclude contracts/performance.md#reuse-equivalent-work One successful build supplies one source/action population; its release receipt is not reused for another build.
|
|
177
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources The native task keeps source and record roots guarded until the helper exits; output retention transfers to the explicit publisher directory.
|
|
178
|
+
*/
|
|
179
|
+
export function seal(recording: {
|
|
180
|
+
helper: string;
|
|
181
|
+
sealArgs: string[];
|
|
182
|
+
env: NodeJS.ProcessEnv;
|
|
183
|
+
outputDir: string;
|
|
184
|
+
}): void {
|
|
185
|
+
OwnedSynchronousProcess.checkpoint();
|
|
186
|
+
const result = launch(recording.helper, recording.sealArgs, recording.env);
|
|
187
|
+
OwnedSynchronousProcess.checkpoint();
|
|
188
|
+
if (result.status !== 0)
|
|
189
|
+
throw new Error(
|
|
190
|
+
`ttsc: publisher package sealing failed: ${result.stderr}`,
|
|
191
|
+
);
|
|
192
|
+
let receipt: unknown;
|
|
193
|
+
try {
|
|
194
|
+
receipt = JSON.parse(result.stdout);
|
|
195
|
+
} catch {
|
|
196
|
+
throw new Error("ttsc: publisher sealing returned an invalid receipt");
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
receipt === null ||
|
|
200
|
+
typeof receipt !== "object" ||
|
|
201
|
+
!("manifestSHA256" in receipt) ||
|
|
202
|
+
typeof receipt.manifestSHA256 !== "string" ||
|
|
203
|
+
!/^[0-9a-f]{64}$/.test(receipt.manifestSHA256)
|
|
204
|
+
)
|
|
205
|
+
throw new Error(
|
|
206
|
+
"ttsc: publisher sealing returned an invalid manifest digest",
|
|
207
|
+
);
|
|
208
|
+
process.stderr.write(
|
|
209
|
+
`ttsc: sealed publisher package actions in ${recording.outputDir}; manifest SHA-256 ${receipt.manifestSHA256}\n`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* A cold source-build backend decision and its bounded transfer receipt.
|
|
215
|
+
*
|
|
216
|
+
* @evidence contracts/common.md#principled-implementation The environment is returned only after the selected helper has admitted its pack population; reason distinguishes ordinary fallback from local preparation.
|
|
217
|
+
* @evidence contracts/common.md#clear-and-simple-design One value carries the invocation environment, decision and optional receipt without changing a cached binary's identity.
|
|
218
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts A receipt reports preparation, not Go package hits; actual ActionID matching remains with the Go client.
|
|
219
|
+
* @evidence contracts/common.md#meaningful-documentation The comment separates transfer evidence from cache-hit claims.
|
|
220
|
+
* @evidenceExclude contracts/portability.md#os-neutral-implementation Paths and environment names are interpreted by the selector, not this value.
|
|
221
|
+
* @evidenceExclude contracts/performance.md#efficient-algorithms This shape chooses no algorithm.
|
|
222
|
+
* @evidenceExclude contracts/performance.md#reuse-equivalent-work The value itself coordinates no reuse.
|
|
223
|
+
* @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The selector and native owner retain resources; the value owns no handle.
|
|
224
|
+
*/
|
|
225
|
+
export interface Selection {
|
|
226
|
+
env: NodeJS.ProcessEnv;
|
|
227
|
+
reason: string;
|
|
228
|
+
artifactRoot?: string;
|
|
229
|
+
usageReceiptPath?: string;
|
|
230
|
+
receipt?: {
|
|
231
|
+
selectedPacks: string[];
|
|
232
|
+
downloadedPacks: number;
|
|
233
|
+
requests: number;
|
|
234
|
+
compressedBytes: number;
|
|
235
|
+
decodedBytes: number;
|
|
236
|
+
downloadMillis: number;
|
|
237
|
+
extractMillis: number;
|
|
238
|
+
admissionMillis: number;
|
|
239
|
+
totalMillis: number;
|
|
240
|
+
storage: {
|
|
241
|
+
baseBytes: number;
|
|
242
|
+
overlayBytes: number;
|
|
243
|
+
stagingBytes: number;
|
|
244
|
+
pinnedBytes: number;
|
|
245
|
+
baseFiles: number;
|
|
246
|
+
overlayFiles: number;
|
|
247
|
+
stagingFiles: number;
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Return the prepared helper environment, or the unchanged environment and a
|
|
254
|
+
* bounded reason when the optional backend cannot be selected.
|
|
255
|
+
*
|
|
256
|
+
* @evidence contracts/common.md#principled-implementation Explicit opt-in, caller cache authority, release digest, matching Go tuple and fully admitted packs precede a GOCACHEPROG assignment.
|
|
257
|
+
* @evidence contracts/common.md#clear-and-simple-design Every refusal returns the same environment and one reason; successful preparation returns a copied environment and receipt.
|
|
258
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts A manifest supplied by an untrusted path cannot self-authenticate, and a failed preflight never starts Go under an unprepared helper.
|
|
259
|
+
* @evidence contracts/common.md#meaningful-documentation The return contract identifies the ordinary path and the success path without implying package hits.
|
|
260
|
+
* @evidence contracts/portability.md#os-neutral-implementation Node path resolution and environment-name identity work across platforms; Go's own quoted command arguments carry spaces without a shell.
|
|
261
|
+
* @evidence contracts/performance.md#efficient-algorithms Only an opted-in cold build reads bounded manifest bytes and runs one tuple plus one preparation process; Go performs any later per-action lookup locally.
|
|
262
|
+
* @evidenceExclude contracts/performance.md#reuse-equivalent-work No positive tuple or preparation result is memoized across source-build invocations.
|
|
263
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources The task's native retirement guard covers the shared artifact root before the helper runs; the child owner settles each synchronous process and the Go helper owns its staging cleanup.
|
|
264
|
+
*/
|
|
265
|
+
export function prepare(opts: {
|
|
266
|
+
artifactRoot: string;
|
|
267
|
+
cwd: string;
|
|
268
|
+
entry: string;
|
|
269
|
+
env: NodeJS.ProcessEnv;
|
|
270
|
+
goBinary: string;
|
|
271
|
+
pluginName: string;
|
|
272
|
+
quiet?: boolean;
|
|
273
|
+
ttscVersion: string;
|
|
274
|
+
}): Selection {
|
|
275
|
+
const optedIn =
|
|
276
|
+
SidecarEnvironment.read(opts.env, "TTSC_PACKAGE_ARTIFACTS") === "1";
|
|
277
|
+
const ordinary = (reason: string): Selection => {
|
|
278
|
+
E2ETrace.capabilityResolution("plugin-package-artifacts", {
|
|
279
|
+
pluginName: opts.pluginName,
|
|
280
|
+
reason,
|
|
281
|
+
selected: false,
|
|
282
|
+
});
|
|
283
|
+
if (optedIn && !opts.quiet)
|
|
284
|
+
process.stderr.write(
|
|
285
|
+
`ttsc: package artifacts unavailable (${reason}); using ordinary Go cache.\n`,
|
|
286
|
+
);
|
|
287
|
+
return { env: opts.env, reason };
|
|
288
|
+
};
|
|
289
|
+
if (!optedIn) return ordinary("disabled");
|
|
290
|
+
if (SidecarEnvironment.read(opts.env, "GOCACHEPROG"))
|
|
291
|
+
return ordinary("caller-gocacheprog");
|
|
292
|
+
const helper = resolveHelper(opts.env);
|
|
293
|
+
if (helper === null) return ordinary("helper-unavailable");
|
|
294
|
+
const release = resolveManifest(opts.env);
|
|
295
|
+
if (release === null) return ordinary("manifest-unavailable");
|
|
296
|
+
let raw: Buffer;
|
|
297
|
+
try {
|
|
298
|
+
const info = fs.statSync(release.path);
|
|
299
|
+
if (!info.isFile() || info.size === 0 || info.size > 16 << 20)
|
|
300
|
+
return ordinary("manifest-size");
|
|
301
|
+
raw = fs.readFileSync(release.path);
|
|
302
|
+
if (raw.length !== info.size) return ordinary("manifest-changed");
|
|
303
|
+
} catch {
|
|
304
|
+
return ordinary("manifest-unreadable");
|
|
305
|
+
}
|
|
306
|
+
if (
|
|
307
|
+
crypto.createHash("sha256").update(raw).digest("hex") !== release.digest
|
|
308
|
+
)
|
|
309
|
+
return ordinary("manifest-untrusted");
|
|
310
|
+
let manifest: {
|
|
311
|
+
release?: unknown;
|
|
312
|
+
tuple?: unknown;
|
|
313
|
+
packs?: unknown;
|
|
314
|
+
actions?: unknown;
|
|
315
|
+
};
|
|
316
|
+
try {
|
|
317
|
+
manifest = JSON.parse(raw.toString("utf8"));
|
|
318
|
+
} catch {
|
|
319
|
+
return ordinary("manifest-invalid");
|
|
320
|
+
}
|
|
321
|
+
if (manifest === null || typeof manifest !== "object")
|
|
322
|
+
return ordinary("manifest-invalid");
|
|
323
|
+
if (manifest.release !== opts.ttscVersion || !Array.isArray(manifest.packs))
|
|
324
|
+
return ordinary("release-mismatch");
|
|
325
|
+
const names = manifest.packs.map((pack: unknown) =>
|
|
326
|
+
pack !== null && typeof pack === "object" && "name" in pack
|
|
327
|
+
? (pack as { name: unknown }).name
|
|
328
|
+
: undefined,
|
|
329
|
+
);
|
|
330
|
+
if (
|
|
331
|
+
names.length === 0 ||
|
|
332
|
+
names.length > 64 ||
|
|
333
|
+
names.some(
|
|
334
|
+
(name) => typeof name !== "string" || !/^[a-z0-9-]{1,80}$/.test(name),
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
return ordinary("pack-selection-unsupported");
|
|
338
|
+
const tuple = launch(helper, ["-tuple", "-go", opts.goBinary], opts.env);
|
|
339
|
+
if (tuple.status !== 0) return ordinary("go-tuple-unsupported");
|
|
340
|
+
let reported: unknown;
|
|
341
|
+
try {
|
|
342
|
+
reported = JSON.parse(tuple.stdout);
|
|
343
|
+
} catch {
|
|
344
|
+
return ordinary("go-tuple-invalid");
|
|
345
|
+
}
|
|
346
|
+
if (!util.isDeepStrictEqual(reported, manifest.tuple))
|
|
347
|
+
return ordinary("go-tuple-mismatch");
|
|
348
|
+
const goVersion =
|
|
349
|
+
reported !== null &&
|
|
350
|
+
typeof reported === "object" &&
|
|
351
|
+
"goVersion" in reported
|
|
352
|
+
? (reported as { goVersion: unknown }).goVersion
|
|
353
|
+
: undefined;
|
|
354
|
+
if (typeof goVersion !== "string") return ordinary("go-tuple-invalid");
|
|
355
|
+
const listed = launch(
|
|
356
|
+
opts.goBinary,
|
|
357
|
+
[
|
|
358
|
+
"list",
|
|
359
|
+
"-deps",
|
|
360
|
+
"-f",
|
|
361
|
+
"{{.ImportPath}}|{{if .Module}}{{.Module.Path}}|{{if .Module.Replace}}{{.Module.Replace.Version}}{{else}}{{.Module.Version}}{{end}}|{{.Module.Main}}{{else}}std||false{{end}}",
|
|
362
|
+
opts.entry,
|
|
363
|
+
],
|
|
364
|
+
opts.env,
|
|
365
|
+
opts.cwd,
|
|
366
|
+
4 << 20,
|
|
367
|
+
);
|
|
368
|
+
if (listed.status !== 0) return ordinary("package-graph-unavailable");
|
|
369
|
+
const packages = new Set<string>();
|
|
370
|
+
for (const line of listed.stdout.trimEnd().split(/\r?\n/)) {
|
|
371
|
+
const fields = line.split("|");
|
|
372
|
+
if (
|
|
373
|
+
fields.length !== 4 ||
|
|
374
|
+
!fields[0] ||
|
|
375
|
+
!fields[1] ||
|
|
376
|
+
(fields[3] !== "true" && fields[3] !== "false")
|
|
377
|
+
)
|
|
378
|
+
return ordinary("package-graph-invalid");
|
|
379
|
+
const role =
|
|
380
|
+
fields[1] === "std"
|
|
381
|
+
? "standard"
|
|
382
|
+
: fields[3] === "true"
|
|
383
|
+
? "main"
|
|
384
|
+
: "dependency";
|
|
385
|
+
const version = role === "standard" ? goVersion : fields[2]!;
|
|
386
|
+
packages.add([fields[0], fields[1], version, role].join("\0"));
|
|
387
|
+
}
|
|
388
|
+
if (!Array.isArray(manifest.actions)) return ordinary("manifest-invalid");
|
|
389
|
+
const selected = new Set<string>();
|
|
390
|
+
for (const action of manifest.actions) {
|
|
391
|
+
if (
|
|
392
|
+
action === null ||
|
|
393
|
+
typeof action !== "object" ||
|
|
394
|
+
!("pack" in action) ||
|
|
395
|
+
!("packagePath" in action) ||
|
|
396
|
+
!("modulePath" in action) ||
|
|
397
|
+
!("moduleVersion" in action) ||
|
|
398
|
+
!("moduleRole" in action)
|
|
399
|
+
)
|
|
400
|
+
return ordinary("manifest-invalid");
|
|
401
|
+
const record = action as {
|
|
402
|
+
pack: unknown;
|
|
403
|
+
packagePath: unknown;
|
|
404
|
+
modulePath: unknown;
|
|
405
|
+
moduleVersion: unknown;
|
|
406
|
+
moduleRole: unknown;
|
|
407
|
+
};
|
|
408
|
+
if (
|
|
409
|
+
typeof record.pack !== "string" ||
|
|
410
|
+
typeof record.packagePath !== "string" ||
|
|
411
|
+
typeof record.modulePath !== "string" ||
|
|
412
|
+
typeof record.moduleVersion !== "string" ||
|
|
413
|
+
typeof record.moduleRole !== "string"
|
|
414
|
+
)
|
|
415
|
+
return ordinary("manifest-invalid");
|
|
416
|
+
if (
|
|
417
|
+
packages.has(
|
|
418
|
+
[
|
|
419
|
+
record.packagePath,
|
|
420
|
+
record.modulePath,
|
|
421
|
+
record.moduleVersion,
|
|
422
|
+
record.moduleRole,
|
|
423
|
+
].join("\0"),
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
selected.add(record.pack);
|
|
427
|
+
}
|
|
428
|
+
const matching: string[] = names.filter(
|
|
429
|
+
(name): name is string => typeof name === "string" && selected.has(name),
|
|
430
|
+
);
|
|
431
|
+
if (matching.length === 0) return ordinary("no-matching-packs");
|
|
432
|
+
if (matching.length > 8) return ordinary("pack-selection-unsupported");
|
|
433
|
+
SourceNativeRetirement.registerSharedRoot(opts.artifactRoot);
|
|
434
|
+
OwnedSynchronousProcess.checkpoint();
|
|
435
|
+
const result = launch(
|
|
436
|
+
helper,
|
|
437
|
+
[
|
|
438
|
+
"-root",
|
|
439
|
+
opts.artifactRoot,
|
|
440
|
+
"-manifest",
|
|
441
|
+
release.path,
|
|
442
|
+
"-manifest-sha256",
|
|
443
|
+
release.digest,
|
|
444
|
+
"-prepare",
|
|
445
|
+
"-packs",
|
|
446
|
+
matching.join(","),
|
|
447
|
+
],
|
|
448
|
+
opts.env,
|
|
449
|
+
);
|
|
450
|
+
OwnedSynchronousProcess.checkpoint();
|
|
451
|
+
if (result.status !== 0) return ordinary("preparation-failed");
|
|
452
|
+
let receipt: Selection["receipt"];
|
|
453
|
+
try {
|
|
454
|
+
receipt = JSON.parse(result.stdout) as Selection["receipt"];
|
|
455
|
+
const storage = receipt?.storage;
|
|
456
|
+
if (
|
|
457
|
+
receipt === undefined ||
|
|
458
|
+
!Array.isArray(receipt.selectedPacks) ||
|
|
459
|
+
receipt.selectedPacks.length !== matching.length ||
|
|
460
|
+
receipt.selectedPacks.some((name, index) => name !== matching[index]) ||
|
|
461
|
+
!Number.isSafeInteger(receipt.downloadedPacks) ||
|
|
462
|
+
!Number.isSafeInteger(receipt.requests) ||
|
|
463
|
+
!Number.isSafeInteger(receipt.compressedBytes) ||
|
|
464
|
+
!Number.isSafeInteger(receipt.decodedBytes) ||
|
|
465
|
+
[
|
|
466
|
+
receipt.downloadMillis,
|
|
467
|
+
receipt.extractMillis,
|
|
468
|
+
receipt.admissionMillis,
|
|
469
|
+
receipt.totalMillis,
|
|
470
|
+
].some((value) => !Number.isSafeInteger(value) || value < 0) ||
|
|
471
|
+
storage === undefined ||
|
|
472
|
+
storage === null ||
|
|
473
|
+
typeof storage !== "object" ||
|
|
474
|
+
[
|
|
475
|
+
"baseBytes",
|
|
476
|
+
"overlayBytes",
|
|
477
|
+
"stagingBytes",
|
|
478
|
+
"pinnedBytes",
|
|
479
|
+
"baseFiles",
|
|
480
|
+
"overlayFiles",
|
|
481
|
+
"stagingFiles",
|
|
482
|
+
].some((key) => {
|
|
483
|
+
const value = storage[key as keyof typeof storage];
|
|
484
|
+
return !Number.isSafeInteger(value) || value < 0;
|
|
485
|
+
})
|
|
486
|
+
)
|
|
487
|
+
return ordinary("preparation-receipt-invalid");
|
|
488
|
+
} catch {
|
|
489
|
+
return ordinary("preparation-receipt-invalid");
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
SourceBuildCacheLayout.replaceCacheMetadataFile(
|
|
493
|
+
path.join(opts.artifactRoot, ".last-used"),
|
|
494
|
+
`${Date.now()}\n`,
|
|
495
|
+
);
|
|
496
|
+
} catch {
|
|
497
|
+
// Failed idle metadata cannot invalidate an admitted pack.
|
|
498
|
+
}
|
|
499
|
+
const receiptName = crypto.randomBytes(16).toString("hex");
|
|
500
|
+
let program: string;
|
|
501
|
+
try {
|
|
502
|
+
program = [
|
|
503
|
+
helper,
|
|
504
|
+
"-root",
|
|
505
|
+
opts.artifactRoot,
|
|
506
|
+
"-manifest",
|
|
507
|
+
release.path,
|
|
508
|
+
"-manifest-sha256",
|
|
509
|
+
release.digest,
|
|
510
|
+
"-receipt-name",
|
|
511
|
+
receiptName,
|
|
512
|
+
]
|
|
513
|
+
.map(quoteGoArgument)
|
|
514
|
+
.join(" ");
|
|
515
|
+
} catch {
|
|
516
|
+
return ordinary("helper-path-unquotable");
|
|
517
|
+
}
|
|
518
|
+
E2ETrace.capabilityResolution("plugin-package-artifacts", {
|
|
519
|
+
pluginName: opts.pluginName,
|
|
520
|
+
selected: true,
|
|
521
|
+
selectedPacksJson: JSON.stringify(receipt.selectedPacks),
|
|
522
|
+
downloadedPacks: receipt.downloadedPacks,
|
|
523
|
+
requests: receipt.requests,
|
|
524
|
+
compressedBytes: receipt.compressedBytes,
|
|
525
|
+
decodedBytes: receipt.decodedBytes,
|
|
526
|
+
downloadMillis: receipt.downloadMillis,
|
|
527
|
+
extractMillis: receipt.extractMillis,
|
|
528
|
+
admissionMillis: receipt.admissionMillis,
|
|
529
|
+
totalMillis: receipt.totalMillis,
|
|
530
|
+
baseBytes: receipt.storage.baseBytes,
|
|
531
|
+
overlayBytes: receipt.storage.overlayBytes,
|
|
532
|
+
stagingBytes: receipt.storage.stagingBytes,
|
|
533
|
+
pinnedBytes: receipt.storage.pinnedBytes,
|
|
534
|
+
});
|
|
535
|
+
if (!opts.quiet)
|
|
536
|
+
process.stderr.write(
|
|
537
|
+
`ttsc: package artifacts prepared ${receipt.selectedPacks.length} pack(s), ` +
|
|
538
|
+
`${receipt.downloadedPacks} downloaded, ${receipt.compressedBytes} compressed bytes.\n`,
|
|
539
|
+
);
|
|
540
|
+
return {
|
|
541
|
+
env: { ...opts.env, GOCACHEPROG: program },
|
|
542
|
+
reason: "selected",
|
|
543
|
+
artifactRoot: opts.artifactRoot,
|
|
544
|
+
usageReceiptPath: path.join(opts.artifactRoot, "receipts", receiptName),
|
|
545
|
+
receipt,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Report the completed Go client's bounded local action counters. A missing
|
|
551
|
+
* or malformed optional receipt never changes build success.
|
|
552
|
+
*
|
|
553
|
+
* @evidence contracts/common.md#principled-implementation The Go helper alone counts exact get outcomes and publishes its receipt after close; preparation counts are not substituted for package hits.
|
|
554
|
+
* @evidence contracts/common.md#clear-and-simple-design A single post-build read emits bounded hit/miss diagnostics and removes the one-use receipt.
|
|
555
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts A missing receipt is reported as unavailable, not as zero misses or complete publisher coverage.
|
|
556
|
+
* @evidence contracts/common.md#meaningful-documentation The comment separates optional observation failure from the completed build result.
|
|
557
|
+
* @evidence contracts/portability.md#os-neutral-implementation Native path and file APIs read the helper's one-use receipt under its selected artifact root.
|
|
558
|
+
* @evidence contracts/performance.md#efficient-algorithms A cold artifact-backed build reads at most one MiB of bounded action IDs once; exact executable hits never enter this path.
|
|
559
|
+
* @evidenceExclude contracts/performance.md#reuse-equivalent-work Each helper process writes a distinct receipt and no prior counters authorize a new build.
|
|
560
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources Finally removes the owned single receipt after reading or refusal, while the helper and native scope own body pins.
|
|
561
|
+
*/
|
|
562
|
+
export function reportUsage(
|
|
563
|
+
selection: Selection,
|
|
564
|
+
pluginName: string,
|
|
565
|
+
quiet: boolean,
|
|
566
|
+
): void {
|
|
567
|
+
if (selection.usageReceiptPath === undefined) return;
|
|
568
|
+
const location = selection.usageReceiptPath;
|
|
569
|
+
try {
|
|
570
|
+
const info = fs.statSync(location);
|
|
571
|
+
if (!info.isFile() || info.size > 1 << 20)
|
|
572
|
+
throw new Error("invalid receipt size");
|
|
573
|
+
const value: unknown = JSON.parse(fs.readFileSync(location, "utf8"));
|
|
574
|
+
if (value === null || typeof value !== "object")
|
|
575
|
+
throw new Error("invalid receipt value");
|
|
576
|
+
const counts = value as Record<string, unknown>;
|
|
577
|
+
for (const name of [
|
|
578
|
+
"gets",
|
|
579
|
+
"baseHits",
|
|
580
|
+
"overlayHits",
|
|
581
|
+
"misses",
|
|
582
|
+
"puts",
|
|
583
|
+
"putBytes",
|
|
584
|
+
])
|
|
585
|
+
if (!Number.isSafeInteger(counts[name]) || (counts[name] as number) < 0)
|
|
586
|
+
throw new Error("invalid receipt count");
|
|
587
|
+
if (
|
|
588
|
+
counts.gets !==
|
|
589
|
+
(counts.baseHits as number) +
|
|
590
|
+
(counts.overlayHits as number) +
|
|
591
|
+
(counts.misses as number)
|
|
592
|
+
)
|
|
593
|
+
throw new Error("inconsistent receipt count");
|
|
594
|
+
const actionFields = [
|
|
595
|
+
"baseActionIDs",
|
|
596
|
+
"overlayActionIDs",
|
|
597
|
+
"missActionIDs",
|
|
598
|
+
"putActionIDs",
|
|
599
|
+
] as const;
|
|
600
|
+
for (const name of actionFields)
|
|
601
|
+
if (
|
|
602
|
+
!Array.isArray(counts[name]) ||
|
|
603
|
+
(counts[name] as unknown[]).length > 4096 ||
|
|
604
|
+
!(counts[name] as unknown[]).every(
|
|
605
|
+
(id) => typeof id === "string" && /^[0-9a-f]{64}$/.test(id),
|
|
606
|
+
)
|
|
607
|
+
)
|
|
608
|
+
throw new Error("invalid action IDs in receipt");
|
|
609
|
+
if (
|
|
610
|
+
typeof counts.truncated !== "boolean" ||
|
|
611
|
+
actionFields.reduce(
|
|
612
|
+
(total, name) => total + (counts[name] as unknown[]).length,
|
|
613
|
+
0,
|
|
614
|
+
) > 4096
|
|
615
|
+
)
|
|
616
|
+
throw new Error("invalid receipt action limit");
|
|
617
|
+
const storage = counts.storage;
|
|
618
|
+
if (storage !== undefined && storage !== null) {
|
|
619
|
+
if (typeof storage !== "object")
|
|
620
|
+
throw new Error("invalid storage accounting");
|
|
621
|
+
for (const name of [
|
|
622
|
+
"baseBytes",
|
|
623
|
+
"overlayBytes",
|
|
624
|
+
"stagingBytes",
|
|
625
|
+
"pinnedBytes",
|
|
626
|
+
"baseFiles",
|
|
627
|
+
"overlayFiles",
|
|
628
|
+
"stagingFiles",
|
|
629
|
+
]) {
|
|
630
|
+
const value = (storage as Record<string, unknown>)[name];
|
|
631
|
+
if (!Number.isSafeInteger(value) || (value as number) < 0)
|
|
632
|
+
throw new Error("invalid storage accounting");
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
E2ETrace.capabilityResolution("plugin-package-artifact-usage", {
|
|
636
|
+
pluginName,
|
|
637
|
+
gets: counts.gets as number,
|
|
638
|
+
baseHits: counts.baseHits as number,
|
|
639
|
+
overlayHits: counts.overlayHits as number,
|
|
640
|
+
misses: counts.misses as number,
|
|
641
|
+
puts: counts.puts as number,
|
|
642
|
+
putBytes: counts.putBytes as number,
|
|
643
|
+
baseActionIDsJson: JSON.stringify(counts.baseActionIDs),
|
|
644
|
+
missActionIDsJson: JSON.stringify(counts.missActionIDs),
|
|
645
|
+
putActionIDsJson: JSON.stringify(counts.putActionIDs),
|
|
646
|
+
truncated: counts.truncated,
|
|
647
|
+
baseBytes:
|
|
648
|
+
storage !== null && typeof storage === "object"
|
|
649
|
+
? (storage as Record<string, number>).baseBytes
|
|
650
|
+
: undefined,
|
|
651
|
+
overlayBytes:
|
|
652
|
+
storage !== null && typeof storage === "object"
|
|
653
|
+
? (storage as Record<string, number>).overlayBytes
|
|
654
|
+
: undefined,
|
|
655
|
+
stagingBytes:
|
|
656
|
+
storage !== null && typeof storage === "object"
|
|
657
|
+
? (storage as Record<string, number>).stagingBytes
|
|
658
|
+
: undefined,
|
|
659
|
+
pinnedBytes:
|
|
660
|
+
storage !== null && typeof storage === "object"
|
|
661
|
+
? (storage as Record<string, number>).pinnedBytes
|
|
662
|
+
: undefined,
|
|
663
|
+
});
|
|
664
|
+
if (!quiet)
|
|
665
|
+
process.stderr.write(
|
|
666
|
+
`ttsc: package artifacts: ${counts.baseHits} publisher hits, ` +
|
|
667
|
+
`${counts.overlayHits} local hits, ${counts.misses} misses, ${counts.puts} puts.\n`,
|
|
668
|
+
);
|
|
669
|
+
} catch {
|
|
670
|
+
if (!quiet)
|
|
671
|
+
process.stderr.write(
|
|
672
|
+
"ttsc: package artifact usage receipt unavailable.\n",
|
|
673
|
+
);
|
|
674
|
+
} finally {
|
|
675
|
+
try {
|
|
676
|
+
fs.rmSync(location, { force: true });
|
|
677
|
+
} catch {}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function resolveHelper(env: NodeJS.ProcessEnv): string | null {
|
|
683
|
+
const explicit = SidecarEnvironment.read(env, "TTSC_PACKAGE_ARTIFACT_HELPER");
|
|
684
|
+
if (explicit !== undefined) {
|
|
685
|
+
return path.isAbsolute(explicit) && ordinaryFile(explicit)
|
|
686
|
+
? explicit
|
|
687
|
+
: null;
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
const location = require.resolve(
|
|
691
|
+
`@ttsc/${process.platform}-${process.arch}/package.json`,
|
|
692
|
+
);
|
|
693
|
+
const candidate = path.join(
|
|
694
|
+
path.dirname(location),
|
|
695
|
+
"bin",
|
|
696
|
+
process.platform === "win32" ? "ttsc-cache.exe" : "ttsc-cache",
|
|
697
|
+
);
|
|
698
|
+
return ordinaryFile(candidate) ? candidate : null;
|
|
699
|
+
} catch {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function resolveManifest(
|
|
705
|
+
env: NodeJS.ProcessEnv,
|
|
706
|
+
): { path: string; digest: string } | null {
|
|
707
|
+
const explicit = SidecarEnvironment.read(
|
|
708
|
+
env,
|
|
709
|
+
"TTSC_PACKAGE_ARTIFACT_MANIFEST",
|
|
710
|
+
);
|
|
711
|
+
const expected = SidecarEnvironment.read(env, "TTSC_PACKAGE_ARTIFACT_SHA256");
|
|
712
|
+
if (explicit !== undefined || expected !== undefined) {
|
|
713
|
+
return explicit &&
|
|
714
|
+
path.isAbsolute(explicit) &&
|
|
715
|
+
expected &&
|
|
716
|
+
/^[0-9a-f]{64}$/.test(expected)
|
|
717
|
+
? { path: explicit, digest: expected }
|
|
718
|
+
: null;
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
const location = require.resolve(
|
|
722
|
+
`@ttsc/${process.platform}-${process.arch}/package.json`,
|
|
723
|
+
);
|
|
724
|
+
const value: unknown = JSON.parse(fs.readFileSync(location, "utf8"));
|
|
725
|
+
if (
|
|
726
|
+
value === null ||
|
|
727
|
+
typeof value !== "object" ||
|
|
728
|
+
!("artifactManifestSHA256" in value)
|
|
729
|
+
)
|
|
730
|
+
return null;
|
|
731
|
+
const digest = (value as { artifactManifestSHA256: unknown })
|
|
732
|
+
.artifactManifestSHA256;
|
|
733
|
+
return typeof digest === "string" && /^[0-9a-f]{64}$/.test(digest)
|
|
734
|
+
? {
|
|
735
|
+
path: path.join(
|
|
736
|
+
path.dirname(location),
|
|
737
|
+
"bin",
|
|
738
|
+
"artifacts",
|
|
739
|
+
"manifest.json",
|
|
740
|
+
),
|
|
741
|
+
digest,
|
|
742
|
+
}
|
|
743
|
+
: null;
|
|
744
|
+
} catch {
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function ordinaryFile(location: string): boolean {
|
|
750
|
+
try {
|
|
751
|
+
const info = fs.lstatSync(location);
|
|
752
|
+
return info.isFile() && !info.isSymbolicLink();
|
|
753
|
+
} catch {
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function launch(
|
|
759
|
+
command: string,
|
|
760
|
+
args: string[],
|
|
761
|
+
env: NodeJS.ProcessEnv,
|
|
762
|
+
cwd?: string,
|
|
763
|
+
maxBuffer = 64 << 10,
|
|
764
|
+
): { status: number | null; stdout: string; stderr: string } {
|
|
765
|
+
const options = {
|
|
766
|
+
encoding: "utf8" as const,
|
|
767
|
+
env,
|
|
768
|
+
cwd,
|
|
769
|
+
maxBuffer,
|
|
770
|
+
windowsHide: true,
|
|
771
|
+
};
|
|
772
|
+
const result =
|
|
773
|
+
OwnedSynchronousProcess.launch<string>(command, args, options) ??
|
|
774
|
+
spawnSync(command, args, options);
|
|
775
|
+
return {
|
|
776
|
+
status: result.status,
|
|
777
|
+
stdout: result.stdout ?? "",
|
|
778
|
+
stderr: result.stderr ?? "",
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function quoteGoArgument(value: string): string {
|
|
783
|
+
if (!/[\s'"]/.test(value)) return value;
|
|
784
|
+
if (!value.includes("'")) return `'${value}'`;
|
|
785
|
+
if (!value.includes('"')) return `"${value}"`;
|
|
786
|
+
throw new Error("Go cache program argument contains both quote forms");
|
|
787
|
+
}
|