feat(fnode): resolve Five path from fivenode_go's go.mod replace pin

When fnode is invoked from a cwd outside fivenode_go (e.g. a sibling
project like /Users/.../solmade), the legacy fallback
"../../fivedev/five" was resolved against the caller's cwd, pointing
fnode at /Users/fivedev/five which doesn't exist. go mod tidy then
failed with "open /Users/fivedev/five/go.mod: no such file".

New resolution order in fiveRoot():
  1. walkUpForModule("five") — same as before, wins inside the
     Five source tree.
  2. fiveFromFnodeGoMod() — parse the `replace five => <path>`
     line in fivenode_go's own go.mod (found via fnodeRoot()),
     resolving relative paths against that root.
  3. Hardcoded relative fallback (legacy).

Lets sibling apps run `fnode build app/foo.prg ...` from their own
directory without needing to cd into fivenode_go first.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 10:23:08 +09:00
parent ed956a1504
commit b19bb0c445

View File

@@ -367,15 +367,69 @@ func goBin() string {
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).
// fiveRoot resolves to Five's source tree.
//
// Order of resolution:
// 1. Walk up looking for a go.mod with `module five` (works when
// fnode was launched from inside the Five source tree).
// 2. Read fnodeRoot's go.mod for a `replace five => <path>` line —
// this is the common case when fnode is invoked from somewhere
// else (e.g. /Users/.../solmade) with the binary living in
// fivenode_go's tree; the replace pin in fivenode_go/go.mod is
// the source of truth for where Five lives.
// 3. Hard-coded relative fallback, resolved against the current
// working directory. Only useful when fnode is run from inside
// fivenode_go itself.
func fiveRoot() string {
if d := walkUpForModule("five"); d != "" {
return d
}
if p := fiveFromFnodeGoMod(); p != "" {
return p
}
return "../../fivedev/five"
}
// fiveFromFnodeGoMod parses fivenode_go's own go.mod for a
//
// replace five => <path>
//
// directive and returns the path resolved against fnodeRoot when it's
// relative. Empty when not found.
func fiveFromFnodeGoMod() string {
fr := fnodeRoot()
if fr == "" {
return ""
}
data, err := os.ReadFile(filepath.Join(fr, "go.mod"))
if err != nil {
return ""
}
for _, raw := range strings.Split(string(data), "\n") {
line := strings.TrimSpace(raw)
if !strings.HasPrefix(line, "replace") {
continue
}
// Match either `replace five => path` or `replace five v0.0.0 => path`.
if !strings.Contains(line, " five ") && !strings.Contains(line, " five\t") &&
!strings.HasSuffix(strings.TrimSpace(strings.SplitN(line, "=>", 2)[0]), " five") {
continue
}
parts := strings.SplitN(line, "=>", 2)
if len(parts) != 2 {
continue
}
p := strings.TrimSpace(parts[1])
if filepath.IsAbs(p) {
return p
}
if abs, err := filepath.Abs(filepath.Join(fr, p)); err == nil {
return abs
}
}
return ""
}
// fnodeRoot resolves to fivenode_go's source tree.
func fnodeRoot() string {
if d := walkUpForModule("fivenode_go"); d != "" {