Skip to content

Commit fb6c330

Browse files
authored
feat: support non-root build paths (#209)
1 parent 3bd8623 commit fb6c330

File tree

8 files changed

+426
-23
lines changed

8 files changed

+426
-23
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/urfave/cli/v3 v3.6.1
1212
golang.org/x/mod v0.30.0
1313
golang.org/x/sync v0.18.0
14+
golang.org/x/tools v0.38.0
1415
gopkg.in/yaml.v3 v3.0.1
1516
gotest.tools/v3 v3.5.2
1617
)
@@ -21,6 +22,5 @@ require (
2122
github.com/kr/pretty v0.3.1 // indirect
2223
github.com/pmezard/go-difflib v1.0.0 // indirect
2324
github.com/rogpeppe/go-internal v1.13.1 // indirect
24-
golang.org/x/tools v0.38.0 // indirect
2525
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
2626
)

pkg/instrumentation/grpc/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ go 1.23.0
44

55
replace github.com/open-telemetry/opentelemetry-go-compile-instrumentation/pkg => ../..
66

7-
require github.com/open-telemetry/opentelemetry-go-compile-instrumentation/pkg 947df235a956
7+
require github.com/open-telemetry/opentelemetry-go-compile-instrumentation/pkg 947df235a956

tool/internal/setup/add.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ package setup
55

