mxbuild.go raw
1 // mxbuild.go provides a custom go/build.Context that discovers .mx source
2 // files by presenting them to go/build as .go files. This lets us reuse
3 // go/build's constraint matching, import resolution, and file classification
4 // without vendoring or modifying the standard library.
5 //
6 // The trick: ReadDir renames .mx → .go in directory listings, and OpenFile
7 // redirects .go reads to the corresponding .mx file on disk.
8
9 package loader
10
11 import (
12 "go/build"
13 "io"
14 "io/fs"
15 "os"
16 "path/filepath"
17 "strings"
18 "time"
19 )
20
21 // mxContext returns a go/build.Context configured for the given target
22 // that transparently finds .mx files wherever go/build expects .go files.
23 func mxContext(goroot, goos, goarch string, buildTags []string) build.Context {
24 ctx := build.Default
25 ctx.GOROOT = goroot
26 ctx.GOOS = goos
27 ctx.GOARCH = goarch
28 ctx.BuildTags = buildTags
29 ctx.CgoEnabled = true
30 ctx.Compiler = "gc"
31
32 ctx.ReadDir = mxReadDir
33 ctx.OpenFile = mxOpenFile
34
35 return ctx
36 }
37
38 // mxReadDir reads a directory and presents .mx files as .go files.
39 // Actual .go files are passed through unchanged (for external deps
40 // in the module cache that remain .go).
41 func mxReadDir(dir string) ([]fs.FileInfo, error) {
42 entries, err := os.ReadDir(dir)
43 if err != nil {
44 return nil, err
45 }
46 // Track .mx base names so we can suppress duplicate .go files
47 // if both foo.mx and foo.go somehow exist in the same directory.
48 mxBases := make(map[string]bool)
49 for _, e := range entries {
50 name := e.Name()
51 if strings.HasSuffix(name, ".mx") {
52 mxBases[strings.TrimSuffix(name, ".mx")] = true
53 }
54 }
55
56 infos := make([]fs.FileInfo, 0, len(entries))
57 for _, e := range entries {
58 name := e.Name()
59 info, err := e.Info()
60 if err != nil {
61 return nil, err
62 }
63
64 if strings.HasSuffix(name, ".mx") {
65 // Present .mx as .go so go/build's matchFile accepts it.
66 infos = append(infos, renamedFileInfo{
67 FileInfo: info,
68 name: strings.TrimSuffix(name, ".mx") + ".go",
69 })
70 } else if strings.HasSuffix(name, ".go") && mxBases[strings.TrimSuffix(name, ".go")] {
71 // Skip .go if a corresponding .mx exists — .mx wins.
72 continue
73 } else {
74 infos = append(infos, info)
75 }
76 }
77 return infos, nil
78 }
79
80 // mxOpenFile opens a file, redirecting .go requests to .mx when the .mx
81 // file exists on disk. For files in the module cache (external deps) where
82 // only .go exists, opens the .go file directly.
83 func mxOpenFile(path string) (io.ReadCloser, error) {
84 if strings.HasSuffix(path, ".go") {
85 mxPath := strings.TrimSuffix(path, ".go") + ".mx"
86 if f, err := os.Open(mxPath); err == nil {
87 return f, nil
88 }
89 }
90 return os.Open(path)
91 }
92
93 // renamedFileInfo wraps fs.FileInfo with an overridden Name().
94 type renamedFileInfo struct {
95 fs.FileInfo
96 name string
97 }
98
99 func (r renamedFileInfo) Name() string { return r.name }
100
101 // mxFileInfo is a minimal fs.FileInfo for synthetic entries.
102 type mxFileInfo struct {
103 name string
104 size int64
105 mode fs.FileMode
106 modTime time.Time
107 isDir bool
108 }
109
110 func (fi mxFileInfo) Name() string { return fi.name }
111 func (fi mxFileInfo) Size() int64 { return fi.size }
112 func (fi mxFileInfo) Mode() fs.FileMode { return fi.mode }
113 func (fi mxFileInfo) ModTime() time.Time { return fi.modTime }
114 func (fi mxFileInfo) IsDir() bool { return fi.isDir }
115 func (fi mxFileInfo) Sys() any { return nil }
116
117 // goToMx maps a list of .go filenames back to .mx where the .mx file
118 // exists on disk. Files that are genuinely .go (external deps) stay as-is.
119 func goToMx(files []string, dir string) []string {
120 out := make([]string, len(files))
121 for i, f := range files {
122 if strings.HasSuffix(f, ".go") {
123 mxName := strings.TrimSuffix(f, ".go") + ".mx"
124 if _, err := os.Stat(filepath.Join(dir, mxName)); err == nil {
125 out[i] = mxName
126 continue
127 }
128 }
129 out[i] = f
130 }
131 return out
132 }
133