cache.go raw
1 // Package cache manages the mxinstall binary and protocol header cache.
2 package cache
3
4 import (
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "fmt"
9 "io/fs"
10 "os"
11 "path/filepath"
12 "sort"
13 "strings"
14 "time"
15
16 "moxie/goenv"
17 )
18
19 // Meta holds installation metadata for a cached binary.
20 type Meta struct {
21 SourceHash string `json:"source_hash"`
22 CompilerVersion string `json:"compiler_version"`
23 Triple string `json:"triple"`
24 InstalledAt time.Time `json:"installed_at"`
25 }
26
27 // Root returns the root of the mxinstall cache.
28 func Root() string {
29 return filepath.Join(goenv.Get("GOCACHE"), "mxinstall")
30 }
31
32 // BinPath returns the path for the installed binary.
33 // triple is the LLVM target triple from compileopts.Config.Triple().
34 func BinPath(triple, importPath, binaryName string) string {
35 return filepath.Join(Root(), triple, importPath, binaryName)
36 }
37
38 // MXHPath returns the path for the cached .mxh (target-independent).
39 func MXHPath(importPath string) string {
40 return filepath.Join(Root(), "protocols", importPath+".mxh")
41 }
42
43 // MetaPath returns the path for the .meta JSON file.
44 func MetaPath(triple, importPath string) string {
45 return filepath.Join(Root(), triple, importPath, ".meta")
46 }
47
48 // SourcePath returns the path where cloned source lives.
49 func SourcePath(importPath string) string {
50 return filepath.Join(Root(), "src", importPath)
51 }
52
53 // ReadMeta reads the cached metadata for a binary.
54 func ReadMeta(triple, importPath string) (*Meta, error) {
55 data, err := os.ReadFile(MetaPath(triple, importPath))
56 if err != nil {
57 return nil, err
58 }
59 var m Meta
60 if err := json.Unmarshal(data, &m); err != nil {
61 return nil, fmt.Errorf("cache meta decode: %w", err)
62 }
63 return &m, nil
64 }
65
66 // WriteMeta writes the cache metadata.
67 func WriteMeta(triple, importPath string, m *Meta) error {
68 path := MetaPath(triple, importPath)
69 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
70 return err
71 }
72 data, err := json.Marshal(m)
73 if err != nil {
74 return err
75 }
76 return os.WriteFile(path, data, 0644)
77 }
78
79 // Clean removes the entire mxinstall cache.
80 func Clean() error {
81 return os.RemoveAll(Root())
82 }
83
84 // List returns the import paths of all installed packages.
85 // Each import path is listed once regardless of how many triples it has.
86 func List() ([]string, error) {
87 seen := make(map[string]bool)
88 var out []string
89
90 root := Root()
91 // Walk triple dirs to find .meta files.
92 entries, err := os.ReadDir(root)
93 if err != nil {
94 if os.IsNotExist(err) {
95 return nil, nil
96 }
97 return nil, err
98 }
99 for _, tripleEntry := range entries {
100 if !tripleEntry.IsDir() || tripleEntry.Name() == "protocols" || tripleEntry.Name() == "src" {
101 continue
102 }
103 tripleDir := filepath.Join(root, tripleEntry.Name())
104 _ = filepath.WalkDir(tripleDir, func(path string, d fs.DirEntry, err error) error {
105 if err != nil || d.Name() != ".meta" {
106 return nil
107 }
108 // path = root/triple/import/path/.meta
109 rel, _ := filepath.Rel(tripleDir, filepath.Dir(path))
110 importPath := filepath.ToSlash(rel)
111 if !seen[importPath] {
112 seen[importPath] = true
113 out = append(out, importPath)
114 }
115 return nil
116 })
117 }
118 sort.Strings(out)
119 return out, nil
120 }
121
122 // SourceHash computes a stable hash of all .mx source files in dir.
123 // Sorts files before hashing so the result is deterministic.
124 func SourceHash(dir string) (string, error) {
125 var files []string
126 _ = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
127 if err == nil && !d.IsDir() && strings.HasSuffix(d.Name(), ".mx") {
128 files = append(files, path)
129 }
130 return nil
131 })
132 sort.Strings(files)
133
134 h := sha256.New()
135 for _, f := range files {
136 data, err := os.ReadFile(f)
137 if err != nil {
138 return "", err
139 }
140 h.Write(data)
141 }
142 return hex.EncodeToString(h.Sum(nil)), nil
143 }
144