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,135 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/sha256"
|
|
5
|
+
"encoding/hex"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"net/http"
|
|
8
|
+
"net/http/httptest"
|
|
9
|
+
"os"
|
|
10
|
+
"os/exec"
|
|
11
|
+
"path/filepath"
|
|
12
|
+
"regexp"
|
|
13
|
+
"runtime"
|
|
14
|
+
"strings"
|
|
15
|
+
"testing"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// TestPublisherSeal verifies an actual recorded Go action through released-pack admission.
|
|
19
|
+
//
|
|
20
|
+
// Verifies: a publisher's full action and body can be sealed, transferred in bulk, and used by a relocated stock-Go consumer without compiling the package.
|
|
21
|
+
// 1. Compile one package with the production helper recording into an empty overlay and retain Go's action graph and package receipts.
|
|
22
|
+
// 2. Seal a release-pinned ZIP, serve it from loopback, and admit it into an independent consumer base.
|
|
23
|
+
// 3. Build the same package from a different source root with a fresh Go cache, then compare archive bytes and compiler commands.
|
|
24
|
+
//
|
|
25
|
+
// @evidence contracts/testing.md#behavioral-verification The test executes the shipped helper's recording, sealing, preparation and protocol modes with stock Go, asserting a complete manifest digest, a real base hit and byte-identical archives.
|
|
26
|
+
// @evidence contracts/testing.md#independent-expectations The literal package source and Go's own cold first-build compiler command establish a compile that the independent second cache must avoid; SHA-256 of each emitted archive establishes output equality.
|
|
27
|
+
// @evidence contracts/testing.md#distinguishing-cases Distinct source, overlay, base and GOCACHE roots distinguish publisher reuse from an ordinary warm build; direct helper tests own malformed transport and protocol cases.
|
|
28
|
+
// @evidence contracts/testing.md#execution-ownership This discoverable Go integration test builds a local helper and two private modules, serves one loopback pack, and removes all files through t.TempDir; no consumer install or external network is involved.
|
|
29
|
+
func TestPublisherSeal(t *testing.T) {
|
|
30
|
+
if !supportedGoVersion.MatchString(runtime.Version()) {
|
|
31
|
+
t.Skip("publisher protocol requires a stock Go 1.26 patch release")
|
|
32
|
+
}
|
|
33
|
+
goBinary, err := exec.LookPath("go")
|
|
34
|
+
if err != nil {
|
|
35
|
+
t.Fatal(err)
|
|
36
|
+
}
|
|
37
|
+
goBinary, err = filepath.Abs(goBinary)
|
|
38
|
+
if err != nil {
|
|
39
|
+
t.Fatal(err)
|
|
40
|
+
}
|
|
41
|
+
root := filepath.Join(t.TempDir(), "publisher path with spaces")
|
|
42
|
+
if err := os.MkdirAll(root, 0700); err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
helper := filepath.Join(root, "cache helper"+exeSuffix())
|
|
46
|
+
buildHelper := exec.Command(goBinary, "build", "-trimpath", "-o", helper, ".")
|
|
47
|
+
if output, err := buildHelper.CombinedOutput(); err != nil {
|
|
48
|
+
t.Fatalf("helper build: %v\n%s", err, output)
|
|
49
|
+
}
|
|
50
|
+
source := "package tiny\nfunc Value() string { return \"sealed action\" }\n"
|
|
51
|
+
makeModule := func(name string) string {
|
|
52
|
+
directory := filepath.Join(root, name)
|
|
53
|
+
if err := os.MkdirAll(directory, 0700); err != nil {
|
|
54
|
+
t.Fatal(err)
|
|
55
|
+
}
|
|
56
|
+
if err := os.WriteFile(filepath.Join(directory, "go.mod"), []byte("module example.org/ttsc-cache-seal\n\ngo 1.26\n"), 0600); err != nil {
|
|
57
|
+
t.Fatal(err)
|
|
58
|
+
}
|
|
59
|
+
if err := os.WriteFile(filepath.Join(directory, "tiny.go"), []byte(source), 0600); err != nil {
|
|
60
|
+
t.Fatal(err)
|
|
61
|
+
}
|
|
62
|
+
return directory
|
|
63
|
+
}
|
|
64
|
+
publisher := makeModule("publisher")
|
|
65
|
+
consumer := makeModule("consumer")
|
|
66
|
+
graph := filepath.Join(root, "actiongraph.json")
|
|
67
|
+
packages := filepath.Join(root, "packages.json")
|
|
68
|
+
list := exec.Command(goBinary, "list", "-deps", "-json", ".")
|
|
69
|
+
list.Dir = publisher
|
|
70
|
+
list.Env = append(os.Environ(), "CGO_ENABLED=0", "GOFLAGS=", "GOWORK=off", "GOPROXY=off", "GOSUMDB=off", "GOTOOLCHAIN=local", "GOCACHEPROG=")
|
|
71
|
+
listOutput, err := list.CombinedOutput()
|
|
72
|
+
if err != nil {
|
|
73
|
+
t.Fatalf("publisher package receipts: %v\n%s", err, listOutput)
|
|
74
|
+
}
|
|
75
|
+
if err := os.WriteFile(packages, listOutput, 0600); err != nil {
|
|
76
|
+
t.Fatal(err)
|
|
77
|
+
}
|
|
78
|
+
serverRoot := filepath.Join(root, "sealed")
|
|
79
|
+
server := httptest.NewServer(http.FileServer(http.Dir(serverRoot)))
|
|
80
|
+
defer server.Close()
|
|
81
|
+
compile := regexp.MustCompile(`(?m)[/\\]compile(?:\.exe)?["']?\s`)
|
|
82
|
+
build := func(directory, cache, program, archive, graphPath string) (string, int) {
|
|
83
|
+
if err := os.MkdirAll(cache, 0700); err != nil {
|
|
84
|
+
t.Fatal(err)
|
|
85
|
+
}
|
|
86
|
+
args := []string{"build", "-trimpath", "-x", "-o", archive}
|
|
87
|
+
if graphPath != "" {
|
|
88
|
+
args = append(args, "-debug-actiongraph="+graphPath)
|
|
89
|
+
}
|
|
90
|
+
args = append(args, ".")
|
|
91
|
+
command := exec.Command(goBinary, args...)
|
|
92
|
+
command.Dir = directory
|
|
93
|
+
command.Env = append(os.Environ(), "CGO_ENABLED=0", "GOFLAGS=", "GOWORK=off", "GOPROXY=off", "GOSUMDB=off", "GOTOOLCHAIN=local", "GOCACHE="+cache, "GOCACHEPROG="+program)
|
|
94
|
+
output, err := command.CombinedOutput()
|
|
95
|
+
if err != nil {
|
|
96
|
+
t.Fatalf("stock Go build: %v\n%s", err, output)
|
|
97
|
+
}
|
|
98
|
+
body, err := os.ReadFile(archive)
|
|
99
|
+
if err != nil {
|
|
100
|
+
t.Fatal(err)
|
|
101
|
+
}
|
|
102
|
+
sum := sha256.Sum256(body)
|
|
103
|
+
return hex.EncodeToString(sum[:]), len(compile.FindAll(output, -1))
|
|
104
|
+
}
|
|
105
|
+
overlay := filepath.Join(root, "publisher overlay")
|
|
106
|
+
firstProgram := quoteGoCacheArgument(helper) + " -root " + quoteGoCacheArgument(overlay)
|
|
107
|
+
firstHash, firstCompiles := build(publisher, filepath.Join(root, "publisher go cache"), firstProgram, filepath.Join(root, "first.a"), graph)
|
|
108
|
+
if firstCompiles == 0 {
|
|
109
|
+
t.Fatal("publisher did not compile the package")
|
|
110
|
+
}
|
|
111
|
+
seal := exec.Command(helper, "-seal", "-root", overlay, "-go", goBinary, "-actiongraph", graph, "-packages", packages, "-release", "0.32.1", "-source-commit", strings.Repeat("a", 40), "-pack-url-base", server.URL, "-pack-name", "tiny", "-out", serverRoot)
|
|
112
|
+
seal.Env = append(os.Environ(), "CGO_ENABLED=0", "GOFLAGS=", "GOWORK=off", "GOPROXY=off", "GOSUMDB=off", "GOTOOLCHAIN=local")
|
|
113
|
+
output, err := seal.CombinedOutput()
|
|
114
|
+
if err != nil {
|
|
115
|
+
t.Fatalf("seal recorded actions: %v\n%s", err, output)
|
|
116
|
+
}
|
|
117
|
+
var sealed struct {
|
|
118
|
+
ManifestSHA256 string `json:"manifestSHA256"`
|
|
119
|
+
Actions int `json:"actions"`
|
|
120
|
+
}
|
|
121
|
+
if err := json.Unmarshal(output, &sealed); err != nil || sealed.Actions == 0 || len(sealed.ManifestSHA256) != 64 {
|
|
122
|
+
t.Fatalf("invalid publisher seal receipt: %s %v", output, err)
|
|
123
|
+
}
|
|
124
|
+
manifest := filepath.Join(serverRoot, "manifest.json")
|
|
125
|
+
base := filepath.Join(root, "independent consumer base")
|
|
126
|
+
prepare := exec.Command(helper, "-root", base, "-manifest", manifest, "-manifest-sha256", sealed.ManifestSHA256, "-prepare", "-packs", "tiny")
|
|
127
|
+
if output, err := prepare.CombinedOutput(); err != nil {
|
|
128
|
+
t.Fatalf("prepare sealed pack: %v\n%s", err, output)
|
|
129
|
+
}
|
|
130
|
+
secondProgram := quoteGoCacheArgument(helper) + " -root " + quoteGoCacheArgument(base) + " -manifest " + quoteGoCacheArgument(manifest) + " -manifest-sha256 " + sealed.ManifestSHA256
|
|
131
|
+
secondHash, secondCompiles := build(consumer, filepath.Join(root, "consumer go cache"), secondProgram, filepath.Join(root, "second.a"), "")
|
|
132
|
+
if secondCompiles != 0 || firstHash != secondHash {
|
|
133
|
+
t.Fatalf("sealed publisher package was not reused: compiles=%d/%d hashes=%s/%s", firstCompiles, secondCompiles, firstHash, secondHash)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"archive/zip"
|
|
5
|
+
"compress/flate"
|
|
6
|
+
"crypto/sha256"
|
|
7
|
+
"encoding/base64"
|
|
8
|
+
"encoding/hex"
|
|
9
|
+
"encoding/json"
|
|
10
|
+
"errors"
|
|
11
|
+
"fmt"
|
|
12
|
+
"io"
|
|
13
|
+
"os"
|
|
14
|
+
"path/filepath"
|
|
15
|
+
"sort"
|
|
16
|
+
"strings"
|
|
17
|
+
|
|
18
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/packageartifacts"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
type sealOptions struct {
|
|
22
|
+
root, goBinary, actionGraph, packages, release, sourceCommit string
|
|
23
|
+
packURLBase, packName, outputDir string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type graphAction struct {
|
|
27
|
+
ID int `json:"ID"`
|
|
28
|
+
Mode string `json:"Mode"`
|
|
29
|
+
Package string `json:"Package"`
|
|
30
|
+
Deps []int `json:"Deps"`
|
|
31
|
+
ActionID string `json:"ActionID"`
|
|
32
|
+
Cmd []string `json:"Cmd"`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type packageReceipt struct {
|
|
36
|
+
ImportPath string
|
|
37
|
+
Dir string
|
|
38
|
+
GoFiles []string
|
|
39
|
+
CgoFiles []string
|
|
40
|
+
SFiles []string
|
|
41
|
+
EmbedFiles []string
|
|
42
|
+
Module *struct {
|
|
43
|
+
Path string
|
|
44
|
+
Version string
|
|
45
|
+
Main bool
|
|
46
|
+
Replace *struct {
|
|
47
|
+
Path string
|
|
48
|
+
Version string
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// SealRecordedActions joins Go's displayed build actions to the full action and
|
|
54
|
+
// output IDs recorded by its cache client. Package receipts annotate those IDs;
|
|
55
|
+
// they never generate or replace a Go cache key. A release package must pin the
|
|
56
|
+
// printed manifest digest before a consumer may trust the result.
|
|
57
|
+
//
|
|
58
|
+
// @evidence contracts/common.md#principled-implementation Only full recorded actions with verified local bodies are sealed; a same-workspace Go action graph and package receipts add diagnostics, while the release pins the resulting manifest digest.
|
|
59
|
+
// @evidence contracts/common.md#clear-and-simple-design One publisher command reads bounded Go receipts, verifies body associations, writes one deterministic pack and authenticates its complete manifest before reporting the digest.
|
|
60
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts A displayed action prefix is joined uniquely to a full recorded key; package names and source hashes cannot manufacture identities or resolve a conflict.
|
|
61
|
+
// @evidence contracts/common.md#meaningful-documentation The contract names the release trust step and separates diagnostic receipts from Go's authoritative keys.
|
|
62
|
+
// @evidence contracts/portability.md#os-neutral-implementation Go path and archive APIs process native publisher paths, while transport names use slash-separated ZIP entries independent of the publisher OS.
|
|
63
|
+
// @evidence contracts/performance.md#efficient-algorithms Each selected package body is streamed once into a level-one ZIP and hashed; action joins use maps rather than comparing every graph node with every recorded action.
|
|
64
|
+
// @evidenceExclude contracts/performance.md#reuse-equivalent-work Publisher sealing is an explicit one-shot release operation and does not memoize mutable source or tool observations.
|
|
65
|
+
// @evidence contracts/performance.md#bound-retention-and-release-resources Receipt/action files have byte caps, package source reads are streamed, body handles close after each entry, and failed staging files are removed; the finished pack remains with the publisher output owner.
|
|
66
|
+
func SealRecordedActions(opts sealOptions) error {
|
|
67
|
+
if !filepath.IsAbs(opts.root) || !filepath.IsAbs(opts.outputDir) || !filepath.IsAbs(opts.actionGraph) || !filepath.IsAbs(opts.packages) || !validPackName(opts.packName) {
|
|
68
|
+
return errors.New("publisher paths must be absolute and pack name must be safe")
|
|
69
|
+
}
|
|
70
|
+
tuple, err := readGoTuple(opts.goBinary)
|
|
71
|
+
if err != nil {
|
|
72
|
+
return err
|
|
73
|
+
}
|
|
74
|
+
graphRaw, err := readBoundedFile(opts.actionGraph, 16<<20)
|
|
75
|
+
if err != nil {
|
|
76
|
+
return err
|
|
77
|
+
}
|
|
78
|
+
var graph []graphAction
|
|
79
|
+
if err := json.Unmarshal(graphRaw, &graph); err != nil {
|
|
80
|
+
return fmt.Errorf("decode Go action graph: %w", err)
|
|
81
|
+
}
|
|
82
|
+
if len(graph) == 0 || len(graph) > 100000 {
|
|
83
|
+
return errors.New("Go action graph exceeds publisher policy")
|
|
84
|
+
}
|
|
85
|
+
receipts, err := readPackageReceipts(opts.packages)
|
|
86
|
+
if err != nil {
|
|
87
|
+
return err
|
|
88
|
+
}
|
|
89
|
+
store, err := openArtifactStore(opts.root, nil)
|
|
90
|
+
if err != nil {
|
|
91
|
+
return err
|
|
92
|
+
}
|
|
93
|
+
defer store.close()
|
|
94
|
+
recorded, err := readRecordedMappings(store)
|
|
95
|
+
if err != nil {
|
|
96
|
+
return err
|
|
97
|
+
}
|
|
98
|
+
byShort := make(map[string]string, len(recorded))
|
|
99
|
+
for actionID := range recorded {
|
|
100
|
+
bytes, _ := hex.DecodeString(actionID)
|
|
101
|
+
short := base64.RawURLEncoding.EncodeToString(bytes[:15])
|
|
102
|
+
if previous := byShort[short]; previous != "" && previous != actionID {
|
|
103
|
+
return fmt.Errorf("ambiguous Go action prefix %s", short)
|
|
104
|
+
}
|
|
105
|
+
byShort[short] = actionID
|
|
106
|
+
}
|
|
107
|
+
selected := make(map[int]packageartifacts.Action)
|
|
108
|
+
outputs := make(map[string]mapping)
|
|
109
|
+
for _, node := range graph {
|
|
110
|
+
if node.Mode != "build" || node.ActionID == "" {
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
fullID := byShort[node.ActionID]
|
|
114
|
+
value, exists := recorded[fullID]
|
|
115
|
+
if !exists || node.Package == "" {
|
|
116
|
+
return fmt.Errorf("Go build action %s has no recorded full body", node.Package)
|
|
117
|
+
}
|
|
118
|
+
receipt, exists := receipts[node.Package]
|
|
119
|
+
if !exists {
|
|
120
|
+
return fmt.Errorf("Go build action %s has no package receipt", node.Package)
|
|
121
|
+
}
|
|
122
|
+
sourceHash, err := hashSelectedSources(receipt)
|
|
123
|
+
if err != nil {
|
|
124
|
+
return fmt.Errorf("package %s sources: %w", node.Package, err)
|
|
125
|
+
}
|
|
126
|
+
command, _ := json.Marshal(node.Cmd)
|
|
127
|
+
commandHash := sha256.Sum256(command)
|
|
128
|
+
role, modulePath, moduleVersion := receiptRole(receipt, tuple.GoVersion)
|
|
129
|
+
selected[node.ID] = packageartifacts.Action{
|
|
130
|
+
ActionID: fullID, OutputID: value.OutputID, BodySHA256: value.OutputID,
|
|
131
|
+
BodyLength: value.Size, Pack: opts.packName, Kind: "archive",
|
|
132
|
+
PackagePath: node.Package, ModulePath: modulePath, ModuleVersion: moduleVersion,
|
|
133
|
+
ModuleRole: role, SelectedSourceSHA: sourceHash,
|
|
134
|
+
CommandSHA256: hex.EncodeToString(commandHash[:]), DirectInputs: []string{},
|
|
135
|
+
}
|
|
136
|
+
if previous, exists := outputs[value.OutputID]; exists && previous.Size != value.Size {
|
|
137
|
+
return errors.New("recorded output has conflicting lengths")
|
|
138
|
+
}
|
|
139
|
+
outputs[value.OutputID] = value
|
|
140
|
+
}
|
|
141
|
+
if len(selected) == 0 {
|
|
142
|
+
return errors.New("no recorded package actions were selected")
|
|
143
|
+
}
|
|
144
|
+
manifest := packageartifacts.Manifest{
|
|
145
|
+
Schema: 1, Release: opts.release, SourceCommit: opts.sourceCommit,
|
|
146
|
+
Tuple: tuple, GraphSHA256: digestBytes(graphRaw), Actions: make([]packageartifacts.Action, 0, len(selected)),
|
|
147
|
+
}
|
|
148
|
+
for _, node := range graph {
|
|
149
|
+
action, exists := selected[node.ID]
|
|
150
|
+
if !exists {
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
for _, dependency := range node.Deps {
|
|
154
|
+
if input, exists := selected[dependency]; exists {
|
|
155
|
+
action.DirectInputs = append(action.DirectInputs, input.OutputID)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
sort.Strings(action.DirectInputs)
|
|
159
|
+
manifest.Actions = append(manifest.Actions, action)
|
|
160
|
+
}
|
|
161
|
+
sort.Slice(manifest.Actions, func(i, j int) bool { return manifest.Actions[i].ActionID < manifest.Actions[j].ActionID })
|
|
162
|
+
if err := os.MkdirAll(opts.outputDir, 0700); err != nil {
|
|
163
|
+
return err
|
|
164
|
+
}
|
|
165
|
+
stage, err := os.CreateTemp(opts.outputDir, ".ttsc-pack-*.zip")
|
|
166
|
+
if err != nil {
|
|
167
|
+
return err
|
|
168
|
+
}
|
|
169
|
+
defer os.Remove(stage.Name())
|
|
170
|
+
writer := zip.NewWriter(stage)
|
|
171
|
+
writer.RegisterCompressor(zip.Deflate, func(destination io.Writer) (io.WriteCloser, error) {
|
|
172
|
+
return flate.NewWriter(destination, flate.BestSpeed)
|
|
173
|
+
})
|
|
174
|
+
ids := make([]string, 0, len(outputs))
|
|
175
|
+
for outputID := range outputs {
|
|
176
|
+
ids = append(ids, outputID)
|
|
177
|
+
}
|
|
178
|
+
sort.Strings(ids)
|
|
179
|
+
var decodedBytes int64
|
|
180
|
+
for _, outputID := range ids {
|
|
181
|
+
value := outputs[outputID]
|
|
182
|
+
location := filepath.ToSlash(filepath.Join("overlay", "bodies", outputID))
|
|
183
|
+
body, err := store.root.Open(location)
|
|
184
|
+
if err != nil {
|
|
185
|
+
writer.Close()
|
|
186
|
+
stage.Close()
|
|
187
|
+
return err
|
|
188
|
+
}
|
|
189
|
+
info, err := body.Stat()
|
|
190
|
+
if err != nil || !info.Mode().IsRegular() || info.Size() != value.Size {
|
|
191
|
+
body.Close()
|
|
192
|
+
writer.Close()
|
|
193
|
+
stage.Close()
|
|
194
|
+
return fmt.Errorf("recorded body %s changed", outputID)
|
|
195
|
+
}
|
|
196
|
+
entry, err := writer.Create("bodies/" + outputID)
|
|
197
|
+
if err != nil {
|
|
198
|
+
body.Close()
|
|
199
|
+
writer.Close()
|
|
200
|
+
stage.Close()
|
|
201
|
+
return err
|
|
202
|
+
}
|
|
203
|
+
hash := sha256.New()
|
|
204
|
+
count, copyErr := io.CopyN(io.MultiWriter(entry, hash), body, value.Size)
|
|
205
|
+
closeErr := body.Close()
|
|
206
|
+
if copyErr != nil || closeErr != nil || count != value.Size || hex.EncodeToString(hash.Sum(nil)) != outputID {
|
|
207
|
+
writer.Close()
|
|
208
|
+
stage.Close()
|
|
209
|
+
return fmt.Errorf("recorded body %s failed verification: %w", outputID, errors.Join(copyErr, closeErr))
|
|
210
|
+
}
|
|
211
|
+
decodedBytes += count
|
|
212
|
+
if decodedBytes > packageartifacts.MaxPackDecodedBytes {
|
|
213
|
+
writer.Close()
|
|
214
|
+
stage.Close()
|
|
215
|
+
return errors.New("pack exceeds decoded byte policy")
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if err := writer.Close(); err != nil {
|
|
219
|
+
stage.Close()
|
|
220
|
+
return err
|
|
221
|
+
}
|
|
222
|
+
if err := stage.Sync(); err != nil {
|
|
223
|
+
stage.Close()
|
|
224
|
+
return err
|
|
225
|
+
}
|
|
226
|
+
if err := stage.Close(); err != nil {
|
|
227
|
+
return err
|
|
228
|
+
}
|
|
229
|
+
packRaw, err := hashFile(stage.Name(), packageartifacts.MaxPackBytes)
|
|
230
|
+
if err != nil {
|
|
231
|
+
return err
|
|
232
|
+
}
|
|
233
|
+
packFile := opts.packName + "-" + packRaw.digest + ".zip"
|
|
234
|
+
manifest.Packs = []packageartifacts.Pack{{
|
|
235
|
+
Name: opts.packName, URL: strings.TrimRight(opts.packURLBase, "/") + "/" + packFile,
|
|
236
|
+
SHA256: packRaw.digest, Length: packRaw.length, DecodedBytes: decodedBytes, FileCount: len(ids),
|
|
237
|
+
}}
|
|
238
|
+
manifestRaw, err := json.MarshalIndent(manifest, "", " ")
|
|
239
|
+
if err != nil {
|
|
240
|
+
return err
|
|
241
|
+
}
|
|
242
|
+
manifestRaw = append(manifestRaw, '\n')
|
|
243
|
+
manifestDigest := digestBytes(manifestRaw)
|
|
244
|
+
if _, err := packageartifacts.ParseTrustedManifest(manifestRaw, manifestDigest); err != nil {
|
|
245
|
+
return fmt.Errorf("publisher manifest rejected: %w", err)
|
|
246
|
+
}
|
|
247
|
+
finalPack := filepath.Join(opts.outputDir, packFile)
|
|
248
|
+
if err := os.Link(stage.Name(), finalPack); err != nil {
|
|
249
|
+
if !errors.Is(err, os.ErrExist) {
|
|
250
|
+
return err
|
|
251
|
+
}
|
|
252
|
+
existing, hashErr := hashFile(finalPack, packageartifacts.MaxPackBytes)
|
|
253
|
+
if hashErr != nil || existing != packRaw {
|
|
254
|
+
return errors.New("existing publisher pack differs from sealed bytes")
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
manifestFile, err := os.OpenFile(filepath.Join(opts.outputDir, "manifest.json"), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
|
258
|
+
if err != nil {
|
|
259
|
+
return err
|
|
260
|
+
}
|
|
261
|
+
_, writeErr := manifestFile.Write(manifestRaw)
|
|
262
|
+
syncErr := manifestFile.Sync()
|
|
263
|
+
closeErr := manifestFile.Close()
|
|
264
|
+
if writeErr != nil || syncErr != nil || closeErr != nil {
|
|
265
|
+
return errors.Join(writeErr, syncErr, closeErr)
|
|
266
|
+
}
|
|
267
|
+
return json.NewEncoder(os.Stdout).Encode(struct {
|
|
268
|
+
ManifestSHA256 string `json:"manifestSHA256"`
|
|
269
|
+
Pack string `json:"pack"`
|
|
270
|
+
Actions int `json:"actions"`
|
|
271
|
+
DecodedBytes int64 `json:"decodedBytes"`
|
|
272
|
+
CompressedBytes int64 `json:"compressedBytes"`
|
|
273
|
+
}{manifestDigest, packFile, len(manifest.Actions), decodedBytes, packRaw.length})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
func readRecordedMappings(store *artifactStore) (map[string]mapping, error) {
|
|
277
|
+
directory, err := store.root.Open("overlay/actions")
|
|
278
|
+
if err != nil {
|
|
279
|
+
return nil, err
|
|
280
|
+
}
|
|
281
|
+
defer directory.Close()
|
|
282
|
+
entries, err := directory.ReadDir(-1)
|
|
283
|
+
if err != nil {
|
|
284
|
+
return nil, err
|
|
285
|
+
}
|
|
286
|
+
if len(entries) > 100000 {
|
|
287
|
+
return nil, errors.New("too many publisher actions")
|
|
288
|
+
}
|
|
289
|
+
result := make(map[string]mapping, len(entries))
|
|
290
|
+
for _, entry := range entries {
|
|
291
|
+
if !validID(entry.Name()) || !entry.Type().IsRegular() {
|
|
292
|
+
return nil, errors.New("publisher action index contains an unsafe entry")
|
|
293
|
+
}
|
|
294
|
+
index, err := store.root.Open(filepath.ToSlash(filepath.Join("overlay", "actions", entry.Name())))
|
|
295
|
+
if err != nil {
|
|
296
|
+
return nil, err
|
|
297
|
+
}
|
|
298
|
+
raw, readErr := io.ReadAll(io.LimitReader(index, 257))
|
|
299
|
+
closeErr := index.Close()
|
|
300
|
+
if readErr != nil || closeErr != nil || len(raw) > 256 {
|
|
301
|
+
return nil, errors.New("publisher action index is unreadable or too large")
|
|
302
|
+
}
|
|
303
|
+
var value mapping
|
|
304
|
+
if err := json.Unmarshal(raw, &value); err != nil || !validID(value.OutputID) || value.Size < 0 || value.Size > 512<<20 {
|
|
305
|
+
return nil, errors.New("publisher action index is invalid")
|
|
306
|
+
}
|
|
307
|
+
result[entry.Name()] = value
|
|
308
|
+
}
|
|
309
|
+
return result, nil
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
func readPackageReceipts(filename string) (map[string]packageReceipt, error) {
|
|
313
|
+
file, err := os.Open(filename)
|
|
314
|
+
if err != nil {
|
|
315
|
+
return nil, err
|
|
316
|
+
}
|
|
317
|
+
defer file.Close()
|
|
318
|
+
info, err := file.Stat()
|
|
319
|
+
if err != nil || !info.Mode().IsRegular() || info.Size() > 256<<20 {
|
|
320
|
+
return nil, errors.New("package receipts exceed publisher policy")
|
|
321
|
+
}
|
|
322
|
+
decoder := json.NewDecoder(io.LimitReader(file, (256<<20)+1))
|
|
323
|
+
result := make(map[string]packageReceipt)
|
|
324
|
+
for {
|
|
325
|
+
var receipt packageReceipt
|
|
326
|
+
if err := decoder.Decode(&receipt); err == io.EOF {
|
|
327
|
+
break
|
|
328
|
+
} else if err != nil {
|
|
329
|
+
return nil, fmt.Errorf("decode package receipt: %w", err)
|
|
330
|
+
}
|
|
331
|
+
if receipt.ImportPath == "" || receipt.Dir == "" {
|
|
332
|
+
return nil, errors.New("incomplete package receipt")
|
|
333
|
+
}
|
|
334
|
+
if _, exists := result[receipt.ImportPath]; exists {
|
|
335
|
+
return nil, errors.New("duplicate package receipt")
|
|
336
|
+
}
|
|
337
|
+
result[receipt.ImportPath] = receipt
|
|
338
|
+
if len(result) > 100000 {
|
|
339
|
+
return nil, errors.New("too many package receipts")
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return result, nil
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
func hashSelectedSources(receipt packageReceipt) (string, error) {
|
|
346
|
+
names := append(append(append([]string{}, receipt.GoFiles...), receipt.CgoFiles...), receipt.SFiles...)
|
|
347
|
+
names = append(names, receipt.EmbedFiles...)
|
|
348
|
+
sort.Strings(names)
|
|
349
|
+
hash := sha256.New()
|
|
350
|
+
for _, name := range names {
|
|
351
|
+
if filepath.IsAbs(name) || name == ".." || strings.HasPrefix(name, ".."+string(filepath.Separator)) {
|
|
352
|
+
return "", errors.New("selected source escaped package directory")
|
|
353
|
+
}
|
|
354
|
+
file, err := os.Open(filepath.Join(receipt.Dir, name))
|
|
355
|
+
if err != nil {
|
|
356
|
+
return "", err
|
|
357
|
+
}
|
|
358
|
+
_, _ = fmt.Fprintf(hash, "%d:%s\n", len(name), filepath.ToSlash(name))
|
|
359
|
+
count, copyErr := io.Copy(hash, io.LimitReader(file, (128<<20)+1))
|
|
360
|
+
closeErr := file.Close()
|
|
361
|
+
if copyErr != nil || closeErr != nil || count > 128<<20 {
|
|
362
|
+
return "", errors.New("selected source is unreadable or too large")
|
|
363
|
+
}
|
|
364
|
+
_, _ = fmt.Fprintf(hash, "\n%d\n", count)
|
|
365
|
+
}
|
|
366
|
+
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
func receiptRole(receipt packageReceipt, goVersion string) (string, string, string) {
|
|
370
|
+
if receipt.Module == nil {
|
|
371
|
+
return "standard", "std", goVersion
|
|
372
|
+
}
|
|
373
|
+
version := receipt.Module.Version
|
|
374
|
+
if receipt.Module.Replace != nil {
|
|
375
|
+
version = receipt.Module.Replace.Version
|
|
376
|
+
}
|
|
377
|
+
if receipt.Module.Main {
|
|
378
|
+
return "main", receipt.Module.Path, version
|
|
379
|
+
}
|
|
380
|
+
return "dependency", receipt.Module.Path, version
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
func readBoundedFile(filename string, limit int64) ([]byte, error) {
|
|
384
|
+
file, err := os.Open(filename)
|
|
385
|
+
if err != nil {
|
|
386
|
+
return nil, err
|
|
387
|
+
}
|
|
388
|
+
defer file.Close()
|
|
389
|
+
info, err := file.Stat()
|
|
390
|
+
if err != nil || !info.Mode().IsRegular() || info.Size() > limit {
|
|
391
|
+
return nil, errors.New("publisher input exceeds byte policy")
|
|
392
|
+
}
|
|
393
|
+
return io.ReadAll(io.LimitReader(file, limit+1))
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
type fileDigest struct {
|
|
397
|
+
digest string
|
|
398
|
+
length int64
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
func hashFile(filename string, limit int64) (fileDigest, error) {
|
|
402
|
+
file, err := os.Open(filename)
|
|
403
|
+
if err != nil {
|
|
404
|
+
return fileDigest{}, err
|
|
405
|
+
}
|
|
406
|
+
defer file.Close()
|
|
407
|
+
hash := sha256.New()
|
|
408
|
+
count, err := io.Copy(hash, io.LimitReader(file, limit+1))
|
|
409
|
+
if err != nil || count > limit {
|
|
410
|
+
return fileDigest{}, errors.New("publisher pack exceeds compressed byte policy")
|
|
411
|
+
}
|
|
412
|
+
return fileDigest{hex.EncodeToString(hash.Sum(nil)), count}, nil
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
func digestBytes(value []byte) string { sum := sha256.Sum256(value); return hex.EncodeToString(sum[:]) }
|
|
416
|
+
|
|
417
|
+
func validPackName(value string) bool {
|
|
418
|
+
if value == "" || len(value) > 80 {
|
|
419
|
+
return false
|
|
420
|
+
}
|
|
421
|
+
for _, char := range value {
|
|
422
|
+
if !strings.ContainsRune("abcdefghijklmnopqrstuvwxyz0123456789-", char) {
|
|
423
|
+
return false
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return true
|
|
427
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"os"
|
|
7
|
+
"os/exec"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"runtime"
|
|
10
|
+
"strings"
|
|
11
|
+
"testing"
|
|
12
|
+
"time"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// TestStockGoCacheFailures verifies the real Go client's response to an already-started bad helper.
|
|
16
|
+
//
|
|
17
|
+
// Verifies: a malformed response and premature helper exit fail the Go build promptly and do not silently replay it against a local cache.
|
|
18
|
+
// 1. Build a small helper that advertises the protocol, then emits malformed JSON or exits.
|
|
19
|
+
// 2. Run stock Go against each mode with a fresh local cache and a fixed failure deadline.
|
|
20
|
+
// 3. Require an identified cache-helper failure, no output archive, and no deadline expiry.
|
|
21
|
+
//
|
|
22
|
+
// @evidence contracts/testing.md#behavioral-verification Runs stock go build with GOCACHEPROG pointing to a deliberately broken executable and checks its process result and missing output.
|
|
23
|
+
// @evidence contracts/testing.md#independent-expectations The stock Go client owns the helper failure contract: invalid JSON or EOF after valid startup cannot represent an action miss, so the build must fail without producing an archive.
|
|
24
|
+
// @evidence contracts/testing.md#distinguishing-cases Covers malformed response and premature exit after valid capabilities; the production helper's valid protocol, hit and miss cases belong to TestStockGoCacheClient and TestCacheProtocol.
|
|
25
|
+
// @evidence contracts/testing.md#execution-ownership This narrow Go subprocess test uses a temporary helper and tiny local module; it never installs a consumer or runs the product E2E suite.
|
|
26
|
+
func TestStockGoCacheFailures(t *testing.T) {
|
|
27
|
+
if !supportedGoVersion.MatchString(runtime.Version()) {
|
|
28
|
+
t.Skip("the helper protocol requires a stock Go 1.26 patch release")
|
|
29
|
+
}
|
|
30
|
+
root := t.TempDir()
|
|
31
|
+
helperSource := filepath.Join(root, "broken-helper.go")
|
|
32
|
+
source := `package main
|
|
33
|
+
import (
|
|
34
|
+
"bufio"
|
|
35
|
+
"fmt"
|
|
36
|
+
"os"
|
|
37
|
+
)
|
|
38
|
+
func main() {
|
|
39
|
+
fmt.Println("{\"ID\":0,\"KnownCommands\":[\"get\",\"put\",\"close\"]}")
|
|
40
|
+
if _, err := bufio.NewReader(os.Stdin).ReadBytes('\n'); err != nil {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
if os.Getenv("TTSC_BAD_HELPER_MODE") == "malformed" {
|
|
44
|
+
fmt.Println("{broken")
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
`
|
|
48
|
+
if err := os.WriteFile(helperSource, []byte(source), 0600); err != nil {
|
|
49
|
+
t.Fatal(err)
|
|
50
|
+
}
|
|
51
|
+
helper := filepath.Join(root, "broken helper"+exeSuffix())
|
|
52
|
+
compile := exec.Command("go", "build", "-o", helper, helperSource)
|
|
53
|
+
compile.Env = append(os.Environ(), "GOCACHEPROG=", "GOTOOLCHAIN=local")
|
|
54
|
+
if output, err := compile.CombinedOutput(); err != nil {
|
|
55
|
+
t.Fatalf("building failing cache helper: %v\n%s", err, output)
|
|
56
|
+
}
|
|
57
|
+
module := filepath.Join(root, "client")
|
|
58
|
+
if err := os.MkdirAll(module, 0700); err != nil {
|
|
59
|
+
t.Fatal(err)
|
|
60
|
+
}
|
|
61
|
+
for name, body := range map[string]string{
|
|
62
|
+
"go.mod": "module example.org/broken-cache-client\n\ngo 1.26\n",
|
|
63
|
+
"tiny.go": "package tiny\nfunc Value() string { return \"stock Go\" }\n",
|
|
64
|
+
} {
|
|
65
|
+
if err := os.WriteFile(filepath.Join(module, name), []byte(body), 0600); err != nil {
|
|
66
|
+
t.Fatal(err)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for _, mode := range []string{"malformed", "premature"} {
|
|
70
|
+
t.Run(mode, func(t *testing.T) {
|
|
71
|
+
cache := filepath.Join(root, "cache-"+mode)
|
|
72
|
+
if err := os.MkdirAll(cache, 0700); err != nil {
|
|
73
|
+
t.Fatal(err)
|
|
74
|
+
}
|
|
75
|
+
archive := filepath.Join(root, "archive-"+mode+".a")
|
|
76
|
+
context, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
77
|
+
defer cancel()
|
|
78
|
+
build := exec.CommandContext(context, "go", "build", "-trimpath", "-o", archive, ".")
|
|
79
|
+
build.Dir = module
|
|
80
|
+
build.Env = append(os.Environ(),
|
|
81
|
+
"CGO_ENABLED=0", "GOFLAGS=", "GOWORK=off", "GOPROXY=off", "GOSUMDB=off", "GOTOOLCHAIN=local",
|
|
82
|
+
"GOCACHE="+cache, "GOCACHEPROG="+quoteGoCacheArgument(helper), "TTSC_BAD_HELPER_MODE="+mode,
|
|
83
|
+
)
|
|
84
|
+
output, err := build.CombinedOutput()
|
|
85
|
+
expected := map[string]string{"malformed": "invalid character", "premature": "exited pre-Close"}[mode]
|
|
86
|
+
if err == nil || context.Err() != nil || !strings.Contains(string(output), "GOCACHEPROG") || !strings.Contains(string(output), expected) {
|
|
87
|
+
t.Fatalf("stock Go did not fail promptly at the helper boundary: error=%v deadline=%v output=%s", err, context.Err(), output)
|
|
88
|
+
}
|
|
89
|
+
if _, err := os.Stat(archive); !errors.Is(err, os.ErrNotExist) {
|
|
90
|
+
t.Fatalf("stock Go produced an archive after a helper failure: %v", err)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|