Skip to content

Commit 0de657d

Browse files
andypostclaude
andcommitted
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 <noreply@anthropic.com>
1 parent d832031 commit 0de657d

File tree

2 files changed

+65
-14
lines changed

2 files changed

+65
-14
lines changed

plugins/builder/environment.go

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ func newBuildEnvironment(b *Builder) (*buildEnvironment, error) {
8181
return env, nil
8282
}
8383

84+
// ensureModuleRequired adds a replaced module to go.mod as a requirement.
85+
// Replaced modules need an explicit require directive with a placeholder version,
86+
// otherwise Go reports "replaced but not required" errors during compilation.
87+
func (env *buildEnvironment) ensureModuleRequired(ctx context.Context, pkg string) error {
88+
// Strip version suffix if present, use placeholder version for replaced modules.
89+
mod, _, _ := strings.Cut(pkg, "@")
90+
return env.execGoMod(ctx, "edit", "-require", mod+"@v0.0.0")
91+
}
92+
8493
func (env *buildEnvironment) CreateModFile(ctx context.Context, opts *BuildOptions) error {
8594
var err error
8695
// Create go.mod.
@@ -89,47 +98,67 @@ func (env *buildEnvironment) CreateModFile(ctx context.Context, opts *BuildOptio
8998
return err
9099
}
91100

92-
// Replace requested modules.
101+
// Apply requested module replacements.
93102
for o, n := range opts.ModReplace {
94103
err = env.execGoMod(ctx, "edit", "-replace", o+"="+n)
95104
if err != nil {
96105
return err
97106
}
98107
}
99108

100-
// Download the requested dependencies directly.
109+
// Download dependencies.
101110
if opts.NoCache {
102-
domains := make([]string, len(opts.Plugins))
103-
for i := 0; i < len(domains); i++ {
104-
domains[i] = opts.Plugins[i].Path
111+
// Set GONOSUMDB and GONOPROXY for modules that should not be cached or verified.
112+
domains := make([]string, 0, len(opts.Plugins)+1)
113+
for _, p := range opts.Plugins {
114+
domains = append(domains, p.Path)
115+
}
116+
if opts.CorePkg.Path != "" {
117+
domains = append(domains, opts.CorePkg.Path)
105118
}
106119
noproxy := strings.Join(domains, ",")
107120
env.env = append(env.env, "GONOSUMDB="+noproxy, "GONOPROXY="+noproxy)
108121
}
109122

110-
// Download core.
111-
err = env.execGoGet(ctx, opts.CorePkg.String())
112-
if err != nil {
123+
// Download core package.
124+
// Replaced modules need an explicit require directive first, otherwise Go
125+
// reports "replaced but not required" errors during compilation.
126+
if _, ok := opts.ModReplace[opts.CorePkg.Path]; ok {
127+
if err = env.ensureModuleRequired(ctx, opts.CorePkg.String()); err != nil {
128+
return err
129+
}
130+
}
131+
if err = env.execGoGet(ctx, opts.CorePkg.String()); err != nil {
113132
return err
114133
}
115134

116135
// Download plugins.
117-
nextPlugin:
118136
for _, p := range opts.Plugins {
119-
// Do not get plugins of module subpath.
137+
// Skip plugins that are subpaths of replaced modules.
138+
isSubpath := false
120139
for repl := range opts.ModReplace {
121140
if p.Path != repl && strings.HasPrefix(p.Path, repl) {
122-
continue nextPlugin
141+
isSubpath = true
142+
break
123143
}
124144
}
125-
err = env.execGoGet(ctx, p.String())
126-
if err != nil {
145+
if isSubpath {
146+
continue
147+
}
148+
149+
// Replaced modules need an explicit require directive before go get.
150+
if _, ok := opts.ModReplace[p.Path]; ok {
151+
if err = env.ensureModuleRequired(ctx, p.String()); err != nil {
152+
return err
153+
}
154+
}
155+
if err = env.execGoGet(ctx, p.String()); err != nil {
127156
return err
128157
}
129158
}
130159
// @todo update all but with fixed versions if requested
131160

132-
return err
161+
return nil
133162
}
134163

135164
func (env *buildEnvironment) Filepath(s string) string {

test/testdata/build/replace.txtar

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Launchr Replace Module Test
2+
#
3+
# Validates that replaced modules are properly required in go.mod.
4+
# Regression test for: "replaced but not required" error when using --replace flag.
5+
# When a module is replaced, `go mod edit -require` must be used to add it
6+
# to go.mod, otherwise Go compilation fails.
7+
8+
env HOME=$REAL_HOME
9+
env APP_PLUGIN_LOCAL=example.com/genaction@v1.1.1
10+
env APP_PLUGIN_LOCAL_PATH=$REPO_PATH/test/plugins/genaction
11+
12+
# Test 1: Build with only replaced plugin (no external plugins).
13+
# This is the minimal reproduction of the "replaced but not required" bug.
14+
! exists launchr
15+
exec launchr build -r $CORE_PKG=$REPO_PATH -p $APP_PLUGIN_LOCAL -r $APP_PLUGIN_LOCAL=$APP_PLUGIN_LOCAL_PATH
16+
! stderr .
17+
exists launchr
18+
19+
# Test 2: Verify the built binary works.
20+
exec ./launchr genaction:example
21+
stdout 'hello world'
22+
! stderr .

0 commit comments

Comments
 (0)