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