package organ import ( "context" "fmt" "os" "os/exec" "path/filepath" ) // CompileOrgan compiles Go source code into a WASM module using the Go // compiler with GOOS=wasip1 GOARCH=wasm. // // The source is written to a temporary directory, compiled, and the // resulting .wasm bytes are returned. The temporary directory is cleaned // up afterward. // // goRoot is the path to the Go installation. If empty, the system default // is used. tinyGo is the path to the TinyGo binary. If provided and // non-empty, TinyGo is used instead of standard Go (producing smaller // modules). func CompileOrgan(ctx context.Context, source []byte, organType OrganType, goRoot string, tinyGo string) ([]byte, error) { // Create temp directory for compilation. tmpDir, err := os.MkdirTemp("", "organ-compile-*") if err != nil { return nil, fmt.Errorf("create temp dir: %w", err) } defer os.RemoveAll(tmpDir) // Write the source file. srcPath := filepath.Join(tmpDir, "main.go") if err := os.WriteFile(srcPath, source, 0o644); err != nil { return nil, fmt.Errorf("write source: %w", err) } // Write a minimal go.mod. modContent := fmt.Sprintf("module organ-%s\n\ngo 1.24\n", organType) modPath := filepath.Join(tmpDir, "go.mod") if err := os.WriteFile(modPath, []byte(modContent), 0o644); err != nil { return nil, fmt.Errorf("write go.mod: %w", err) } wasmPath := filepath.Join(tmpDir, "organ.wasm") if tinyGo != "" { // TinyGo compilation (smaller modules, ~60-260KB). err = compileTinyGo(ctx, tinyGo, tmpDir, wasmPath) } else { // Standard Go compilation (~1.6MB, but no external dependency). err = compileStdGo(ctx, goRoot, tmpDir, wasmPath) } if err != nil { return nil, err } // Read the compiled WASM. wasmBytes, err := os.ReadFile(wasmPath) if err != nil { return nil, fmt.Errorf("read wasm: %w", err) } return wasmBytes, nil } // compileStdGo compiles using standard Go with GOOS=wasip1 GOARCH=wasm. func compileStdGo(ctx context.Context, goRoot string, srcDir string, outPath string) error { goBin := "go" if goRoot != "" { goBin = filepath.Join(goRoot, "bin", "go") } cmd := exec.CommandContext(ctx, goBin, "build", "-buildmode=c-shared", "-o", outPath, ".", ) cmd.Dir = srcDir cmd.Env = buildEnv(goRoot, "wasip1", "wasm") output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("go build: %w\n%s", err, output) } return nil } // compileTinyGo compiles using TinyGo for smaller WASM modules. func compileTinyGo(ctx context.Context, tinyGoBin string, srcDir string, outPath string) error { cmd := exec.CommandContext(ctx, tinyGoBin, "build", "-o", outPath, "-target=wasip1", "-scheduler=none", ".", ) cmd.Dir = srcDir output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("tinygo build: %w\n%s", err, output) } return nil } // buildEnv constructs the environment for WASM compilation. func buildEnv(goRoot string, goos string, goarch string) []string { env := os.Environ() clean := make([]string, 0, len(env)+5) for _, e := range env { // Filter out variables we'll override. switch { case len(e) > 5 && e[:5] == "GOOS=": continue case len(e) > 7 && e[:7] == "GOARCH=": continue case len(e) > 7 && e[:7] == "GOROOT=": continue case len(e) > 12 && e[:12] == "GOTOOLCHAIN=": continue } clean = append(clean, e) } clean = append(clean, "GOOS="+goos, "GOARCH="+goarch, ) if goRoot != "" { clean = append(clean, "GOROOT="+goRoot, "GOTOOLCHAIN=local", ) } return clean }