Skip to content

Commit 082cb2e

Browse files
committed
chore(cli): implement Paket 2.1 DX hardening (skip-checks, ringbuffer, rename-to-trash, matrix async)
1 parent 85f2a93 commit 082cb2e

9 files changed

Lines changed: 449 additions & 84 deletions

File tree

cli/toob-cli/cmd/build.go

Lines changed: 259 additions & 52 deletions
Large diffs are not rendered by default.

cli/toob-cli/cmd/clean.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,47 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"time"
78

89
"github.com/spf13/cobra"
910
"github.com/toob-boot/toob/internal/paths"
1011
)
1112

13+
var flagToolchains bool
14+
15+
func init() {
16+
cleanCmd.Flags().BoolVar(&flagToolchains, "toolchains", false, "Remove all globally cached cross-compiler toolchains to free up disk space")
17+
}
18+
1219
var cleanCmd = &cobra.Command{
1320
Use: "clean",
1421
Short: "Remove all build artifacts (builds/ directory)",
1522
RunE: func(cmd *cobra.Command, args []string) error {
23+
if flagToolchains {
24+
home, err := os.UserHomeDir()
25+
if err != nil {
26+
return err
27+
}
28+
tcDir := filepath.Join(home, ".toob", "toolchains")
29+
if _, err := os.Stat(tcDir); os.IsNotExist(err) {
30+
fmt.Println("[toob] No toolchains found. Nothing to clean.")
31+
return nil
32+
}
33+
fmt.Printf("[toob] Removing globally cached toolchains at %s ...\n", tcDir)
34+
// Safe rename-to-trash pattern for Windows locking safety
35+
trashDir := filepath.Join(home, ".toob", ".trash", "toolchains-"+time.Now().Format("20060102150405"))
36+
os.MkdirAll(filepath.Dir(trashDir), 0o755)
37+
if err := os.Rename(tcDir, trashDir); err != nil {
38+
return fmt.Errorf("failed to unlock toolchains directory (is a file currently open in an IDE or terminal?): %w", err)
39+
}
40+
41+
if err := os.RemoveAll(trashDir); err != nil {
42+
fmt.Printf("\033[33m[toob] Warning: Could not fully delete all files (some are locked), but toolchains are deactivated.\033[0m\n")
43+
}
44+
fmt.Println("\033[32m[toob] Successfully freed disk space.\033[0m")
45+
return nil
46+
}
47+
1648
root, err := paths.FindProjectRoot("")
1749
if err != nil || root == "" {
1850
return fmt.Errorf("not in a Toob-Loader project (device.toml not found)")

cli/toob-cli/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
github.com/rivo/uniseg v0.4.7 // indirect
2020
github.com/spf13/pflag v1.0.9 // indirect
2121
golang.org/x/crypto v0.45.0 // indirect
22+
golang.org/x/sync v0.20.0 // indirect
2223
golang.org/x/sys v0.38.0 // indirect
2324
golang.org/x/term v0.37.0 // indirect
2425
)

cli/toob-cli/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
4343
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
4444
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
4545
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
46+
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
47+
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
4648
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
4749
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
4850
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

cli/toob-cli/internal/installer/installer.go

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ func parseChipArg(arg string) (string, string) {
4848

4949
// Add installs a chip from the registry (not tracked by git).
5050
func (inst *Installer) Add(arg string) error {
51+
// Async Matrix Fetch (Gap 4.1)
52+
matrixChan := make(chan *registry.Matrix, 1)
53+
go func() {
54+
m, _ := inst.cache.FetchLiveMatrix()
55+
matrixChan <- m
56+
}()
5157
name, version := parseChipArg(arg)
5258
if inst.lock.HasChip(name) {
5359
e := inst.lock.GetChip(name)
@@ -115,16 +121,26 @@ func (inst *Installer) Add(arg string) error {
115121
}
116122
fmt.Printf("Added chip '%s' (v%s) to lockfile [arch=%s, vendor=%s].\n", name, ci.Version, ci.Arch, ci.Vendor)
117123

118-
if !ci.Verified {
119-
fmt.Println("\n\033[33mWarning: This hardware configuration is marked as UNVERIFIED by the CI Compatibility Matrix. Build stability is not guaranteed.\033[0m")
124+
// Wait up to 1 second for the matrix to avoid blocking
125+
var matrix *registry.Matrix
126+
select {
127+
case matrix = <-matrixChan:
128+
case <-time.After(1 * time.Second):
120129
}
130+
printMatrixCompatibility(matrix, ci.Name, ci.Version, ci.Verified)
121131

122132
fmt.Println("Registry link established. Run `toob build` to compile.")
123133
return nil
124134
}
125135

126136
// Spawn installs a chip as locally editable (tracked by git).
127137
func (inst *Installer) Spawn(arg string) error {
138+
// Async Matrix Fetch (Gap 4.1)
139+
matrixChan := make(chan *registry.Matrix, 1)
140+
go func() {
141+
m, _ := inst.cache.FetchLiveMatrix()
142+
matrixChan <- m
143+
}()
128144
name, version := parseChipArg(arg)
129145
if inst.lock.HasChip(name) {
130146
e := inst.lock.GetChip(name)
@@ -229,9 +245,13 @@ func (inst *Installer) Spawn(arg string) error {
229245
}
230246
fmt.Printf("Spawned chip '%s' (v%s) [locally editable]\n", name, ci.Version)
231247

232-
if !ci.Verified {
233-
fmt.Println("\n\033[33mWarning: This hardware configuration is marked as UNVERIFIED by the CI Compatibility Matrix. Build stability is not guaranteed.\033[0m")
248+
// Wait up to 1 second for the matrix to avoid blocking
249+
var matrix *registry.Matrix
250+
select {
251+
case matrix = <-matrixChan:
252+
case <-time.After(1 * time.Second):
234253
}
254+
printMatrixCompatibility(matrix, ci.Name, ci.Version, ci.Verified)
235255

236256
return nil
237257
}
@@ -246,6 +266,36 @@ func moveToTrash(dir string) {
246266
os.Rename(dir, trashDir)
247267
}
248268

269+
func printMatrixCompatibility(matrix *registry.Matrix, chipName, chipVersion string, verified bool) {
270+
if !verified {
271+
fmt.Println("\n\033[33mWarning: This hardware configuration is marked as UNVERIFIED by the CI Compatibility Matrix. Build stability is not guaranteed.\033[0m")
272+
return
273+
}
274+
275+
if matrix == nil {
276+
return
277+
}
278+
279+
if chipEntry, has := (*matrix)[chipName]; has {
280+
// Normalize version string to handle mismatching prefixes (Gap 4.2)
281+
searchVer := strings.TrimPrefix(chipVersion, "v")
282+
for vKey, verEntry := range chipEntry.Versions {
283+
if strings.TrimPrefix(vKey, "v") == searchVer {
284+
var verifiedClis []string
285+
for cliVer, info := range verEntry.VerifiedCliVersions {
286+
if info.Status == "SUCCESS" {
287+
verifiedClis = append(verifiedClis, cliVer)
288+
}
289+
}
290+
if len(verifiedClis) > 0 {
291+
fmt.Printf("\n\033[32m[toob] Chip %s v%s — Verified with CLI: %s\033[0m\n", chipName, chipVersion, strings.Join(verifiedClis, ", "))
292+
}
293+
break
294+
}
295+
}
296+
}
297+
}
298+
249299
// Remove uninstalls a chip and cleans up unshared dependencies.
250300
func (inst *Installer) Remove(name string) error {
251301
entry := inst.lock.GetChip(name)
@@ -349,6 +399,7 @@ func (inst *Installer) installDeps(ci *registry.ChipInfo) ([]string, error) {
349399
}
350400

351401
// copyTree recursively copies src to dst.
402+
// Uses hard-links where possible for instant, zero-disk-cost copies.
352403
func copyTree(src, dst string) error {
353404
info, err := os.Stat(src)
354405
if err != nil {
@@ -373,12 +424,25 @@ func copyTree(src, dst string) error {
373424
if d.IsDir() {
374425
return os.MkdirAll(target, 0o755)
375426
}
376-
data, err := os.ReadFile(path)
377-
if err != nil {
378-
return err
379-
}
380-
return os.WriteFile(target, data, 0o644)
427+
return linkOrCopy(path, target)
381428
})
382429
}
383430

431+
// linkOrCopy attempts a hard-link first, falling back to a full byte-copy.
432+
// Hard-links share the inode and are instant with zero additional disk cost.
433+
// Fallback handles cross-device mounts, FAT32, and Windows restrictions.
434+
func linkOrCopy(src, dst string) error {
435+
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
436+
return err
437+
}
438+
if err := os.Link(src, dst); err == nil {
439+
return nil
440+
}
441+
data, err := os.ReadFile(src)
442+
if err != nil {
443+
return err
444+
}
445+
return os.WriteFile(dst, data, 0o644)
446+
}
447+
384448

cli/toob-cli/internal/manifest/generator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ func VerifyMacroUsage(headerPath, bootloaderDir string) error {
298298
errb.WriteString(" 2. The manifest is generating dead code that should be pruned.\n")
299299
errb.WriteString("Fix the C code to use these macros, or remove them from the generator.\n")
300300
errb.WriteString("======================================================================\n")
301-
return fmt.Errorf(errb.String())
301+
return fmt.Errorf("%s", errb.String())
302302
}
303303

304304
fmt.Println("[Manifest Verifier] SUCCESS: All generated macros are perfectly synchronized with the C code!")

cli/toob-cli/internal/registry/cache.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os"
1414
"path/filepath"
1515
"strings"
16+
"syscall"
1617
"time"
1718

1819
"github.com/toob-boot/toob/internal/paths"
@@ -119,13 +120,51 @@ func (c *Cache) lock() (func(), error) {
119120
lockDir := filepath.Join(filepath.Dir(c.dir), "registry.lock")
120121
for i := 0; i < 100; i++ { // wait up to 10 seconds
121122
if err := os.Mkdir(lockDir, 0o755); err == nil {
122-
return func() { os.Remove(lockDir) }, nil
123+
// Write PID file for stale-lock detection
124+
pidFile := filepath.Join(lockDir, "pid")
125+
_ = os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0o644)
126+
return func() { os.RemoveAll(lockDir) }, nil
127+
}
128+
129+
// Check if the locking process is still alive
130+
if i%10 == 9 { // every ~1 second
131+
if c.tryCleanStaleLock(lockDir) {
132+
continue // Retry immediately after cleaning stale lock
133+
}
123134
}
124135
time.Sleep(100 * time.Millisecond)
125136
}
126137
return nil, fmt.Errorf("timeout waiting for registry lock. Is another toob process running? (If not, delete %s)", lockDir)
127138
}
128139

140+
// tryCleanStaleLock reads the PID from the lock directory and checks if the process is alive.
141+
// Returns true if a stale lock was cleaned up.
142+
func (c *Cache) tryCleanStaleLock(lockDir string) bool {
143+
pidBytes, err := os.ReadFile(filepath.Join(lockDir, "pid"))
144+
if err != nil {
145+
return false
146+
}
147+
pid := 0
148+
if _, err := fmt.Sscanf(string(pidBytes), "%d", &pid); err != nil || pid == 0 {
149+
return false
150+
}
151+
proc, err := os.FindProcess(pid)
152+
if err != nil {
153+
// Process doesn't exist — stale lock
154+
fmt.Printf("[toob] Cleaning stale registry lock (PID %d no longer running)\n", pid)
155+
os.RemoveAll(lockDir)
156+
return true
157+
}
158+
// On Unix, FindProcess always succeeds. Send signal 0 to probe liveness.
159+
// On Windows, FindProcess fails for dead processes, so reaching here means alive.
160+
if err := proc.Signal(syscall.Signal(0)); err != nil {
161+
fmt.Printf("[toob] Cleaning stale registry lock (PID %d no longer running)\n", pid)
162+
os.RemoveAll(lockDir)
163+
return true
164+
}
165+
return false
166+
}
167+
129168
// getHubURL returns the URL of the Toob Hub API
130169
func getHubURL() string {
131170
if url := os.Getenv("TOOB_HUB_URL"); url != "" {

cli/toob-cli/internal/registry/zip.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package registry
22

33
import (
44
"archive/zip"
5-
"bytes"
65
"fmt"
76
"io"
87
"net/http"
@@ -11,8 +10,9 @@ import (
1110
"strings"
1211
)
1312

14-
// downloadAndExtractZip downloads a ZIP archive and extracts it to targetDir.
15-
// It automatically strips the root folder (e.g. 'Toob-Registry-main/') from the zip entries.
13+
// downloadAndExtractZip downloads a ZIP archive to a temp file and extracts it to targetDir.
14+
// Streams directly to disk to avoid holding the full archive in RAM.
15+
// Automatically strips the root folder (e.g. 'Toob-Registry-main/') from zip entries.
1616
func downloadAndExtractZip(url string, targetDir string) error {
1717
resp, err := http.Get(url)
1818
if err != nil {
@@ -24,15 +24,24 @@ func downloadAndExtractZip(url string, targetDir string) error {
2424
return fmt.Errorf("bad status %d from %s", resp.StatusCode, url)
2525
}
2626

27-
body, err := io.ReadAll(resp.Body)
27+
// Stream to temp file instead of buffering entire archive in RAM
28+
tmpFile, err := os.CreateTemp("", "toob-registry-*.zip")
2829
if err != nil {
29-
return err
30+
return fmt.Errorf("failed to create temp file: %w", err)
31+
}
32+
defer os.Remove(tmpFile.Name())
33+
defer tmpFile.Close()
34+
35+
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
36+
return fmt.Errorf("failed to download archive: %w", err)
3037
}
38+
tmpFile.Close()
3139

32-
zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
40+
zipReader, err := zip.OpenReader(tmpFile.Name())
3341
if err != nil {
3442
return err
3543
}
44+
defer zipReader.Close()
3645

3746
if err := os.MkdirAll(targetDir, 0o755); err != nil {
3847
return err
@@ -42,9 +51,9 @@ func downloadAndExtractZip(url string, targetDir string) error {
4251
// Strip the top-level directory
4352
parts := strings.SplitN(filepath.ToSlash(f.Name), "/", 2)
4453
if len(parts) < 2 || parts[1] == "" {
45-
continue // Skip the root directory itself
54+
continue
4655
}
47-
56+
4857
relPath := filepath.FromSlash(parts[1])
4958
destPath := filepath.Join(targetDir, relPath)
5059

0 commit comments

Comments
 (0)