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