// Byggverktyg: `go run ./tools/build [flaggor]` // // Skrivet i Go så att samma kommando fungerar på Linux och Windows utan // make, bash eller andra extra verktyg. Bygger binären med rätt flaggor för // debug/release, stämplar version + läge via -ldflags och kopierar // resources/ bredvid binären. // // debug -> build/debug/ + resources/ // release -> build/release/-/ + resources/ package main import ( "flag" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" "time" ) const ( appName = "metro-puzzle-jp" mainPkg = "./cmd/" + appName ) func main() { mode := flag.String("mode", "debug", "debug | release") goos := flag.String("os", runtime.GOOS, "målplattform: linux | windows") goarch := flag.String("arch", runtime.GOARCH, "arkitektur: amd64 | arm64") run := flag.Bool("run", false, "starta binären efter bygget (kräver att os/arch matchar den här datorn)") flag.Parse() if *mode != "debug" && *mode != "release" { fatal("-mode måste vara debug eller release") } switch *goos { case "linux", "windows": case "darwin": fatal("macOS kräver cgo och måste byggas på en Mac (`go build` fungerar där, men inte korsbygge härifrån)") default: fatal("stöd saknas för -os " + *goos) } if *goos == "linux" && (*goos != runtime.GOOS || *goarch != runtime.GOARCH) { fatal("korsbygge till linux/" + *goarch + " kräver en C-korskompilator (cgo); bygg på målmaskinen i stället") } root := repoRoot() module := modulePath(root) version := gitVersion(root) outDir := filepath.Join(root, "build", "debug") if *mode == "release" { outDir = filepath.Join(root, "build", "release", *goos+"-"+*goarch) } exe := appName if *goos == "windows" { exe += ".exe" } exePath := filepath.Join(outDir, exe) must(os.MkdirAll(outDir, 0o755)) ldflags := []string{ "-X", module + "/internal/buildinfo.Version=" + version, "-X", module + "/internal/buildinfo.Mode=" + *mode, } args := []string{"build", "-o", exePath} if *mode == "release" { ldflags = append(ldflags, "-s", "-w") // strippa symboler/debuginfo if *goos == "windows" { ldflags = append(ldflags, "-H", "windowsgui") // inget konsolfönster } args = append(args, "-trimpath") } else { args = append(args, "-gcflags=all=-N -l") // ingen optimering/inlining -> bättre debugging } args = append(args, "-ldflags", strings.Join(ldflags, " "), mainPkg) cmd := exec.Command("go", args...) cmd.Dir = root cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr cmd.Env = append(os.Environ(), "GOOS="+*goos, "GOARCH="+*goarch) if *goos == "windows" { // Ebitengine på Windows är ren Go -> korsbygge från Linux utan C-verktyg. cmd.Env = append(cmd.Env, "CGO_ENABLED=0") } start := time.Now() fmt.Printf("» go %s\n", strings.Join(args, " ")) must(cmd.Run()) // Kopiera resources/ bredvid binären (ersätt gammal kopia). dst := filepath.Join(outDir, "resources") must(os.RemoveAll(dst)) n, err := copyTree(filepath.Join(root, "resources"), dst) must(err) st, _ := os.Stat(exePath) fmt.Printf("✓ %s-bygge klart på %s\n binär: %s (%.1f MB, %s)\n resurser: %s (%d filer)\n", *mode, time.Since(start).Round(time.Millisecond), rel(root, exePath), float64(st.Size())/1e6, version, rel(root, dst), n) if *run { if *goos != runtime.GOOS || *goarch != runtime.GOARCH { fatal("kan inte köra en " + *goos + "/" + *goarch + "-binär här") } r := exec.Command(exePath, flag.Args()...) r.Dir = outDir r.Stdout, r.Stderr, r.Stdin = os.Stdout, os.Stderr, os.Stdin if err := r.Run(); err != nil { fatal(err.Error()) } } } // repoRoot letar uppåt från arbetskatalogen efter go.mod. func repoRoot() string { dir, err := os.Getwd() must(err) for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir } parent := filepath.Dir(dir) if parent == dir { fatal("hittade ingen go.mod ovanför arbetskatalogen – kör från repot") } dir = parent } } func modulePath(root string) string { b, err := os.ReadFile(filepath.Join(root, "go.mod")) must(err) for _, line := range strings.Split(string(b), "\n") { if strings.HasPrefix(line, "module ") { return strings.TrimSpace(strings.TrimPrefix(line, "module ")) } } fatal("go.mod saknar module-rad") return "" } func gitVersion(root string) string { // Bara v*-taggar räknas; den rullande release-taggen "latest" ska inte synas i versionen. cmd := exec.Command("git", "describe", "--tags", "--match", "v*", "--always", "--dirty") cmd.Dir = root out, err := cmd.Output() if err != nil { return "dev" } return strings.TrimSpace(string(out)) } // copyTree kopierar en mapp rekursivt. README.md-filer i resursmappen är // dokumentation för utvecklare och hoppas över. func copyTree(src, dst string) (int, error) { n := 0 err := filepath.WalkDir(src, func(p string, d os.DirEntry, err error) error { if err != nil { return err } relp, _ := filepath.Rel(src, p) target := filepath.Join(dst, relp) if d.IsDir() { return os.MkdirAll(target, 0o755) } if strings.EqualFold(d.Name(), "README.md") { return nil } in, err := os.Open(p) if err != nil { return err } defer in.Close() out, err := os.Create(target) if err != nil { return err } defer out.Close() if _, err := io.Copy(out, in); err != nil { return err } n++ return nil }) return n, err } func rel(root, p string) string { if r, err := filepath.Rel(root, p); err == nil { return r } return p } func must(err error) { if err != nil { fatal(err.Error()) } } func fatal(msg string) { fmt.Fprintln(os.Stderr, "build: "+msg) os.Exit(1) }