compile.go raw
1 package organ
2
3 import (
4 "context"
5 "fmt"
6 "os"
7 "os/exec"
8 "path/filepath"
9 )
10
11 // CompileOrgan compiles Go source code into a WASM module using the Go
12 // compiler with GOOS=wasip1 GOARCH=wasm.
13 //
14 // The source is written to a temporary directory, compiled, and the
15 // resulting .wasm bytes are returned. The temporary directory is cleaned
16 // up afterward.
17 //
18 // goRoot is the path to the Go installation. If empty, the system default
19 // is used. tinyGo is the path to the TinyGo binary. If provided and
20 // non-empty, TinyGo is used instead of standard Go (producing smaller
21 // modules).
22 func CompileOrgan(ctx context.Context, source []byte, organType OrganType, goRoot string, tinyGo string) ([]byte, error) {
23 // Create temp directory for compilation.
24 tmpDir, err := os.MkdirTemp("", "organ-compile-*")
25 if err != nil {
26 return nil, fmt.Errorf("create temp dir: %w", err)
27 }
28 defer os.RemoveAll(tmpDir)
29
30 // Write the source file.
31 srcPath := filepath.Join(tmpDir, "main.go")
32 if err := os.WriteFile(srcPath, source, 0o644); err != nil {
33 return nil, fmt.Errorf("write source: %w", err)
34 }
35
36 // Write a minimal go.mod.
37 modContent := fmt.Sprintf("module organ-%s\n\ngo 1.24\n", organType)
38 modPath := filepath.Join(tmpDir, "go.mod")
39 if err := os.WriteFile(modPath, []byte(modContent), 0o644); err != nil {
40 return nil, fmt.Errorf("write go.mod: %w", err)
41 }
42
43 wasmPath := filepath.Join(tmpDir, "organ.wasm")
44
45 if tinyGo != "" {
46 // TinyGo compilation (smaller modules, ~60-260KB).
47 err = compileTinyGo(ctx, tinyGo, tmpDir, wasmPath)
48 } else {
49 // Standard Go compilation (~1.6MB, but no external dependency).
50 err = compileStdGo(ctx, goRoot, tmpDir, wasmPath)
51 }
52 if err != nil {
53 return nil, err
54 }
55
56 // Read the compiled WASM.
57 wasmBytes, err := os.ReadFile(wasmPath)
58 if err != nil {
59 return nil, fmt.Errorf("read wasm: %w", err)
60 }
61
62 return wasmBytes, nil
63 }
64
65 // compileStdGo compiles using standard Go with GOOS=wasip1 GOARCH=wasm.
66 func compileStdGo(ctx context.Context, goRoot string, srcDir string, outPath string) error {
67 goBin := "go"
68 if goRoot != "" {
69 goBin = filepath.Join(goRoot, "bin", "go")
70 }
71
72 cmd := exec.CommandContext(ctx, goBin, "build",
73 "-buildmode=c-shared",
74 "-o", outPath,
75 ".",
76 )
77 cmd.Dir = srcDir
78 cmd.Env = buildEnv(goRoot, "wasip1", "wasm")
79
80 output, err := cmd.CombinedOutput()
81 if err != nil {
82 return fmt.Errorf("go build: %w\n%s", err, output)
83 }
84 return nil
85 }
86
87 // compileTinyGo compiles using TinyGo for smaller WASM modules.
88 func compileTinyGo(ctx context.Context, tinyGoBin string, srcDir string, outPath string) error {
89 cmd := exec.CommandContext(ctx, tinyGoBin, "build",
90 "-o", outPath,
91 "-target=wasip1",
92 "-scheduler=none",
93 ".",
94 )
95 cmd.Dir = srcDir
96
97 output, err := cmd.CombinedOutput()
98 if err != nil {
99 return fmt.Errorf("tinygo build: %w\n%s", err, output)
100 }
101 return nil
102 }
103
104 // buildEnv constructs the environment for WASM compilation.
105 func buildEnv(goRoot string, goos string, goarch string) []string {
106 env := os.Environ()
107 clean := make([]string, 0, len(env)+5)
108 for _, e := range env {
109 // Filter out variables we'll override.
110 switch {
111 case len(e) > 5 && e[:5] == "GOOS=":
112 continue
113 case len(e) > 7 && e[:7] == "GOARCH=":
114 continue
115 case len(e) > 7 && e[:7] == "GOROOT=":
116 continue
117 case len(e) > 12 && e[:12] == "GOTOOLCHAIN=":
118 continue
119 }
120 clean = append(clean, e)
121 }
122 clean = append(clean,
123 "GOOS="+goos,
124 "GOARCH="+goarch,
125 )
126 if goRoot != "" {
127 clean = append(clean,
128 "GOROOT="+goRoot,
129 "GOTOOLCHAIN=local",
130 )
131 }
132 return clean
133 }
134