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,163 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"archive/zip"
|
|
5
|
+
"bytes"
|
|
6
|
+
"crypto/sha256"
|
|
7
|
+
"encoding/hex"
|
|
8
|
+
"net/url"
|
|
9
|
+
"os"
|
|
10
|
+
"os/exec"
|
|
11
|
+
"path/filepath"
|
|
12
|
+
"regexp"
|
|
13
|
+
"runtime"
|
|
14
|
+
"strings"
|
|
15
|
+
"testing"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// TestStockGoCacheClient verifies the helper against the actual supported Go client.
|
|
19
|
+
//
|
|
20
|
+
// Verifies: a relocated module and dependency cache reuse exact actions, while a dependency update misses.
|
|
21
|
+
// 1. Build the helper and serve two versions of a dependency from a private file proxy.
|
|
22
|
+
// 2. Build identical modules under distinct paths and empty Go and module caches with one helper store.
|
|
23
|
+
// 3. Compare archive bytes and compile counts, then change the dependency version and require compilation.
|
|
24
|
+
//
|
|
25
|
+
// @evidence contracts/testing.md#behavioral-verification Runs stock go build with GOCACHEPROG through the production helper executable, then observes compiler commands and archive bytes.
|
|
26
|
+
// @evidence contracts/testing.md#independent-expectations Identical source and dependency bytes under -trimpath should request the same actions; a changed dependency version must change the archive and require compilation.
|
|
27
|
+
// @evidence contracts/testing.md#distinguishing-cases Exercises stock Go 1.26 protocol startup, put/get, path quoting, source and module-cache relocation, and a changed dependency version.
|
|
28
|
+
// @evidence contracts/testing.md#execution-ownership The test runs stock Go in temporary directories with a private file proxy and never contacts the network or publishes a release artifact.
|
|
29
|
+
func TestStockGoCacheClient(t *testing.T) {
|
|
30
|
+
if !supportedGoVersion.MatchString(runtime.Version()) {
|
|
31
|
+
t.Skip("the helper protocol requires a stock Go 1.26 patch release")
|
|
32
|
+
}
|
|
33
|
+
root := filepath.Join(t.TempDir(), "native artifact path with spaces")
|
|
34
|
+
if err := os.MkdirAll(root, 0700); err != nil {
|
|
35
|
+
t.Fatal(err)
|
|
36
|
+
}
|
|
37
|
+
helper := filepath.Join(root, "cache helper"+exeSuffix())
|
|
38
|
+
buildHelper := exec.Command("go", "build", "-trimpath", "-o", helper, ".")
|
|
39
|
+
if output, err := buildHelper.CombinedOutput(); err != nil {
|
|
40
|
+
t.Fatalf("building cache helper: %v\n%s", err, output)
|
|
41
|
+
}
|
|
42
|
+
store := filepath.Join(root, "shared artifact store")
|
|
43
|
+
program := quoteGoCacheArgument(helper) + " -root " + quoteGoCacheArgument(store)
|
|
44
|
+
proxy := filepath.Join(root, "private module proxy")
|
|
45
|
+
for _, version := range []string{"v1.0.0", "v1.0.1"} {
|
|
46
|
+
directory := filepath.Join(proxy, "example.org", "cache-dep", "@v")
|
|
47
|
+
if err := os.MkdirAll(directory, 0700); err != nil {
|
|
48
|
+
t.Fatal(err)
|
|
49
|
+
}
|
|
50
|
+
module := []byte("module example.org/cache-dep\n\ngo 1.26\n")
|
|
51
|
+
info := []byte(`{"Version":"` + version + `","Time":"2026-01-01T00:00:00Z"}`)
|
|
52
|
+
var archive bytes.Buffer
|
|
53
|
+
writer := zip.NewWriter(&archive)
|
|
54
|
+
for name, body := range map[string][]byte{
|
|
55
|
+
"go.mod": module,
|
|
56
|
+
"dep.go": []byte("package dep\nfunc Value() string { return \"" + version + "\" }\n"),
|
|
57
|
+
} {
|
|
58
|
+
entry, err := writer.Create("example.org/cache-dep@" + version + "/" + name)
|
|
59
|
+
if err != nil {
|
|
60
|
+
t.Fatal(err)
|
|
61
|
+
}
|
|
62
|
+
if _, err := entry.Write(body); err != nil {
|
|
63
|
+
t.Fatal(err)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if err := writer.Close(); err != nil {
|
|
67
|
+
t.Fatal(err)
|
|
68
|
+
}
|
|
69
|
+
for extension, body := range map[string][]byte{
|
|
70
|
+
".mod": module, ".info": info, ".zip": archive.Bytes(),
|
|
71
|
+
} {
|
|
72
|
+
if err := os.WriteFile(filepath.Join(directory, version+extension), body, 0600); err != nil {
|
|
73
|
+
t.Fatal(err)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
proxyPath := filepath.ToSlash(proxy)
|
|
78
|
+
if !strings.HasPrefix(proxyPath, "/") {
|
|
79
|
+
proxyPath = "/" + proxyPath
|
|
80
|
+
}
|
|
81
|
+
proxyURL := (&url.URL{Scheme: "file", Path: proxyPath}).String()
|
|
82
|
+
compileCommand := regexp.MustCompile(`(?m)[/\\]compile(?:\.exe)?["']?\s`)
|
|
83
|
+
var archiveDigests [3]string
|
|
84
|
+
var compileCounts [3]int
|
|
85
|
+
for phase, name := range []string{"publisher", "relocated consumer", "updated dependency"} {
|
|
86
|
+
version := "v1.0.0"
|
|
87
|
+
if phase == 2 {
|
|
88
|
+
version = "v1.0.1"
|
|
89
|
+
}
|
|
90
|
+
directory := filepath.Join(root, name)
|
|
91
|
+
if err := os.MkdirAll(directory, 0700); err != nil {
|
|
92
|
+
t.Fatal(err)
|
|
93
|
+
}
|
|
94
|
+
for file, source := range map[string]string{
|
|
95
|
+
"go.mod": "module example.org/ttsc-cache-client\n\ngo 1.26\n\nrequire example.org/cache-dep " + version + "\n",
|
|
96
|
+
"tiny.go": "package tiny\nimport \"example.org/cache-dep\"\nfunc Value() string { return dep.Value() }\n",
|
|
97
|
+
} {
|
|
98
|
+
if err := os.WriteFile(filepath.Join(directory, file), []byte(source), 0600); err != nil {
|
|
99
|
+
t.Fatal(err)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
goCache := filepath.Join(directory, "empty go cache")
|
|
103
|
+
moduleCache := filepath.Join(directory, "empty module cache")
|
|
104
|
+
if err := os.MkdirAll(goCache, 0700); err != nil {
|
|
105
|
+
t.Fatal(err)
|
|
106
|
+
}
|
|
107
|
+
if err := os.MkdirAll(moduleCache, 0700); err != nil {
|
|
108
|
+
t.Fatal(err)
|
|
109
|
+
}
|
|
110
|
+
t.Cleanup(func() {
|
|
111
|
+
clean := exec.Command("go", "clean", "-modcache")
|
|
112
|
+
clean.Env = append(os.Environ(),
|
|
113
|
+
"GOMODCACHE="+moduleCache, "GOCACHEPROG=", "GOFLAGS=", "GOTOOLCHAIN=local", "GOWORK=off",
|
|
114
|
+
)
|
|
115
|
+
if output, err := clean.CombinedOutput(); err != nil {
|
|
116
|
+
t.Errorf("cleaning %s private module cache: %v\n%s", name, err, output)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
archive := filepath.Join(directory, "result.a")
|
|
120
|
+
build := exec.Command("go", "build", "-mod=mod", "-trimpath", "-x", "-o", archive, ".")
|
|
121
|
+
build.Dir = directory
|
|
122
|
+
build.Env = append(os.Environ(),
|
|
123
|
+
"CGO_ENABLED=0", "GOFLAGS=", "GOWORK=off", "GOCACHE="+goCache,
|
|
124
|
+
"GOMODCACHE="+moduleCache, "GOCACHEPROG="+program,
|
|
125
|
+
"GOPROXY="+proxyURL, "GOSUMDB=off", "GOTOOLCHAIN=local",
|
|
126
|
+
)
|
|
127
|
+
output, err := build.CombinedOutput()
|
|
128
|
+
if err != nil {
|
|
129
|
+
t.Fatalf("%s stock Go build: %v\n%s", name, err, output)
|
|
130
|
+
}
|
|
131
|
+
if _, err := os.Stat(filepath.Join(moduleCache, "example.org", "cache-dep@"+version, "dep.go")); err != nil {
|
|
132
|
+
t.Fatalf("%s did not populate its separate dependency cache: %v", name, err)
|
|
133
|
+
}
|
|
134
|
+
compileCounts[phase] = len(compileCommand.FindAll(output, -1))
|
|
135
|
+
raw, err := os.ReadFile(archive)
|
|
136
|
+
if err != nil {
|
|
137
|
+
t.Fatal(err)
|
|
138
|
+
}
|
|
139
|
+
digest := sha256.Sum256(raw)
|
|
140
|
+
archiveDigests[phase] = hex.EncodeToString(digest[:])
|
|
141
|
+
}
|
|
142
|
+
if compileCounts[0] == 0 || compileCounts[1] != 0 || compileCounts[2] == 0 ||
|
|
143
|
+
archiveDigests[0] != archiveDigests[1] || archiveDigests[1] == archiveDigests[2] {
|
|
144
|
+
t.Fatalf("stock Go did not distinguish relocated and updated actions: compiles=%v archives=%v", compileCounts, archiveDigests)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func exeSuffix() string {
|
|
149
|
+
if runtime.GOOS == "windows" {
|
|
150
|
+
return ".exe"
|
|
151
|
+
}
|
|
152
|
+
return ""
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func quoteGoCacheArgument(value string) string {
|
|
156
|
+
if !strings.Contains(value, "'") {
|
|
157
|
+
return "'" + value + "'"
|
|
158
|
+
}
|
|
159
|
+
if !strings.Contains(value, "\"") {
|
|
160
|
+
return "\"" + value + "\""
|
|
161
|
+
}
|
|
162
|
+
panic("test path cannot be represented by Go's command quoting")
|
|
163
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/rand"
|
|
5
|
+
"crypto/sha256"
|
|
6
|
+
"encoding/hex"
|
|
7
|
+
"encoding/json"
|
|
8
|
+
"errors"
|
|
9
|
+
"fmt"
|
|
10
|
+
"io"
|
|
11
|
+
"os"
|
|
12
|
+
"path/filepath"
|
|
13
|
+
"strings"
|
|
14
|
+
|
|
15
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/packageartifacts"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
type mapping struct {
|
|
19
|
+
OutputID string `json:"outputID"`
|
|
20
|
+
Size int64 `json:"size"`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type artifactStore struct {
|
|
24
|
+
path string
|
|
25
|
+
root *os.Root
|
|
26
|
+
base map[string]packageartifacts.Action
|
|
27
|
+
packs map[string]packageartifacts.Pack
|
|
28
|
+
pins []*os.File
|
|
29
|
+
pinned map[string]bool
|
|
30
|
+
verified map[string]bool
|
|
31
|
+
receiptName string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func openArtifactStore(directory string, manifest *packageartifacts.Manifest) (*artifactStore, error) {
|
|
35
|
+
if err := os.MkdirAll(directory, 0700); err != nil {
|
|
36
|
+
return nil, err
|
|
37
|
+
}
|
|
38
|
+
info, err := os.Lstat(directory)
|
|
39
|
+
if err != nil {
|
|
40
|
+
return nil, err
|
|
41
|
+
}
|
|
42
|
+
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
|
43
|
+
return nil, fmt.Errorf("artifact root is not an ordinary directory: %s", directory)
|
|
44
|
+
}
|
|
45
|
+
physical, err := filepath.EvalSymlinks(directory)
|
|
46
|
+
if err != nil {
|
|
47
|
+
return nil, err
|
|
48
|
+
}
|
|
49
|
+
root, err := os.OpenRoot(physical)
|
|
50
|
+
if err != nil {
|
|
51
|
+
return nil, err
|
|
52
|
+
}
|
|
53
|
+
store := &artifactStore{
|
|
54
|
+
path: physical, root: root,
|
|
55
|
+
base: make(map[string]packageartifacts.Action),
|
|
56
|
+
packs: make(map[string]packageartifacts.Pack),
|
|
57
|
+
pinned: make(map[string]bool),
|
|
58
|
+
verified: make(map[string]bool),
|
|
59
|
+
}
|
|
60
|
+
for _, name := range []string{"base", "base/packs", "base/staging", "overlay", "overlay/bodies", "overlay/actions", "overlay/staging", "receipts"} {
|
|
61
|
+
if err := root.Mkdir(name, 0700); err != nil && !errors.Is(err, os.ErrExist) {
|
|
62
|
+
root.Close()
|
|
63
|
+
return nil, err
|
|
64
|
+
}
|
|
65
|
+
info, err := root.Lstat(name)
|
|
66
|
+
if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
|
67
|
+
root.Close()
|
|
68
|
+
return nil, fmt.Errorf("artifact directory is aliased or unreadable: %s", name)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if manifest != nil {
|
|
72
|
+
for _, pack := range manifest.Packs {
|
|
73
|
+
store.packs[pack.Name] = pack
|
|
74
|
+
}
|
|
75
|
+
for _, action := range manifest.Actions {
|
|
76
|
+
store.base[action.ActionID] = action
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return store, nil
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func (store *artifactStore) close() error {
|
|
83
|
+
var first error
|
|
84
|
+
for _, file := range store.pins {
|
|
85
|
+
if err := file.Close(); err != nil && first == nil {
|
|
86
|
+
first = err
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if err := store.root.Close(); err != nil && first == nil {
|
|
90
|
+
first = err
|
|
91
|
+
}
|
|
92
|
+
return first
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func (store *artifactStore) get(actionID string) (mapping, string, bool, error) {
|
|
96
|
+
index := filepath.ToSlash(filepath.Join("overlay", "actions", actionID))
|
|
97
|
+
indexFile, err := store.root.Open(index)
|
|
98
|
+
if err == nil {
|
|
99
|
+
raw, readErr := io.ReadAll(io.LimitReader(indexFile, 257))
|
|
100
|
+
closeErr := indexFile.Close()
|
|
101
|
+
if readErr != nil || closeErr != nil {
|
|
102
|
+
return mapping{}, "", false, errors.Join(readErr, closeErr)
|
|
103
|
+
}
|
|
104
|
+
var value mapping
|
|
105
|
+
if len(raw) > 256 || json.Unmarshal(raw, &value) != nil || !validID(value.OutputID) || value.Size < 0 {
|
|
106
|
+
return mapping{}, "", false, fmt.Errorf("invalid overlay mapping for action %s", actionID)
|
|
107
|
+
}
|
|
108
|
+
location, err := store.pinBody("overlay", value)
|
|
109
|
+
if errors.Is(err, os.ErrNotExist) {
|
|
110
|
+
return mapping{}, "", false, nil
|
|
111
|
+
}
|
|
112
|
+
return value, location, err == nil, err
|
|
113
|
+
}
|
|
114
|
+
if !errors.Is(err, os.ErrNotExist) {
|
|
115
|
+
return mapping{}, "", false, err
|
|
116
|
+
}
|
|
117
|
+
base, exists := store.base[actionID]
|
|
118
|
+
if !exists {
|
|
119
|
+
return mapping{}, "", false, nil
|
|
120
|
+
}
|
|
121
|
+
value := mapping{OutputID: base.OutputID, Size: base.BodyLength}
|
|
122
|
+
pack := store.packs[base.Pack]
|
|
123
|
+
owner := filepath.ToSlash(filepath.Join("base", "packs", pack.Name+"-"+pack.SHA256))
|
|
124
|
+
info, err := store.root.Lstat(owner)
|
|
125
|
+
if errors.Is(err, os.ErrNotExist) {
|
|
126
|
+
return mapping{}, "", false, nil
|
|
127
|
+
}
|
|
128
|
+
if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
|
129
|
+
return mapping{}, "", false, fmt.Errorf("invalid admitted pack %s", pack.Name)
|
|
130
|
+
}
|
|
131
|
+
marker, err := store.root.ReadFile(filepath.ToSlash(filepath.Join(owner, ".complete")))
|
|
132
|
+
if errors.Is(err, os.ErrNotExist) {
|
|
133
|
+
return mapping{}, "", false, nil
|
|
134
|
+
}
|
|
135
|
+
if err != nil || string(marker) != pack.SHA256+"\n" {
|
|
136
|
+
return mapping{}, "", false, fmt.Errorf("invalid admission marker for pack %s", pack.Name)
|
|
137
|
+
}
|
|
138
|
+
location, err := store.pinBody(owner, value)
|
|
139
|
+
if errors.Is(err, os.ErrNotExist) {
|
|
140
|
+
return mapping{}, "", false, nil
|
|
141
|
+
}
|
|
142
|
+
return value, location, err == nil, err
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func (store *artifactStore) put(actionID, outputID string, size int64, body io.Reader) (string, error) {
|
|
146
|
+
if !validID(actionID) || !validID(outputID) || size < 0 || size > 512<<20 {
|
|
147
|
+
return "", errors.New("invalid Go cache put identity or body size")
|
|
148
|
+
}
|
|
149
|
+
staging := filepath.ToSlash(filepath.Join("overlay", "staging", randomName()))
|
|
150
|
+
file, err := store.root.OpenFile(staging, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
|
151
|
+
if err != nil {
|
|
152
|
+
return "", err
|
|
153
|
+
}
|
|
154
|
+
defer store.root.Remove(staging)
|
|
155
|
+
hash := sha256.New()
|
|
156
|
+
written, copyErr := io.CopyN(io.MultiWriter(file, hash), body, size)
|
|
157
|
+
syncErr := file.Sync()
|
|
158
|
+
closeErr := file.Close()
|
|
159
|
+
if copyErr != nil || written != size {
|
|
160
|
+
return "", fmt.Errorf("incomplete Go cache body: %w", copyErr)
|
|
161
|
+
}
|
|
162
|
+
if syncErr != nil {
|
|
163
|
+
return "", syncErr
|
|
164
|
+
}
|
|
165
|
+
if closeErr != nil {
|
|
166
|
+
return "", closeErr
|
|
167
|
+
}
|
|
168
|
+
if hex.EncodeToString(hash.Sum(nil)) != outputID {
|
|
169
|
+
return "", errors.New("Go cache body differs from its OutputID")
|
|
170
|
+
}
|
|
171
|
+
destination := filepath.ToSlash(filepath.Join("overlay", "bodies", outputID))
|
|
172
|
+
if err := store.root.Link(staging, destination); err != nil && !errors.Is(err, os.ErrExist) {
|
|
173
|
+
return "", err
|
|
174
|
+
}
|
|
175
|
+
value := mapping{OutputID: outputID, Size: size}
|
|
176
|
+
location, err := store.pinBody("overlay", value)
|
|
177
|
+
if err != nil {
|
|
178
|
+
return "", err
|
|
179
|
+
}
|
|
180
|
+
data, err := json.Marshal(value)
|
|
181
|
+
if err != nil {
|
|
182
|
+
return "", err
|
|
183
|
+
}
|
|
184
|
+
indexStage := filepath.ToSlash(filepath.Join("overlay", "staging", randomName()))
|
|
185
|
+
indexFile, err := store.root.OpenFile(indexStage, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
|
186
|
+
if err != nil {
|
|
187
|
+
return "", err
|
|
188
|
+
}
|
|
189
|
+
defer store.root.Remove(indexStage)
|
|
190
|
+
_, writeErr := indexFile.Write(data)
|
|
191
|
+
syncErr = indexFile.Sync()
|
|
192
|
+
closeErr = indexFile.Close()
|
|
193
|
+
if writeErr != nil || syncErr != nil || closeErr != nil {
|
|
194
|
+
return "", errors.Join(writeErr, syncErr, closeErr)
|
|
195
|
+
}
|
|
196
|
+
index := filepath.ToSlash(filepath.Join("overlay", "actions", actionID))
|
|
197
|
+
if err := store.root.Rename(indexStage, index); err != nil {
|
|
198
|
+
return "", err
|
|
199
|
+
}
|
|
200
|
+
return location, nil
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func (store *artifactStore) pinBody(owner string, value mapping) (string, error) {
|
|
204
|
+
relative := filepath.ToSlash(filepath.Join(owner, "bodies", value.OutputID))
|
|
205
|
+
info, err := store.root.Lstat(relative)
|
|
206
|
+
if err != nil {
|
|
207
|
+
return "", err
|
|
208
|
+
}
|
|
209
|
+
if !info.Mode().IsRegular() || info.Size() != value.Size {
|
|
210
|
+
return "", fmt.Errorf("invalid artifact body %s", relative)
|
|
211
|
+
}
|
|
212
|
+
file, err := store.root.Open(relative)
|
|
213
|
+
if err != nil {
|
|
214
|
+
return "", err
|
|
215
|
+
}
|
|
216
|
+
key := owner + "/" + value.OutputID
|
|
217
|
+
if !store.verified[key] {
|
|
218
|
+
hash := sha256.New()
|
|
219
|
+
count, err := io.Copy(hash, file)
|
|
220
|
+
if err != nil || count != value.Size || hex.EncodeToString(hash.Sum(nil)) != value.OutputID {
|
|
221
|
+
file.Close()
|
|
222
|
+
return "", fmt.Errorf("artifact body failed validation: %s", relative)
|
|
223
|
+
}
|
|
224
|
+
store.verified[key] = true
|
|
225
|
+
}
|
|
226
|
+
if store.pinned[key] {
|
|
227
|
+
file.Close()
|
|
228
|
+
} else {
|
|
229
|
+
store.pins = append(store.pins, file)
|
|
230
|
+
store.pinned[key] = true
|
|
231
|
+
}
|
|
232
|
+
return filepath.Join(store.path, filepath.FromSlash(relative)), nil
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
func validID(value string) bool {
|
|
236
|
+
if len(value) != 64 {
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
_, err := hex.DecodeString(value)
|
|
240
|
+
return err == nil && value == strings.ToLower(value)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
func randomName() string {
|
|
244
|
+
var value [16]byte
|
|
245
|
+
if _, err := rand.Read(value[:]); err != nil {
|
|
246
|
+
panic(err)
|
|
247
|
+
}
|
|
248
|
+
return hex.EncodeToString(value[:])
|
|
249
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"crypto/sha256"
|
|
6
|
+
"encoding/hex"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
|
+
"testing"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// TestArtifactStore verifies the overlay's complete-body and generation contract.
|
|
14
|
+
//
|
|
15
|
+
// Verifies: an action is published only after its OutputID body is valid, and a later put replaces its mapping without invalidating an earlier reader.
|
|
16
|
+
// 1. Put an archive, read it, and put a second body for the same action.
|
|
17
|
+
// 2. Reopen the store and require the new generation while the old body still exists.
|
|
18
|
+
// 3. Reject a bad digest, a truncated body and an aliased body, then treat a missing body after interrupted cleanup as a miss.
|
|
19
|
+
//
|
|
20
|
+
// @evidence contracts/testing.md#behavioral-verification Uses actual owned files and the store's put/get operations, asserting returned bytes and the surviving index after failed writes.
|
|
21
|
+
// @evidence contracts/testing.md#independent-expectations SHA-256 is computed directly from literal body bytes; a second mapping for one ActionID follows stock Go's last-valid-put policy, while invalid bytes cannot match their claimed OutputID.
|
|
22
|
+
// @evidence contracts/testing.md#distinguishing-cases Covers first put, same-action replacement, restart, zero body, wrong digest, truncated input, deduplicated output, body alias rejection and a missing body after partial cleanup; helper protocol and native clean races have separate owners.
|
|
23
|
+
// @evidence contracts/testing.md#execution-ownership This Go unit test owns a temporary directory and calls storage directly; it starts no Go build or external host.
|
|
24
|
+
func TestArtifactStore(t *testing.T) {
|
|
25
|
+
root := t.TempDir()
|
|
26
|
+
store, err := openArtifactStore(root, nil)
|
|
27
|
+
if err != nil {
|
|
28
|
+
t.Fatal(err)
|
|
29
|
+
}
|
|
30
|
+
defer store.close()
|
|
31
|
+
action := strings.Repeat("a", 64)
|
|
32
|
+
first := []byte("first archive")
|
|
33
|
+
second := []byte("second archive")
|
|
34
|
+
digest := func(body []byte) string {
|
|
35
|
+
sum := sha256.Sum256(body)
|
|
36
|
+
return hex.EncodeToString(sum[:])
|
|
37
|
+
}
|
|
38
|
+
firstPath, err := store.put(action, digest(first), int64(len(first)), bytes.NewReader(first))
|
|
39
|
+
if err != nil {
|
|
40
|
+
t.Fatal(err)
|
|
41
|
+
}
|
|
42
|
+
value, location, found, err := store.get(action)
|
|
43
|
+
if err != nil || !found || location != firstPath || value.OutputID != digest(first) {
|
|
44
|
+
t.Fatalf("first generation not readable: %+v %q %v %v", value, location, found, err)
|
|
45
|
+
}
|
|
46
|
+
secondPath, err := store.put(action, digest(second), int64(len(second)), bytes.NewReader(second))
|
|
47
|
+
if err != nil {
|
|
48
|
+
t.Fatal(err)
|
|
49
|
+
}
|
|
50
|
+
value, location, found, err = store.get(action)
|
|
51
|
+
if err != nil || !found || location != secondPath || value.OutputID != digest(second) {
|
|
52
|
+
t.Fatalf("new generation not readable: %+v %q %v %v", value, location, found, err)
|
|
53
|
+
}
|
|
54
|
+
if raw, err := os.ReadFile(firstPath); err != nil || !bytes.Equal(raw, first) {
|
|
55
|
+
t.Fatalf("old pinned body changed: %q %v", raw, err)
|
|
56
|
+
}
|
|
57
|
+
if _, err := store.put(action, digest(first), int64(len(second)), bytes.NewReader(second)); err == nil {
|
|
58
|
+
t.Fatal("wrong OutputID accepted")
|
|
59
|
+
}
|
|
60
|
+
if _, err := store.put(action, digest(first), int64(len(first)), bytes.NewReader(first[:3])); err == nil {
|
|
61
|
+
t.Fatal("truncated body accepted")
|
|
62
|
+
}
|
|
63
|
+
if _, err := store.put(strings.Repeat("b", 64), digest(second), int64(len(second)), bytes.NewReader(second)); err != nil {
|
|
64
|
+
t.Fatal(err)
|
|
65
|
+
}
|
|
66
|
+
empty := []byte{}
|
|
67
|
+
if _, err := store.put(strings.Repeat("c", 64), digest(empty), 0, bytes.NewReader(empty)); err != nil {
|
|
68
|
+
t.Fatal(err)
|
|
69
|
+
}
|
|
70
|
+
if err := store.close(); err != nil {
|
|
71
|
+
t.Fatal(err)
|
|
72
|
+
}
|
|
73
|
+
reopened, err := openArtifactStore(root, nil)
|
|
74
|
+
if err != nil {
|
|
75
|
+
t.Fatal(err)
|
|
76
|
+
}
|
|
77
|
+
defer reopened.close()
|
|
78
|
+
value, location, found, err = reopened.get(action)
|
|
79
|
+
if err != nil || !found || value.OutputID != digest(second) {
|
|
80
|
+
t.Fatalf("restart lost last valid generation: %+v %q %v %v", value, location, found, err)
|
|
81
|
+
}
|
|
82
|
+
aliasAction := strings.Repeat("d", 64)
|
|
83
|
+
alias := filepath.Join(root, "overlay", "bodies", digest([]byte("alias")))
|
|
84
|
+
if err := os.Symlink(secondPath, alias); err == nil {
|
|
85
|
+
if _, err := reopened.put(aliasAction, digest([]byte("alias")), int64(len("alias")), bytes.NewReader([]byte("alias"))); err == nil {
|
|
86
|
+
t.Fatal("aliased body accepted")
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if err := reopened.close(); err != nil {
|
|
90
|
+
t.Fatal(err)
|
|
91
|
+
}
|
|
92
|
+
if err := os.Remove(secondPath); err != nil {
|
|
93
|
+
t.Fatal(err)
|
|
94
|
+
}
|
|
95
|
+
recovering, err := openArtifactStore(root, nil)
|
|
96
|
+
if err != nil {
|
|
97
|
+
t.Fatal(err)
|
|
98
|
+
}
|
|
99
|
+
defer recovering.close()
|
|
100
|
+
_, _, found, err = recovering.get(action)
|
|
101
|
+
if err != nil || found {
|
|
102
|
+
t.Fatalf("an interrupted cleanup's missing body did not become a miss: %v %v", found, err)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"crypto/sha256"
|
|
7
|
+
"encoding/hex"
|
|
8
|
+
"encoding/json"
|
|
9
|
+
"errors"
|
|
10
|
+
"fmt"
|
|
11
|
+
"io"
|
|
12
|
+
"os"
|
|
13
|
+
"os/exec"
|
|
14
|
+
"path/filepath"
|
|
15
|
+
"regexp"
|
|
16
|
+
"strings"
|
|
17
|
+
"time"
|
|
18
|
+
|
|
19
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/packageartifacts"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
func readGoTuple(goBinary string) (packageartifacts.Tuple, error) {
|
|
23
|
+
var tuple packageartifacts.Tuple
|
|
24
|
+
if !filepath.IsAbs(goBinary) {
|
|
25
|
+
return tuple, errors.New("artifact Go executable must be absolute")
|
|
26
|
+
}
|
|
27
|
+
info, err := os.Stat(goBinary)
|
|
28
|
+
if err != nil || !info.Mode().IsRegular() || info.Size() > 256<<20 {
|
|
29
|
+
return tuple, errors.New("artifact Go executable is missing or too large")
|
|
30
|
+
}
|
|
31
|
+
file, err := os.Open(goBinary)
|
|
32
|
+
if err != nil {
|
|
33
|
+
return tuple, err
|
|
34
|
+
}
|
|
35
|
+
goHash := sha256.New()
|
|
36
|
+
_, copyErr := io.Copy(goHash, io.LimitReader(file, (256<<20)+1))
|
|
37
|
+
closeErr := file.Close()
|
|
38
|
+
if copyErr != nil || closeErr != nil {
|
|
39
|
+
return tuple, errors.Join(copyErr, closeErr)
|
|
40
|
+
}
|
|
41
|
+
output, err := runGoProbe(goBinary, "env", "-json", "GOVERSION", "GOOS", "GOARCH", "GOAMD64", "GOARM", "GOARM64", "GO386", "GOMIPS", "GOMIPS64", "GOPPC64", "GORISCV64", "GOWASM", "CGO_ENABLED", "GOEXPERIMENT", "GOFLAGS")
|
|
42
|
+
if err != nil {
|
|
43
|
+
return tuple, err
|
|
44
|
+
}
|
|
45
|
+
var values map[string]string
|
|
46
|
+
if err := json.Unmarshal(output, &values); err != nil {
|
|
47
|
+
return tuple, fmt.Errorf("decode Go tuple: %w", err)
|
|
48
|
+
}
|
|
49
|
+
if values["CGO_ENABLED"] != "0" || values["GOFLAGS"] != "" {
|
|
50
|
+
return tuple, errors.New("publisher artifacts require effective CGO_ENABLED=0 and no GOFLAGS")
|
|
51
|
+
}
|
|
52
|
+
if !supportedGoVersion.MatchString(values["GOVERSION"]) {
|
|
53
|
+
return tuple, errors.New("publisher cache protocol requires a stock Go 1.26 patch release")
|
|
54
|
+
}
|
|
55
|
+
if values["GOVERSION"] == "" || values["GOOS"] == "" || values["GOARCH"] == "" {
|
|
56
|
+
return tuple, errors.New("Go did not report a complete artifact tuple")
|
|
57
|
+
}
|
|
58
|
+
toolHash := sha256.New()
|
|
59
|
+
for _, name := range []string{"asm", "cgo", "compile", "cover", "link", "vet"} {
|
|
60
|
+
version, err := runGoProbe(goBinary, "tool", name, "-V=full")
|
|
61
|
+
if err != nil {
|
|
62
|
+
return tuple, fmt.Errorf("Go tool %s identity unavailable: %w", name, err)
|
|
63
|
+
}
|
|
64
|
+
_, _ = fmt.Fprintf(toolHash, "%s:%d:%s\n", name, len(version), version)
|
|
65
|
+
}
|
|
66
|
+
architecture := make(map[string]string)
|
|
67
|
+
for _, key := range []string{"GOAMD64", "GOARM", "GOARM64", "GO386", "GOMIPS", "GOMIPS64", "GOPPC64", "GORISCV64", "GOWASM"} {
|
|
68
|
+
if value := values[key]; value != "" {
|
|
69
|
+
architecture[key] = value
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return packageartifacts.Tuple{
|
|
73
|
+
GoVersion: values["GOVERSION"], GoToolSHA256: hex.EncodeToString(goHash.Sum(nil)),
|
|
74
|
+
GOOS: values["GOOS"], GOARCH: values["GOARCH"], Architecture: architecture,
|
|
75
|
+
CGOEnabled: values["CGO_ENABLED"], GoExperiment: values["GOEXPERIMENT"],
|
|
76
|
+
BuildFlags: []string{"-trimpath"}, BuildTags: []string{},
|
|
77
|
+
ToolIDsSHA256: hex.EncodeToString(toolHash.Sum(nil)),
|
|
78
|
+
}, nil
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
var supportedGoVersion = regexp.MustCompile(`^go1\.26\.(0|[1-9][0-9]*)$`)
|
|
82
|
+
|
|
83
|
+
func runGoProbe(goBinary string, arguments ...string) ([]byte, error) {
|
|
84
|
+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
85
|
+
defer cancel()
|
|
86
|
+
command := exec.CommandContext(ctx, goBinary, arguments...)
|
|
87
|
+
output := boundedBuffer{limit: 64 << 10}
|
|
88
|
+
command.Stdout = &output
|
|
89
|
+
diagnostic := boundedBuffer{limit: 16 << 10}
|
|
90
|
+
command.Stderr = &diagnostic
|
|
91
|
+
if err := command.Run(); err != nil {
|
|
92
|
+
return nil, fmt.Errorf("stock Go probe %s failed: %w (%s)", strings.Join(arguments, " "), err, strings.TrimSpace(diagnostic.String()))
|
|
93
|
+
}
|
|
94
|
+
return output.Bytes(), nil
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type boundedBuffer struct {
|
|
98
|
+
bytes.Buffer
|
|
99
|
+
limit int
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func (buffer *boundedBuffer) Write(value []byte) (int, error) {
|
|
103
|
+
if len(value) > buffer.limit-buffer.Len() {
|
|
104
|
+
return 0, errors.New("stock Go identity output is too large")
|
|
105
|
+
}
|
|
106
|
+
return buffer.Buffer.Write(value)
|
|
107
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/sha256"
|
|
5
|
+
"encoding/hex"
|
|
6
|
+
"os"
|
|
7
|
+
"os/exec"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"runtime"
|
|
10
|
+
"testing"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// TestReadGoTuple verifies the selected stock-Go tool and effective build mode.
|
|
14
|
+
//
|
|
15
|
+
// Verifies: an eligible native tuple names the actual tool bytes and reported architecture, while unsupported flags and CGO retain source-build selection.
|
|
16
|
+
// 1. Probe the host Go executable with CGO disabled and no ambient GOFLAGS.
|
|
17
|
+
// 2. Compare its digest and version with independent Go and file readings.
|
|
18
|
+
// 3. Set GOFLAGS and CGO independently and require tuple rejection.
|
|
19
|
+
//
|
|
20
|
+
// @evidence contracts/testing.md#behavioral-verification Calls the production tuple probe against the actual installed Go command and observes both eligible fields and rejected environments.
|
|
21
|
+
// @evidence contracts/testing.md#independent-expectations Direct file SHA-256 and runtime's Go version provide independent tool expectations; Go's effective environment, not a caller's guessed default, decides CGO eligibility.
|
|
22
|
+
// @evidence contracts/testing.md#distinguishing-cases Covers a complete CGO=0 tuple, an effective flag override and CGO=1; release manifest authentication is covered by the parser test.
|
|
23
|
+
// @evidence contracts/testing.md#execution-ownership Runs bounded local Go identity probes without building source modules, downloading packages or writing repository files.
|
|
24
|
+
func TestReadGoTuple(t *testing.T) {
|
|
25
|
+
if !supportedGoVersion.MatchString(runtime.Version()) {
|
|
26
|
+
t.Skip("the artifact tuple requires a stock Go 1.26 patch release")
|
|
27
|
+
}
|
|
28
|
+
goBinary, err := exec.LookPath("go")
|
|
29
|
+
if err != nil {
|
|
30
|
+
t.Fatal(err)
|
|
31
|
+
}
|
|
32
|
+
goBinary, err = filepath.Abs(goBinary)
|
|
33
|
+
if err != nil {
|
|
34
|
+
t.Fatal(err)
|
|
35
|
+
}
|
|
36
|
+
t.Setenv("CGO_ENABLED", "0")
|
|
37
|
+
t.Setenv("GOFLAGS", "")
|
|
38
|
+
t.Setenv("GOOS", runtime.GOOS)
|
|
39
|
+
t.Setenv("GOARCH", runtime.GOARCH)
|
|
40
|
+
t.Setenv("GOTOOLCHAIN", "local")
|
|
41
|
+
tuple, err := readGoTuple(goBinary)
|
|
42
|
+
if err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
raw, err := os.ReadFile(goBinary)
|
|
46
|
+
if err != nil {
|
|
47
|
+
t.Fatal(err)
|
|
48
|
+
}
|
|
49
|
+
digest := sha256.Sum256(raw)
|
|
50
|
+
if tuple.GoToolSHA256 != hex.EncodeToString(digest[:]) || tuple.GoVersion != runtime.Version() || tuple.GOOS != runtime.GOOS || tuple.GOARCH != runtime.GOARCH || tuple.CGOEnabled != "0" || tuple.ToolIDsSHA256 == "" {
|
|
51
|
+
t.Fatalf("reported tuple differs from the selected Go tool: %+v", tuple)
|
|
52
|
+
}
|
|
53
|
+
t.Setenv("GOFLAGS", "-tags=foreign")
|
|
54
|
+
if _, err := readGoTuple(goBinary); err == nil {
|
|
55
|
+
t.Fatal("GOFLAGS override was admitted")
|
|
56
|
+
}
|
|
57
|
+
t.Setenv("GOFLAGS", "")
|
|
58
|
+
t.Setenv("CGO_ENABLED", "1")
|
|
59
|
+
if _, err := readGoTuple(goBinary); err == nil {
|
|
60
|
+
t.Fatal("effective CGO mode was admitted")
|
|
61
|
+
}
|
|
62
|
+
}
|