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:
@@ -20,6 +20,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"five/compiler/analyzer"
|
||||
@@ -68,8 +69,8 @@ func printUsage() {
|
||||
fmt.Fprintln(os.Stderr, version)
|
||||
fmt.Fprintln(os.Stderr, "")
|
||||
fmt.Fprintln(os.Stderr, "Usage:")
|
||||
fmt.Fprintln(os.Stderr, " fnode build <file.prg>... [-o out] [-I dir]... [--rtl pkg]...")
|
||||
fmt.Fprintln(os.Stderr, " fnode run <file.prg> [-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]... [--go-replace pkg=path]...")
|
||||
fmt.Fprintln(os.Stderr, " fnode version")
|
||||
}
|
||||
|
||||
@@ -78,10 +79,24 @@ type buildOpts struct {
|
||||
output string
|
||||
includes []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 {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user