main.go raw
1 //go:build ignore
2
3 package main
4
5 import (
6 "os"
7 "unsafe"
8 )
9
10 //export mxc_run
11 func cRun(argv unsafe.Pointer, argc int32) int32
12
13 //export mxc_getenv
14 func cGetenv(name unsafe.Pointer, nameLen int32, buf unsafe.Pointer, bufCap int32) int32
15
16 //export write
17 func cWrite(fd int32, buf unsafe.Pointer, count uint) int
18
19 func writeStr(fd int32, s string) {
20 if len(s) > 0 {
21 cWrite(fd, unsafe.Pointer(&[]byte(s)[0]), uint(len(s)))
22 }
23 }
24
25 func fatal(msg string) {
26 writeStr(2, "mxc: ")
27 writeStr(2, msg)
28 writeStr(2, "\n")
29 os.Exit(1)
30 }
31
32 func getenv(name string) string {
33 b := []byte(name)
34 buf := make([]byte, 4096)
35 n := cGetenv(unsafe.Pointer(&b[0]), int32(len(b)), unsafe.Pointer(&buf[0]), int32(len(buf)))
36 if n <= 0 {
37 return ""
38 }
39 return string(buf[:n])
40 }
41
42 func getArgs() []string {
43 data, err := os.ReadFile("/proc/self/cmdline")
44 if err != nil {
45 return nil
46 }
47 var args []string
48 start := 0
49 for i := 0; i < len(data); i++ {
50 if data[i] == 0 {
51 if i > start {
52 args = append(args, string(data[start:i]))
53 }
54 start = i + 1
55 }
56 }
57 if start < len(data) {
58 args = append(args, string(data[start:]))
59 }
60 return args
61 }
62
63 func listFiles(dir, ext string) []string {
64 entries, err := os.ReadDir(dir)
65 if err != nil {
66 return nil
67 }
68 var result []string
69 for _, e := range entries {
70 name := e.Name()
71 if hasSuffix(name, ext) {
72 result = append(result, name)
73 }
74 }
75 return result
76 }
77
78 func run(args []string) int32 {
79 argv := make([]unsafe.Pointer, len(args)+1)
80 cstrs := make([][]byte, len(args))
81 for i, a := range args {
82 b := make([]byte, len(a)+1)
83 copy(b, a)
84 b[len(a)] = 0
85 cstrs[i] = b
86 argv[i] = unsafe.Pointer(&b[0])
87 }
88 return cRun(unsafe.Pointer(&argv[0]), int32(len(args)))
89 }
90
91 func splitLines(s string) []string {
92 var result []string
93 start := 0
94 for i := 0; i < len(s); i++ {
95 if s[i] == '\n' {
96 if i > start {
97 result = append(result, s[start:i])
98 }
99 start = i + 1
100 }
101 }
102 if start < len(s) {
103 result = append(result, s[start:])
104 }
105 return result
106 }
107
108 func hasPrefix(s, prefix string) bool {
109 return len(s) >= len(prefix) && s[:len(prefix)] == prefix
110 }
111
112 func hasSuffix(s, suffix string) bool {
113 return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
114 }
115
116 func trimSpace(s string) string {
117 i := 0
118 for i < len(s) && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') {
119 i++
120 }
121 j := len(s)
122 for j > i && (s[j-1] == ' ' || s[j-1] == '\t' || s[j-1] == '\n' || s[j-1] == '\r') {
123 j--
124 }
125 return s[i:j]
126 }
127
128 func joinPath(a, b string) string {
129 if len(a) == 0 {
130 return b
131 }
132 if a[len(a)-1] == '/' {
133 return a + b
134 }
135 return a + "/" + b
136 }
137
138 func pathBase(path string) string {
139 for i := len(path) - 1; i >= 0; i-- {
140 if path[i] == '/' {
141 return path[i+1:]
142 }
143 }
144 return path
145 }
146
147 func joinStrings(ss []string, sep string) string {
148 if len(ss) == 0 {
149 return ""
150 }
151 n := 0
152 for _, s := range ss {
153 n += len(s)
154 }
155 n += len(sep) * (len(ss) - 1)
156 buf := make([]byte, 0, n)
157 for i, s := range ss {
158 if i > 0 {
159 buf = append(buf, sep...)
160 }
161 buf = append(buf, s...)
162 }
163 return string(buf)
164 }
165
166 func appendUniq(ss []string, s string) []string {
167 for _, x := range ss {
168 if x == s {
169 return ss
170 }
171 }
172 return append(ss, s)
173 }
174
175 func loadSysroot(path string) {
176 data, err := os.ReadFile(path)
177 if err != nil {
178 return
179 }
180 for _, line := range splitLines(string(data)) {
181 line = trimSpace(line)
182 if len(line) == 0 || line[0] == '#' {
183 continue
184 }
185 eq := -1
186 for i := 0; i < len(line); i++ {
187 if line[i] == '=' {
188 eq = i
189 break
190 }
191 }
192 if eq < 0 {
193 continue
194 }
195 key := line[:eq]
196 val := line[eq+1:]
197 os.Setenv(key, val)
198 }
199 }
200
201 func splitBySpace(s string) []string {
202 var result []string
203 start := 0
204 inWord := false
205 for i := 0; i < len(s); i++ {
206 if s[i] == ' ' || s[i] == '\t' {
207 if inWord {
208 result = append(result, s[start:i])
209 inWord = false
210 }
211 } else {
212 if !inWord {
213 start = i
214 inWord = true
215 }
216 }
217 }
218 if inWord {
219 result = append(result, s[start:])
220 }
221 return result
222 }
223
224 func safeName(pkg string) string {
225 b := make([]byte, len(pkg))
226 for i := 0; i < len(pkg); i++ {
227 if pkg[i] == '/' {
228 b[i] = '_'
229 } else {
230 b[i] = pkg[i]
231 }
232 }
233 return string(b)
234 }
235
236 func extractPkgName(data []byte) string {
237 for i := 0; i < len(data); i++ {
238 if i+8 < len(data) && string(data[i:i+8]) == "package " {
239 j := i + 8
240 for j < len(data) && data[j] != '\n' && data[j] != '\r' && data[j] != ' ' {
241 j++
242 }
243 return string(data[i+8 : j])
244 }
245 if data[i] == '\n' {
246 continue
247 }
248 if data[i] == '/' && i+1 < len(data) && data[i+1] == '/' {
249 for i < len(data) && data[i] != '\n' {
250 i++
251 }
252 continue
253 }
254 break
255 }
256 return "main"
257 }
258
259 func extractImports(data []byte) []string {
260 var imports []string
261 lines := splitLines(string(data))
262 inBlock := false
263 for _, line := range lines {
264 line = trimSpace(line)
265 if line == "import (" {
266 inBlock = true
267 continue
268 }
269 if inBlock && line == ")" {
270 inBlock = false
271 continue
272 }
273 if inBlock {
274 imp := extractQuoted(line)
275 if imp != "" {
276 imports = appendUniq(imports, imp)
277 }
278 }
279 if hasPrefix(line, "import \"") {
280 imp := extractQuoted(line[7:])
281 if imp != "" {
282 imports = appendUniq(imports, imp)
283 }
284 }
285 }
286 return imports
287 }
288
289 func extractQuoted(s string) string {
290 start := -1
291 for i := 0; i < len(s); i++ {
292 if s[i] == '"' {
293 if start < 0 {
294 start = i + 1
295 } else {
296 return s[start:i]
297 }
298 }
299 }
300 return ""
301 }
302
303 func concatSources(dir string, files []string) []byte {
304 imports := map[string]bool{}
305 var bodies [][]byte
306 pkgName := ""
307 for _, f := range files {
308 data, err := os.ReadFile(joinPath(dir, f))
309 if err != nil {
310 continue
311 }
312 if pkgName == "" {
313 pkgName = extractPkgName(data)
314 }
315 lines := splitLines(string(data))
316 var body []string
317 i := 0
318 for i < len(lines) {
319 line := trimSpace(lines[i])
320 if hasPrefix(line, "package ") {
321 i++
322 continue
323 }
324 if line == "import (" {
325 i++
326 for i < len(lines) {
327 imp := trimSpace(lines[i])
328 i++
329 if imp == ")" {
330 break
331 }
332 if imp != "" {
333 imports[imp] = true
334 }
335 }
336 continue
337 }
338 if hasPrefix(line, "import ") && !hasPrefix(line, "import (") {
339 imp := line[7:]
340 imports[imp] = true
341 i++
342 continue
343 }
344 body = append(body, lines[i])
345 i++
346 }
347 bodies = append(bodies, []byte(joinStrings(body, "\n")))
348 }
349 var out []byte
350 out = append(out, []byte("package "+pkgName+"\n")...)
351 if len(imports) > 0 {
352 out = append(out, []byte("import (\n")...)
353 for imp := range imports {
354 out = append(out, '\t')
355 out = append(out, []byte(imp)...)
356 out = append(out, '\n')
357 }
358 out = append(out, []byte(")\n")...)
359 }
360 for _, b := range bodies {
361 out = append(out, b...)
362 out = append(out, '\n')
363 }
364 return out
365 }
366
367 func shouldSkipFile(name string) bool {
368 if hasSuffix(name, "_test.go") || hasSuffix(name, "_test.mx") {
369 return true
370 }
371 skip := []string{"_wasm"}
372 base := name
373 if hasSuffix(base, ".go") {
374 base = base[:len(base)-3]
375 } else if hasSuffix(base, ".mx") {
376 base = base[:len(base)-3]
377 }
378 for _, s := range skip {
379 if hasSuffix(base, s) {
380 return true
381 }
382 }
383 return false
384 }
385
386 func hasBuildConstraint(data []byte, forbidden []string) bool {
387 for i := 0; i < len(data) && i < 512; i++ {
388 if data[i] == 'p' {
389 return false
390 }
391 if i+10 < len(data) && string(data[i:i+10]) == "//go:build " {
392 line := ""
393 for j := i + 10; j < len(data) && data[j] != '\n'; j++ {
394 line = line + string(data[j])
395 }
396 for _, f := range forbidden {
397 if line == f || hasPrefix(line, f+" ") || hasPrefix(line, f+"\t") {
398 return true
399 }
400 }
401 return false
402 }
403 if data[i] == '\n' {
404 continue
405 }
406 if data[i] == '/' && i+1 < len(data) && data[i+1] == '/' {
407 for i < len(data) && data[i] != '\n' {
408 i++
409 }
410 continue
411 }
412 }
413 return false
414 }
415
416 type pkgInfo struct {
417 path string
418 name string
419 dir string
420 files []string
421 cfiles []string
422 imports []string
423 }
424
425 func discoverPkg(pkgPath, root string) *pkgInfo {
426 var dir string
427 if hasPrefix(pkgPath, "/") || hasPrefix(pkgPath, "./") || hasPrefix(pkgPath, "../") {
428 dir = pkgPath
429 } else {
430 dir = joinPath(root, joinPath("src", pkgPath))
431 }
432 goFiles := listFiles(dir, ".go")
433 mxFiles := listFiles(dir, ".mx")
434 cFiles := listFiles(dir, ".c")
435 allMx := append(mxFiles, goFiles...)
436 forbidden := []string{"wasm", "js"}
437 var files []string
438 for _, f := range allMx {
439 if shouldSkipFile(f) {
440 continue
441 }
442 data, err := os.ReadFile(joinPath(dir, f))
443 if err != nil {
444 continue
445 }
446 if hasBuildConstraint(data, forbidden) {
447 continue
448 }
449 files = append(files, f)
450 }
451 if len(files) == 0 {
452 return nil
453 }
454 var allImports []string
455 pkgName := ""
456 for _, f := range files {
457 data, err := os.ReadFile(joinPath(dir, f))
458 if err != nil {
459 continue
460 }
461 if pkgName == "" {
462 pkgName = extractPkgName(data)
463 }
464 for _, imp := range extractImports(data) {
465 allImports = appendUniq(allImports, imp)
466 }
467 }
468 return &pkgInfo{
469 path: pkgPath,
470 name: pkgName,
471 dir: dir,
472 files: files,
473 cfiles: cFiles,
474 imports: allImports,
475 }
476 }
477
478 func resolveAll(rootPkg string, root string) []*pkgInfo {
479 visited := map[string]bool{}
480 var order []*pkgInfo
481 var walk func(string)
482 walk = func(path string) {
483 if visited[path] {
484 return
485 }
486 visited[path] = true
487 if path == "unsafe" || path == "moxie" || path == "C" {
488 return
489 }
490 if hasPrefix(path, "runtime/") {
491 return
492 }
493 pkg := discoverPkg(path, root)
494 if pkg == nil {
495 writeStr(2, "mxc: cannot find package: "+path+"\n")
496 return
497 }
498 for _, imp := range pkg.imports {
499 walk(imp)
500 }
501 order = append(order, pkg)
502 }
503 walk(rootPkg)
504 return order
505 }
506
507 func registerBuiltins() {
508 initUniverse()
509 ensureImportRegistry()
510 importRegistry = map[string]*TCPackage{}
511 unsafePkg := NewTCPackage("unsafe", "unsafe")
512 unsafePkg.Scope().Insert(NewTypeName(unsafePkg, "Pointer", Typ[UnsafePointer]))
513 unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Sizeof", parseSignatureDesc("interface{}->int32")))
514 unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Offsetof", parseSignatureDesc("interface{}->int32")))
515 unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Alignof", parseSignatureDesc("interface{}->int32")))
516 unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "Slice", parseSignatureDesc("ptr,int32->[]uint8")))
517 unsafePkg.Scope().Insert(NewTCFunc(unsafePkg, "SliceData", parseSignatureDesc("[]uint8->ptr")))
518 importRegistry["unsafe"] = unsafePkg
519 importRegistry["moxie"] = NewTCPackage("moxie", "moxie")
520 }
521
522 func compileSource(src []byte, pkgName, triple string) string {
523 ir := CompileToIR(src, pkgName, triple)
524 if len(ir) > 0 && ir[0] == ';' {
525 writeStr(2, ir)
526 fatal("compile error for " + pkgName)
527 }
528 if len(ir) == 0 {
529 fatal("empty IR for " + pkgName)
530 }
531 return ir
532 }
533
534 func main() {
535 args := getArgs()
536 if len(args) >= 2 {
537 cmd := args[1]
538 if cmd == "version" {
539 writeStr(1, "mxc 1.6.1\n")
540 return
541 }
542 if cmd != "build" || len(args) < 3 {
543 writeStr(2, "usage: mxc build [-o output] <package>\n")
544 os.Exit(1)
545 }
546 } else {
547 writeStr(2, "usage: mxc build [-o output] <package>\n")
548 os.Exit(1)
549 }
550
551 outpath := ""
552 pkgPath := ""
553 printIR := false
554 i := 2
555 for i < len(args) {
556 if args[i] == "-o" && i+1 < len(args) {
557 outpath = args[i+1]
558 i += 2
559 } else if args[i] == "-print-ir" {
560 printIR = true
561 i++
562 } else {
563 pkgPath = args[i]
564 i++
565 }
566 }
567 if pkgPath == "" {
568 fatal("no package specified")
569 }
570 if outpath == "" {
571 outpath = pathBase(pkgPath)
572 }
573
574 root := getenv("MOXIEROOT")
575 if root == "" {
576 root = "."
577 }
578 triple := "x86_64-unknown-linux-musleabihf"
579 clang := "clang-21"
580
581 registerBuiltins()
582 pkgs := resolveAll(pkgPath, root)
583 if len(pkgs) == 0 {
584 fatal("no packages to compile")
585 }
586
587 tmpdir := "/tmp/mxc-build"
588 os.MkdirAll(tmpdir, 0755)
589
590 var userBcFiles []string
591
592 for _, pkg := range pkgs {
593 writeStr(2, " compile "+pkg.path+"\n")
594 src := concatSources(pkg.dir, pkg.files)
595 compilePath := pkg.path
596 if hasPrefix(compilePath, "/") || hasPrefix(compilePath, "./") || hasPrefix(compilePath, "../") {
597 compilePath = pkg.name
598 }
599 ir := compileSource(src, compilePath, triple)
600 if printIR {
601 writeStr(1, ir)
602 }
603 llFile := joinPath(tmpdir, safeName(pkg.path)+".ll")
604 os.WriteFile(llFile, []byte(ir), 0644)
605 bcFile := joinPath(tmpdir, safeName(pkg.path)+".bc")
606 rc := run([]string{clang, "--target="+triple, "-emit-llvm", "-c", llFile, "-o", bcFile})
607 if rc != 0 {
608 fatal("clang failed for " + pkg.path)
609 }
610 userBcFiles = append(userBcFiles, bcFile)
611 }
612
613 loadSysroot(joinPath(root, "_sysroot.env"))
614
615 runtimeBc := getenv("RUNTIME_BC")
616 if runtimeBc == "" {
617 runtimeBc = joinPath(root, "_runtime.bc")
618 }
619 muslDir := getenv("MUSL_DIR")
620 if muslDir == "" {
621 fatal("MUSL_DIR not set - run build-self.sh first")
622 }
623
624 writeStr(2, " generate initAll\n")
625 var initCalls string
626 for _, pkg := range pkgs {
627 compilePath := pkg.path
628 if hasPrefix(compilePath, "/") || hasPrefix(compilePath, "./") || hasPrefix(compilePath, "../") {
629 compilePath = pkg.name
630 }
631 initCalls = initCalls + " call void @" + compilePath + ".init(ptr %context)\n"
632 }
633 initAllIR := "define void @runtime.initAll(ptr %context) {\nentry:\n" + initCalls + " ret void\n}\n"
634 for _, pkg := range pkgs {
635 compilePath := pkg.path
636 if hasPrefix(compilePath, "/") || hasPrefix(compilePath, "./") || hasPrefix(compilePath, "../") {
637 compilePath = pkg.name
638 }
639 initAllIR = initAllIR + "declare void @" + compilePath + ".init(ptr)\n"
640 }
641 initAllFile := joinPath(tmpdir, "initall.ll")
642 os.WriteFile(initAllFile, []byte(initAllIR), 0644)
643 initAllBc := joinPath(tmpdir, "initall.bc")
644 rc := run([]string{clang, "--target="+triple, "-emit-llvm", "-c", initAllFile, "-o", initAllBc})
645 if rc != 0 {
646 fatal("clang failed for initall")
647 }
648
649 writeStr(2, " merge bitcode\n")
650 llvmLink := "llvm-link-21"
651 mergedBc := joinPath(tmpdir, "merged.bc")
652 linkArgs := []string{llvmLink, "-o", mergedBc, runtimeBc}
653 linkArgs = append(linkArgs, userBcFiles...)
654 linkArgs = append(linkArgs, initAllBc)
655 rc = run(linkArgs)
656 if rc != 0 {
657 fatal("llvm-link failed")
658 }
659
660 writeStr(2, " link\n")
661 comprtLib := getenv("COMPRT_LIB")
662 bdwgcLib := getenv("BDWGC_LIB")
663 muslLib := getenv("MUSL_LIB")
664 objFiles := getenv("OBJ_FILES")
665 linker := "ld.lld-21"
666 ldArgs := []string{linker, "--gc-sections", "-o", outpath,
667 joinPath(muslDir, "crt1.o"),
668 mergedBc}
669 for _, f := range splitBySpace(objFiles) {
670 if f != "" {
671 ldArgs = append(ldArgs, f)
672 }
673 }
674 ldArgs = append(ldArgs, comprtLib, bdwgcLib, muslLib, "--lto-O0")
675 rc = run(ldArgs)
676 if rc != 0 {
677 fatal("linker failed")
678 }
679 writeStr(2, " -> "+outpath+"\n")
680 }
681