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