package main import ( "flag" "fmt" "log/slog" "os" "os/exec" "path/filepath" "runtime" "strings" "time" ) /* larc compiler wrapper * builds larc with experimental Go features enabled */ const ( defaultGoExperiment = "fieldtrack,boringcrypto,greenteagc" ) var ( flagOutput = flag.String("o", "", "output binary path (default: ./larc)") flagVerbose = flag.Bool("v", false, "verbose output") flagRace = flag.Bool("race", false, "enable race detector") flagTrimpath = flag.Bool("trimpath", true, "remove file system paths from binary") flagLdflags = flag.String("ldflags", "", "additional ldflags") flagTags = flag.String("tags", "", "build tags") flagExperiment = flag.String("goexperiment", defaultGoExperiment, "GOEXPERIMENT value") flagTarget = flag.String("target", "larc", "target to build (larc, larcs, all)") flagClean = flag.Bool("clean", false, "clean build cache before building") ) func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: go run ./cmd/compile [flags]\n\n") fmt.Fprintf(os.Stderr, "Builds larc with experimental Go features:\n") fmt.Fprintf(os.Stderr, " GOEXPERIMENT=%s\n\n", defaultGoExperiment) fmt.Fprintf(os.Stderr, "Flags:\n") flag.PrintDefaults() } flag.Parse() logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ Level: func() slog.Level { if *flagVerbose { return slog.LevelDebug } return slog.LevelInfo }(), })) slog.SetDefault(logger) /* find project root */ projectRoot, err := findProjectRoot() if err != nil { slog.Error("failed to find project root", "error", err) os.Exit(1) } slog.Debug("project root", "path", projectRoot) /* clean if requested */ if *flagClean { slog.Info("cleaning build cache") cleanCmd := exec.Command("go", "clean", "-cache") cleanCmd.Dir = projectRoot cleanCmd.Stdout = os.Stdout cleanCmd.Stderr = os.Stderr if err := cleanCmd.Run(); err != nil { slog.Warn("clean failed", "error", err) } } /* determine targets */ targets := []string{} switch *flagTarget { case "all": targets = []string{"larc", "larcs"} case "larc", "larcs": targets = []string{*flagTarget} default: slog.Error("unknown target", "target", *flagTarget) os.Exit(1) } startTime := time.Now() for _, target := range targets { if err := buildTarget(projectRoot, target); err != nil { slog.Error("build failed", "target", target, "error", err) os.Exit(1) } } elapsed := time.Since(startTime) slog.Info("build completed", "duration", elapsed.Round(time.Millisecond)) } func buildTarget(projectRoot, target string) error { slog.Info("building", "target", target, "goexperiment", *flagExperiment) /* build output path */ output := *flagOutput if output == "" { output = filepath.Join(projectRoot, target) if runtime.GOOS == "windows" { output += ".exe" } } /* construct ldflags */ ldflags := []string{"-s", "-w"} // strip debug info if *flagLdflags != "" { ldflags = append(ldflags, *flagLdflags) } /* build version info into binary */ buildTime := time.Now().UTC().Format(time.RFC3339) ldflags = append(ldflags, fmt.Sprintf("-X main.buildTime=%s", buildTime), fmt.Sprintf("-X main.goExperiment=%s", *flagExperiment), ) /* construct go build args */ args := []string{"build"} if *flagVerbose { args = append(args, "-v") } if *flagRace { args = append(args, "-race") } if *flagTrimpath { args = append(args, "-trimpath") } if *flagTags != "" { args = append(args, "-tags", *flagTags) } args = append(args, "-ldflags", strings.Join(ldflags, " ")) args = append(args, "-o", output) args = append(args, fmt.Sprintf("./cmd/%s", target)) slog.Debug("go build", "args", args) /* setup environment */ env := os.Environ() env = setEnv(env, "GOEXPERIMENT", *flagExperiment) env = setEnv(env, "CGO_ENABLED", "1") // sqlite // mb replace with gh:glebarez/go-sqlite? /* run build */ cmd := exec.Command("go", args...) cmd.Dir = projectRoot cmd.Env = env cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("go build failed: %w", err) } /* get binary info */ info, err := os.Stat(output) if err != nil { return fmt.Errorf("stat output: %w", err) } slog.Info("built", "binary", output, "size", formatSize(info.Size()), "goos", runtime.GOOS, "goarch", runtime.GOARCH, ) return nil } func findProjectRoot() (string, error) { /* try to find go.mod */ dir, err := os.Getwd() if err != nil { return "", err } for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir, nil } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } /* fallback to executable dir */ exe, err := os.Executable() if err != nil { return "", fmt.Errorf("cannot find project root: %w", err) } return filepath.Dir(filepath.Dir(exe)), nil } func setEnv(env []string, key, value string) []string { prefix := key + "=" for i, e := range env { if strings.HasPrefix(e, prefix) { env[i] = prefix + value return env } } return append(env, prefix+value) } func formatSize(bytes int64) string { const unit = 1024 if bytes < unit { return fmt.Sprintf("%d B", bytes) } div, exp := int64(unit), 0 for n := bytes / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) }