feat(fnode): --go-replace and --module for BEGINDUMP-import use cases

Two CLI knobs that together let PRG #pragma BEGINDUMP blocks import
private Go modules and their internal/* packages:

  --go-replace pkg=path
    Adds `require pkg v0.0.0` + `replace pkg => path` to the temp
    go.mod, so `go mod tidy` doesn't try (and fail) to fetch a
    module that lives only on local disk or behind a private
    forge (Gitea, etc.).

  --module <name>
    Overrides the temp-build module name (default fnode_build).
    Use when the BEGINDUMP block needs to reach into another
    module's internal/ packages — Go's internal-visibility rule
    requires the importer to share a parent path with the
    importee, so the build module needs to live somewhere under
    that parent.

Worked example (solmade integration PoC):

  fnode build /tmp/poc_dartapi.prg \\
    --go-replace gitea.fivego.org/kwon_ai/solmade=/Users/charleskwon/solmade \\
    --module gitea.fivego.org/kwon_ai/solmade/_fnode_build \\
    -o poc_dart

A 4-line BEGINDUMP block now imports
"gitea.fivego.org/kwon_ai/solmade/internal/dartapi", links its
AllAliases() function, and returns the real count (3) at run time.
Single 23 MB Go binary, no Node, no FFI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 14:33:39 +09:00
parent b19bb0c445
commit 8ddd6abc69

View File

@@ -20,6 +20,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"five/compiler/analyzer" "five/compiler/analyzer"
@@ -68,20 +69,34 @@ func printUsage() {
fmt.Fprintln(os.Stderr, version) fmt.Fprintln(os.Stderr, version)
fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Usage:") fmt.Fprintln(os.Stderr, "Usage:")
fmt.Fprintln(os.Stderr, " fnode build <file.prg>... [-o out] [-I dir]... [--rtl pkg]...") fmt.Fprintln(os.Stderr, " fnode build <file.prg>... [-o out] [-I dir]... [--rtl pkg]... [--go-replace pkg=path]...")
fmt.Fprintln(os.Stderr, " fnode run <file.prg> [-I dir]... [--rtl pkg]...") fmt.Fprintln(os.Stderr, " fnode run <file.prg> [-I dir]... [--rtl pkg]... [--go-replace pkg=path]...")
fmt.Fprintln(os.Stderr, " fnode version") fmt.Fprintln(os.Stderr, " fnode version")
} }
type buildOpts struct { type buildOpts struct {
prgFiles []string prgFiles []string
output string output string
includes []string includes []string
rtlPkgs []string rtlPkgs []string
// goReplace maps a Go module path to a local filesystem path that
// gets emitted as `replace <path> => <local>` 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 { func parseCommon(args []string) buildOpts {
var o buildOpts o := buildOpts{goReplace: map[string]string{}}
for i := 0; i < len(args); i++ { for i := 0; i < len(args); i++ {
a := args[i] a := args[i]
switch { switch {
@@ -96,6 +111,18 @@ func parseCommon(args []string) buildOpts {
case a == "--rtl" && i+1 < len(args): case a == "--rtl" && i+1 < len(args):
o.rtlPkgs = append(o.rtlPkgs, args[i+1]) o.rtlPkgs = append(o.rtlPkgs, args[i+1])
i++ 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: default:
o.prgFiles = append(o.prgFiles, a) o.prgFiles = append(o.prgFiles, a)
} }
@@ -124,7 +151,7 @@ func cmdBuild(args []string) {
emitGeneratedSources(tmpDir, o) emitGeneratedSources(tmpDir, o)
writeRootGo(tmpDir, o.rtlPkgs) writeRootGo(tmpDir, o.rtlPkgs)
writeGoMod(tmpDir) writeGoMod(tmpDir, o.goReplace, o.goModule)
runGo(tmpDir, "mod", "tidy") runGo(tmpDir, "mod", "tidy")
runGo(tmpDir, "build", "-o", absOut, ".") runGo(tmpDir, "build", "-o", absOut, ".")
@@ -141,7 +168,7 @@ func cmdRun(args []string) {
emitGeneratedSources(tmpDir, o) emitGeneratedSources(tmpDir, o)
writeRootGo(tmpDir, o.rtlPkgs) writeRootGo(tmpDir, o.rtlPkgs)
writeGoMod(tmpDir) writeGoMod(tmpDir, o.goReplace, o.goModule)
runGo(tmpDir, "mod", "tidy") runGo(tmpDir, "mod", "tidy")
runGoInteractive(tmpDir, "run", ".") runGoInteractive(tmpDir, "run", ".")
@@ -288,10 +315,14 @@ func writeRootGo(tmpDir string, rtlPkgs []string) {
writeFile(filepath.Join(tmpDir, "fnode_rtl.go"), sb.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()) five := mustAbs(fiveRoot())
fnode := mustAbs(fnodeRoot()) 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 go 1.21
@@ -303,8 +334,22 @@ require (
replace five => %s replace five => %s
replace fivenode_go => %s replace fivenode_go => %s
`, five, fnode) `, modName, five, fnode)
writeFile(filepath.Join(tmpDir, "go.mod"), mod) // 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 { func mustAbs(p string) string {