From b19bb0c4454ace3b2a6314c62b4231c909039591 Mon Sep 17 00:00:00 2001 From: Charles KWON OhJun Date: Thu, 28 May 2026 10:23:08 +0900 Subject: [PATCH] feat(fnode): resolve Five path from fivenode_go's go.mod replace pin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 => ` 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) --- cmd/fnode/main.go | 58 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/cmd/fnode/main.go b/cmd/fnode/main.go index ed7f8c3..e1a758f 100644 --- a/cmd/fnode/main.go +++ b/cmd/fnode/main.go @@ -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 => ` 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 => +// +// 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 != "" {