larc r21

232 lines ยท 5.3 KB Raw
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"
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