mod.mx raw

   1  package mod
   2  
   3  import (
   4  	"os"
   5  	"git.smesh.lol/moxie/pkg/mxutil"
   6  )
   7  
   8  type ModRequire struct {
   9  	Path    string
  10  	Version string
  11  }
  12  
  13  type PkgInfo struct {
  14  	Path    string
  15  	Name    string
  16  	Dir     string
  17  	Files   []string
  18  	CFiles  []string
  19  	BCFiles []string
  20  	Imports []string
  21  	Version string
  22  }
  23  
  24  func ParseReplaceLine(line string, baseDir string) (string, string, bool) {
  25  	arrow := int32(-1)
  26  	for k := int32(0); k < int32(len(line))-1; k++ {
  27  		if line[k] == '=' && line[k+1] == '>' {
  28  			arrow = k
  29  			break
  30  		}
  31  	}
  32  	if arrow < 0 {
  33  		return "", "", false
  34  	}
  35  	from := mxutil.TrimSpace(line[:arrow])
  36  	to := mxutil.TrimSpace(line[arrow+2:])
  37  	for fi := int32(0); fi < int32(len(from)); fi++ {
  38  		if from[fi] == ' ' {
  39  			from = from[:fi]
  40  			break
  41  		}
  42  	}
  43  	if mxutil.HasPrefix(to, "./") || mxutil.HasPrefix(to, "../") || mxutil.HasPrefix(to, "/") {
  44  		to = mxutil.CleanPath(mxutil.JoinPath(baseDir, to))
  45  	}
  46  	return from, to, true
  47  }
  48  
  49  func FindModuleRoot(startDir string) (string, string, map[string]string) {
  50  	dir := startDir
  51  	for {
  52  		data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, "moxie.mod"))
  53  		if ok {
  54  			modName := ""
  55  			replaces := map[string]string{}
  56  			lines := mxutil.SplitLines(string(data))
  57  			inReplace := false
  58  			for _, line := range lines {
  59  				line = mxutil.TrimSpace(line)
  60  				if mxutil.HasPrefix(line, "module ") {
  61  					modName = mxutil.TrimSpace(line[7:])
  62  					continue
  63  				}
  64  				if line == "replace (" {
  65  					inReplace = true
  66  					continue
  67  				}
  68  				if inReplace {
  69  					if line == ")" {
  70  						inReplace = false
  71  						continue
  72  					}
  73  					from, to, ok2 := ParseReplaceLine(line, dir)
  74  					if ok2 {
  75  						replaces[from] = to
  76  					}
  77  					continue
  78  				}
  79  				if mxutil.HasPrefix(line, "replace ") {
  80  					from, to, ok2 := ParseReplaceLine(line[8:], dir)
  81  					if ok2 {
  82  						replaces[from] = to
  83  					}
  84  				}
  85  			}
  86  			if modName != "" {
  87  				return modName, dir, replaces
  88  			}
  89  		}
  90  		parent := mxutil.PathDir(dir)
  91  		if parent == dir || parent == "" || parent == "." {
  92  			break
  93  		}
  94  		dir = parent
  95  	}
  96  	return "", "", nil
  97  }
  98  
  99  func MergeModReplaces(dir string, replaces map[string]string) {
 100  	data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, "moxie.mod"))
 101  	if !ok {
 102  		return
 103  	}
 104  	if replaces == nil {
 105  		return
 106  	}
 107  	inReplace := false
 108  	for _, line := range mxutil.SplitLines(string(data)) {
 109  		line = mxutil.TrimSpace(line)
 110  		if line == "replace (" {
 111  			inReplace = true
 112  			continue
 113  		}
 114  		if inReplace {
 115  			if line == ")" {
 116  				inReplace = false
 117  				continue
 118  			}
 119  			from, to, ok2 := ParseReplaceLine(line, dir)
 120  			if ok2 {
 121  				if _, exists := replaces[from]; !exists {
 122  					replaces[from] = to
 123  				}
 124  			}
 125  			continue
 126  		}
 127  		if mxutil.HasPrefix(line, "replace ") {
 128  			from, to, ok2 := ParseReplaceLine(line[8:], dir)
 129  			if ok2 {
 130  				if _, exists := replaces[from]; !exists {
 131  					replaces[from] = to
 132  				}
 133  			}
 134  		}
 135  	}
 136  }
 137  
 138  func ParseModRequires(dir string) (string, []ModRequire, map[string]string) {
 139  	data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, "moxie.mod"))
 140  	if !ok {
 141  		return "", nil, nil
 142  	}
 143  	modName := ""
 144  	var reqs []ModRequire
 145  	repls := map[string]string{}
 146  	inRequire := false
 147  	inReplace := false
 148  	for _, line := range mxutil.SplitLines(string(data)) {
 149  		line = mxutil.TrimSpace(line)
 150  		if mxutil.HasPrefix(line, "module ") {
 151  			modName = mxutil.TrimSpace(line[7:])
 152  			continue
 153  		}
 154  		if line == "require (" {
 155  			inRequire = true
 156  			continue
 157  		}
 158  		if inRequire {
 159  			if line == ")" {
 160  				inRequire = false
 161  				continue
 162  			}
 163  			parts := mxutil.SplitBySpace(line)
 164  			if len(parts) >= 2 {
 165  				push(reqs, ModRequire{Path: parts[0], Version: parts[1]})
 166  			} else if len(parts) == 1 {
 167  				push(reqs, ModRequire{Path: parts[0], Version: ""})
 168  			}
 169  			continue
 170  		}
 171  		if mxutil.HasPrefix(line, "require ") {
 172  			parts := mxutil.SplitBySpace(line[8:])
 173  			if len(parts) >= 2 {
 174  				push(reqs, ModRequire{Path: parts[0], Version: parts[1]})
 175  			} else if len(parts) == 1 {
 176  				push(reqs, ModRequire{Path: parts[0], Version: ""})
 177  			}
 178  			continue
 179  		}
 180  		if line == "replace (" {
 181  			inReplace = true
 182  			continue
 183  		}
 184  		if inReplace {
 185  			if line == ")" {
 186  				inReplace = false
 187  				continue
 188  			}
 189  			from, to, ok2 := ParseReplaceLine(line, dir)
 190  			if ok2 {
 191  				repls[from] = to
 192  			}
 193  			continue
 194  		}
 195  		if mxutil.HasPrefix(line, "replace ") {
 196  			from, to, ok2 := ParseReplaceLine(line[8:], dir)
 197  			if ok2 {
 198  				repls[from] = to
 199  			}
 200  		}
 201  	}
 202  	return modName, reqs, repls
 203  }
 204  
 205  func IsRemotePath(path string) (ok bool) {
 206  	slash := int32(-1)
 207  	for i := int32(0); i < int32(len(path)); i++ {
 208  		if path[i] == '/' {
 209  			slash = i
 210  			break
 211  		}
 212  	}
 213  	if slash < 0 {
 214  		return false
 215  	}
 216  	host := path[:slash]
 217  	for i := int32(0); i < int32(len(host)); i++ {
 218  		if host[i] == '.' {
 219  			return true
 220  		}
 221  	}
 222  	return false
 223  }
 224  
 225  func GitURLToModPath(url string) (s string) {
 226  	u := url
 227  	for _, scheme := range []string{"ssh://", "https://", "http://", "git://"} {
 228  		if mxutil.HasPrefix(u, scheme) {
 229  			u = u[len(scheme):]
 230  			break
 231  		}
 232  	}
 233  	for i := int32(0); i < int32(len(u)); i++ {
 234  		if u[i] == '@' {
 235  			u = u[i+1:]
 236  			break
 237  		}
 238  		if u[i] == '/' {
 239  			break
 240  		}
 241  	}
 242  	host := ""
 243  	rest := ""
 244  	for i := int32(0); i < int32(len(u)); i++ {
 245  		if u[i] == ':' {
 246  			host = u[:i]
 247  			after := u[i+1:]
 248  			j := int32(0)
 249  			for j < int32(len(after)) && after[j] >= '0' && after[j] <= '9' {
 250  				j++
 251  			}
 252  			if j > 0 && j < int32(len(after)) && after[j] == '/' {
 253  				rest = after[j+1:]
 254  			} else if j > 0 && j == int32(len(after)) {
 255  				rest = ""
 256  			} else {
 257  				rest = after
 258  			}
 259  			break
 260  		}
 261  		if u[i] == '/' {
 262  			host = u[:i]
 263  			rest = u[i+1:]
 264  			break
 265  		}
 266  	}
 267  	if host == "" {
 268  		return ""
 269  	}
 270  	if mxutil.HasPrefix(rest, "~/") {
 271  		rest = rest[2:]
 272  	}
 273  	if mxutil.HasSuffix(rest, ".git") {
 274  		rest = rest[:len(rest)-4]
 275  	}
 276  	if rest == "" {
 277  		return ""
 278  	}
 279  	return host | "/" | rest
 280  }
 281  
 282  func RepoLockPath(mpath, repoPath string) (s string) {
 283  	return mxutil.JoinPath(mpath, repoPath | ".lock")
 284  }
 285  
 286  func AcquireRepoLock(mpath, repoPath string) (ok bool) {
 287  	lp := RepoLockPath(mpath, repoPath)
 288  	_, locked := mxutil.ReadFile(lp)
 289  	if locked {
 290  		return false
 291  	}
 292  	mxutil.MkdirAll(mxutil.PathDir(lp), 0755)
 293  	mxutil.WriteFile(lp, []byte("1"), 0644)
 294  	return true
 295  }
 296  
 297  func ReleaseRepoLock(mpath, repoPath string) {
 298  	os.Remove(RepoLockPath(mpath, repoPath))
 299  }
 300  
 301  func IsStdlib(path string) (ok bool) {
 302  	return !mxutil.HasPrefix(path, "/") && !mxutil.HasPrefix(path, "./") && !mxutil.HasPrefix(path, "../")
 303  }
 304  
 305  func ExtractPkgName(data []byte) (s string) {
 306  	for i := int32(0); i < int32(len(data)); i++ {
 307  		if i+8 < int32(len(data)) && string(data[i:i+8]) == "package " {
 308  			j := i + 8
 309  			for j < int32(len(data)) && data[j] != '\n' && data[j] != '\r' && data[j] != ' ' {
 310  				j++
 311  			}
 312  			return string(data[i+8 : j])
 313  		}
 314  		if data[i] == '\n' {
 315  			continue
 316  		}
 317  		if data[i] == '/' && i+1 < int32(len(data)) && data[i+1] == '/' {
 318  			for i < int32(len(data)) && data[i] != '\n' {
 319  				i++
 320  			}
 321  			continue
 322  		}
 323  		break
 324  	}
 325  	return "main"
 326  }
 327  
 328  func extractImports(data []byte) (ss []string) {
 329  	var imports []string
 330  	lines := mxutil.SplitLines(string(data))
 331  	inBlock := false
 332  	for _, line := range lines {
 333  		line = mxutil.TrimSpace(line)
 334  		if line == "import (" {
 335  			inBlock = true
 336  			continue
 337  		}
 338  		if inBlock && line == ")" {
 339  			inBlock = false
 340  			continue
 341  		}
 342  		if inBlock {
 343  			imp := mxutil.ExtractQuoted(line)
 344  			if imp != "" {
 345  				imports = mxutil.AppendUniq(imports, imp)
 346  			}
 347  		}
 348  		if mxutil.HasPrefix(line, "import \"") {
 349  			imp := mxutil.ExtractQuoted(line[7:])
 350  			if imp != "" {
 351  				imports = mxutil.AppendUniq(imports, imp)
 352  			}
 353  		}
 354  	}
 355  	return imports
 356  }
 357  
 358  func shouldSkipFile(name string) (ok bool) {
 359  	if mxutil.HasSuffix(name, "_test.mx") {
 360  		return true
 361  	}
 362  	base := name
 363  	if mxutil.HasSuffix(base, ".mx") {
 364  		base = base[:len(base)-3]
 365  	}
 366  	archSkip := []string{"arm64"}
 367  	for _, a := range archSkip {
 368  		if mxutil.FileContainsPart(base, a) {
 369  			return true
 370  		}
 371  	}
 372  	suffixSkip := []string{"_wasm"}
 373  	for _, s := range suffixSkip {
 374  		if mxutil.HasSuffix(base, s) {
 375  			return true
 376  		}
 377  	}
 378  	return false
 379  }
 380  
 381  func hasBuildConstraint(data []byte, forbidden []string) (ok bool) {
 382  	for i := int32(0); i < int32(len(data)) && i < 512; i++ {
 383  		if data[i] == 'p' {
 384  			return false
 385  		}
 386  		skip := int32(0)
 387  		if i+11 < int32(len(data)) && string(data[i:i+11]) == "//go:build " {
 388  			skip = 11
 389  		} else if i+9 < int32(len(data)) && string(data[i:i+9]) == "//:build " {
 390  			skip = 9
 391  		}
 392  		if skip > 0 {
 393  			var buf []byte
 394  			for j := i + skip; j < int32(len(data)) && data[j] != '\n'; j++ {
 395  				buf = append(buf, data[j])
 396  			}
 397  			line := string(buf)
 398  			for _, f := range forbidden {
 399  				if line == f || mxutil.HasPrefix(line, f | " ") || mxutil.HasPrefix(line, f | "\t") {
 400  					return true
 401  				}
 402  			}
 403  			return false
 404  		}
 405  		if data[i] == '\n' {
 406  			continue
 407  		}
 408  		if data[i] == '/' && i+1 < int32(len(data)) && data[i+1] == '/' {
 409  			for i < int32(len(data)) && data[i] != '\n' {
 410  				i++
 411  			}
 412  			continue
 413  		}
 414  	}
 415  	return false
 416  }
 417  
 418  func hasImport(data []byte, pkg string) (ok bool) {
 419  	target := "\"" | pkg | "\""
 420  	for i := int32(0); i <= int32(len(data))-int32(len(target)); i++ {
 421  		if string(data[i:i+int32(len(target))]) == target {
 422  			return true
 423  		}
 424  	}
 425  	return false
 426  }
 427  
 428  func DiscoverPkg(pkgPath, root, modPrefix, modDir, moxiePath string, replaces map[string]string, builtinOnly []string, mustExist bool) (p *PkgInfo) {
 429  	var dir string
 430  	if pkgPath == "." || pkgPath == ".." || mxutil.HasPrefix(pkgPath, "/") || mxutil.HasPrefix(pkgPath, "./") || mxutil.HasPrefix(pkgPath, "../") {
 431  		dir = pkgPath
 432  	} else if replaces != nil {
 433  		if rdir, ok := replaces[pkgPath]; ok {
 434  			dir = rdir
 435  		}
 436  		if dir == "" {
 437  			for from, to := range replaces {
 438  				if mxutil.HasPrefix(pkgPath, from | "/") {
 439  					rel := pkgPath[len(from)+1:]
 440  					dir = mxutil.JoinPath(to, rel)
 441  					break
 442  				}
 443  			}
 444  		}
 445  	}
 446  	if dir == "" && modPrefix != "" && pkgPath == modPrefix {
 447  		dir = modDir
 448  	}
 449  	if dir == "" && modPrefix != "" && mxutil.HasPrefix(pkgPath, modPrefix | "/") {
 450  		rel := pkgPath[len(modPrefix)+1:]
 451  		dir = mxutil.JoinPath(modDir, rel)
 452  	}
 453  	if dir == "" && moxiePath != "" {
 454  		candidate := mxutil.JoinPath(moxiePath, pkgPath)
 455  		mxF := mxutil.ListFiles(candidate, ".mx")
 456  		if len(mxF) > 0 {
 457  			dir = candidate
 458  		}
 459  	}
 460  	if dir == "" && mustExist && IsRemotePath(pkgPath) {
 461  		return nil
 462  	}
 463  	if dir != "" && dir != pkgPath {
 464  		MergeModReplaces(dir, replaces)
 465  	}
 466  	if dir == "" {
 467  		dir = mxutil.JoinPath(root, mxutil.JoinPath("src", pkgPath))
 468  		cand := mxutil.ListFiles(dir, ".mx")
 469  		if len(cand) == 0 {
 470  			dir = mxutil.JoinPath(root, mxutil.JoinPath("src/vendor", pkgPath))
 471  		}
 472  	}
 473  	mxFiles := mxutil.ListFiles(dir, ".mx")
 474  	cFiles := mxutil.ListFiles(dir, ".c")
 475  	bcFiles := mxutil.ListFiles(dir, ".bc")
 476  	allMx := mxFiles
 477  	forbidden := []string{"wasm", "js", "ignore", "compiler_bootstrap",
 478  		"scheduler.tasks", "scheduler.cores", "scheduler.asyncify",
 479  		"faketime", "baremetal", "nintendoswitch", "boringcrypto"}
 480  	var files []string
 481  	for _, f := range allMx {
 482  		if shouldSkipFile(f) {
 483  			continue
 484  		}
 485  		data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, f))
 486  		if !ok {
 487  			continue
 488  		}
 489  		bc := hasBuildConstraint(data, forbidden)
 490  		if bc {
 491  			mxutil.WriteStr(2, "    skip-constraint: " | f | "\n")
 492  			continue
 493  		}
 494  		fileImps := extractImports(data)
 495  		skipFile := ""
 496  		for _, imp := range fileImps {
 497  			if imp == "iter" || imp == "weak" || imp == "crypto/internal/fips140cache" {
 498  				skipFile = imp
 499  				break
 500  			}
 501  		}
 502  		if skipFile != "" {
 503  			mxutil.WriteStr(2, "    skip-import(" | skipFile | "): " | f | "\n")
 504  			continue
 505  		}
 506  		mxutil.WriteStr(2, "    include: " | f | "\n")
 507  		push(files, f)
 508  	}
 509  	if len(files) == 0 {
 510  		return nil
 511  	}
 512  	var allImports []string
 513  	pkgName := ""
 514  	for _, f := range files {
 515  		data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, f))
 516  		if !ok {
 517  			continue
 518  		}
 519  		if pkgName == "" {
 520  			pkgName = ExtractPkgName(data)
 521  		}
 522  		for _, imp := range extractImports(data) {
 523  			allImports = mxutil.AppendUniq(allImports, imp)
 524  		}
 525  	}
 526  	mxutil.WriteStr(2, "  discover " | pkgPath | " [")
 527  	for fi, ff := range files {
 528  		if fi > 0 {
 529  			mxutil.WriteStr(2, ", ")
 530  		}
 531  		mxutil.WriteStr(2, ff)
 532  	}
 533  	mxutil.WriteStr(2, "]\n")
 534  	version := ""
 535  	if IsRemotePath(pkgPath) && modDir != "" {
 536  		_, reqs, _ := ParseModRequires(modDir)
 537  		for _, r := range reqs {
 538  			if r.Path == pkgPath || mxutil.HasPrefix(pkgPath, r.Path | "/") {
 539  				version = r.Version
 540  				break
 541  			}
 542  		}
 543  	}
 544  	return &PkgInfo{
 545  		Path:    pkgPath,
 546  		Name:    pkgName,
 547  		Dir:     dir,
 548  		Files:   files,
 549  		CFiles:  cFiles,
 550  		BCFiles: bcFiles,
 551  		Imports: allImports,
 552  		Version: version,
 553  	}
 554  }
 555  
 556  type Resolver struct {
 557  	Visited  map[string]bool
 558  	Order    []*PkgInfo
 559  	Root     string
 560  	ModPrefix string
 561  	ModDir   string
 562  	MoxiePath string
 563  	Replaces map[string]string
 564  	BuiltinOnly []string
 565  	MustExist bool
 566  }
 567  
 568  func (r *Resolver) Walk(path string) {
 569  	if r.Visited[path] {
 570  		return
 571  	}
 572  	r.Visited[path] = true
 573  	for _, b := range r.BuiltinOnly {
 574  		if path == b {
 575  			return
 576  		}
 577  	}
 578  	pkg := DiscoverPkg(path, r.Root, r.ModPrefix, r.ModDir, r.MoxiePath, r.Replaces, r.BuiltinOnly, r.MustExist)
 579  	if pkg == nil {
 580  		return
 581  	}
 582  	for _, imp := range pkg.Imports {
 583  		r.Walk(imp)
 584  	}
 585  	push(r.Order, pkg)
 586  }
 587  
 588  func ResolveAll(rootPkg, root, modPrefix, modDir, moxiePath string, replaces map[string]string, builtinOnly []string, mustExist bool) (ss []*PkgInfo) {
 589  	r := &Resolver{
 590  		Visited:  map[string]bool{},
 591  		Root:     root,
 592  		ModPrefix: modPrefix,
 593  		ModDir:   modDir,
 594  		MoxiePath: moxiePath,
 595  		Replaces: replaces,
 596  		BuiltinOnly: builtinOnly,
 597  		MustExist: mustExist,
 598  	}
 599  	r.Walk(rootPkg)
 600  	return r.Order
 601  }
 602