66
import (
77
"fmt"
8+
"maps"
9+
"path/filepath"
10+
"slices"
811

912
"github.com/dave/dst"
1013

@@ -27,9 +30,10 @@ func genImportDecl(matched []*rule.InstFuncRule) []dst.Decl {
2730
for _, m := range matched {
2831
requiredImports[m.Path] = ast.IdentIgnore
2932
}
30-
importDecls := make([]dst.Decl, 0)
31-
for k, v := range requiredImports {
32-
importDecls = append(importDecls, ast.ImportDecl(v, k))
33+
importDecls := make([]dst.Decl, 0, len(requiredImports))
34+
// Sort the keys to ensure deterministic order
35+
for _, k := range slices.Sorted(maps.Keys(requiredImports)) {
36+
importDecls = append(importDecls, ast.ImportDecl(requiredImports[k], k))
3337
}
3438
return importDecls
3539
}
@@ -96,7 +100,9 @@ func buildOtelRuntimeAst(decls []dst.Decl) *dst.File {
96100
}
97101
}
98102

99-
func (sp *SetupPhase) addDeps(matched []*rule.InstRuleSet) error {
103+
// addDeps generates and writes otel.runtime.go with required imports and variable
104+
// declarations for OpenTelemetry instrumentation based on matched rules.
105+
func (sp *SetupPhase) addDeps(matched []*rule.InstRuleSet, packagePath string) error {
100106
rules := make([]*rule.InstFuncRule, 0)
101107
for _, m := range matched {
102108
funcRules := m.GetFuncRules()
@@ -113,10 +119,12 @@ func (sp *SetupPhase) addDeps(matched []*rule.InstRuleSet) error {
113119
// Build the ast
114120
root := buildOtelRuntimeAst(append(importDecls, varDecls...))
115121
// Write the ast to file
116-
err := ast.WriteFile(OtelRuntimeFile, root)
122+
otelRuntimeFilePath := filepath.Join(packagePath, OtelRuntimeFile)
123+
err := ast.WriteFile(otelRuntimeFilePath, root)
117124
if err != nil {
118125
return err
119126
}
120-
sp.keepForDebug(OtelRuntimeFile)
127+
sp.keepForDebug(otelRuntimeFilePath)
128+
sp.Info("Created otel.runtime.go", "path", otelRuntimeFilePath)
121129
return nil
122130
}

tool/internal/setup/add_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//go:build !windows
5+
6+
// Package setup tests verify that the addDeps function generates
7+
// the expected otel.runtime.go file by comparing against golden files.
8+
//
9+
// To update golden files after intentional changes:
10+
//
11+
// go test -update ./tool/internal/setup/...
12+
13+
package setup
14+
15+
import (
16+
"io"
17+
"log/slog"
18+
"os"
19+
"path/filepath"
20+
"testing"
21+
22+
"github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/internal/rule"
23+
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
25+
"gotest.tools/v3/golden"
26+
)
27+
28+
func TestAddDeps(t *testing.T) {
29+
tests := []struct {
30+
name string
31+
matched []*rule.InstRuleSet
32+
goldenFile string // Empty means no file should be generated
33+
}{
34+
{
35+
name: "empty_matched_rules",
36+
matched: []*rule.InstRuleSet{},
37+
goldenFile: "",
38+
},
39+
{
40+
name: "single_func_rule",
41+
matched: []*rule.InstRuleSet{
42+
newTestRuleSet(
43+
"github.com/example/pkg",
44+
newTestFuncRule("github.com/example/pkg", "github.com/example/pkg"),
45+
),
46+
},
47+
goldenFile: "single_func_rule.otel.runtime.go.golden",
48+
},
49+
{
50+
name: "no_func_rules",
51+
matched: []*rule.InstRuleSet{
52+
newTestRuleSet("github.com/example/pkg"),
53+
},
54+
goldenFile: "",
55+
},
56+
{
57+
name: "multiple_rule_sets",
58+
matched: []*rule.InstRuleSet{
59+
newTestRuleSet(
60+
"github.com/example/pkg1",
61+
newTestFuncRule("github.com/example/pkg1", "github.com/example/pkg1"),
62+
),
63+
newTestRuleSet(
64+
"github.com/example/pkg2",
65+
newTestFuncRule("github.com/example/pkg2", "github.com/example/pkg2"),
66+
),
67+
},
68+
goldenFile: "multiple_rule_sets.otel.runtime.go.golden",
69+
},
70+
}
71+
72+
for _, tt := range tests {
73+
t.Run(tt.name, func(t *testing.T) {
74+
tmpDir := t.TempDir()
75+
sp := newTestSetupPhase()
76+
77+
err := sp.addDeps(tt.matched, tmpDir)
78+
require.NoError(t, err)
79+
80+
runtimeFilePath := filepath.Join(tmpDir, OtelRuntimeFile)
81+
82+
if tt.goldenFile == "" {
83+
assert.NoFileExists(t, runtimeFilePath)
84+
return
85+
}
86+
87+
assert.FileExists(t, runtimeFilePath)
88+
actual, err := os.ReadFile(runtimeFilePath)
89+
require.NoError(t, err)
90+
91+
golden.Assert(t, string(actual), tt.goldenFile)
92+
})
93+
}
94+
}
95+
96+
func TestAddDeps_FileWriteError(t *testing.T) {
97+
matched := []*rule.InstRuleSet{
98+
newTestRuleSet(
99+
"github.com/example/pkg",
100+
newTestFuncRule("github.com/example/pkg", "github.com/example/pkg"),
101+
),
102+
}
103+
104+
// Use a non-existent parent directory to cause write error
105+
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "subdir")
106+
sp := newTestSetupPhase()
107+
108+
err := sp.addDeps(matched, invalidPath)
109+
assert.Error(t, err)
110+
}
111+
112+
// Helper functions for constructing test data
113+
114+
func newTestSetupPhase() *SetupPhase {
115+
return &SetupPhase{
116+
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
117+
}
118+
}
119+
120+
func newTestFuncRule(path, target string) *rule.InstFuncRule {
121+
return &rule.InstFuncRule{
122+
InstBaseRule: rule.InstBaseRule{
123+
Target: target,
124+
},
125+
Path: path,
126+
}
127+
}
128+
129+
func newTestRuleSet(modulePath string, funcRules ...*rule.InstFuncRule) *rule.InstRuleSet {
130+
rs := rule.NewInstRuleSet(modulePath)
131+
fakeFilePath := filepath.Join(os.TempDir(), "file.go")
132+
for _, fr := range funcRules {
133+
rs.AddFuncRule(fakeFilePath, fr)
134+
}
135+
return rs
136+
}

tool/internal/setup/setup.go

Lines changed: 111 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import (
99
"log/slog"
1010
"os"
1111
"path/filepath"
12+
"strings"
1213

1314
"github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/ex"
1415
"github.com/open-telemetry/opentelemetry-go-compile-instrumentation/tool/util"
16+
"golang.org/x/tools/go/packages"
1517
)
1618

1719
type SetupPhase struct {
@@ -39,6 +41,90 @@ func isSetup() bool {
3941
return false
4042
}
4143

44+
// flagsWithPathValues contains flags that accept a value from "go build" command.
45+
//
46+
//nolint:gochecknoglobals // private lookup table
47+
var flagsWithPathValues = map[string]bool{
48+
"-C": true,
49+
"-o": true,
50+
"-p": true,
51+
"-covermode": true,
52+
"-coverpkg": true,
53+
"-asmflags": true,
54+
"-buildmode": true,
55+
"-buildvcs": true,
56+
"-compiler": true,
57+
"-gccgoflags": true,
58+
"-gcflags": true,
59+
"-installsuffix": true,
60+
"-ldflags": true,
61+
"-mod": true,
62+
"-modfile": true,
63+
"-overlay": true,
64+
"-pgo": true,
65+
"-pkgdir": true,
66+
"-tags": true,
67+
"-toolexec": true,
68+
}
69+
70+
// GetBuildPackages loads all packages from the go build command arguments.
71+
// Returns a list of loaded packages. If no package patterns are found in args,
72+
// defaults to loading the current directory package.
73+
// The args parameter should be the go build command arguments (e.g., ["build", "-a", "./cmd"]).
74+
// Returns an error if package loading fails or if invalid patterns are provided.
75+
// For example:
76+
// - args ["build", "-a", "./cmd"] returns packages for "./cmd"
77+
// - args ["build", "-a", "cmd"] returns packages for the "cmd" package in the module
78+
// - args ["build", "-a", ".", "./cmd"] returns packages for both "." and "./cmd"
79+
// - args ["build"] returns packages for "."
80+
func getBuildPackages(ctx context.Context, args []string) ([]*packages.Package, error) {
81+
logger := util.LoggerFromContext(ctx)
82+
83+
buildPkgs := make([]*packages.Package, 0)
84+
cfg := &packages.Config{
85+
Mode: packages.NeedName | packages.NeedFiles | packages.NeedModule,
86+
}
87+
found := false
88+
for i := len(args) - 1; i >= 0; i-- {
89+
arg := args[i]
90+
91+
// If preceded by a flag that takes a path value, this is a flag value
92+
// We want to avoid scenarios like "go build -o ./tmp ./app" where tmp also contains Go files,
93+
// as it would be treated as a package.
94+
if i > 0 && flagsWithPathValues[args[i-1]] {
95+
break
96+
}
97+
98+
// If we hit a flag, stop. Packages come after all flags
99+
// go build [-o output] [build flags] [packages]
100+
if strings.HasPrefix(arg, "-") || arg == "go" || arg == "build" || arg == "install" {
101+
break
102+
}
103+
104+
pkgs, err := packages.Load(cfg, arg)
105+
if err != nil {
106+
return nil, ex.Wrapf(err, "failed to load packages for pattern %s", arg)
107+
}
108+
for _, pkg := range pkgs {
109+
if pkg.Errors != nil || pkg.Module == nil {
110+
logger.DebugContext(ctx, "skipping package", "pattern", arg, "errors", pkg.Errors, "module", pkg.Module)
111+
continue
112+
}
113+
buildPkgs = append(buildPkgs, pkg)
114+
found = true
115+
}
116+
}
117+
118+
if !found {
119+
var err error
120+
buildPkgs, err = packages.Load(cfg, ".")
121+
if err != nil {
122+
return nil, ex.Wrapf(err, "failed to load packages for pattern .")
123+
}
124+
}
125+
return buildPkgs, nil
126+
}
127+
42128
// Setup prepares the environment for further instrumentation.
43129
func Setup(ctx context.Context, args []string) error {
44130
logger := util.LoggerFromContext(ctx)
@@ -51,6 +137,14 @@ func Setup(ctx context.Context, args []string) error {
51137
sp := &SetupPhase{
52138
logger: logger,
53139
}
140+
141+
// Introduce additional hook code by generating otel.runtime.go
142+
// Use GetPackage to determine the build target directory
143+
pkgs, err := getBuildPackages(ctx, args)
144+
if err != nil {
145+
return err
146+
}
147+
54148
// Find all dependencies of the project being build
55149
deps, err := sp.findDeps(ctx, args)
56150
if err != nil {
@@ -61,10 +155,11 @@ func Setup(ctx context.Context, args []string) error {
61155
if err != nil {
62156
return err
63157
}
64-
// Introduce additional hook code by generating otel.instrumentation.go
65-
err = sp.addDeps(matched)
66-
if err != nil {
67-
return err
158+
for _, pkg := range pkgs {
159+
// Introduce additional hook code by generating otel.runtime.go
160+
if err = sp.addDeps(matched, pkg.Dir); err != nil {
161+
return err
162+
}
68163
}
69164
// Extract the embedded instrumentation modules into local directory
70165
err = sp.extract()
@@ -77,11 +172,7 @@ func Setup(ctx context.Context, args []string) error {
77172
return err
78173
}
79174
// Write the matched hook to matched.txt for further instrument phase
80-
err = sp.store(matched)
81-
if err != nil {
82-
return err
83-
}
84-
return nil
175+
return sp.store(matched)
85176
}
86177

87178
// BuildWithToolexec builds the project with the toolexec mode
@@ -128,16 +219,21 @@ func GoBuild(ctx context.Context, args []string) error {
128219
logger.DebugContext(ctx, "failed to back up files", "error", err)
129220
}
130221
defer func() {
131-
err = os.RemoveAll(OtelRuntimeFile)
222+
var pkgs []*packages.Package
223+
pkgs, err = getBuildPackages(ctx, args)
132224
if err != nil {
133-
logger.DebugContext(ctx, "failed to remove otel runtime file", "error", err)
225+
logger.DebugContext(ctx, "failed to get build packages", "error", err)
134226
}
135-
err = os.RemoveAll(unzippedPkgDir)
136-
if err != nil {
227+
for _, pkg := range pkgs {
228+
if err = os.RemoveAll(filepath.Join(pkg.Dir, OtelRuntimeFile)); err != nil {
229+
logger.DebugContext(ctx, "failed to remove generated file from package",
230+
"file", filepath.Join(pkg.Dir, OtelRuntimeFile), "error", err)
231+
}
232+
}
233+
if err = os.RemoveAll(unzippedPkgDir); err != nil {
137234
logger.DebugContext(ctx, "failed to remove unzipped pkg", "error", err)
138235
}
139-
err = util.RestoreFile(backupFiles)
140-
if err != nil {
236+
if err = util.RestoreFile(backupFiles); err != nil {
141237
logger.DebugContext(ctx, "failed to restore files", "error", err)
142238
}
143239
}()

0 commit comments

Comments
 (0)