Skip to content

Commit f41cd8f

Browse files
committed
feat(ocb): support arbitrary replace paths
Convert all relative paths to be absolute for `replace` directives the same way that is done for modules. This ensures that the `output_path` isn't tied to the location of the builder YAML config.
1 parent 0e9e259 commit f41cd8f

File tree

2 files changed

+101
-8
lines changed

2 files changed

+101
-8
lines changed

cmd/builder/internal/builder/config.go

+74-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package builder // import "go.opentelemetry.io/collector/cmd/builder/internal/bu
66
import (
77
"errors"
88
"fmt"
9+
"golang.org/x/mod/modfile"
910
"os"
1011
"os/exec"
1112
"path/filepath"
@@ -205,6 +206,12 @@ func (c *Config) ParseModules() error {
205206
if err != nil {
206207
return err
207208
}
209+
210+
c.Replaces, err = parseReplaces(c.Replaces)
211+
if err != nil {
212+
return err
213+
}
214+
208215
return nil
209216
}
210217

@@ -248,16 +255,11 @@ func parseModules(mods []Module, usedNames map[string]int) ([]Module, error) {
248255
}
249256
usedNames[originalModName] = 1
250257

251-
// Check if path is empty, otherwise filepath.Abs replaces it with current path ".".
252258
if mod.Path != "" {
253259
var err error
254-
mod.Path, err = filepath.Abs(mod.Path)
260+
mod.Path, err = preparePath(mod.Path)
255261
if err != nil {
256-
return mods, fmt.Errorf("module has a relative \"path\" element, but we couldn't resolve the current working dir: %w", err)
257-
}
258-
// Check if the path exists
259-
if _, err := os.Stat(mod.Path); os.IsNotExist(err) {
260-
return mods, fmt.Errorf("filepath does not exist: %s", mod.Path)
262+
return mods, err
261263
}
262264
}
263265

@@ -266,3 +268,68 @@ func parseModules(mods []Module, usedNames map[string]int) ([]Module, error) {
266268

267269
return parsedModules, nil
268270
}
271+
272+
// preparePath ensures paths (e.g. for `replace` directives) exist and are re-written to be absolute.
273+
//
274+
// This ensures the generated code path does not have be adjacent to the builder config YAML.
275+
func preparePath(path string) (string, error) {
276+
p, err := filepath.Abs(path)
277+
if err != nil {
278+
return "", fmt.Errorf("replace has a relative \"path\" element, but we couldn't resolve the current working dir: %w", err)
279+
}
280+
// Check if the path exists
281+
if _, err := os.Stat(path); os.IsNotExist(err) {
282+
return "", fmt.Errorf("filepath does not exist: %s", path)
283+
}
284+
return p, nil
285+
}
286+
287+
// parseReplaces rewrites `replace` directives for the generated `go.mod`.
288+
//
289+
// Currently, this consists of converting any relative paths in `ModulePath [ Version ] => FilePath` replacements
290+
// to absolute paths. This is required because the `output_path` can be at an arbitrary location (such as in `/tmp`),
291+
// so paths relative to the builder config will not necessarily be valid.
292+
//
293+
// From https://go.dev/ref/mod#go-mod-file-replace, there are two valid formats for a `ReplaceSpec`:
294+
// - ModulePath [ Version ] "=>" FilePath
295+
// - ModulePath [ Version ] "=>" ModulePath Version
296+
//
297+
// ModulePath => FilePath are 3 or 4 tokens long depending on the presence of Version, and there is
298+
// always a single token (the path) after the `=>` divider token.
299+
//
300+
// ModulePath => ModulePath can also be 4 tokens long, but there are always two tokens after the `=>` divider
301+
// token.
302+
func parseReplaces(replaces []string) ([]string, error) {
303+
// Unfortunately the modfile library does not fully parse these fragments, but it handles lexing the entries.
304+
mf, err := modfile.ParseLax("replace-go-mod", []byte(strings.Join(replaces, "\n")), nil)
305+
if err != nil {
306+
return replaces, err
307+
}
308+
var parsedReplaces []string
309+
for i, stmt := range mf.Syntax.Stmt {
310+
line, ok := stmt.(*modfile.Line)
311+
if !ok || len(line.Token) < 3 || len(line.Token) > 4 {
312+
parsedReplaces = append(parsedReplaces, replaces[i])
313+
continue
314+
}
315+
316+
dividerIndex := slices.Index(line.Token, "=>")
317+
if dividerIndex < 0 || dividerIndex != len(line.Token)-2 {
318+
// If the divider is not the second-to-last token, then this is a ModulePath => ModulePath replacement,
319+
// so there is nothing that needs replacing.
320+
parsedReplaces = append(parsedReplaces, replaces[i])
321+
continue
322+
}
323+
324+
// This is a ModulePath => FilePath replacement, so ensure it exists & make it absolute.
325+
p, err := preparePath(strings.Trim(line.Token[dividerIndex+1], `"`))
326+
if err != nil {
327+
return replaces, err
328+
}
329+
// It's possible that the absolute path needs to be quoted to be safe to use in the go.mod file.
330+
line.Token[dividerIndex+1] = modfile.AutoQuote(p)
331+
332+
parsedReplaces = append(parsedReplaces, strings.Join(line.Token, " "))
333+
}
334+
return parsedReplaces, nil
335+
}

cmd/builder/internal/builder/config_test.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package builder
55

66
import (
77
"os"
8+
"path/filepath"
89
"strings"
910
"testing"
1011

@@ -163,7 +164,7 @@ func TestInvalidConverter(t *testing.T) {
163164
require.Error(t, err, "expected an error when parsing invalid modules")
164165
}
165166

166-
func TestRelativePath(t *testing.T) {
167+
func TestRelativePath_Module(t *testing.T) {
167168
// prepare
168169
cfg := Config{
169170
Extensions: []Module{{
@@ -182,6 +183,31 @@ func TestRelativePath(t *testing.T) {
182183
assert.True(t, strings.HasPrefix(cfg.Extensions[0].Path, cwd))
183184
}
184185

186+
func TestRelativePath_Replace(t *testing.T) {
187+
// prepare
188+
cfg := Config{
189+
Replaces: []string{
190+
"module1 => ./templates",
191+
"module2 v0.0.1 => \"./templates\"",
192+
"module3 v1.2.3 => fake.test/module3 latest",
193+
},
194+
}
195+
196+
// test
197+
err := cfg.ParseModules()
198+
require.NoError(t, err)
199+
200+
// verify
201+
cwd, err := os.Getwd()
202+
require.NoError(t, err)
203+
204+
if assert.Len(t, cfg.Replaces, 3) {
205+
assert.Equal(t, "module1 => "+filepath.Join(cwd, "templates"), cfg.Replaces[0])
206+
assert.Equal(t, "module2 v0.0.1 => "+filepath.Join(cwd, "templates"), cfg.Replaces[1])
207+
assert.Equal(t, "module3 v1.2.3 => fake.test/module3 latest", cfg.Replaces[2])
208+
}
209+
}
210+
185211
func TestModuleFromCore(t *testing.T) {
186212
// prepare
187213
cfg := Config{

0 commit comments

Comments
 (0)