ttsc 0.32.1 → 0.33.0-next.1
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/internal/packageartifacts/manifest.go +288 -0
- package/internal/packageartifacts/manifest_test.go +127 -0
- package/lib/launcher/internal/watch/WatchTopology.js +5 -4
- package/lib/launcher/internal/watch/WatchTopology.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/package.json +8 -8
- package/src/launcher/internal/watch/WatchTopology.ts +5 -4
- 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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"errors"
|
|
5
|
+
"fmt"
|
|
6
|
+
"io"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
type artifactAccounting struct {
|
|
12
|
+
BaseBytes int64 `json:"baseBytes"`
|
|
13
|
+
OverlayBytes int64 `json:"overlayBytes"`
|
|
14
|
+
StagingBytes int64 `json:"stagingBytes"`
|
|
15
|
+
PinnedBytes int64 `json:"pinnedBytes"`
|
|
16
|
+
BaseFiles int64 `json:"baseFiles"`
|
|
17
|
+
OverlayFiles int64 `json:"overlayFiles"`
|
|
18
|
+
StagingFiles int64 `json:"stagingFiles"`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// accountArtifactStore reads bounded rooted metadata for every retained cache
|
|
22
|
+
// population. The byte totals are logical file lengths, not free-space or
|
|
23
|
+
// physical allocated-byte claims; concurrent writers can change them later.
|
|
24
|
+
func accountArtifactStore(store *artifactStore) (artifactAccounting, error) {
|
|
25
|
+
var result artifactAccounting
|
|
26
|
+
var entries int
|
|
27
|
+
var walk func(string, int, *int64, *int64) error
|
|
28
|
+
walk = func(directory string, depth int, bytes, files *int64) error {
|
|
29
|
+
if depth > 4 {
|
|
30
|
+
return errors.New("artifact storage exceeds directory depth policy")
|
|
31
|
+
}
|
|
32
|
+
opened, err := store.root.Open(directory)
|
|
33
|
+
if err != nil {
|
|
34
|
+
return err
|
|
35
|
+
}
|
|
36
|
+
defer opened.Close()
|
|
37
|
+
for {
|
|
38
|
+
batch, readErr := opened.ReadDir(256)
|
|
39
|
+
for _, entry := range batch {
|
|
40
|
+
entries++
|
|
41
|
+
if entries > 100000 {
|
|
42
|
+
return errors.New("artifact storage exceeds entry count policy")
|
|
43
|
+
}
|
|
44
|
+
child := filepath.ToSlash(filepath.Join(directory, entry.Name()))
|
|
45
|
+
info, err := store.root.Lstat(child)
|
|
46
|
+
if err != nil {
|
|
47
|
+
return err
|
|
48
|
+
}
|
|
49
|
+
if info.IsDir() && info.Mode()&os.ModeSymlink == 0 {
|
|
50
|
+
if err := walk(child, depth+1, bytes, files); err != nil {
|
|
51
|
+
return err
|
|
52
|
+
}
|
|
53
|
+
} else if info.Mode().IsRegular() {
|
|
54
|
+
if info.Size() < 0 || info.Size() > 1<<40 || *bytes > (1<<50)-info.Size() {
|
|
55
|
+
return fmt.Errorf("artifact storage has an invalid file size: %s", child)
|
|
56
|
+
}
|
|
57
|
+
*bytes += info.Size()
|
|
58
|
+
*files += 1
|
|
59
|
+
} else {
|
|
60
|
+
return fmt.Errorf("artifact storage contains an aliased or unknown entry: %s", child)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if errors.Is(readErr, io.EOF) {
|
|
64
|
+
return nil
|
|
65
|
+
}
|
|
66
|
+
if readErr != nil {
|
|
67
|
+
return readErr
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for _, part := range []struct {
|
|
72
|
+
path string
|
|
73
|
+
bytes *int64
|
|
74
|
+
files *int64
|
|
75
|
+
}{
|
|
76
|
+
{"base/packs", &result.BaseBytes, &result.BaseFiles},
|
|
77
|
+
{"base/staging", &result.StagingBytes, &result.StagingFiles},
|
|
78
|
+
{"overlay/bodies", &result.OverlayBytes, &result.OverlayFiles},
|
|
79
|
+
{"overlay/actions", &result.OverlayBytes, &result.OverlayFiles},
|
|
80
|
+
{"overlay/staging", &result.StagingBytes, &result.StagingFiles},
|
|
81
|
+
} {
|
|
82
|
+
if err := walk(part.path, 0, part.bytes, part.files); err != nil {
|
|
83
|
+
return artifactAccounting{}, err
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for _, pinned := range store.pins {
|
|
87
|
+
info, err := pinned.Stat()
|
|
88
|
+
if err != nil || !info.Mode().IsRegular() || info.Size() < 0 || result.PinnedBytes > (1<<50)-info.Size() {
|
|
89
|
+
return artifactAccounting{}, errors.New("artifact pinned body accounting failed")
|
|
90
|
+
}
|
|
91
|
+
result.PinnedBytes += info.Size()
|
|
92
|
+
}
|
|
93
|
+
return result, nil
|
|
94
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"bytes"
|
|
6
|
+
"crypto/sha256"
|
|
7
|
+
"encoding/base64"
|
|
8
|
+
"encoding/hex"
|
|
9
|
+
"encoding/json"
|
|
10
|
+
"errors"
|
|
11
|
+
"fmt"
|
|
12
|
+
"io"
|
|
13
|
+
"os"
|
|
14
|
+
"os/exec"
|
|
15
|
+
"path/filepath"
|
|
16
|
+
"strings"
|
|
17
|
+
"testing"
|
|
18
|
+
"time"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// TestCacheHelperInterruption verifies that an interrupted protocol operation cannot publish an incomplete action.
|
|
22
|
+
//
|
|
23
|
+
// Verifies: killing a helper during a streamed put leaves the new action absent, and a broken get response leaves an older complete action readable.
|
|
24
|
+
// 1. Seed a complete action, start the production helper protocol, and send only the beginning of a larger put body.
|
|
25
|
+
// 2. Kill the helper after its staging file appears, then reopen the store and compare the old hit and interrupted miss.
|
|
26
|
+
// 3. Break the response pipe during a get and reopen the store to require the same complete hit.
|
|
27
|
+
//
|
|
28
|
+
// @evidence contracts/testing.md#behavioral-verification Runs the production helper as a child through its public protocol, kills it during a partial streamed put, and interrupts the direct server's response writer on a real get.
|
|
29
|
+
// @evidence contracts/testing.md#independent-expectations An action mapping requires a complete body matching its SHA-256 OutputID; a partly received body has no valid mapping, while an earlier completed mapping must survive both failures.
|
|
30
|
+
// @evidence contracts/testing.md#distinguishing-cases Covers process death after a partial body is sent and a get response failure with a prior valid hit and a new missing action; stock Go's reaction to a bad helper is owned by TestStockGoCacheFailures.
|
|
31
|
+
// @evidence contracts/testing.md#execution-ownership This Go test uses a temporary local helper process and store; it does not install a consumer or run a product E2E build.
|
|
32
|
+
func TestCacheHelperInterruption(t *testing.T) {
|
|
33
|
+
root := t.TempDir()
|
|
34
|
+
oldAction := bytes.Repeat([]byte{0x31}, 32)
|
|
35
|
+
newAction := bytes.Repeat([]byte{0x32}, 32)
|
|
36
|
+
oldBody := []byte("completed archive")
|
|
37
|
+
oldOutput := sha256.Sum256(oldBody)
|
|
38
|
+
initial, err := openArtifactStore(root, nil)
|
|
39
|
+
if err != nil {
|
|
40
|
+
t.Fatal(err)
|
|
41
|
+
}
|
|
42
|
+
if _, err := initial.put(hex.EncodeToString(oldAction), hex.EncodeToString(oldOutput[:]), int64(len(oldBody)), bytes.NewReader(oldBody)); err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
if err := initial.close(); err != nil {
|
|
46
|
+
t.Fatal(err)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
helper := filepath.Join(t.TempDir(), "cache helper"+exeSuffix())
|
|
50
|
+
build := exec.Command("go", "build", "-trimpath", "-o", helper, ".")
|
|
51
|
+
build.Env = append(os.Environ(), "GOCACHEPROG=", "GOTOOLCHAIN=local")
|
|
52
|
+
if output, err := build.CombinedOutput(); err != nil {
|
|
53
|
+
t.Fatalf("building cache helper: %v\n%s", err, output)
|
|
54
|
+
}
|
|
55
|
+
command := exec.Command(helper, "-root", root)
|
|
56
|
+
stdin, err := command.StdinPipe()
|
|
57
|
+
if err != nil {
|
|
58
|
+
t.Fatal(err)
|
|
59
|
+
}
|
|
60
|
+
stdout, err := command.StdoutPipe()
|
|
61
|
+
if err != nil {
|
|
62
|
+
t.Fatal(err)
|
|
63
|
+
}
|
|
64
|
+
var stderr bytes.Buffer
|
|
65
|
+
command.Stderr = &stderr
|
|
66
|
+
if err := command.Start(); err != nil {
|
|
67
|
+
t.Fatal(err)
|
|
68
|
+
}
|
|
69
|
+
defer func() {
|
|
70
|
+
if command.ProcessState == nil {
|
|
71
|
+
command.Process.Kill()
|
|
72
|
+
command.Wait()
|
|
73
|
+
}
|
|
74
|
+
}()
|
|
75
|
+
startup, err := bufio.NewReader(stdout).ReadBytes('\n')
|
|
76
|
+
if err != nil {
|
|
77
|
+
t.Fatalf("reading helper startup: %v; stderr: %s", err, &stderr)
|
|
78
|
+
}
|
|
79
|
+
var capability response
|
|
80
|
+
if err := json.Unmarshal(startup, &capability); err != nil || fmt.Sprint(capability.KnownCommands) != "[get put close]" {
|
|
81
|
+
t.Fatalf("helper did not advertise the Go cache protocol: %s; %v", startup, err)
|
|
82
|
+
}
|
|
83
|
+
body := bytes.Repeat([]byte("interrupted archive body"), 50000)
|
|
84
|
+
outputID := sha256.Sum256(body)
|
|
85
|
+
header, err := json.Marshal(request{ID: 1, Command: "put", ActionID: newAction, OutputID: outputID[:], BodySize: int64(len(body))})
|
|
86
|
+
if err != nil {
|
|
87
|
+
t.Fatal(err)
|
|
88
|
+
}
|
|
89
|
+
if _, err := fmt.Fprintf(stdin, "%s\n\n\"%s", header, base64.StdEncoding.EncodeToString(body[:98304])); err != nil {
|
|
90
|
+
t.Fatalf("sending partial Go cache body: %v", err)
|
|
91
|
+
}
|
|
92
|
+
staging := filepath.Join(root, "overlay", "staging")
|
|
93
|
+
deadline := time.Now().Add(10 * time.Second)
|
|
94
|
+
staged := false
|
|
95
|
+
for time.Now().Before(deadline) {
|
|
96
|
+
entries, err := os.ReadDir(staging)
|
|
97
|
+
if err != nil {
|
|
98
|
+
t.Fatal(err)
|
|
99
|
+
}
|
|
100
|
+
staged = len(entries) > 0
|
|
101
|
+
if staged {
|
|
102
|
+
break
|
|
103
|
+
}
|
|
104
|
+
time.Sleep(10 * time.Millisecond)
|
|
105
|
+
}
|
|
106
|
+
if !staged {
|
|
107
|
+
entries, _ := os.ReadDir(staging)
|
|
108
|
+
command.Process.Kill()
|
|
109
|
+
command.Wait()
|
|
110
|
+
remaining, _ := io.ReadAll(stdout)
|
|
111
|
+
t.Fatalf("helper never opened a put staging file: entries=%d remaining=%q stderr=%s", len(entries), remaining, &stderr)
|
|
112
|
+
}
|
|
113
|
+
if err := command.Process.Kill(); err != nil {
|
|
114
|
+
t.Fatal(err)
|
|
115
|
+
}
|
|
116
|
+
command.Wait()
|
|
117
|
+
stdin.Close()
|
|
118
|
+
stdout.Close()
|
|
119
|
+
|
|
120
|
+
reopened, err := openArtifactStore(root, nil)
|
|
121
|
+
if err != nil {
|
|
122
|
+
t.Fatal(err)
|
|
123
|
+
}
|
|
124
|
+
_, _, found, err := reopened.get(hex.EncodeToString(newAction))
|
|
125
|
+
if err != nil || found {
|
|
126
|
+
t.Fatalf("interrupted put became a hit: found=%v error=%v", found, err)
|
|
127
|
+
}
|
|
128
|
+
oldValue, oldPath, found, err := reopened.get(hex.EncodeToString(oldAction))
|
|
129
|
+
if err != nil || !found || oldValue.OutputID != hex.EncodeToString(oldOutput[:]) {
|
|
130
|
+
t.Fatalf("interrupted put damaged the complete action: value=%+v found=%v error=%v", oldValue, found, err)
|
|
131
|
+
}
|
|
132
|
+
if raw, err := os.ReadFile(oldPath); err != nil || !bytes.Equal(raw, oldBody) {
|
|
133
|
+
t.Fatalf("complete body changed after interruption: %q %v", raw, err)
|
|
134
|
+
}
|
|
135
|
+
if err := reopened.close(); err != nil {
|
|
136
|
+
t.Fatal(err)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interruptedGet, err := openArtifactStore(root, nil)
|
|
140
|
+
if err != nil {
|
|
141
|
+
t.Fatal(err)
|
|
142
|
+
}
|
|
143
|
+
getHeader, err := json.Marshal(request{ID: 2, Command: "get", ActionID: oldAction})
|
|
144
|
+
if err != nil {
|
|
145
|
+
t.Fatal(err)
|
|
146
|
+
}
|
|
147
|
+
broken := &failSecondResponse{}
|
|
148
|
+
err = serve(strings.NewReader(string(getHeader)+"\n"), broken, interruptedGet)
|
|
149
|
+
if !errors.Is(err, errBrokenResponse) {
|
|
150
|
+
t.Fatalf("interrupted get did not report its broken response: %v", err)
|
|
151
|
+
}
|
|
152
|
+
if err := interruptedGet.close(); err != nil {
|
|
153
|
+
t.Fatal(err)
|
|
154
|
+
}
|
|
155
|
+
settled, err := openArtifactStore(root, nil)
|
|
156
|
+
if err != nil {
|
|
157
|
+
t.Fatal(err)
|
|
158
|
+
}
|
|
159
|
+
defer settled.close()
|
|
160
|
+
value, location, found, err := settled.get(hex.EncodeToString(oldAction))
|
|
161
|
+
if err != nil || !found || value.OutputID != hex.EncodeToString(oldOutput[:]) {
|
|
162
|
+
t.Fatalf("interrupted get damaged the complete action: value=%+v path=%q found=%v error=%v", value, location, found, err)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
var errBrokenResponse = errors.New("client response pipe closed")
|
|
167
|
+
|
|
168
|
+
type failSecondResponse struct{ writes int }
|
|
169
|
+
|
|
170
|
+
func (writer *failSecondResponse) Write(value []byte) (int, error) {
|
|
171
|
+
writer.writes++
|
|
172
|
+
if writer.writes == 2 {
|
|
173
|
+
return 0, errBrokenResponse
|
|
174
|
+
}
|
|
175
|
+
return len(value), nil
|
|
176
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"bytes"
|
|
6
|
+
"context"
|
|
7
|
+
"encoding/base64"
|
|
8
|
+
"encoding/hex"
|
|
9
|
+
"encoding/json"
|
|
10
|
+
"errors"
|
|
11
|
+
"flag"
|
|
12
|
+
"fmt"
|
|
13
|
+
"io"
|
|
14
|
+
"os"
|
|
15
|
+
"path/filepath"
|
|
16
|
+
"strings"
|
|
17
|
+
|
|
18
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/packageartifacts"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
type request struct {
|
|
22
|
+
ID int64
|
|
23
|
+
Command string
|
|
24
|
+
ActionID []byte
|
|
25
|
+
OutputID []byte
|
|
26
|
+
BodySize int64
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type response struct {
|
|
30
|
+
ID int64
|
|
31
|
+
Err string `json:",omitempty"`
|
|
32
|
+
KnownCommands []string `json:",omitempty"`
|
|
33
|
+
Miss bool `json:",omitempty"`
|
|
34
|
+
OutputID []byte `json:",omitempty"`
|
|
35
|
+
Size int64 `json:",omitempty"`
|
|
36
|
+
DiskPath string `json:",omitempty"`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func main() {
|
|
40
|
+
if err := run(); err != nil {
|
|
41
|
+
fmt.Fprintln(os.Stderr, "ttsc cache helper:", err)
|
|
42
|
+
os.Exit(1)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func run() error {
|
|
47
|
+
rootPath := flag.String("root", "", "owned artifact cache root")
|
|
48
|
+
manifestPath := flag.String("manifest", "", "authenticated publisher manifest")
|
|
49
|
+
manifestDigest := flag.String("manifest-sha256", "", "trusted release metadata digest")
|
|
50
|
+
prepare := flag.Bool("prepare", false, "download and verify selected packs before a Go build")
|
|
51
|
+
packNames := flag.String("packs", "", "comma-separated selected pack names")
|
|
52
|
+
tupleMode := flag.Bool("tuple", false, "report the effective stock-Go artifact tuple")
|
|
53
|
+
goBinary := flag.String("go", "", "regular Go executable used by the source builder")
|
|
54
|
+
seal := flag.Bool("seal", false, "seal recorded publisher actions into an immutable pack")
|
|
55
|
+
merge := flag.Bool("merge", false, "merge independently sealed publisher packs")
|
|
56
|
+
plan := flag.String("plan", "", "absolute JSON release merge plan")
|
|
57
|
+
verify := flag.Bool("verify", false, "validate a trusted release manifest without using a cache")
|
|
58
|
+
actionGraph := flag.String("actiongraph", "", "Go -debug-actiongraph JSON from the recording build")
|
|
59
|
+
packageReceipts := flag.String("packages", "", "Go list -deps -json -compiled receipts from the same workspace")
|
|
60
|
+
release := flag.String("release", "", "ttsc release version")
|
|
61
|
+
sourceCommit := flag.String("source-commit", "", "full source commit")
|
|
62
|
+
packURLBase := flag.String("pack-url-base", "", "immutable pack URL directory")
|
|
63
|
+
packName := flag.String("pack-name", "", "safe transport pack name")
|
|
64
|
+
outputDir := flag.String("out", "", "publisher output directory")
|
|
65
|
+
receiptName := flag.String("receipt-name", "", "optional local per-build usage receipt ID")
|
|
66
|
+
flag.Parse()
|
|
67
|
+
if *merge {
|
|
68
|
+
if *plan == "" || *rootPath != "" || *manifestPath != "" || *manifestDigest != "" || *prepare || *packNames != "" || *tupleMode || *goBinary != "" || *seal || *verify || *actionGraph != "" || *packageReceipts != "" || *release != "" || *sourceCommit != "" || *packURLBase != "" || *packName != "" || *outputDir != "" || *receiptName != "" {
|
|
69
|
+
return errors.New("supply only -merge and -plan for release union")
|
|
70
|
+
}
|
|
71
|
+
return MergeRecordedPacks(*plan)
|
|
72
|
+
}
|
|
73
|
+
if *plan != "" {
|
|
74
|
+
return errors.New("-plan requires -merge")
|
|
75
|
+
}
|
|
76
|
+
if *verify {
|
|
77
|
+
if *manifestPath == "" || *manifestDigest == "" || *rootPath != "" || *prepare || *packNames != "" || *tupleMode || *goBinary != "" || *seal || *actionGraph != "" || *packageReceipts != "" || *release != "" || *sourceCommit != "" || *packURLBase != "" || *packName != "" || *outputDir != "" || *receiptName != "" {
|
|
78
|
+
return errors.New("supply only -verify and trusted manifest arguments")
|
|
79
|
+
}
|
|
80
|
+
_, err := loadTrustedManifest(*manifestPath, *manifestDigest)
|
|
81
|
+
return err
|
|
82
|
+
}
|
|
83
|
+
if *seal {
|
|
84
|
+
if *rootPath == "" || *goBinary == "" || *actionGraph == "" || *packageReceipts == "" || *release == "" || *sourceCommit == "" || *packURLBase == "" || *packName == "" || *outputDir == "" || *manifestPath != "" || *manifestDigest != "" || *prepare || *tupleMode || *packNames != "" || *receiptName != "" {
|
|
85
|
+
return errors.New("incomplete or conflicting publisher seal arguments")
|
|
86
|
+
}
|
|
87
|
+
return SealRecordedActions(sealOptions{root: *rootPath, goBinary: *goBinary, actionGraph: *actionGraph, packages: *packageReceipts, release: *release, sourceCommit: *sourceCommit, packURLBase: *packURLBase, packName: *packName, outputDir: *outputDir})
|
|
88
|
+
}
|
|
89
|
+
if *tupleMode {
|
|
90
|
+
if *goBinary == "" || *rootPath != "" || *manifestPath != "" || *manifestDigest != "" || *prepare || *packNames != "" || *receiptName != "" {
|
|
91
|
+
return errors.New("supply only -tuple and -go for tuple inspection")
|
|
92
|
+
}
|
|
93
|
+
value, err := readGoTuple(*goBinary)
|
|
94
|
+
if err != nil {
|
|
95
|
+
return err
|
|
96
|
+
}
|
|
97
|
+
return json.NewEncoder(os.Stdout).Encode(value)
|
|
98
|
+
}
|
|
99
|
+
if *rootPath == "" || (*manifestPath == "") != (*manifestDigest == "") || (*prepare && (*manifestPath == "" || *packNames == "")) || (!*prepare && *packNames != "") || (*prepare && *receiptName != "") || (*receiptName != "" && !validReceiptName(*receiptName)) {
|
|
100
|
+
return errors.New("supply -root and either both publisher manifest arguments or neither")
|
|
101
|
+
}
|
|
102
|
+
var manifest *packageartifacts.Manifest
|
|
103
|
+
if *manifestPath != "" {
|
|
104
|
+
decoded, err := loadTrustedManifest(*manifestPath, *manifestDigest)
|
|
105
|
+
if err != nil {
|
|
106
|
+
return err
|
|
107
|
+
}
|
|
108
|
+
manifest = &decoded
|
|
109
|
+
}
|
|
110
|
+
store, err := openArtifactStore(*rootPath, manifest)
|
|
111
|
+
if err != nil {
|
|
112
|
+
return err
|
|
113
|
+
}
|
|
114
|
+
defer store.close()
|
|
115
|
+
store.receiptName = *receiptName
|
|
116
|
+
if *prepare {
|
|
117
|
+
receipt, err := preparePacks(context.Background(), store, *manifest, strings.Split(*packNames, ","))
|
|
118
|
+
if err != nil {
|
|
119
|
+
return err
|
|
120
|
+
}
|
|
121
|
+
return json.NewEncoder(os.Stdout).Encode(receipt)
|
|
122
|
+
}
|
|
123
|
+
return serve(os.Stdin, os.Stdout, store)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func loadTrustedManifest(location, digest string) (packageartifacts.Manifest, error) {
|
|
127
|
+
raw, err := readBoundedFile(location, packageartifacts.MaxManifestBytes)
|
|
128
|
+
if err != nil {
|
|
129
|
+
return packageartifacts.Manifest{}, err
|
|
130
|
+
}
|
|
131
|
+
return packageartifacts.ParseTrustedManifest(raw, digest)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func serve(source io.Reader, destination io.Writer, store *artifactStore) error {
|
|
135
|
+
usage := usageReceipt{
|
|
136
|
+
BaseActionIDs: []string{}, OverlayActionIDs: []string{},
|
|
137
|
+
MissActionIDs: []string{}, PutActionIDs: []string{},
|
|
138
|
+
}
|
|
139
|
+
input := bufio.NewReaderSize(source, 64<<10)
|
|
140
|
+
output := bufio.NewWriter(destination)
|
|
141
|
+
if err := writeResponse(output, response{KnownCommands: []string{"get", "put", "close"}}); err != nil {
|
|
142
|
+
return err
|
|
143
|
+
}
|
|
144
|
+
for {
|
|
145
|
+
line, err := input.ReadSlice('\n')
|
|
146
|
+
if err != nil {
|
|
147
|
+
return fmt.Errorf("reading Go cache request: %w", err)
|
|
148
|
+
}
|
|
149
|
+
if len(bytes.TrimSpace(line)) == 0 {
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
decoder := json.NewDecoder(bytes.NewReader(line))
|
|
153
|
+
decoder.DisallowUnknownFields()
|
|
154
|
+
var current request
|
|
155
|
+
if err := decoder.Decode(¤t); err != nil {
|
|
156
|
+
return fmt.Errorf("decoding Go cache request: %w", err)
|
|
157
|
+
}
|
|
158
|
+
answer := response{ID: current.ID}
|
|
159
|
+
switch current.Command {
|
|
160
|
+
case "get":
|
|
161
|
+
usage.Gets++
|
|
162
|
+
if len(current.ActionID) != 32 {
|
|
163
|
+
answer.Err = "invalid full Go ActionID"
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
actionID := hex.EncodeToString(current.ActionID)
|
|
167
|
+
value, location, found, err := store.get(actionID)
|
|
168
|
+
if err != nil {
|
|
169
|
+
answer.Err = err.Error()
|
|
170
|
+
} else if !found {
|
|
171
|
+
answer.Miss = true
|
|
172
|
+
usage.Misses++
|
|
173
|
+
usage.record(&usage.MissActionIDs, actionID)
|
|
174
|
+
} else {
|
|
175
|
+
if strings.HasPrefix(location, filepath.Join(store.path, "base")+string(os.PathSeparator)) {
|
|
176
|
+
usage.BaseHits++
|
|
177
|
+
usage.record(&usage.BaseActionIDs, actionID)
|
|
178
|
+
} else {
|
|
179
|
+
usage.OverlayHits++
|
|
180
|
+
usage.record(&usage.OverlayActionIDs, actionID)
|
|
181
|
+
}
|
|
182
|
+
answer.OutputID, _ = hex.DecodeString(value.OutputID)
|
|
183
|
+
answer.Size = value.Size
|
|
184
|
+
answer.DiskPath = location
|
|
185
|
+
}
|
|
186
|
+
case "put":
|
|
187
|
+
if len(current.ActionID) != 32 || len(current.OutputID) != 32 || current.BodySize < 0 || current.BodySize > 512<<20 {
|
|
188
|
+
return errors.New("invalid full Go cache put identity or body size")
|
|
189
|
+
}
|
|
190
|
+
// Go 1.26 writes an extra newline after the encoded request.
|
|
191
|
+
separator, err := input.ReadByte()
|
|
192
|
+
if err != nil || separator != '\n' {
|
|
193
|
+
return errors.New("missing Go cache request separator")
|
|
194
|
+
}
|
|
195
|
+
var body io.Reader = bytes.NewReader(nil)
|
|
196
|
+
if current.BodySize > 0 {
|
|
197
|
+
quote, err := input.ReadByte()
|
|
198
|
+
if err != nil || quote != '"' {
|
|
199
|
+
return errors.New("missing Go cache body framing")
|
|
200
|
+
}
|
|
201
|
+
encodedLength := ((current.BodySize + 2) / 3) * 4
|
|
202
|
+
body = base64.NewDecoder(base64.StdEncoding, io.LimitReader(input, encodedLength))
|
|
203
|
+
}
|
|
204
|
+
location, putErr := store.put(hex.EncodeToString(current.ActionID), hex.EncodeToString(current.OutputID), current.BodySize, body)
|
|
205
|
+
if current.BodySize > 0 {
|
|
206
|
+
end, err := input.ReadByte()
|
|
207
|
+
if err != nil || end != '"' {
|
|
208
|
+
return errors.New("truncated Go cache body framing")
|
|
209
|
+
}
|
|
210
|
+
end, err = input.ReadByte()
|
|
211
|
+
if err != nil || end != '\n' {
|
|
212
|
+
return errors.New("invalid Go cache body terminator")
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if putErr != nil {
|
|
216
|
+
answer.Err = putErr.Error()
|
|
217
|
+
} else {
|
|
218
|
+
answer.DiskPath = location
|
|
219
|
+
usage.Puts++
|
|
220
|
+
usage.PutBytes += current.BodySize
|
|
221
|
+
usage.record(&usage.PutActionIDs, hex.EncodeToString(current.ActionID))
|
|
222
|
+
}
|
|
223
|
+
case "close":
|
|
224
|
+
if store.receiptName != "" {
|
|
225
|
+
if accounting, err := accountArtifactStore(store); err == nil {
|
|
226
|
+
usage.Storage = &accounting
|
|
227
|
+
} else {
|
|
228
|
+
fmt.Fprintln(os.Stderr, "ttsc cache helper: storage accounting unavailable:", err)
|
|
229
|
+
}
|
|
230
|
+
if err := writeUsageReceipt(store, usage); err != nil {
|
|
231
|
+
fmt.Fprintln(os.Stderr, "ttsc cache helper: usage receipt unavailable:", err)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if err := writeResponse(output, answer); err != nil {
|
|
235
|
+
return err
|
|
236
|
+
}
|
|
237
|
+
return nil
|
|
238
|
+
default:
|
|
239
|
+
answer.Err = "unsupported Go cache command"
|
|
240
|
+
}
|
|
241
|
+
if err := writeResponse(output, answer); err != nil {
|
|
242
|
+
return err
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
func writeResponse(output *bufio.Writer, value response) error {
|
|
248
|
+
if err := json.NewEncoder(output).Encode(value); err != nil {
|
|
249
|
+
return err
|
|
250
|
+
}
|
|
251
|
+
return output.Flush()
|
|
252
|
+
}
|