loader_core.mx raw
1 package main
2
3 import (
4 "bytes"
5 "os"
6 "path/filepath"
7 )
8
9 // LoaderPackage holds metadata for a discovered package.
10 type LoaderPackage struct {
11 Dir string
12 ImportPath string
13 PkgName string
14 Files []string
15 Imports []string
16 IsMXH bool
17 }
18
19 // LoaderProgram holds the result of loading a package and its dependencies.
20 type LoaderProgram struct {
21 ModPath string
22 ModDir string
23 Packages []*LoaderPackage
24 }
25
26 type loaderResolver struct {
27 modPath string
28 modDir string
29 goroot string
30 modCache string
31 requires map[string]string
32 replaces map[string]string
33 mxhDir string
34 seen map[string]*LoaderPackage
35 sorted []*LoaderPackage
36 }
37
38 // LoadPackages discovers a package and its transitive dependencies,
39 // returning them in dependency order. rootDir is the working directory,
40 // inputPkg is the import path or "." for the package in rootDir.
41 // goroot is the path to the Moxie GOROOT (or synthetic merged GOROOT).
42 func LoadPackages(rootDir string, inputPkg string, goroot string) (*LoaderProgram, error) {
43 InitKeywords()
44 modPath, modDir, err := loaderReadModInfo(rootDir)
45 if err != nil {
46 return nil, err
47 }
48
49 modFile := loaderFindModFile(modDir)
50 requires := loaderReadModRequires(modFile)
51 replaces := loaderReadModReplaces(modFile, modDir)
52
53 r := &loaderResolver{
54 modPath: modPath,
55 modDir: modDir,
56 goroot: goroot,
57 modCache: loaderModCache(),
58 requires: requires,
59 replaces: replaces,
60 mxhDir: loaderMXHDir(),
61 seen: map[string]*LoaderPackage{},
62 }
63
64 // Walk runtime as implicit dependency.
65 r.walk("runtime", "")
66 r.walk("moxie", "")
67
68 // Resolve input package.
69 if inputPkg == "." {
70 inputPkg = modPath
71 } else if bytes.HasPrefix(inputPkg, "./") {
72 rel := inputPkg[2:]
73 inputPkg = modPath | "/" | rel
74 }
75
76 r.walk(inputPkg, rootDir)
77
78 return &LoaderProgram{
79 ModPath: modPath,
80 ModDir: modDir,
81 Packages: r.sorted,
82 }, nil
83 }
84
85 func (r *loaderResolver) walk(importPath string, srcDir string) {
86 if r.seen[importPath] != nil {
87 return
88 }
89 if importPath == "C" {
90 return
91 }
92 if importPath == "unsafe" {
93 pj := &LoaderPackage{
94 ImportPath: "unsafe",
95 PkgName: "unsafe",
96 }
97 r.seen[importPath] = pj
98 r.sorted = append(r.sorted, pj)
99 return
100 }
101
102 // Mark to break cycles.
103 r.seen[importPath] = &LoaderPackage{}
104
105 dir := r.resolveDir(importPath, srcDir)
106 if dir == "" {
107 // Check .mxh cache.
108 if r.mxhDir != "" {
109 mxhPath := filepath.Join(r.mxhDir, importPath|".mxh")
110 if loaderFileExists(mxhPath) {
111 idx := bytes.LastIndex(importPath, "/")
112 name := importPath
113 if idx >= 0 {
114 name = importPath[idx+1:]
115 }
116 pj := &LoaderPackage{
117 ImportPath: importPath,
118 PkgName: name,
119 IsMXH: true,
120 }
121 r.seen[importPath] = pj
122 r.sorted = append(r.sorted, pj)
123 return
124 }
125 }
126 // Package not found - create stub.
127 pj := &LoaderPackage{
128 ImportPath: importPath,
129 PkgName: importPath,
130 }
131 r.seen[importPath] = pj
132 r.sorted = append(r.sorted, pj)
133 return
134 }
135
136 // Discover .mx files and extract imports.
137 files, imports, pkgName := loaderScanDir(dir)
138
139 // Walk dependencies first (depth-first).
140 for _, imp := range imports {
141 r.walk(imp, dir)
142 }
143
144 pj := &LoaderPackage{
145 Dir: dir,
146 ImportPath: importPath,
147 PkgName: pkgName,
148 Files: files,
149 Imports: imports,
150 }
151 r.seen[importPath] = pj
152 r.sorted = append(r.sorted, pj)
153 }
154
155 func (r *loaderResolver) resolveDir(importPath string, srcDir string) string {
156 // Local/relative.
157 if importPath == "." || bytes.HasPrefix(importPath, "./") || bytes.HasPrefix(importPath, "../") {
158 return filepath.Join(srcDir, importPath)
159 }
160
161 // Stdlib.
162 if r.goroot != "" {
163 stdDir := filepath.Join(r.goroot, "src", importPath)
164 if loaderIsDir(stdDir) {
165 return stdDir
166 }
167 stdVendorDir := filepath.Join(r.goroot, "src", "vendor", importPath)
168 if loaderIsDir(stdVendorDir) {
169 return stdVendorDir
170 }
171 }
172
173 // Module-local.
174 if r.modPath != "" && (bytes.HasPrefix(importPath, r.modPath|"/") || importPath == r.modPath) {
175 rel := bytes.TrimPrefix(importPath, r.modPath)
176 rel = bytes.TrimPrefix(rel, "/")
177 dir := filepath.Join(r.modDir, rel)
178 if loaderIsDir(dir) {
179 return dir
180 }
181 }
182
183 // Replace directives.
184 for mod, localDir := range r.replaces {
185 if importPath == mod || bytes.HasPrefix(importPath, mod|"/") {
186 rel := bytes.TrimPrefix(importPath, mod)
187 rel = bytes.TrimPrefix(rel, "/")
188 dir := filepath.Join(localDir, rel)
189 if loaderIsDir(dir) {
190 return dir
191 }
192 }
193 }
194
195 // Vendor.
196 if r.modDir != "" {
197 vendorDir := filepath.Join(r.modDir, "vendor", importPath)
198 if loaderIsDir(vendorDir) {
199 return vendorDir
200 }
201 }
202
203 // Module cache.
204 bestMod := ""
205 bestVer := ""
206 for mod, ver := range r.requires {
207 if (importPath == mod || bytes.HasPrefix(importPath, mod|"/")) && len(mod) > len(bestMod) {
208 bestMod = mod
209 bestVer = ver
210 }
211 }
212 if bestMod != "" {
213 modCacheDir := filepath.Join(r.modCache, bestMod|"@"|bestVer)
214 rel := bytes.TrimPrefix(importPath, bestMod)
215 rel = bytes.TrimPrefix(rel, "/")
216 dir := filepath.Join(modCacheDir, rel)
217 if loaderIsDir(dir) {
218 return dir
219 }
220 }
221
222 return ""
223 }
224
225 // loaderScanDir reads all .mx files in dir, parses them to extract
226 // the package name and unique imports. Returns (files, imports, pkgName).
227 func loaderScanDir(dir string) ([]string, []string, string) {
228 entries, err := os.ReadDir(dir)
229 if err != nil {
230 return nil, nil, ""
231 }
232
233 var files []string
234 var pkgName string
235 importSet := map[string]bool{}
236
237 for _, e := range entries {
238 name := e.Name()
239 if e.IsDir() {
240 continue
241 }
242 if !bytes.HasSuffix(name, ".mx") && !bytes.HasSuffix(name, ".go") {
243 continue
244 }
245 // Skip test files.
246 if bytes.HasSuffix(name, "_test.mx") || bytes.HasSuffix(name, "_test.go") {
247 continue
248 }
249
250 files = append(files, name)
251
252 // Parse to get package name and imports.
253 path := filepath.Join(dir, name)
254 data, err := os.ReadFile(path)
255 if err != nil {
256 continue
257 }
258 pn, imps := loaderExtractImports(data, name)
259 if pn != "" && pkgName == "" {
260 pkgName = pn
261 }
262 for _, imp := range imps {
263 importSet[imp] = true
264 }
265 }
266
267 var imports []string
268 for imp := range importSet {
269 imports = append(imports, imp)
270 }
271
272 return files, imports, pkgName
273 }
274
275 // loaderExtractImports parses Moxie source to extract the package name and
276 // import paths. Uses our built-in parser.
277 func loaderExtractImports(src []byte, name string) (string, []string) {
278 r := bytes.NewReader(src)
279 f, err := Parse(NewFileBase(name), r, nil, nil, 0)
280 if err != nil || f == nil {
281 return "", nil
282 }
283
284 pkgName := ""
285 if f.PkgName != nil {
286 pkgName = f.PkgName.Value
287 }
288
289 var imports []string
290 for _, d := range f.DeclList {
291 imp, ok := d.(*ImportDecl)
292 if !ok {
293 continue
294 }
295 if imp.Path == nil {
296 continue
297 }
298 path := imp.Path.Value
299 // Strip quotes.
300 if len(path) >= 2 && path[0] == '"' {
301 path = path[1 : len(path)-1]
302 }
303 if path != "" {
304 imports = append(imports, path)
305 }
306 }
307 return pkgName, imports
308 }
309
310 // Module file helpers.
311
312 func loaderFindModFile(dir string) string {
313 mxmod := filepath.Join(dir, "moxie.mod")
314 if loaderFileExists(mxmod) {
315 return mxmod
316 }
317 return filepath.Join(dir, "go.mod")
318 }
319
320 func loaderReadModInfo(startDir string) (string, string, error) {
321 dir := startDir
322 for {
323 modFile := loaderFindModFile(dir)
324 data, err := os.ReadFile(modFile)
325 if err == nil {
326 modPath := loaderParseModPath(string(data))
327 return modPath, dir, nil
328 }
329 parent := filepath.Dir(dir)
330 if parent == dir {
331 return "", startDir, nil
332 }
333 dir = parent
334 }
335 }
336
337 func loaderParseModPath(content string) string {
338 for _, line := range bytes.Split(content, "\n") {
339 line = bytes.TrimSpace(line)
340 if bytes.HasPrefix(line, "module ") {
341 return bytes.TrimSpace(line[7:])
342 }
343 }
344 return ""
345 }
346
347 func loaderReadModRequires(modFile string) map[string]string {
348 data, err := os.ReadFile(modFile)
349 if err != nil {
350 return map[string]string{}
351 }
352 requires := map[string]string{}
353 inRequire := false
354 for _, line := range bytes.Split(string(data), "\n") {
355 line = bytes.TrimSpace(line)
356 if line == ")" {
357 inRequire = false
358 continue
359 }
360 if bytes.HasPrefix(line, "require (") {
361 inRequire = true
362 continue
363 }
364 if bytes.HasPrefix(line, "require ") && !bytes.Contains(line, "(") {
365 parts := bytes.Fields(line)
366 if len(parts) >= 3 {
367 requires[parts[1]] = parts[2]
368 }
369 continue
370 }
371 if inRequire {
372 parts := bytes.Fields(line)
373 if len(parts) >= 2 && !bytes.HasPrefix(parts[0], "//") {
374 requires[parts[0]] = parts[1]
375 }
376 }
377 }
378 return requires
379 }
380
381 func loaderReadModReplaces(modFile string, modDir string) map[string]string {
382 data, err := os.ReadFile(modFile)
383 if err != nil {
384 return map[string]string{}
385 }
386 replaces := map[string]string{}
387 inReplace := false
388 for _, line := range bytes.Split(string(data), "\n") {
389 line = bytes.TrimSpace(line)
390 if line == ")" {
391 inReplace = false
392 continue
393 }
394 if bytes.HasPrefix(line, "replace (") {
395 inReplace = true
396 continue
397 }
398 parseLine := ""
399 if bytes.HasPrefix(line, "replace ") && !bytes.Contains(line, "(") {
400 parseLine = line[8:]
401 } else if inReplace && !bytes.HasPrefix(line, "//") && line != "" {
402 parseLine = line
403 }
404 if parseLine == "" {
405 continue
406 }
407 parts := bytes.Split(parseLine, "=>")
408 if len(parts) != 2 {
409 continue
410 }
411 lhsParts := bytes.Fields(bytes.TrimSpace(parts[0]))
412 rhsParts := bytes.Fields(bytes.TrimSpace(parts[1]))
413 if len(lhsParts) < 1 || len(rhsParts) < 1 {
414 continue
415 }
416 replacePath := rhsParts[0]
417 if bytes.HasPrefix(replacePath, ".") || bytes.HasPrefix(replacePath, "/") {
418 if !filepath.IsAbs(replacePath) {
419 replacePath = filepath.Join(modDir, replacePath)
420 }
421 replaces[lhsParts[0]] = filepath.Clean(replacePath)
422 }
423 }
424 return replaces
425 }
426
427 func loaderModCache() string {
428 home := os.Getenv("HOME")
429 if home == "" {
430 return ""
431 }
432 gopath := os.Getenv("GOPATH")
433 if gopath == "" {
434 gopath = filepath.Join(home, "go")
435 }
436 return filepath.Join(gopath, "pkg", "mod")
437 }
438
439 func loaderMXHDir() string {
440 cacheDir := os.Getenv("GOCACHE")
441 if cacheDir == "" {
442 home := os.Getenv("HOME")
443 if home != "" {
444 cacheDir = filepath.Join(home, ".cache", "go-build")
445 }
446 }
447 if cacheDir != "" {
448 return filepath.Join(cacheDir, "mxinstall", "protocols")
449 }
450 return ""
451 }
452
453 func loaderIsDir(path string) bool {
454 fi, err := os.Stat(path)
455 return err == nil && fi.IsDir()
456 }
457
458 func loaderFileExists(path string) bool {
459 _, err := os.Stat(path)
460 return err == nil
461 }
462