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