ttsc 0.34.3 → 0.35.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/lib/compiler/internal/build/BuildExecution.d.ts +26 -0
- package/lib/compiler/internal/build/BuildExecution.js +13 -3
- package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
- package/lib/compiler/internal/build/RunBuildOptions.d.ts +2 -0
- package/lib/compiler/internal/build/RuntimeBuildCache.d.ts +47 -0
- package/lib/compiler/internal/build/RuntimeBuildCache.js +235 -0
- package/lib/compiler/internal/build/RuntimeBuildCache.js.map +1 -0
- package/lib/compiler/internal/build/RuntimeBuildInputs.d.ts +44 -0
- package/lib/compiler/internal/build/RuntimeBuildInputs.js +282 -0
- package/lib/compiler/internal/build/RuntimeBuildInputs.js.map +1 -0
- package/lib/compiler/internal/build/RuntimeInputProof.d.ts +50 -0
- package/lib/compiler/internal/build/RuntimeInputProof.js +315 -0
- package/lib/compiler/internal/build/RuntimeInputProof.js.map +1 -0
- package/lib/compiler/internal/build/TsgoArguments.d.ts +5 -4
- package/lib/compiler/internal/build/TsgoArguments.js +11 -5
- package/lib/compiler/internal/build/TsgoArguments.js.map +1 -1
- package/lib/compiler/internal/build/runBuild.d.ts +3 -3
- package/lib/compiler/internal/build/runBuild.js +17 -4
- package/lib/compiler/internal/build/runBuild.js.map +1 -1
- package/lib/compiler/internal/runNativeCheckWithObservations.d.ts +18 -14
- package/lib/compiler/internal/runNativeCheckWithObservations.js +42 -18
- package/lib/compiler/internal/runNativeCheckWithObservations.js.map +1 -1
- package/lib/flags/FLAG_SCHEMA.js +7 -0
- package/lib/flags/FLAG_SCHEMA.js.map +1 -1
- package/lib/launcher/internal/parseTtsxCLI.d.ts +1 -0
- package/lib/launcher/internal/parseTtsxCLI.js +1 -0
- package/lib/launcher/internal/parseTtsxCLI.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.d.ts +2 -0
- package/lib/launcher/internal/prepareExecution.js +6 -0
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +5 -0
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtime/installRuntimeHooks.js +7 -0
- package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
- package/lib/plugin/internal/load/loadProjectPlugins.js +1 -0
- 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 +3 -0
- package/lib/plugin/internal/source/SourceBuildCacheLayout.js.map +1 -1
- package/lib/structures/ITtscPluginCapabilities.d.ts +13 -2
- package/package.json +8 -8
- package/src/compiler/internal/build/BuildExecution.ts +32 -16
- package/src/compiler/internal/build/RunBuildOptions.ts +3 -0
- package/src/compiler/internal/build/RuntimeBuildCache.ts +252 -0
- package/src/compiler/internal/build/RuntimeBuildInputs.ts +349 -0
- package/src/compiler/internal/build/RuntimeInputProof.ts +346 -0
- package/src/compiler/internal/build/TsgoArguments.ts +11 -5
- package/src/compiler/internal/build/runBuild.ts +20 -5
- package/src/compiler/internal/runNativeCheckWithObservations.ts +41 -18
- package/src/flags/FLAG_SCHEMA.ts +7 -0
- package/src/launcher/internal/parseTtsxCLI.ts +1 -0
- package/src/launcher/internal/prepareExecution.ts +13 -0
- package/src/launcher/internal/runTtsx.ts +5 -0
- package/src/launcher/internal/runtime/installRuntimeHooks.ts +11 -0
- package/src/plugin/internal/load/loadProjectPlugins.ts +1 -0
- package/src/plugin/internal/source/SourceBuildCacheLayout.ts +4 -0
- package/src/structures/ITtscPluginCapabilities.ts +14 -2
- package/utility/host.go +22 -1
|
@@ -79,12 +79,23 @@ export interface ITtscPluginCapabilities {
|
|
|
79
79
|
* from an output filename or configuration prediction.
|
|
80
80
|
*
|
|
81
81
|
* When omitted, ttsc does not pass the option to a strict third-party host. A
|
|
82
|
-
* runtime operation
|
|
83
|
-
*
|
|
82
|
+
* runtime operation then uses the existing externally observed provenance
|
|
83
|
+
* adapter, whose incomplete proof remains visible to the runtime consumer.
|
|
84
84
|
*
|
|
85
85
|
* @default false
|
|
86
86
|
*/
|
|
87
87
|
emitProvenance?: true;
|
|
88
|
+
/**
|
|
89
|
+
* Whether build accepts `--emit-observations-json=<absolute private path>`
|
|
90
|
+
* and publishes its generation's compiler and native input observations.
|
|
91
|
+
* Missing support keeps runtime emits fresh rather than assuming reusable
|
|
92
|
+
* input completeness. This protocol is independent of output ownership. Hosts
|
|
93
|
+
* publish the same observation envelope as checkObservations, including
|
|
94
|
+
* failures, before releasing their Program.
|
|
95
|
+
*
|
|
96
|
+
* @default false
|
|
97
|
+
*/
|
|
98
|
+
emitObservations?: true;
|
|
88
99
|
/**
|
|
89
100
|
* Whether the sidecar accepts ttsc's `--project-context-json` identity
|
|
90
101
|
* protocol. The payload keeps lexical selection paths, physical Program
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ttsc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -64,13 +64,13 @@
|
|
|
64
64
|
},
|
|
65
65
|
"optionalDependencies": {
|
|
66
66
|
"fsevents": "^2.3.3",
|
|
67
|
-
"@ttsc/linux-x64": "0.
|
|
68
|
-
"@ttsc/linux-
|
|
69
|
-
"@ttsc/darwin-x64": "0.
|
|
70
|
-
"@ttsc/
|
|
71
|
-
"@ttsc/
|
|
72
|
-
"@ttsc/win32-
|
|
73
|
-
"@ttsc/
|
|
67
|
+
"@ttsc/linux-x64": "0.35.0",
|
|
68
|
+
"@ttsc/linux-arm64": "0.35.0",
|
|
69
|
+
"@ttsc/darwin-x64": "0.35.0",
|
|
70
|
+
"@ttsc/linux-arm": "0.35.0",
|
|
71
|
+
"@ttsc/darwin-arm64": "0.35.0",
|
|
72
|
+
"@ttsc/win32-x64": "0.35.0",
|
|
73
|
+
"@ttsc/win32-arm64": "0.35.0"
|
|
74
74
|
},
|
|
75
75
|
"sideEffects": [
|
|
76
76
|
"./lib/register.js",
|
|
@@ -728,20 +728,33 @@ export namespace BuildExecution {
|
|
|
728
728
|
): TtscBuildResult {
|
|
729
729
|
const host = selectSharedHostPlugin(plugins);
|
|
730
730
|
const run = (provenancePath?: string): TtscBuildResult =>
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
)
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
731
|
+
runNativeCheckWithObservations(
|
|
732
|
+
{
|
|
733
|
+
capabilities:
|
|
734
|
+
options.runtimeCacheDir === undefined
|
|
735
|
+
? undefined
|
|
736
|
+
: host.capabilities,
|
|
737
|
+
},
|
|
738
|
+
(observationArgs) =>
|
|
739
|
+
runNativePluginCommand(
|
|
740
|
+
host,
|
|
741
|
+
[
|
|
742
|
+
...NativePluginArguments.createNativeBuildArgs(
|
|
743
|
+
execution,
|
|
744
|
+
options,
|
|
745
|
+
plugins,
|
|
746
|
+
provenancePath,
|
|
747
|
+
),
|
|
748
|
+
...observationArgs,
|
|
749
|
+
],
|
|
750
|
+
options,
|
|
751
|
+
execution,
|
|
752
|
+
"ttsc.build",
|
|
753
|
+
timing,
|
|
754
|
+
NativePluginArguments.transformHostTimingLabel(plugins),
|
|
755
|
+
TsgoArguments.createNativeBuildTsgoArgs(execution, options),
|
|
756
|
+
),
|
|
757
|
+
"build",
|
|
745
758
|
);
|
|
746
759
|
if (options.forceEmitProvenance !== true) return run();
|
|
747
760
|
if (host.capabilities?.emitProvenance === true)
|
|
@@ -1365,6 +1378,7 @@ export namespace BuildExecution {
|
|
|
1365
1378
|
});
|
|
1366
1379
|
let pluginSetupFailure: TtscBuildResult | undefined;
|
|
1367
1380
|
let nativePlugins: ITtscLoadedNativePlugin[] = [];
|
|
1381
|
+
let pluginLoad: ReturnType<typeof loadProjectPlugins> | undefined;
|
|
1368
1382
|
// Opened only for an invocation that loads plugins, so a project without
|
|
1369
1383
|
// any never gains a cache directory; the load and every later native spawn
|
|
1370
1384
|
// of this invocation prove their unchanged inputs through it (#1722, #1723).
|
|
@@ -1387,7 +1401,7 @@ export namespace BuildExecution {
|
|
|
1387
1401
|
cacheDir,
|
|
1388
1402
|
env,
|
|
1389
1403
|
});
|
|
1390
|
-
|
|
1404
|
+
pluginLoad = yield* loadProjectPlugins.steps({
|
|
1391
1405
|
binary: resolveBinary({ env }) ?? "",
|
|
1392
1406
|
cacheDir,
|
|
1393
1407
|
cwd,
|
|
@@ -1398,7 +1412,8 @@ export namespace BuildExecution {
|
|
|
1398
1412
|
pluginConfigDir: options.pluginConfigDir,
|
|
1399
1413
|
projectRoot,
|
|
1400
1414
|
tsconfig,
|
|
1401
|
-
})
|
|
1415
|
+
});
|
|
1416
|
+
nativePlugins = pluginLoad.nativePlugins;
|
|
1402
1417
|
} else {
|
|
1403
1418
|
yield options.onWatchInputs?.([]);
|
|
1404
1419
|
}
|
|
@@ -1415,6 +1430,7 @@ export namespace BuildExecution {
|
|
|
1415
1430
|
cwd,
|
|
1416
1431
|
...(identities === undefined ? {} : { identities }),
|
|
1417
1432
|
nativePlugins,
|
|
1433
|
+
...(pluginLoad === undefined ? {} : { pluginLoad }),
|
|
1418
1434
|
pluginConfigDir: resolvePluginConfigDir({
|
|
1419
1435
|
cwd,
|
|
1420
1436
|
pluginConfigDir: options.pluginConfigDir,
|
|
@@ -20,6 +20,9 @@ import type {
|
|
|
20
20
|
* @evidenceExclude contracts/performance.md#reuse-equivalent-work A type declaration computes nothing, so there is no work to share.
|
|
21
21
|
*/
|
|
22
22
|
export type RunBuildOptions = TtscBuildOptions & {
|
|
23
|
+
/** Persistent private-runtime emit documents; absence keeps ordinary builds. */
|
|
24
|
+
runtimeCacheDir?: string;
|
|
25
|
+
|
|
23
26
|
/**
|
|
24
27
|
* Skip the independent direct-compiler type-check gate and the added
|
|
25
28
|
* `--noEmitOnError` guard. Configured plugin checks and native hosts still
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { E2ETrace } from "../../../internal/E2ETrace";
|
|
6
|
+
import { recordCacheFileUse } from "../../../plugin/internal/source/recordCacheFileUse";
|
|
7
|
+
import type { TtscBuildResult } from "../../../structures/internal/TtscBuildResult";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Persist completed private runtime emits as checksummed, atomically published
|
|
11
|
+
* documents. Readers copy admitted bytes into their own run directory, so cache
|
|
12
|
+
* collection and concurrent publications cannot remove an executing run's emit.
|
|
13
|
+
* Input discovery and producer-proof admission belong to the supplied owners.
|
|
14
|
+
*
|
|
15
|
+
* @evidence contracts/common.md#principled-implementation Content-addressed snapshots select complete checksummed result and output documents. SHA-256 consumes exact UTF-8 text; native normalization rejects absolute, dot, parent and Windows drive-relative output coordinates. Relocation changes producer output coordinates and private-root stream text while retaining source paths.
|
|
16
|
+
* @evidence contracts/common.md#clear-and-simple-design One operation coordinates lookup, proof, restore and publication, with private hashing, containment and relocation helpers. Input capture and producer validation remain separate callbacks.
|
|
17
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Misses execute the original build exactly once; failed or absent proofs never manufacture successful output. Every transported output must be a normalized private descendant, without filename or consumer-name exceptions.
|
|
18
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose distinguishes persistent storage, run-owned copies and proof ownership; run states callback stability and failure behavior, and private helper comments identify byte, path and relocation domains.
|
|
19
|
+
* @evidence contracts/portability.md#os-neutral-implementation Native paths, lstat, relative/join and rename own storage and containment, including Windows drive-relative rejection. Source coordinates remain unchanged; transported symlinks are refused rather than followed.
|
|
20
|
+
* @evidence contracts/performance.md#efficient-algorithms Lookup hashes snapshot text and reads/hashes the bounded document. Restore and publication scale with output bytes and file count, without unrelated source traversal; normalization scans path bytes and relocation scans output paths and stream text.
|
|
21
|
+
* @evidence contracts/performance.md#reuse-equivalent-work Equal snapshots reuse completed emits only after independent producer admission and unchanged recapture. A restored run owns its copied bytes and rebased output ledger; concurrent publishers may duplicate compilation but cannot expose partial documents.
|
|
22
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources Documents are capped at 128 MiB, with fresh output collection limited to a 0.7 raw-byte budget before encoding, and join the existing collected single-file cache population. Snapshots and decoded bytes are invocation-owned; staging removal is attempted in finally and run owners release output. Cleanup or collector failures can retain files; helpers retain no handles or history.
|
|
23
|
+
*/
|
|
24
|
+
export namespace RuntimeBuildCache {
|
|
25
|
+
const FORMAT = "ttsx-runtime-build-v1";
|
|
26
|
+
const MAX_BYTES = 128 * 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reuse a proven private emit, or invoke the original producer once. Capture
|
|
30
|
+
* returns a deterministic complete input snapshot, or undefined to decline
|
|
31
|
+
* caching. Admission must validate the producer's extra input observations;
|
|
32
|
+
* the second argument is the output root that producer actually used. Cache
|
|
33
|
+
* IO or cache-admission failure costs reuse only. Errors thrown by the
|
|
34
|
+
* original build retain their normal failure policy.
|
|
35
|
+
*
|
|
36
|
+
* @evidence contracts/common.md#principled-implementation Lookup and publication bracket current input snapshots, preserve the actual producer result and require explicit extra-input admission; cache-hit output is copied before the result is rebased.
|
|
37
|
+
* @evidence contracts/common.md#clear-and-simple-design Snapshot, proof and build callbacks expose their independent responsibilities while the operation alone owns storage and restore cleanup.
|
|
38
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Only successful complete emits are published; malformed, oversized, damaged or unproved documents fall through to the caller's real producer without changing compiler selection.
|
|
39
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose states complete-snapshot meaning, producer-root coordinates, callback error policy and recoverable cache IO.
|
|
40
|
+
* @evidence contracts/portability.md#os-neutral-implementation Native lstat refuses aliases, path.relative checks containment and native mkdir/write/rename preserve byte payloads and host path semantics.
|
|
41
|
+
* @evidence contracts/performance.md#efficient-algorithms Snapshot owners dominate input work; document read/parse/hash and base64 restore scale with bounded encoded bytes, while publication walks only private output and serializes it once.
|
|
42
|
+
* @evidence contracts/performance.md#reuse-equivalent-work A format-and-snapshot digest names one candidate; checksum, output grammar, producer admission and unchanged recapture are independent reuse gates. Every miss calls build once.
|
|
43
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources Call-local decoded outputs and serialized documents are bounded by the 128 MiB admission ceiling, except fresh output collection precedes serialization; partial restores are unlinked and staging removal is attempted in finally. Collector failures can retain files under the existing cache policy.
|
|
44
|
+
*/
|
|
45
|
+
export function run(props: {
|
|
46
|
+
/** Collected single-file cache root, or undefined to bypass persistence. */
|
|
47
|
+
directory?: string;
|
|
48
|
+
/** Empty private output directory owned by this build's caller. */
|
|
49
|
+
emitDir: string;
|
|
50
|
+
/** Fresh complete input snapshot; undefined preserves ordinary execution. */
|
|
51
|
+
capture: () => string | undefined;
|
|
52
|
+
/** Producer-specific proof, with its original private output root. */
|
|
53
|
+
admit: (result: TtscBuildResult, emitDir: string) => boolean;
|
|
54
|
+
/** Original build operation, including its diagnostic policy. */
|
|
55
|
+
build: () => TtscBuildResult;
|
|
56
|
+
}): TtscBuildResult {
|
|
57
|
+
if (props.directory === undefined) return props.build();
|
|
58
|
+
let snapshot: string | undefined;
|
|
59
|
+
try {
|
|
60
|
+
snapshot = props.capture();
|
|
61
|
+
} catch {
|
|
62
|
+
/* An unavailable proof is a miss. */
|
|
63
|
+
}
|
|
64
|
+
if (snapshot === undefined) return props.build();
|
|
65
|
+
const key = digest(FORMAT + "\0" + snapshot);
|
|
66
|
+
const file = path.join(props.directory, key + ".json");
|
|
67
|
+
const restored: string[] = [];
|
|
68
|
+
try {
|
|
69
|
+
const stat = fs.lstatSync(file);
|
|
70
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_BYTES)
|
|
71
|
+
throw new Error("unusable runtime cache document");
|
|
72
|
+
const document = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
73
|
+
if (
|
|
74
|
+
document.format !== FORMAT ||
|
|
75
|
+
typeof document.payload !== "string" ||
|
|
76
|
+
document.hash !== digest(document.payload)
|
|
77
|
+
)
|
|
78
|
+
throw new Error("invalid runtime cache checksum");
|
|
79
|
+
const entry = JSON.parse(document.payload);
|
|
80
|
+
if (
|
|
81
|
+
entry.snapshot !== snapshot ||
|
|
82
|
+
typeof entry.emitDir !== "string" ||
|
|
83
|
+
!path.isAbsolute(entry.emitDir) ||
|
|
84
|
+
entry.result?.status !== 0 ||
|
|
85
|
+
!Array.isArray(entry.outputs) ||
|
|
86
|
+
!props.admit(entry.result, entry.emitDir)
|
|
87
|
+
)
|
|
88
|
+
throw new Error("unproved runtime cache document");
|
|
89
|
+
const names = new Set<string>();
|
|
90
|
+
for (const output of entry.outputs) {
|
|
91
|
+
if (
|
|
92
|
+
!Array.isArray(output) ||
|
|
93
|
+
output.length !== 2 ||
|
|
94
|
+
typeof output[0] !== "string" ||
|
|
95
|
+
typeof output[1] !== "string" ||
|
|
96
|
+
!safeRelative(output[0]) ||
|
|
97
|
+
names.has(output[0]) ||
|
|
98
|
+
Buffer.from(output[1], "base64").toString("base64") !== output[1]
|
|
99
|
+
)
|
|
100
|
+
throw new Error("invalid runtime cache output");
|
|
101
|
+
names.add(output[0]);
|
|
102
|
+
}
|
|
103
|
+
for (const output of Object.keys(entry.result.emittedSources ?? {}))
|
|
104
|
+
if (!names.has(path.relative(entry.emitDir, output)))
|
|
105
|
+
throw new Error("missing runtime cache provenance output");
|
|
106
|
+
for (const [name, bytes] of entry.outputs) {
|
|
107
|
+
const target = path.join(props.emitDir, name);
|
|
108
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
109
|
+
fs.writeFileSync(target, Buffer.from(bytes, "base64"), { flag: "wx" });
|
|
110
|
+
restored.push(target);
|
|
111
|
+
}
|
|
112
|
+
if (
|
|
113
|
+
props.capture() !== snapshot ||
|
|
114
|
+
!props.admit(entry.result, entry.emitDir)
|
|
115
|
+
)
|
|
116
|
+
throw new Error("runtime inputs moved during restore");
|
|
117
|
+
recordCacheFileUse(file);
|
|
118
|
+
E2ETrace.capabilityResolution("runtime-build-cache", {
|
|
119
|
+
outcome: "reused",
|
|
120
|
+
key,
|
|
121
|
+
});
|
|
122
|
+
return relocate(entry.result, entry.emitDir, props.emitDir);
|
|
123
|
+
} catch {
|
|
124
|
+
for (const output of restored)
|
|
125
|
+
try {
|
|
126
|
+
fs.unlinkSync(output);
|
|
127
|
+
} catch {
|
|
128
|
+
/* Rejected cache output cannot prevent the ordinary producer. */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const result = props.build();
|
|
132
|
+
if (result.status !== 0) return result;
|
|
133
|
+
let temporary: string | undefined;
|
|
134
|
+
try {
|
|
135
|
+
const outputs: [string, string][] = [];
|
|
136
|
+
let outputBytes = 0;
|
|
137
|
+
const pending = [props.emitDir];
|
|
138
|
+
while (pending.length !== 0) {
|
|
139
|
+
const directory = pending.pop()!;
|
|
140
|
+
for (const child of fs.readdirSync(directory, {
|
|
141
|
+
withFileTypes: true,
|
|
142
|
+
})) {
|
|
143
|
+
const target = path.join(directory, child.name);
|
|
144
|
+
if (child.isSymbolicLink()) throw new Error("aliased runtime output");
|
|
145
|
+
if (child.isDirectory()) pending.push(target);
|
|
146
|
+
else if (child.isFile()) {
|
|
147
|
+
outputBytes += fs.statSync(target).size;
|
|
148
|
+
if (outputBytes > MAX_BYTES * 0.7) return result;
|
|
149
|
+
outputs.push([
|
|
150
|
+
path.relative(props.emitDir, target),
|
|
151
|
+
fs.readFileSync(target).toString("base64"),
|
|
152
|
+
]);
|
|
153
|
+
} else throw new Error("nonordinary runtime output");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (outputs.length === 0) return result;
|
|
157
|
+
const payload = JSON.stringify({
|
|
158
|
+
snapshot,
|
|
159
|
+
emitDir: props.emitDir,
|
|
160
|
+
result,
|
|
161
|
+
outputs,
|
|
162
|
+
});
|
|
163
|
+
const text = JSON.stringify({
|
|
164
|
+
format: FORMAT,
|
|
165
|
+
hash: digest(payload),
|
|
166
|
+
payload,
|
|
167
|
+
});
|
|
168
|
+
if (Buffer.byteLength(text) > MAX_BYTES) return result;
|
|
169
|
+
if (props.capture() !== snapshot || !props.admit(result, props.emitDir))
|
|
170
|
+
return result;
|
|
171
|
+
fs.mkdirSync(props.directory, { recursive: true });
|
|
172
|
+
if (fs.lstatSync(props.directory).isSymbolicLink()) return result;
|
|
173
|
+
temporary = path.join(
|
|
174
|
+
props.directory,
|
|
175
|
+
`${key}.${crypto.randomUUID()}.tmp`,
|
|
176
|
+
);
|
|
177
|
+
fs.writeFileSync(temporary, text, { flag: "wx" });
|
|
178
|
+
fs.renameSync(temporary, file);
|
|
179
|
+
E2ETrace.capabilityResolution("runtime-build-cache", {
|
|
180
|
+
outcome: "recorded",
|
|
181
|
+
key,
|
|
182
|
+
});
|
|
183
|
+
} catch {
|
|
184
|
+
/* Persistence never replaces a fresh successful result. */
|
|
185
|
+
} finally {
|
|
186
|
+
if (temporary !== undefined) {
|
|
187
|
+
try {
|
|
188
|
+
fs.unlinkSync(temporary);
|
|
189
|
+
} catch {
|
|
190
|
+
/* Rename or later collection owns it. */
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Hash encoded input or document bytes with SHA-256. */
|
|
198
|
+
function digest(text: string): string {
|
|
199
|
+
return crypto.createHash("sha256").update(text).digest("hex");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Admit only a nonempty relative descendant coordinate. */
|
|
203
|
+
function safeRelative(name: string): boolean {
|
|
204
|
+
return (
|
|
205
|
+
name.length !== 0 &&
|
|
206
|
+
!name.includes("\0") &&
|
|
207
|
+
!path.isAbsolute(name) &&
|
|
208
|
+
path.parse(name).root === "" &&
|
|
209
|
+
path.normalize(name) === name &&
|
|
210
|
+
name !== "." &&
|
|
211
|
+
name !== ".." &&
|
|
212
|
+
!name.startsWith(".." + path.sep)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Move producer-owned output coordinates to the current private run. */
|
|
217
|
+
function relocate(
|
|
218
|
+
result: TtscBuildResult,
|
|
219
|
+
previous: string,
|
|
220
|
+
current: string,
|
|
221
|
+
): TtscBuildResult {
|
|
222
|
+
const move = (output: string): string => {
|
|
223
|
+
const relative = path.relative(previous, output);
|
|
224
|
+
if (!safeRelative(relative))
|
|
225
|
+
throw new Error("runtime provenance escapes its output root");
|
|
226
|
+
return path.join(current, relative);
|
|
227
|
+
};
|
|
228
|
+
return {
|
|
229
|
+
...result,
|
|
230
|
+
emittedFiles: result.emittedFiles?.map(move),
|
|
231
|
+
emittedSources:
|
|
232
|
+
result.emittedSources === undefined
|
|
233
|
+
? undefined
|
|
234
|
+
: Object.fromEntries(
|
|
235
|
+
Object.entries(result.emittedSources).map(([output, sources]) => [
|
|
236
|
+
move(output),
|
|
237
|
+
sources,
|
|
238
|
+
]),
|
|
239
|
+
),
|
|
240
|
+
emittedSourceProofFailures:
|
|
241
|
+
result.emittedSourceProofFailures === undefined
|
|
242
|
+
? undefined
|
|
243
|
+
: Object.fromEntries(
|
|
244
|
+
Object.entries(result.emittedSourceProofFailures).map(
|
|
245
|
+
([output, reason]) => [move(output), reason],
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
stdout: result.stdout.split(previous).join(current),
|
|
249
|
+
stderr: result.stderr.split(previous).join(current),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|