// fnode — fivenode_go build/run CLI. // // Reuses Five's compiler packages to turn PRG sources into Go, then // builds them together with fivenode_go's own hbrtl_ext/* packages into // a single self-contained binary. No FFI, no Apache, no Node.js. // // Usage: // // fnode build [file2.prg ...] [-o output] [-I includedir ...] [--rtl pkg ...] // fnode run [-I includedir ...] [--rtl pkg ...] // fnode version // // --rtl blank-imports that fivenode_go package so its init() // registers RTL functions. Default is the full hbrtl_ext set (currently // just "hello"); pass --rtl explicitly to override. package main import ( "fmt" "os" "os/exec" "path/filepath" "strings" "five/compiler/analyzer" "five/compiler/ast" "five/compiler/gengo" "five/compiler/parser" "five/compiler/pp" ) const version = "fnode 0.1.0 — fivenode_go builder for Five" // defaultRTL is the blank-import list used when --rtl is not specified. // Order is irrelevant — each package's init() is independent. var defaultRTL = []string{ "fivenode_go/hbrtl_ext/hello", "fivenode_go/hbrtl_ext/httpserver", "fivenode_go/hbrtl_ext/bridge_capi", "fivenode_go/hbrtl_ext/pgrtl", } func main() { if len(os.Args) < 2 { printUsage() os.Exit(1) } switch os.Args[1] { case "build": cmdBuild(os.Args[2:]) case "run": cmdRun(os.Args[2:]) case "version", "-v", "--version": fmt.Println(version) case "help", "-h", "--help": printUsage() default: fmt.Fprintf(os.Stderr, "fnode: unknown command %q\n", os.Args[1]) printUsage() os.Exit(1) } } func printUsage() { fmt.Fprintln(os.Stderr, version) fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "Usage:") fmt.Fprintln(os.Stderr, " fnode build ... [-o out] [-I dir]... [--rtl pkg]...") fmt.Fprintln(os.Stderr, " fnode run [-I dir]... [--rtl pkg]...") fmt.Fprintln(os.Stderr, " fnode version") } type buildOpts struct { prgFiles []string output string includes []string rtlPkgs []string } func parseCommon(args []string) buildOpts { var o buildOpts for i := 0; i < len(args); i++ { a := args[i] switch { case a == "-o" && i+1 < len(args): o.output = args[i+1] i++ case a == "-I" && i+1 < len(args): o.includes = append(o.includes, args[i+1]) i++ case strings.HasPrefix(a, "-I"): o.includes = append(o.includes, a[2:]) case a == "--rtl" && i+1 < len(args): o.rtlPkgs = append(o.rtlPkgs, args[i+1]) i++ default: o.prgFiles = append(o.prgFiles, a) } } if len(o.rtlPkgs) == 0 { o.rtlPkgs = append([]string{}, defaultRTL...) } return o } func cmdBuild(args []string) { o := parseCommon(args) if len(o.prgFiles) == 0 { fatal("build: no PRG files given") } if o.output == "" { base := strings.TrimSuffix(filepath.Base(o.prgFiles[0]), filepath.Ext(o.prgFiles[0])) o.output = base } absOut, err := filepath.Abs(o.output) if err != nil { fatal("cannot resolve output path: " + err.Error()) } tmpDir := mkBuildDir() defer cleanupBuildDir(tmpDir) emitGeneratedSources(tmpDir, o) writeRootGo(tmpDir, o.rtlPkgs) writeGoMod(tmpDir) runGo(tmpDir, "mod", "tidy") runGo(tmpDir, "build", "-o", absOut, ".") fmt.Fprintf(os.Stderr, "Built: %s\n", absOut) } func cmdRun(args []string) { o := parseCommon(args) if len(o.prgFiles) == 0 { fatal("run: no PRG file given") } tmpDir := mkBuildDir() defer cleanupBuildDir(tmpDir) emitGeneratedSources(tmpDir, o) writeRootGo(tmpDir, o.rtlPkgs) writeGoMod(tmpDir) runGo(tmpDir, "mod", "tidy") runGoInteractive(tmpDir, "run", ".") } // emitGeneratedSources preprocesses, parses, analyzes, and emits Go for // every PRG file. The first file is treated as the entry module // (gengo.Generate emits a main()), the rest as library modules. func emitGeneratedSources(tmpDir string, o buildOpts) { // Parse all files first so cross-file function references resolve. type parsed struct { file *ast.File prgFile string } var ps []parsed crossFile := map[string]bool{} for _, f := range o.prgFiles { file := parseOne(f, o.includes) ps = append(ps, parsed{file: file, prgFile: f}) for _, d := range file.Decls { switch decl := d.(type) { case *ast.FuncDecl: crossFile[strings.ToUpper(decl.Name)] = true case *ast.ClassDecl: crossFile[strings.ToUpper(decl.Name)] = true } } } for i, p := range ps { diags := analyzer.Analyze(p.file, crossFile) for _, d := range diags { if d.Severity <= analyzer.SevWarning { fmt.Fprintf(os.Stderr, "%s\n", d) } } var code string if i == 0 { code = gengo.Generate(p.file) } else { code = gengo.GenerateLibrary(p.file) } path := filepath.Join(tmpDir, fmt.Sprintf("prg_%d.go", i)) writeFile(path, code) } } func parseOne(prgFile string, includes []string) *ast.File { src, err := os.ReadFile(prgFile) if err != nil { fatal("cannot read " + prgFile + ": " + err.Error()) } pre := pp.New() pre.AddIncludeDir(filepath.Dir(prgFile)) pre.AddIncludeDir(filepath.Join(filepath.Dir(prgFile), "include")) pre.AddIncludeDir(filepath.Join(fiveRoot(), "include")) for _, d := range includes { pre.AddIncludeDir(d) } 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, ppErrs := pre.Process(prgFile, string(src)) for _, e := range ppErrs { fmt.Fprintf(os.Stderr, "pp: %s\n", e) } if len(ppErrs) > 0 { fatal(fmt.Sprintf("%d preprocessor error(s) in %s", len(ppErrs), prgFile)) } file, parseErrs := parser.ParseWithGoDumps(prgFile, processed, pre.GoDumps) if len(parseErrs) > 0 { for _, e := range parseErrs { fmt.Fprintf(os.Stderr, "%s\n", e) } fatal(fmt.Sprintf("%d parse error(s) in %s", len(parseErrs), prgFile)) } return file } // writeRootGo writes the file that anchors the build's blank imports of // hbrtl_ext packages. Each import's init() registers its RTL functions // before the generated PRG main runs. func writeRootGo(tmpDir string, rtlPkgs []string) { var sb strings.Builder sb.WriteString("package main\n\n") if len(rtlPkgs) > 0 { sb.WriteString("import (\n") for _, p := range rtlPkgs { fmt.Fprintf(&sb, "\t_ %q\n", p) } sb.WriteString(")\n") } writeFile(filepath.Join(tmpDir, "fnode_rtl.go"), sb.String()) } func writeGoMod(tmpDir string) { five := mustAbs(fiveRoot()) fnode := mustAbs(fnodeRoot()) mod := fmt.Sprintf(`module fnode_build go 1.21 require ( five v0.0.0 fivenode_go v0.0.0 ) replace five => %s replace fivenode_go => %s `, five, fnode) writeFile(filepath.Join(tmpDir, "go.mod"), mod) } func mustAbs(p string) string { a, err := filepath.Abs(p) if err != nil { return p } return a } func mkBuildDir() string { dir, err := os.MkdirTemp("", "fnode-build-*") if err != nil { fatal("mkdir temp: " + err.Error()) } return dir } func cleanupBuildDir(dir string) { if os.Getenv("FNODE_KEEP_BUILD") != "" { fmt.Fprintln(os.Stderr, "[FNODE_KEEP_BUILD] keeping:", dir) return } os.RemoveAll(dir) } func runGo(dir string, args ...string) { cmd := exec.Command(goBin(), args...) cmd.Dir = dir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fatal("go " + strings.Join(args, " ") + " failed: " + err.Error()) } } func runGoInteractive(dir string, args ...string) { cmd := exec.Command(goBin(), args...) cmd.Dir = dir cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { if ee, ok := err.(*exec.ExitError); ok { os.Exit(ee.ExitCode()) } fatal("go " + strings.Join(args, " ") + " failed: " + err.Error()) } } func goBin() string { if p, err := exec.LookPath("go"); err == nil { return p } for _, p := range []string{"/usr/local/go/bin/go", "/usr/bin/go", "/opt/homebrew/bin/go"} { if _, err := os.Stat(p); err == nil { return p } } return "go" } // fiveRoot resolves to Five's source tree (walk up from this binary or // from cwd until a directory with module name "five" is found). func fiveRoot() string { if d := walkUpForModule("five"); d != "" { return d } return "../../fivedev/five" } // fnodeRoot resolves to fivenode_go's source tree. func fnodeRoot() string { if d := walkUpForModule("fivenode_go"); d != "" { return d } cwd, _ := os.Getwd() return cwd } func walkUpForModule(name string) string { starts := []string{} if cwd, err := os.Getwd(); err == nil { starts = append(starts, cwd) } if exe, err := os.Executable(); err == nil { starts = append(starts, filepath.Dir(exe)) } for _, start := range starts { dir := start for { gm := filepath.Join(dir, "go.mod") if data, err := os.ReadFile(gm); err == nil { first := strings.SplitN(string(data), "\n", 2)[0] if strings.TrimSpace(first) == "module "+name { return dir } } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } } return "" } func writeFile(path, content string) { if err := os.WriteFile(path, []byte(content), 0644); err != nil { fatal("write " + path + ": " + err.Error()) } } func fatal(msg string) { fmt.Fprintln(os.Stderr, "fnode:", msg) os.Exit(1) }