package mod import ( "os" "git.smesh.lol/moxie/pkg/mxutil" ) type ModRequire struct { Path string Version string } type PkgInfo struct { Path string Name string Dir string Files []string CFiles []string BCFiles []string Imports []string Version string } func ParseReplaceLine(line string, baseDir string) (string, string, bool) { arrow := int32(-1) for k := int32(0); k < int32(len(line))-1; k++ { if line[k] == '=' && line[k+1] == '>' { arrow = k break } } if arrow < 0 { return "", "", false } from := mxutil.TrimSpace(line[:arrow]) to := mxutil.TrimSpace(line[arrow+2:]) for fi := int32(0); fi < int32(len(from)); fi++ { if from[fi] == ' ' { from = from[:fi] break } } if mxutil.HasPrefix(to, "./") || mxutil.HasPrefix(to, "../") || mxutil.HasPrefix(to, "/") { to = mxutil.CleanPath(mxutil.JoinPath(baseDir, to)) } return from, to, true } func FindModuleRoot(startDir string) (string, string, map[string]string) { dir := startDir for { data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, "moxie.mod")) if ok { modName := "" replaces := map[string]string{} lines := mxutil.SplitLines(string(data)) inReplace := false for _, line := range lines { line = mxutil.TrimSpace(line) if mxutil.HasPrefix(line, "module ") { modName = mxutil.TrimSpace(line[7:]) continue } if line == "replace (" { inReplace = true continue } if inReplace { if line == ")" { inReplace = false continue } from, to, ok2 := ParseReplaceLine(line, dir) if ok2 { replaces[from] = to } continue } if mxutil.HasPrefix(line, "replace ") { from, to, ok2 := ParseReplaceLine(line[8:], dir) if ok2 { replaces[from] = to } } } if modName != "" { return modName, dir, replaces } } parent := mxutil.PathDir(dir) if parent == dir || parent == "" || parent == "." { break } dir = parent } return "", "", nil } func MergeModReplaces(dir string, replaces map[string]string) { data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, "moxie.mod")) if !ok { return } if replaces == nil { return } inReplace := false for _, line := range mxutil.SplitLines(string(data)) { line = mxutil.TrimSpace(line) if line == "replace (" { inReplace = true continue } if inReplace { if line == ")" { inReplace = false continue } from, to, ok2 := ParseReplaceLine(line, dir) if ok2 { if _, exists := replaces[from]; !exists { replaces[from] = to } } continue } if mxutil.HasPrefix(line, "replace ") { from, to, ok2 := ParseReplaceLine(line[8:], dir) if ok2 { if _, exists := replaces[from]; !exists { replaces[from] = to } } } } } func ParseModRequires(dir string) (string, []ModRequire, map[string]string) { data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, "moxie.mod")) if !ok { return "", nil, nil } modName := "" var reqs []ModRequire repls := map[string]string{} inRequire := false inReplace := false for _, line := range mxutil.SplitLines(string(data)) { line = mxutil.TrimSpace(line) if mxutil.HasPrefix(line, "module ") { modName = mxutil.TrimSpace(line[7:]) continue } if line == "require (" { inRequire = true continue } if inRequire { if line == ")" { inRequire = false continue } parts := mxutil.SplitBySpace(line) if len(parts) >= 2 { push(reqs, ModRequire{Path: parts[0], Version: parts[1]}) } else if len(parts) == 1 { push(reqs, ModRequire{Path: parts[0], Version: ""}) } continue } if mxutil.HasPrefix(line, "require ") { parts := mxutil.SplitBySpace(line[8:]) if len(parts) >= 2 { push(reqs, ModRequire{Path: parts[0], Version: parts[1]}) } else if len(parts) == 1 { push(reqs, ModRequire{Path: parts[0], Version: ""}) } continue } if line == "replace (" { inReplace = true continue } if inReplace { if line == ")" { inReplace = false continue } from, to, ok2 := ParseReplaceLine(line, dir) if ok2 { repls[from] = to } continue } if mxutil.HasPrefix(line, "replace ") { from, to, ok2 := ParseReplaceLine(line[8:], dir) if ok2 { repls[from] = to } } } return modName, reqs, repls } func IsRemotePath(path string) (ok bool) { slash := int32(-1) for i := int32(0); i < int32(len(path)); i++ { if path[i] == '/' { slash = i break } } if slash < 0 { return false } host := path[:slash] for i := int32(0); i < int32(len(host)); i++ { if host[i] == '.' { return true } } return false } func GitURLToModPath(url string) (s string) { u := url for _, scheme := range []string{"ssh://", "https://", "http://", "git://"} { if mxutil.HasPrefix(u, scheme) { u = u[len(scheme):] break } } for i := int32(0); i < int32(len(u)); i++ { if u[i] == '@' { u = u[i+1:] break } if u[i] == '/' { break } } host := "" rest := "" for i := int32(0); i < int32(len(u)); i++ { if u[i] == ':' { host = u[:i] after := u[i+1:] j := int32(0) for j < int32(len(after)) && after[j] >= '0' && after[j] <= '9' { j++ } if j > 0 && j < int32(len(after)) && after[j] == '/' { rest = after[j+1:] } else if j > 0 && j == int32(len(after)) { rest = "" } else { rest = after } break } if u[i] == '/' { host = u[:i] rest = u[i+1:] break } } if host == "" { return "" } if mxutil.HasPrefix(rest, "~/") { rest = rest[2:] } if mxutil.HasSuffix(rest, ".git") { rest = rest[:len(rest)-4] } if rest == "" { return "" } return host | "/" | rest } func RepoLockPath(mpath, repoPath string) (s string) { return mxutil.JoinPath(mpath, repoPath | ".lock") } func AcquireRepoLock(mpath, repoPath string) (ok bool) { lp := RepoLockPath(mpath, repoPath) _, locked := mxutil.ReadFile(lp) if locked { return false } mxutil.MkdirAll(mxutil.PathDir(lp), 0755) mxutil.WriteFile(lp, []byte("1"), 0644) return true } func ReleaseRepoLock(mpath, repoPath string) { os.Remove(RepoLockPath(mpath, repoPath)) } func IsStdlib(path string) (ok bool) { return !mxutil.HasPrefix(path, "/") && !mxutil.HasPrefix(path, "./") && !mxutil.HasPrefix(path, "../") } func ExtractPkgName(data []byte) (s string) { for i := int32(0); i < int32(len(data)); i++ { if i+8 < int32(len(data)) && string(data[i:i+8]) == "package " { j := i + 8 for j < int32(len(data)) && data[j] != '\n' && data[j] != '\r' && data[j] != ' ' { j++ } return string(data[i+8 : j]) } if data[i] == '\n' { continue } if data[i] == '/' && i+1 < int32(len(data)) && data[i+1] == '/' { for i < int32(len(data)) && data[i] != '\n' { i++ } continue } break } return "main" } func extractImports(data []byte) (ss []string) { var imports []string lines := mxutil.SplitLines(string(data)) inBlock := false for _, line := range lines { line = mxutil.TrimSpace(line) if line == "import (" { inBlock = true continue } if inBlock && line == ")" { inBlock = false continue } if inBlock { imp := mxutil.ExtractQuoted(line) if imp != "" { imports = mxutil.AppendUniq(imports, imp) } } if mxutil.HasPrefix(line, "import \"") { imp := mxutil.ExtractQuoted(line[7:]) if imp != "" { imports = mxutil.AppendUniq(imports, imp) } } } return imports } func shouldSkipFile(name string) (ok bool) { if mxutil.HasSuffix(name, "_test.mx") { return true } base := name if mxutil.HasSuffix(base, ".mx") { base = base[:len(base)-3] } archSkip := []string{"arm64"} for _, a := range archSkip { if mxutil.FileContainsPart(base, a) { return true } } suffixSkip := []string{"_wasm"} for _, s := range suffixSkip { if mxutil.HasSuffix(base, s) { return true } } return false } func hasBuildConstraint(data []byte, forbidden []string) (ok bool) { for i := int32(0); i < int32(len(data)) && i < 512; i++ { if data[i] == 'p' { return false } skip := int32(0) if i+11 < int32(len(data)) && string(data[i:i+11]) == "//go:build " { skip = 11 } else if i+9 < int32(len(data)) && string(data[i:i+9]) == "//:build " { skip = 9 } if skip > 0 { var buf []byte for j := i + skip; j < int32(len(data)) && data[j] != '\n'; j++ { buf = append(buf, data[j]) } line := string(buf) for _, f := range forbidden { if line == f || mxutil.HasPrefix(line, f | " ") || mxutil.HasPrefix(line, f | "\t") { return true } } return false } if data[i] == '\n' { continue } if data[i] == '/' && i+1 < int32(len(data)) && data[i+1] == '/' { for i < int32(len(data)) && data[i] != '\n' { i++ } continue } } return false } func hasImport(data []byte, pkg string) (ok bool) { target := "\"" | pkg | "\"" for i := int32(0); i <= int32(len(data))-int32(len(target)); i++ { if string(data[i:i+int32(len(target))]) == target { return true } } return false } func DiscoverPkg(pkgPath, root, modPrefix, modDir, moxiePath string, replaces map[string]string, builtinOnly []string, mustExist bool) (p *PkgInfo) { var dir string if pkgPath == "." || pkgPath == ".." || mxutil.HasPrefix(pkgPath, "/") || mxutil.HasPrefix(pkgPath, "./") || mxutil.HasPrefix(pkgPath, "../") { dir = pkgPath } else if replaces != nil { if rdir, ok := replaces[pkgPath]; ok { dir = rdir } if dir == "" { for from, to := range replaces { if mxutil.HasPrefix(pkgPath, from | "/") { rel := pkgPath[len(from)+1:] dir = mxutil.JoinPath(to, rel) break } } } } if dir == "" && modPrefix != "" && pkgPath == modPrefix { dir = modDir } if dir == "" && modPrefix != "" && mxutil.HasPrefix(pkgPath, modPrefix | "/") { rel := pkgPath[len(modPrefix)+1:] dir = mxutil.JoinPath(modDir, rel) } if dir == "" && moxiePath != "" { candidate := mxutil.JoinPath(moxiePath, pkgPath) mxF := mxutil.ListFiles(candidate, ".mx") if len(mxF) > 0 { dir = candidate } } if dir == "" && mustExist && IsRemotePath(pkgPath) { return nil } if dir != "" && dir != pkgPath { MergeModReplaces(dir, replaces) } if dir == "" { dir = mxutil.JoinPath(root, mxutil.JoinPath("src", pkgPath)) cand := mxutil.ListFiles(dir, ".mx") if len(cand) == 0 { dir = mxutil.JoinPath(root, mxutil.JoinPath("src/vendor", pkgPath)) } } mxFiles := mxutil.ListFiles(dir, ".mx") cFiles := mxutil.ListFiles(dir, ".c") bcFiles := mxutil.ListFiles(dir, ".bc") allMx := mxFiles forbidden := []string{"wasm", "js", "ignore", "compiler_bootstrap", "scheduler.tasks", "scheduler.cores", "scheduler.asyncify", "faketime", "baremetal", "nintendoswitch", "boringcrypto"} var files []string for _, f := range allMx { if shouldSkipFile(f) { continue } data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, f)) if !ok { continue } bc := hasBuildConstraint(data, forbidden) if bc { mxutil.WriteStr(2, " skip-constraint: " | f | "\n") continue } fileImps := extractImports(data) skipFile := "" for _, imp := range fileImps { if imp == "iter" || imp == "weak" || imp == "crypto/internal/fips140cache" { skipFile = imp break } } if skipFile != "" { mxutil.WriteStr(2, " skip-import(" | skipFile | "): " | f | "\n") continue } mxutil.WriteStr(2, " include: " | f | "\n") push(files, f) } if len(files) == 0 { return nil } var allImports []string pkgName := "" for _, f := range files { data, ok := mxutil.ReadFile(mxutil.JoinPath(dir, f)) if !ok { continue } if pkgName == "" { pkgName = ExtractPkgName(data) } for _, imp := range extractImports(data) { allImports = mxutil.AppendUniq(allImports, imp) } } mxutil.WriteStr(2, " discover " | pkgPath | " [") for fi, ff := range files { if fi > 0 { mxutil.WriteStr(2, ", ") } mxutil.WriteStr(2, ff) } mxutil.WriteStr(2, "]\n") version := "" if IsRemotePath(pkgPath) && modDir != "" { _, reqs, _ := ParseModRequires(modDir) for _, r := range reqs { if r.Path == pkgPath || mxutil.HasPrefix(pkgPath, r.Path | "/") { version = r.Version break } } } return &PkgInfo{ Path: pkgPath, Name: pkgName, Dir: dir, Files: files, CFiles: cFiles, BCFiles: bcFiles, Imports: allImports, Version: version, } } type Resolver struct { Visited map[string]bool Order []*PkgInfo Root string ModPrefix string ModDir string MoxiePath string Replaces map[string]string BuiltinOnly []string MustExist bool } func (r *Resolver) Walk(path string) { if r.Visited[path] { return } r.Visited[path] = true for _, b := range r.BuiltinOnly { if path == b { return } } pkg := DiscoverPkg(path, r.Root, r.ModPrefix, r.ModDir, r.MoxiePath, r.Replaces, r.BuiltinOnly, r.MustExist) if pkg == nil { return } for _, imp := range pkg.Imports { r.Walk(imp) } push(r.Order, pkg) } func ResolveAll(rootPkg, root, modPrefix, modDir, moxiePath string, replaces map[string]string, builtinOnly []string, mustExist bool) (ss []*PkgInfo) { r := &Resolver{ Visited: map[string]bool{}, Root: root, ModPrefix: modPrefix, ModDir: modDir, MoxiePath: moxiePath, Replaces: replaces, BuiltinOnly: builtinOnly, MustExist: mustExist, } r.Walk(rootPkg) return r.Order }