diff --git a/cmd/fnode/main.go b/cmd/fnode/main.go index e1a758f..db312ae 100644 --- a/cmd/fnode/main.go +++ b/cmd/fnode/main.go @@ -20,6 +20,7 @@ import ( "os" "os/exec" "path/filepath" + "sort" "strings" "five/compiler/analyzer" @@ -68,20 +69,34 @@ 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 build ... [-o out] [-I dir]... [--rtl pkg]... [--go-replace pkg=path]...") + fmt.Fprintln(os.Stderr, " fnode run [-I dir]... [--rtl pkg]... [--go-replace pkg=path]...") fmt.Fprintln(os.Stderr, " fnode version") } type buildOpts struct { - prgFiles []string - output string - includes []string - rtlPkgs []string + prgFiles []string + output string + includes []string + rtlPkgs []string + // goReplace maps a Go module path to a local filesystem path that + // gets emitted as `replace => ` in the temp go.mod. + // Lets PRG #pragma BEGINDUMP blocks import private modules that + // `go mod tidy` would otherwise fail to fetch (e.g. a sibling + // project living outside any module proxy). + goReplace map[string]string + // goModule overrides the default temp-build module name + // ("fnode_build"). Set this to a path inside an existing module + // when the BEGINDUMP block needs to import that module's + // internal/* packages — Go's internal-visibility rule only lets + // callers whose own module path lives under the same parent see + // internal/. + // --module gitea.fivego.org/kwon_ai/solmade/_fnode_build + goModule string } func parseCommon(args []string) buildOpts { - var o buildOpts + o := buildOpts{goReplace: map[string]string{}} for i := 0; i < len(args); i++ { a := args[i] switch { @@ -96,6 +111,18 @@ func parseCommon(args []string) buildOpts { case a == "--rtl" && i+1 < len(args): o.rtlPkgs = append(o.rtlPkgs, args[i+1]) i++ + case a == "--go-replace" && i+1 < len(args): + // --go-replace gitea.fivego.org/kwon_ai/solmade=/abs/path + spec := args[i+1] + i++ + eq := strings.IndexByte(spec, '=') + if eq <= 0 || eq == len(spec)-1 { + fatal("--go-replace expects pkg=path, got: " + spec) + } + o.goReplace[spec[:eq]] = spec[eq+1:] + case a == "--module" && i+1 < len(args): + o.goModule = args[i+1] + i++ default: o.prgFiles = append(o.prgFiles, a) } @@ -124,7 +151,7 @@ func cmdBuild(args []string) { emitGeneratedSources(tmpDir, o) writeRootGo(tmpDir, o.rtlPkgs) - writeGoMod(tmpDir) + writeGoMod(tmpDir, o.goReplace, o.goModule) runGo(tmpDir, "mod", "tidy") runGo(tmpDir, "build", "-o", absOut, ".") @@ -141,7 +168,7 @@ func cmdRun(args []string) { emitGeneratedSources(tmpDir, o) writeRootGo(tmpDir, o.rtlPkgs) - writeGoMod(tmpDir) + writeGoMod(tmpDir, o.goReplace, o.goModule) runGo(tmpDir, "mod", "tidy") runGoInteractive(tmpDir, "run", ".") @@ -288,10 +315,14 @@ func writeRootGo(tmpDir string, rtlPkgs []string) { writeFile(filepath.Join(tmpDir, "fnode_rtl.go"), sb.String()) } -func writeGoMod(tmpDir string) { +func writeGoMod(tmpDir string, extraReplace map[string]string, modName string) { five := mustAbs(fiveRoot()) fnode := mustAbs(fnodeRoot()) - mod := fmt.Sprintf(`module fnode_build + if modName == "" { + modName = "fnode_build" + } + var sb strings.Builder + fmt.Fprintf(&sb, `module %s go 1.21 @@ -303,8 +334,22 @@ require ( replace five => %s replace fivenode_go => %s -`, five, fnode) - writeFile(filepath.Join(tmpDir, "go.mod"), mod) +`, modName, five, fnode) + // Sorted iteration for stable go.mod output. + keys := make([]string, 0, len(extraReplace)) + for k := range extraReplace { + keys = append(keys, k) + } + sort.Strings(keys) + for _, pkg := range keys { + path := extraReplace[pkg] + if !filepath.IsAbs(path) { + path = mustAbs(path) + } + fmt.Fprintf(&sb, "\nrequire %s v0.0.0\n", pkg) + fmt.Fprintf(&sb, "replace %s => %s\n", pkg, path) + } + writeFile(filepath.Join(tmpDir, "go.mod"), sb.String()) } func mustAbs(p string) string {