From 468aa1efbd2427fef6462f70b080cf13b4983785 Mon Sep 17 00:00:00 2001 From: Charles KWON OhJun Date: Sat, 11 Apr 2026 11:57:56 +0900 Subject: [PATCH] fix: add cmd/five/main.go to repo (was excluded by .gitignore) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changed .gitignore: "five" → "/five" to only ignore root binary - cmd/five/main.go (702 LOC): Five CLI entry point (run, build, gen, debug, frb) Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 2 +- cmd/five/main.go | 702 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 703 insertions(+), 1 deletion(-) create mode 100644 cmd/five/main.go diff --git a/.gitignore b/.gitignore index 2f7ff12..c99f2ce 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ *.dll *.so *.dylib -five +/five # Test binary *.test diff --git a/cmd/five/main.go b/cmd/five/main.go new file mode 100644 index 0000000..ca6dbda --- /dev/null +++ b/cmd/five/main.go @@ -0,0 +1,702 @@ +// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) +// All rights reserved. + +// Five CLI — the Harbour + Go fusion language tool. +// +// Usage: +// five run Compile and run a PRG file +// five build [-o out] Compile to native binary +// five gen Generate Go source (for debugging) +package main + +import ( + "five/compiler/analyzer" + "five/compiler/gengo" + "five/compiler/genpc" + "five/compiler/parser" + "five/compiler/pp" + "five/hbrt" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +func main() { + if len(os.Args) < 2 { + printUsage() + os.Exit(1) + } + + cmd := os.Args[1] + switch cmd { + case "run": + if len(os.Args) < 3 { + fatal("usage: five run ") + } + runPRG(os.Args[2]) + case "build": + if len(os.Args) < 3 { + fatal("usage: five build [file2.prg ...] [-o output] [-I includedir]") + } + output := "" + var prgFiles []string + var includeDirs []string + for i := 2; i < len(os.Args); i++ { + if os.Args[i] == "-o" && i+1 < len(os.Args) { + output = os.Args[i+1] + i++ + } else if os.Args[i] == "-I" && i+1 < len(os.Args) { + includeDirs = append(includeDirs, os.Args[i+1]) + i++ + } else if strings.HasPrefix(os.Args[i], "-I") { + includeDirs = append(includeDirs, os.Args[i][2:]) + } else { + prgFiles = append(prgFiles, os.Args[i]) + } + } + if len(prgFiles) == 1 { + buildPRGWithIncludes(prgFiles[0], output, includeDirs) + } else { + buildMultiPRGWithIncludes(prgFiles, output, includeDirs) + } + case "gen": + if len(os.Args) < 3 { + fatal("usage: five gen ") + } + genPRG(os.Args[2]) + case "debug": + if len(os.Args) < 3 { + fatal("usage: five debug ") + } + debugPRG(os.Args[2]) + case "frb": + if len(os.Args) < 3 { + fatal("usage: five frb [-o output.frb] [--pcode]") + } + output := "" + prgFile := os.Args[2] + pcodeMode := false + for i := 3; i < len(os.Args); i++ { + if os.Args[i] == "-o" && i+1 < len(os.Args) { + output = os.Args[i+1] + i++ + } else if os.Args[i] == "--pcode" { + pcodeMode = true + } + } + if output == "" { + base := strings.TrimSuffix(filepath.Base(prgFile), filepath.Ext(prgFile)) + output = base + ".frb" + } + if pcodeMode { + buildFRBPcode(prgFile, output) + } else { + buildFRB(prgFile, output) + } + case "version": + fmt.Println("Five 0.1.0 — Harbour + Go fusion language") + fmt.Println("Copyright (c) 2026 Charles KWON OhJun") + default: + fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd) + printUsage() + os.Exit(1) + } +} + +func printUsage() { + fmt.Fprintln(os.Stderr, "Five — Harbour + Go fusion language") + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "Usage:") + fmt.Fprintln(os.Stderr, " five run Compile and run") + fmt.Fprintln(os.Stderr, " five build [-o out] Compile to binary") + fmt.Fprintln(os.Stderr, " five gen Show generated Go code") + fmt.Fprintln(os.Stderr, " five debug Debug with interactive debugger") + fmt.Fprintln(os.Stderr, " five frb [-o out] Compile to FRB module") + fmt.Fprintln(os.Stderr, " five version Show version") +} + +// runPRG compiles a PRG file and runs it immediately. +func runPRG(prgFile string) { + goCode := compilePRG(prgFile) + + // Create temp build directory + tmpDir, err := os.MkdirTemp("", "five-build-*") + if err != nil { + fatal("failed to create temp dir: " + err.Error()) + } + defer os.RemoveAll(tmpDir) + + writeGoProject(tmpDir, prgFile, goCode) + + // go run + cmd := exec.Command(goPath(), "run", ".") + cmd.Dir = tmpDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + os.Exit(exitErr.ExitCode()) + } + fatal("run failed: " + err.Error()) + } +} + +// buildPRG compiles a PRG file to a native binary. +func buildPRG(prgFile, output string) { + goCode := compilePRG(prgFile) + + if output == "" { + base := strings.TrimSuffix(filepath.Base(prgFile), filepath.Ext(prgFile)) + output = base + } + + // Create temp build directory + tmpDir, err := os.MkdirTemp("", "five-build-*") + if err != nil { + fatal("failed to create temp dir: " + err.Error()) + } + defer os.RemoveAll(tmpDir) + + writeGoProject(tmpDir, prgFile, goCode) + + // Resolve absolute path for output + absOutput, err := filepath.Abs(output) + if err != nil { + fatal("cannot resolve output path: " + err.Error()) + } + + // go build + cmd := exec.Command(goPath(), "build", "-o", absOutput, ".") + cmd.Dir = tmpDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fatal("build failed: " + err.Error()) + } + + fmt.Fprintf(os.Stderr, "Built: %s\n", absOutput) +} + +// genPRG generates Go source and prints it to stdout. +func genPRG(prgFile string) { + goCode := compilePRG(prgFile) + fmt.Print(goCode) +} + +// buildMultiPRG compiles multiple PRG files into one binary. +func buildMultiPRG(prgFiles []string, output string) { + if output == "" { + base := strings.TrimSuffix(filepath.Base(prgFiles[0]), filepath.Ext(prgFiles[0])) + output = base + } + + tmpDir, err := os.MkdirTemp("", "five-build-*") + if err != nil { + fatal("failed to create temp dir: " + err.Error()) + } + defer os.RemoveAll(tmpDir) + + // Compile each PRG to a .go file + // First file with MAIN gets Generate(), rest get GenerateLibrary() + for i, prgFile := range prgFiles { + goCode := compilePRGMode(prgFile, i > 0) // i>0 = library mode + goFile := fmt.Sprintf("prg_%d.go", i) + writeFile(filepath.Join(tmpDir, goFile), goCode) + } + + // Write go.mod + fiveRoot := findFiveRoot() + goMod := fmt.Sprintf("module five-generated\n\ngo 1.21\n\nrequire five v0.0.0\n\nreplace five => %s\n", fiveRoot) + writeFile(filepath.Join(tmpDir, "go.mod"), goMod) + + // go mod tidy + tidy := exec.Command(goPath(), "mod", "tidy") + tidy.Dir = tmpDir + tidy.Stderr = os.Stderr + if err := tidy.Run(); err != nil { + fmt.Fprintf(os.Stderr, "warning: go mod tidy: %v\n", err) + } + + // go build + absOutput, err := filepath.Abs(output) + if err != nil { + fatal("cannot resolve output path: " + err.Error()) + } + cmd := exec.Command(goPath(), "build", "-o", absOutput, ".") + cmd.Dir = tmpDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fatal("build failed: " + err.Error()) + } + fmt.Fprintf(os.Stderr, "Built: %s\n", absOutput) +} + +// Global user include dirs (set by build command, used by compilePRGMode) +var userIncludeDirs []string + +// buildPRGWithIncludes is buildPRG with -I support. +func buildPRGWithIncludes(prgFile, output string, includes []string) { + userIncludeDirs = includes + buildPRG(prgFile, output) +} + +// buildMultiPRGWithIncludes is buildMultiPRG with -I support. +func buildMultiPRGWithIncludes(prgFiles []string, output string, includes []string) { + userIncludeDirs = includes + buildMultiPRG(prgFiles, output) +} + +// compilePRGMode compiles with library flag support. +func compilePRGMode(prgFile string, isLibrary bool) string { + source, err := os.ReadFile(prgFile) + if err != nil { + fatal("cannot read file: " + err.Error()) + } + + pre := pp.New() + pre.AddIncludeDir(filepath.Dir(prgFile)) + pre.AddIncludeDir(filepath.Join(filepath.Dir(prgFile), "include")) + // Five's own include directory (from project root) — must come BEFORE Harbour system includes + fiveRoot := findFiveRoot() + pre.AddIncludeDir(filepath.Join(fiveRoot, "include")) + if exePath, err := os.Executable(); err == nil { + pre.AddIncludeDir(filepath.Join(filepath.Dir(exePath), "include")) + } + // User-specified -I directories — before Harbour system includes + for _, dir := range userIncludeDirs { + pre.AddIncludeDir(dir) + } + // Harbour include paths from environment or standard locations (LAST) + if hbInc := os.Getenv("HB_INC"); hbInc != "" { + pre.AddIncludeDir(hbInc) + } + for _, p := range []string{"/usr/local/include/harbour", "/usr/include/harbour"} { + if _, err := os.Stat(p); err == nil { + pre.AddIncludeDir(p) + } + } + + processed, ppErrors := pre.Process(prgFile, string(source)) + for _, e := range ppErrors { + fmt.Fprintf(os.Stderr, "pp: %s\n", e) + } + + file, errs := parser.ParseWithGoDumps(prgFile, processed, pre.GoDumps) + if len(errs) > 0 { + for _, e := range errs { + fmt.Fprintf(os.Stderr, "%s\n", e) + } + fatal(fmt.Sprintf("%d parse error(s) in %s", len(errs), prgFile)) + } + + // Semantic analysis (warnings — non-fatal) + diags := analyzer.Analyze(file) + for _, d := range diags { + if d.Severity <= analyzer.SevWarning { + fmt.Fprintf(os.Stderr, "%s\n", d) + } + } + + if isLibrary { + return gengo.GenerateLibrary(file) + } + return gengo.Generate(file) +} + +// compilePRG preprocesses, parses, and generates Go source. +func compilePRG(prgFile string) string { + source, err := os.ReadFile(prgFile) + if err != nil { + fatal("cannot read file: " + err.Error()) + } + + // Phase 1: Preprocessor (#include, #define, #ifdef) + pre := pp.New() + pre.AddIncludeDir(filepath.Dir(prgFile)) + pre.AddIncludeDir(filepath.Join(filepath.Dir(prgFile), "include")) + // Five's own include directory — MUST come before Harbour system includes + fiveRoot := findFiveRoot() + pre.AddIncludeDir(filepath.Join(fiveRoot, "include")) + if exePath, err := os.Executable(); err == nil { + pre.AddIncludeDir(filepath.Join(filepath.Dir(exePath), "include")) + } + // User-specified -I directories + for _, dir := range userIncludeDirs { + pre.AddIncludeDir(dir) + } + // Harbour include paths (LAST — Five's own headers take priority) + if harbourInc := os.Getenv("HB_INC"); harbourInc != "" { + pre.AddIncludeDir(harbourInc) + } + for _, p := range []string{"/usr/local/include/harbour", "/usr/include/harbour"} { + if _, err := os.Stat(p); err == nil { + pre.AddIncludeDir(p) + } + } + + processed, ppErrors := pre.Process(prgFile, string(source)) + for _, e := range ppErrors { + fmt.Fprintf(os.Stderr, "pp: %s\n", e) + } + + // Phase 2: Parse + file, errs := parser.ParseWithGoDumps(prgFile, processed, pre.GoDumps) + if len(errs) > 0 { + for _, e := range errs { + fmt.Fprintf(os.Stderr, "%s\n", e) + } + fatal(fmt.Sprintf("%d parse error(s)", len(errs))) + } + + // Phase 2.5: Analyze (warnings — non-fatal) + diags := analyzer.Analyze(file) + for _, d := range diags { + if d.Severity <= analyzer.SevWarning { + fmt.Fprintf(os.Stderr, "%s\n", d) + } + } + + // Phase 3: Generate + return gengo.Generate(file) +} + +// writeGoProject creates a temporary Go project with the generated code. +func writeGoProject(dir, prgFile, goCode string) { + // Find the Five module path (where go.mod is) + fiveRoot := findFiveRoot() + + // go.mod with replace directive pointing to Five source + goMod := fmt.Sprintf(`module five-generated + +go 1.21 + +require five v0.0.0 + +replace five => %s +`, fiveRoot) + + writeFile(filepath.Join(dir, "go.mod"), goMod) + writeFile(filepath.Join(dir, "main.go"), goCode) + + // Run go mod tidy to resolve dependencies + tidy := exec.Command(goPath(), "mod", "tidy") + tidy.Dir = dir + tidy.Stderr = os.Stderr + if err := tidy.Run(); err != nil { + // Non-fatal — some environments may not need it + _ = err + } +} + +// findFiveRoot finds the root directory of the Five project (where go.mod is). +// findFiveRoot finds the Five project root (directory containing go.mod with "module five"). +// Tries: 1) walk up from cwd, 2) walk up from executable. Fallback: cwd. +func findFiveRoot() string { + if root := walkUpForGoMod(""); root != "" { + return root + } + if exe, err := os.Executable(); err == nil { + if root := walkUpForGoMod(filepath.Dir(exe)); root != "" { + return root + } + } + abs, _ := filepath.Abs(".") + return abs +} + +// walkUpForGoMod walks up from startDir (or cwd if empty) looking for go.mod. +func walkUpForGoMod(startDir string) string { + dir := startDir + if dir == "" { + dir, _ = os.Getwd() + } + for { + modPath := filepath.Join(dir, "go.mod") + if _, err := os.Stat(modPath); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + return "" +} + +// goPath is an alias for findGoBin (deduplicated). +func goPath() string { return findGoBin() } + +func writeFile(path, content string) { + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + fatal("cannot write " + path + ": " + err.Error()) + } +} + +// buildFRB compiles a PRG to Five Runtime Binary (.frb). +// FRB = Go plugin (.so) wrapped in FRB header for runtime loading. +func buildFRB(prgFile, outputFile string) { + source, err := os.ReadFile(prgFile) + if err != nil { + fatal("cannot read file: " + err.Error()) + } + + // Phase 1: Preprocess + pre := pp.New() + pre.AddIncludeDir(filepath.Dir(prgFile)) + pre.AddIncludeDir(filepath.Join(filepath.Dir(prgFile), "include")) + if exePath, err := os.Executable(); err == nil { + pre.AddIncludeDir(filepath.Join(filepath.Dir(exePath), "include")) + } + processed, ppErrors := pre.Process(prgFile, string(source)) + for _, e := range ppErrors { + fmt.Fprintf(os.Stderr, "pp: %s\n", e) + } + + // Phase 2: Parse + file, parseErrors := parser.ParseWithGoDumps(prgFile, processed, pre.GoDumps) + if len(parseErrors) > 0 { + for _, e := range parseErrors { + fmt.Fprintf(os.Stderr, "%s\n", e) + } + fatal(fmt.Sprintf("%d parse error(s)", len(parseErrors))) + } + + // Phase 3: Generate Go source as library + goSrc := gengo.GenerateLibrary(file) + + // Phase 4: Build as Go plugin + tmpDir, err := os.MkdirTemp("", "five-frb-*") + if err != nil { + fatal("cannot create temp dir: " + err.Error()) + } + defer os.RemoveAll(tmpDir) + + // Write go.mod — point to Five's module root + fiveRoot := mustAbs(".") + if exePath, err := os.Executable(); err == nil { + fiveRoot = filepath.Dir(exePath) + } + // Find go.mod in fiveRoot or parent directories + for d := fiveRoot; ; d = filepath.Dir(d) { + if _, err := os.Stat(filepath.Join(d, "go.mod")); err == nil { + fiveRoot = d + break + } + if d == filepath.Dir(d) { + break + } + } + modName := fmt.Sprintf("frbmod_%d_%d", os.Getpid(), time.Now().UnixNano()) + goModContent := fmt.Sprintf("module %s\n\ngo 1.21.13\n\nrequire five v0.0.0\n\nreplace five => %s\n", modName, fiveRoot) + writeFile(filepath.Join(tmpDir, "go.mod"), goModContent) + + // Write Go source (no additional exports needed — init() handles registration) + writeFile(filepath.Join(tmpDir, "module.go"), goSrc) + + // Find go binary + goBin := findGoBin() + + // go mod tidy + tidyCmd := exec.Command(goBin, "mod", "tidy") + tidyCmd.Dir = tmpDir + if output, err := tidyCmd.CombinedOutput(); err != nil { + fmt.Fprintf(os.Stderr, "tidy error: %s\n%s\n", err, string(output)) + fatal("go mod tidy failed") + } + + // Build plugin + soFile := filepath.Join(tmpDir, "module.so") + buildCmd := exec.Command(goBin, "build", "-buildmode=plugin", "-o", soFile, ".") + buildCmd.Dir = tmpDir + if output, err := buildCmd.CombinedOutput(); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", string(output)) + fatal("plugin build failed") + } + + // Phase 5: Package as FRB + soData, err := os.ReadFile(soFile) + if err != nil { + fatal("cannot read .so: " + err.Error()) + } + + frbData := make([]byte, hbrt.FrbHeaderSize+len(soData)) + frbData[0] = hbrt.FrbMagic0 + frbData[1] = hbrt.FrbMagic1 + frbData[2] = hbrt.FrbMagic2 + frbData[3] = hbrt.FrbMagic3 + frbData[4] = hbrt.FrbVersion1 + frbData[5] = 0 // flags + // Flags: 0 + frbData[6] = 0 + frbData[7] = 0 + // SymCount: 0 (symbols are self-registering via init()) + frbData[8] = 0 + frbData[9] = 0 + frbData[10] = 0 + frbData[11] = 0 + copy(frbData[12:], soData) + + if err := os.WriteFile(outputFile, frbData, 0755); err != nil { + fatal("cannot write FRB: " + err.Error()) + } + + fmt.Fprintf(os.Stderr, "FRB: %s (%d bytes)\n", outputFile, len(frbData)) +} + +// debugPRG compiles PRG with debug info and runs with interactive debugger. +func debugPRG(prgFile string) { + source, err := os.ReadFile(prgFile) + if err != nil { + fatal("cannot read file: " + err.Error()) + } + + // Phase 1: Preprocess + pre := pp.New() + pre.AddIncludeDir(filepath.Dir(prgFile)) + pre.AddIncludeDir(filepath.Join(filepath.Dir(prgFile), "include")) + if exePath, err := os.Executable(); err == nil { + pre.AddIncludeDir(filepath.Join(filepath.Dir(exePath), "include")) + } + processed, ppErrors := pre.Process(prgFile, string(source)) + for _, e := range ppErrors { + fmt.Fprintf(os.Stderr, "pp: %s\n", e) + } + + // Phase 2: Parse + file, parseErrors := parser.ParseWithGoDumps(prgFile, processed, pre.GoDumps) + if len(parseErrors) > 0 { + for _, e := range parseErrors { + fmt.Fprintf(os.Stderr, "%s\n", e) + } + fatal(fmt.Sprintf("%d parse error(s)", len(parseErrors))) + } + + // Phase 3: Generate Go with debug info + goSrc := gengo.GenerateWithDebug(file) + + // Phase 4: Build and run (same as runPRG but with debug setup) + tmpDir, err := os.MkdirTemp("", "five-debug-*") + if err != nil { + fatal("cannot create temp dir: " + err.Error()) + } + defer os.RemoveAll(tmpDir) + + fiveRoot := findProjectRoot() + goMod := fmt.Sprintf("module five-generated\n\ngo 1.21.13\n\nrequire five v0.0.0\n\nreplace five => %s\n", fiveRoot) + writeFile(filepath.Join(tmpDir, "go.mod"), goMod) + + // Add debug setup to main (use %q for safe path escaping) + debugSetup := fmt.Sprintf( + "vm.Debugger = hbrt.NewDebugger()\n\tvm.Debugger.SourceDir = %s\n\tvm.Debugger.Callback = hbrt.TUIDebugger()\n\tvm.Run(\"MAIN\")", + fmt.Sprintf("%q", mustAbs("."))) + goSrc = strings.Replace(goSrc, "vm.Run(\"MAIN\")", debugSetup, 1) + // Remove unused fmt import if added + // (no longer needed since we don't use fmt.Println in generated code) + + writeFile(filepath.Join(tmpDir, "main.go"), goSrc) + + goBin := findGoBin() + tidyCmd := exec.Command(goBin, "mod", "tidy") + tidyCmd.Dir = tmpDir + if out, err := tidyCmd.CombinedOutput(); err != nil { + fmt.Fprintf(os.Stderr, "warning: go mod tidy: %v\n%s", err, out) + } + + runCmd := exec.Command(goBin, "run", ".") + runCmd.Dir = tmpDir + runCmd.Stdin = os.Stdin + runCmd.Stdout = os.Stdout + runCmd.Stderr = os.Stderr + runCmd.Run() +} + +// findProjectRoot is an alias for findFiveRoot (deduplicated). +func findProjectRoot() string { return findFiveRoot() } + +// buildFRBPcode compiles PRG to pcode FRB (no Go compiler needed to run). +func buildFRBPcode(prgFile, outputFile string) { + source, err := os.ReadFile(prgFile) + if err != nil { + fatal("cannot read file: " + err.Error()) + } + + // Phase 1: Preprocess + pre := pp.New() + pre.AddIncludeDir(filepath.Dir(prgFile)) + pre.AddIncludeDir(filepath.Join(filepath.Dir(prgFile), "include")) + if exePath, err := os.Executable(); err == nil { + pre.AddIncludeDir(filepath.Join(filepath.Dir(exePath), "include")) + } + processed, _ := pre.Process(prgFile, string(source)) + + // Phase 2: Parse + file, parseErrors := parser.ParseWithGoDumps(prgFile, processed, pre.GoDumps) + if len(parseErrors) > 0 { + for _, e := range parseErrors { + fmt.Fprintf(os.Stderr, "%s\n", e) + } + fatal(fmt.Sprintf("%d parse error(s)", len(parseErrors))) + } + + // Phase 3: Generate pcode + pcMod := genpc.Generate(file) + + // Phase 4: Serialize + pcData := hbrt.SerializePcodeModule(pcMod) + + // Phase 5: Write FRB + frbData := make([]byte, hbrt.FrbHeaderSize+len(pcData)) + frbData[0] = hbrt.FrbMagic0 + frbData[1] = hbrt.FrbMagic1 + frbData[2] = hbrt.FrbMagic2 + frbData[3] = hbrt.FrbMagic3 + frbData[4] = hbrt.FrbVersion1 + frbData[5] = 0 + frbData[6] = hbrt.FrbModePcode + frbData[7] = 0 + // func count + frbData[8] = byte(len(pcMod.Funcs)) + frbData[9] = byte(len(pcMod.Funcs) >> 8) + frbData[10] = 0 + frbData[11] = 0 + frbData = append(frbData[:12], pcData...) + + if err := os.WriteFile(outputFile, frbData, 0755); err != nil { + fatal("cannot write FRB: " + err.Error()) + } + + fmt.Fprintf(os.Stderr, "FRB (pcode): %s (%d bytes, %d functions)\n", outputFile, len(frbData), len(pcMod.Funcs)) +} + + +func findGoBin() string { + // Try PATH first + if p, err := exec.LookPath("go"); err == nil { + return p + } + // Common locations + for _, p := range []string{"/usr/local/go/bin/go", "/usr/bin/go", "/snap/bin/go"} { + if _, err := os.Stat(p); err == nil { + return p + } + } + return "go" +} + +func mustAbs(path string) string { + abs, err := filepath.Abs(path) + if err != nil { + return path + } + return abs +} + +func fatal(msg string) { + fmt.Fprintf(os.Stderr, "five: %s\n", msg) + os.Exit(1) +}