From 0de657d4ab807755b0b1c0920ce4aed1cbf771d8 Mon Sep 17 00:00:00 2001 From: Andy Postnikov Date: Sun, 8 Feb 2026 07:27:09 +0100 Subject: [PATCH] fix: ensure replaced modules are required before go get (#87) When using --replace flag, the build process was skipping go get for replaced modules but not adding them as requirements, causing Go compilation errors ("replaced but not required"). Add ensureModuleRequired() that calls go mod edit -require with a placeholder version before go get for replaced modules. Also include core package in GONOSUMDB/GONOPROXY for no-cache builds. Co-Authored-By: Claude Opus 4.6 --- plugins/builder/environment.go | 57 +++++++++++++++++++++++-------- test/testdata/build/replace.txtar | 22 ++++++++++++ 2 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 test/testdata/build/replace.txtar diff --git a/plugins/builder/environment.go b/plugins/builder/environment.go index 61d5412..1def723 100644 --- a/plugins/builder/environment.go +++ b/plugins/builder/environment.go @@ -81,6 +81,15 @@ func newBuildEnvironment(b *Builder) (*buildEnvironment, error) { return env, nil } +// ensureModuleRequired adds a replaced module to go.mod as a requirement. +// Replaced modules need an explicit require directive with a placeholder version, +// otherwise Go reports "replaced but not required" errors during compilation. +func (env *buildEnvironment) ensureModuleRequired(ctx context.Context, pkg string) error { + // Strip version suffix if present, use placeholder version for replaced modules. + mod, _, _ := strings.Cut(pkg, "@") + return env.execGoMod(ctx, "edit", "-require", mod+"@v0.0.0") +} + func (env *buildEnvironment) CreateModFile(ctx context.Context, opts *BuildOptions) error { var err error // Create go.mod. @@ -89,7 +98,7 @@ func (env *buildEnvironment) CreateModFile(ctx context.Context, opts *BuildOptio return err } - // Replace requested modules. + // Apply requested module replacements. for o, n := range opts.ModReplace { err = env.execGoMod(ctx, "edit", "-replace", o+"="+n) if err != nil { @@ -97,39 +106,59 @@ func (env *buildEnvironment) CreateModFile(ctx context.Context, opts *BuildOptio } } - // Download the requested dependencies directly. + // Download dependencies. if opts.NoCache { - domains := make([]string, len(opts.Plugins)) - for i := 0; i < len(domains); i++ { - domains[i] = opts.Plugins[i].Path + // Set GONOSUMDB and GONOPROXY for modules that should not be cached or verified. + domains := make([]string, 0, len(opts.Plugins)+1) + for _, p := range opts.Plugins { + domains = append(domains, p.Path) + } + if opts.CorePkg.Path != "" { + domains = append(domains, opts.CorePkg.Path) } noproxy := strings.Join(domains, ",") env.env = append(env.env, "GONOSUMDB="+noproxy, "GONOPROXY="+noproxy) } - // Download core. - err = env.execGoGet(ctx, opts.CorePkg.String()) - if err != nil { + // Download core package. + // Replaced modules need an explicit require directive first, otherwise Go + // reports "replaced but not required" errors during compilation. + if _, ok := opts.ModReplace[opts.CorePkg.Path]; ok { + if err = env.ensureModuleRequired(ctx, opts.CorePkg.String()); err != nil { + return err + } + } + if err = env.execGoGet(ctx, opts.CorePkg.String()); err != nil { return err } // Download plugins. -nextPlugin: for _, p := range opts.Plugins { - // Do not get plugins of module subpath. + // Skip plugins that are subpaths of replaced modules. + isSubpath := false for repl := range opts.ModReplace { if p.Path != repl && strings.HasPrefix(p.Path, repl) { - continue nextPlugin + isSubpath = true + break } } - err = env.execGoGet(ctx, p.String()) - if err != nil { + if isSubpath { + continue + } + + // Replaced modules need an explicit require directive before go get. + if _, ok := opts.ModReplace[p.Path]; ok { + if err = env.ensureModuleRequired(ctx, p.String()); err != nil { + return err + } + } + if err = env.execGoGet(ctx, p.String()); err != nil { return err } } // @todo update all but with fixed versions if requested - return err + return nil } func (env *buildEnvironment) Filepath(s string) string { diff --git a/test/testdata/build/replace.txtar b/test/testdata/build/replace.txtar new file mode 100644 index 0000000..887cd44 --- /dev/null +++ b/test/testdata/build/replace.txtar @@ -0,0 +1,22 @@ +# Launchr Replace Module Test +# +# Validates that replaced modules are properly required in go.mod. +# Regression test for: "replaced but not required" error when using --replace flag. +# When a module is replaced, `go mod edit -require` must be used to add it +# to go.mod, otherwise Go compilation fails. + +env HOME=$REAL_HOME +env APP_PLUGIN_LOCAL=example.com/genaction@v1.1.1 +env APP_PLUGIN_LOCAL_PATH=$REPO_PATH/test/plugins/genaction + +# Test 1: Build with only replaced plugin (no external plugins). +# This is the minimal reproduction of the "replaced but not required" bug. +! exists launchr +exec launchr build -r $CORE_PKG=$REPO_PATH -p $APP_PLUGIN_LOCAL -r $APP_PLUGIN_LOCAL=$APP_PLUGIN_LOCAL_PATH +! stderr . +exists launchr + +# Test 2: Verify the built binary works. +exec ./launchr genaction:example +stdout 'hello world' +! stderr .