| 1 |
package main |
| 2 |
|
| 3 |
import ( |
| 4 |
"flag" |
| 5 |
"fmt" |
| 6 |
"log/slog" |
| 7 |
"os" |
| 8 |
"os/exec" |
| 9 |
"path/filepath" |
| 10 |
"runtime" |
| 11 |
"strings" |
| 12 |
"time" |
| 13 |
) |
| 14 |
|
| 15 |
/* larc compiler wrapper |
| 16 |
* builds larc with experimental Go features enabled */ |
| 17 |
|
| 18 |
const ( |
| 19 |
defaultGoExperiment = "fieldtrack,boringcrypto,greenteagc,randomizedheapbase64,simd" |
| 20 |
) |
| 21 |
|
| 22 |
var ( |
| 23 |
flagOutput = flag.String("o", "", "output binary path (default: ./larc)") |
| 24 |
flagVerbose = flag.Bool("v", false, "verbose output") |
| 25 |
flagRace = flag.Bool("race", false, "enable race detector") |
| 26 |
flagTrimpath = flag.Bool("trimpath", true, "remove file system paths from binary") |
| 27 |
flagLdflags = flag.String("ldflags", "", "additional ldflags") |
| 28 |
flagTags = flag.String("tags", "", "build tags") |
| 29 |
flagExperiment = flag.String("goexperiment", defaultGoExperiment, "GOEXPERIMENT value") |
| 30 |
flagTarget = flag.String("target", "larc", "target to build (larc, larcs, all)") |
| 31 |
flagClean = flag.Bool("clean", false, "clean build cache before building") |
| 32 |
) |
| 33 |
|
| 34 |
func main() { |
| 35 |
flag.Usage = func() { |
| 36 |
fmt.Fprintf(os.Stderr, "Usage: go run ./cmd/compile [flags]\n\n") |
| 37 |
fmt.Fprintf(os.Stderr, "Builds larc with experimental Go features:\n") |
| 38 |
fmt.Fprintf(os.Stderr, " GOEXPERIMENT=%s\n\n", defaultGoExperiment) |
| 39 |
fmt.Fprintf(os.Stderr, "Flags:\n") |
| 40 |
flag.PrintDefaults() |
| 41 |
} |
| 42 |
flag.Parse() |
| 43 |
|
| 44 |
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ |
| 45 |
Level: func() slog.Level { |
| 46 |
if *flagVerbose { |
| 47 |
return slog.LevelDebug |
| 48 |
} |
| 49 |
return slog.LevelInfo |
| 50 |
}(), |
| 51 |
})) |
| 52 |
slog.SetDefault(logger) |
| 53 |
|
| 54 |
/* find project root */ |
| 55 |
projectRoot, err := findProjectRoot() |
| 56 |
if err != nil { |
| 57 |
slog.Error("failed to find project root", "error", err) |
| 58 |
os.Exit(1) |
| 59 |
} |
| 60 |
slog.Debug("project root", "path", projectRoot) |
| 61 |
|
| 62 |
/* clean if requested */ |
| 63 |
if *flagClean { |
| 64 |
slog.Info("cleaning build cache") |
| 65 |
cleanCmd := exec.Command("go", "clean", "-cache") |
| 66 |
cleanCmd.Dir = projectRoot |
| 67 |
cleanCmd.Stdout = os.Stdout |
| 68 |
cleanCmd.Stderr = os.Stderr |
| 69 |
if err := cleanCmd.Run(); err != nil { |
| 70 |
slog.Warn("clean failed", "error", err) |
| 71 |
} |
| 72 |
} |
| 73 |
|
| 74 |
/* determine targets */ |
| 75 |
targets := []string{} |
| 76 |
switch *flagTarget { |
| 77 |
case "all": |
| 78 |
targets = []string{"larc", "larcs"} |
| 79 |
case "larc", "larcs": |
| 80 |
targets = []string{*flagTarget} |
| 81 |
default: |
| 82 |
slog.Error("unknown target", "target", *flagTarget) |
| 83 |
os.Exit(1) |
| 84 |
} |
| 85 |
|
| 86 |
startTime := time.Now() |
| 87 |
|
| 88 |
for _, target := range targets { |
| 89 |
if err := buildTarget(projectRoot, target); err != nil { |
| 90 |
slog.Error("build failed", "target", target, "error", err) |
| 91 |
os.Exit(1) |
| 92 |
} |
| 93 |
} |
| 94 |
|
| 95 |
elapsed := time.Since(startTime) |
| 96 |
slog.Info("build completed", "duration", elapsed.Round(time.Millisecond)) |
| 97 |
} |
| 98 |
|
| 99 |
func buildTarget(projectRoot, target string) error { |
| 100 |
slog.Info("building", "target", target, "goexperiment", *flagExperiment) |
| 101 |
|
| 102 |
/* build output path */ |
| 103 |
output := *flagOutput |
| 104 |
if output == "" { |
| 105 |
output = filepath.Join(projectRoot, target) |
| 106 |
if runtime.GOOS == "windows" { |
| 107 |
output += ".exe" |
| 108 |
} |
| 109 |
} |
| 110 |
|
| 111 |
/* construct ldflags */ |
| 112 |
ldflags := []string{"-s", "-w"} // strip debug info |
| 113 |
if *flagLdflags != "" { |
| 114 |
ldflags = append(ldflags, *flagLdflags) |
| 115 |
} |
| 116 |
|
| 117 |
/* build version info into binary */ |
| 118 |
buildTime := time.Now().UTC().Format(time.RFC3339) |
| 119 |
ldflags = append(ldflags, |
| 120 |
fmt.Sprintf("-X main.buildTime=%s", buildTime), |
| 121 |
fmt.Sprintf("-X main.goExperiment=%s", *flagExperiment), |
| 122 |
) |
| 123 |
|
| 124 |
/* construct go build args */ |
| 125 |
args := []string{"build"} |
| 126 |
|
| 127 |
if *flagVerbose { |
| 128 |
args = append(args, "-v") |
| 129 |
} |
| 130 |
|
| 131 |
if *flagRace { |
| 132 |
args = append(args, "-race") |
| 133 |
} |
| 134 |
|
| 135 |
if *flagTrimpath { |
| 136 |
args = append(args, "-trimpath") |
| 137 |
} |
| 138 |
|
| 139 |
if *flagTags != "" { |
| 140 |
args = append(args, "-tags", *flagTags) |
| 141 |
} |
| 142 |
|
| 143 |
args = append(args, "-ldflags", strings.Join(ldflags, " ")) |
| 144 |
args = append(args, "-o", output) |
| 145 |
args = append(args, fmt.Sprintf("./cmd/%s", target)) |
| 146 |
|
| 147 |
slog.Debug("go build", "args", args) |
| 148 |
|
| 149 |
/* setup environment */ |
| 150 |
env := os.Environ() |
| 151 |
env = setEnv(env, "GOEXPERIMENT", *flagExperiment) |
| 152 |
env = setEnv(env, "CGO_ENABLED", "1") // sqlite // mb replace with gh:glebarez/go-sqlite? |
| 153 |
|
| 154 |
/* run build */ |
| 155 |
cmd := exec.Command("go", args...) |
| 156 |
cmd.Dir = projectRoot |
| 157 |
cmd.Env = env |
| 158 |
cmd.Stdout = os.Stdout |
| 159 |
cmd.Stderr = os.Stderr |
| 160 |
|
| 161 |
if err := cmd.Run(); err != nil { |
| 162 |
return fmt.Errorf("go build failed: %w", err) |
| 163 |
} |
| 164 |
|
| 165 |
/* get binary info */ |
| 166 |
info, err := os.Stat(output) |
| 167 |
if err != nil { |
| 168 |
return fmt.Errorf("stat output: %w", err) |
| 169 |
} |
| 170 |
|
| 171 |
slog.Info("built", |
| 172 |
"binary", output, |
| 173 |
"size", formatSize(info.Size()), |
| 174 |
"goos", runtime.GOOS, |
| 175 |
"goarch", runtime.GOARCH, |
| 176 |
) |
| 177 |
|
| 178 |
return nil |
| 179 |
} |
| 180 |
|
| 181 |
func findProjectRoot() (string, error) { |
| 182 |
/* try to find go.mod */ |
| 183 |
dir, err := os.Getwd() |
| 184 |
if err != nil { |
| 185 |
return "", err |
| 186 |
} |
| 187 |
|
| 188 |
for { |
| 189 |
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { |
| 190 |
return dir, nil |
| 191 |
} |
| 192 |
|
| 193 |
parent := filepath.Dir(dir) |
| 194 |
if parent == dir { |
| 195 |
break |
| 196 |
} |
| 197 |
dir = parent |
| 198 |
} |
| 199 |
|
| 200 |
/* fallback to executable dir */ |
| 201 |
exe, err := os.Executable() |
| 202 |
if err != nil { |
| 203 |
return "", fmt.Errorf("cannot find project root: %w", err) |
| 204 |
} |
| 205 |
|
| 206 |
return filepath.Dir(filepath.Dir(exe)), nil |
| 207 |
} |
| 208 |
|
| 209 |
func setEnv(env []string, key, value string) []string { |
| 210 |
prefix := key + "=" |
| 211 |
for i, e := range env { |
| 212 |
if strings.HasPrefix(e, prefix) { |
| 213 |
env[i] = prefix + value |
| 214 |
return env |
| 215 |
} |
| 216 |
} |
| 217 |
return append(env, prefix+value) |
| 218 |
} |
| 219 |
|
| 220 |
func formatSize(bytes int64) string { |
| 221 |
const unit = 1024 |
| 222 |
if bytes < unit { |
| 223 |
return fmt.Sprintf("%d B", bytes) |
| 224 |
} |
| 225 |
div, exp := int64(unit), 0 |
| 226 |
for n := bytes / unit; n >= unit; n /= unit { |
| 227 |
div *= unit |
| 228 |
exp++ |
| 229 |
} |
| 230 |
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) |
| 231 |
} |
| 232 |
|