Skip to content

[builder] Rewrite replace paths to be absolute in generated go.mod #12638

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 74 additions & 7 deletions cmd/builder/internal/builder/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

"go.uber.org/multierr"
"go.uber.org/zap"
"golang.org/x/mod/modfile"
)

const (
Expand Down Expand Up @@ -205,6 +206,12 @@
if err != nil {
return err
}

c.Replaces, err = parseReplaces(c.Replaces)
if err != nil {
return err
}

Check warning on line 213 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L212-L213

Added lines #L212 - L213 were not covered by tests

return nil
}

Expand Down Expand Up @@ -248,16 +255,11 @@
}
usedNames[originalModName] = 1

// Check if path is empty, otherwise filepath.Abs replaces it with current path ".".
if mod.Path != "" {
var err error
mod.Path, err = filepath.Abs(mod.Path)
mod.Path, err = preparePath(mod.Path)
if err != nil {
return mods, fmt.Errorf("module has a relative \"path\" element, but we couldn't resolve the current working dir: %w", err)
}
// Check if the path exists
if _, err := os.Stat(mod.Path); os.IsNotExist(err) {
return mods, fmt.Errorf("filepath does not exist: %s", mod.Path)
return mods, err
}
}

Expand All @@ -266,3 +268,68 @@

return parsedModules, nil
}

// preparePath ensures paths (e.g. for `replace` directives) exist and are re-written to be absolute.
//
// This ensures the generated code path does not have be adjacent to the builder config YAML.
func preparePath(path string) (string, error) {
p, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("replace has a relative \"path\" element, but we couldn't resolve the current working dir: %w", err)
}

Check warning on line 279 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L278-L279

Added lines #L278 - L279 were not covered by tests
// Check if the path exists
if _, err := os.Stat(path); os.IsNotExist(err) {
return "", fmt.Errorf("filepath does not exist: %s", path)
}
return p, nil
}

// parseReplaces rewrites `replace` directives for the generated `go.mod`.
//
// Currently, this consists of converting any relative paths in `ModulePath [ Version ] => FilePath` replacements
// to absolute paths. This is required because the `output_path` can be at an arbitrary location (such as in `/tmp`),
// so paths relative to the builder config will not necessarily be valid.
//
// From https://go.dev/ref/mod#go-mod-file-replace, there are two valid formats for a `ReplaceSpec`:
// - ModulePath [ Version ] "=>" FilePath
// - ModulePath [ Version ] "=>" ModulePath Version
//
// ModulePath => FilePath are 3 or 4 tokens long depending on the presence of Version, and there is
// always a single token (the path) after the `=>` divider token.
//
// ModulePath => ModulePath can also be 4 tokens long, but there are always two tokens after the `=>` divider
// token.
func parseReplaces(replaces []string) ([]string, error) {
// Unfortunately the modfile library does not fully parse these fragments, but it handles lexing the entries.
mf, err := modfile.ParseLax("replace-go-mod", []byte(strings.Join(replaces, "\n")), nil)
if err != nil {
return replaces, err
}

Check warning on line 307 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L306-L307

Added lines #L306 - L307 were not covered by tests
var parsedReplaces []string
for i, stmt := range mf.Syntax.Stmt {
line, ok := stmt.(*modfile.Line)
if !ok || len(line.Token) < 3 || len(line.Token) > 4 {
parsedReplaces = append(parsedReplaces, replaces[i])
continue
}

dividerIndex := slices.Index(line.Token, "=>")
if dividerIndex < 0 || dividerIndex != len(line.Token)-2 {
// If the divider is not the second-to-last token, then this is a ModulePath => ModulePath replacement,
// so there is nothing that needs replacing.
parsedReplaces = append(parsedReplaces, replaces[i])
continue

Check warning on line 321 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L318-L321

Added lines #L318 - L321 were not covered by tests
}

// This is a ModulePath => FilePath replacement, so ensure it exists & make it absolute.
p, err := preparePath(strings.Trim(line.Token[dividerIndex+1], `"`))
if err != nil {
return replaces, err
}

Check warning on line 328 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L327-L328

Added lines #L327 - L328 were not covered by tests
// It's possible that the absolute path needs to be quoted to be safe to use in the go.mod file.
line.Token[dividerIndex+1] = modfile.AutoQuote(p)

parsedReplaces = append(parsedReplaces, strings.Join(line.Token, " "))
}
return parsedReplaces, nil
}
28 changes: 27 additions & 1 deletion cmd/builder/internal/builder/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package builder

import (
"os"
"path/filepath"
"strings"
"testing"

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

func TestRelativePath(t *testing.T) {
func TestRelativePath_Module(t *testing.T) {
// prepare
cfg := Config{
Extensions: []Module{{
Expand All @@ -182,6 +183,31 @@ func TestRelativePath(t *testing.T) {
assert.True(t, strings.HasPrefix(cfg.Extensions[0].Path, cwd))
}

func TestRelativePath_Replace(t *testing.T) {
// prepare
cfg := Config{
Replaces: []string{
"module1 => ./templates",
"module2 v0.0.1 => \"./templates\"",
"module3 v1.2.3 => fake.test/module3 latest",
},
}

// test
err := cfg.ParseModules()
require.NoError(t, err)

// verify
cwd, err := os.Getwd()
require.NoError(t, err)

if assert.Len(t, cfg.Replaces, 3) {
assert.Equal(t, "module1 => "+filepath.Join(cwd, "templates"), cfg.Replaces[0])
assert.Equal(t, "module2 v0.0.1 => "+filepath.Join(cwd, "templates"), cfg.Replaces[1])
assert.Equal(t, "module3 v1.2.3 => fake.test/module3 latest", cfg.Replaces[2])
}
}

func TestModuleFromCore(t *testing.T) {
// prepare
cfg := Config{
Expand Down
Loading