Skip to content

Commit aaec3e8

Browse files
authored
Make the fetcher update transitive maven deps in buf.plugin.yml (#2342)
1 parent ff0b429 commit aaec3e8

2 files changed

Lines changed: 235 additions & 2 deletions

File tree

internal/maven/regenerate.go

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"strings"
78

89
"github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremotepluginconfig"
10+
"golang.org/x/mod/semver"
911
)
1012

1113
// RegenerateMavenDeps processes a Maven plugin version directory by
1214
// merging transitive deps, deduplicating, and rendering POM to a
13-
// pom.xml file. Returns nil without changes if the plugin has no
14-
// Maven registry config.
15+
// pom.xml file. When transitive deps bring in newer versions of
16+
// artifacts already pinned in the plugin's buf.plugin.yaml, the YAML
17+
// file is updated first so that deduplication and POM generation see
18+
// consistent versions. Returns nil without changes if the plugin has
19+
// no Maven registry config.
1520
func RegenerateMavenDeps(pluginVersionDir, pluginsDir string) error {
1621
yamlPath := filepath.Join(pluginVersionDir, "buf.plugin.yaml")
1722
pluginConfig, err := bufremotepluginconfig.ParseConfig(yamlPath)
@@ -21,6 +26,23 @@ func RegenerateMavenDeps(pluginVersionDir, pluginsDir string) error {
2126
if pluginConfig.Registry == nil || pluginConfig.Registry.Maven == nil {
2227
return nil
2328
}
29+
// Collect the versions declared by transitive deps so we can detect
30+
// stale pins in the plugin's own buf.plugin.yaml.
31+
transitiveDeps, err := collectTransitiveMavenDeps(pluginConfig, pluginsDir)
32+
if err != nil {
33+
return fmt.Errorf("collecting transitive deps: %w", err)
34+
}
35+
// Update buf.plugin.yaml if any direct dep versions are older than
36+
// what transitive deps declare.
37+
if err := updateBufPluginYAML(yamlPath, pluginConfig.Registry.Maven, transitiveDeps); err != nil {
38+
return fmt.Errorf("updating buf.plugin.yaml: %w", err)
39+
}
40+
// Re-parse the (potentially updated) YAML so the in-memory config
41+
// reflects the updated versions.
42+
pluginConfig, err = bufremotepluginconfig.ParseConfig(yamlPath)
43+
if err != nil {
44+
return err
45+
}
2446
if err := MergeTransitiveDeps(pluginConfig, pluginsDir); err != nil {
2547
return fmt.Errorf("merging transitive deps: %w", err)
2648
}
@@ -37,3 +59,118 @@ func RegenerateMavenDeps(pluginVersionDir, pluginsDir string) error {
3759
}
3860
return nil
3961
}
62+
63+
// mavenDepKey returns the deduplication key for a Maven dependency
64+
// (groupId:artifactId, optionally with classifier).
65+
func mavenDepKey(dep bufremotepluginconfig.MavenDependencyConfig) string {
66+
key := dep.GroupID + ":" + dep.ArtifactID
67+
if dep.Classifier != "" {
68+
key += ":" + dep.Classifier
69+
}
70+
return key
71+
}
72+
73+
// collectTransitiveMavenDeps walks the plugin's dependency tree and
74+
// returns a map of artifact key -> version for every Maven dep found
75+
// in transitive dependencies. This does not mutate pluginConfig.
76+
func collectTransitiveMavenDeps(
77+
pluginConfig *bufremotepluginconfig.Config,
78+
pluginsDir string,
79+
) (map[string]string, error) {
80+
versions := make(map[string]string)
81+
visited := make(map[string]bool)
82+
if err := collectTransitiveMavenDepsRecursive(pluginConfig, pluginsDir, visited, versions); err != nil {
83+
return nil, err
84+
}
85+
return versions, nil
86+
}
87+
88+
func collectTransitiveMavenDepsRecursive(
89+
pluginConfig *bufremotepluginconfig.Config,
90+
pluginsDir string,
91+
visited map[string]bool,
92+
versions map[string]string,
93+
) error {
94+
for _, dep := range pluginConfig.Dependencies {
95+
depKey := dep.IdentityString() + ":" + dep.Version()
96+
if visited[depKey] {
97+
continue
98+
}
99+
visited[depKey] = true
100+
depPath := filepath.Join(
101+
pluginsDir, dep.Owner(), dep.Plugin(),
102+
dep.Version(), "buf.plugin.yaml",
103+
)
104+
depConfig, err := bufremotepluginconfig.ParseConfig(depPath)
105+
if err != nil {
106+
return fmt.Errorf("loading dep config %s from %s: %w", depKey, depPath, err)
107+
}
108+
if err := collectTransitiveMavenDepsRecursive(depConfig, pluginsDir, visited, versions); err != nil {
109+
return err
110+
}
111+
if depConfig.Registry == nil || depConfig.Registry.Maven == nil {
112+
continue
113+
}
114+
for _, d := range depConfig.Registry.Maven.Deps {
115+
versions[mavenDepKey(d)] = d.Version
116+
}
117+
for _, rt := range depConfig.Registry.Maven.AdditionalRuntimes {
118+
for _, d := range rt.Deps {
119+
versions[mavenDepKey(d)] = d.Version
120+
}
121+
}
122+
}
123+
return nil
124+
}
125+
126+
// updateBufPluginYAML rewrites buf.plugin.yaml when transitive deps
127+
// declare newer versions of artifacts already pinned in the plugin's
128+
// Maven config. It performs targeted text replacements of the Maven
129+
// dep strings (group:artifact:oldVer -> group:artifact:newVer) so
130+
// that comments and formatting are preserved.
131+
func updateBufPluginYAML(
132+
yamlPath string,
133+
maven *bufremotepluginconfig.MavenRegistryConfig,
134+
transitiveDeps map[string]string,
135+
) error {
136+
replacements := make(map[string]string) // "group:artifact:old" -> "group:artifact:new"
137+
collectReplacements := func(deps []bufremotepluginconfig.MavenDependencyConfig) {
138+
for _, dep := range deps {
139+
key := mavenDepKey(dep)
140+
transitiveVersion, ok := transitiveDeps[key]
141+
if !ok || transitiveVersion == dep.Version {
142+
continue
143+
}
144+
// Only upgrade if the transitive version is higher.
145+
oldSemver := "v" + dep.Version
146+
newSemver := "v" + transitiveVersion
147+
if !semver.IsValid(oldSemver) || !semver.IsValid(newSemver) {
148+
continue
149+
}
150+
if semver.Compare(newSemver, oldSemver) <= 0 {
151+
continue
152+
}
153+
ref := dep.GroupID + ":" + dep.ArtifactID
154+
if dep.Classifier != "" {
155+
ref += ":" + dep.Classifier
156+
}
157+
replacements[ref+":"+dep.Version] = ref + ":" + transitiveVersion
158+
}
159+
}
160+
collectReplacements(maven.Deps)
161+
for _, rt := range maven.AdditionalRuntimes {
162+
collectReplacements(rt.Deps)
163+
}
164+
if len(replacements) == 0 {
165+
return nil
166+
}
167+
content, err := os.ReadFile(yamlPath)
168+
if err != nil {
169+
return err
170+
}
171+
result := string(content)
172+
for oldRef, newRef := range replacements {
173+
result = strings.ReplaceAll(result, oldRef, newRef)
174+
}
175+
return os.WriteFile(yamlPath, []byte(result), 0644) //nolint:gosec
176+
}

internal/maven/regenerate_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,102 @@ import (
1010
"github.com/stretchr/testify/require"
1111
)
1212

13+
func TestRegenerateMavenDepsUpdatesStaleVersions(t *testing.T) {
14+
t.Parallel()
15+
tmpDir := t.TempDir()
16+
17+
// Simulate the grpc/java scenario: the dep plugin (protocolbuffers/java)
18+
// was updated to v34.0 which uses protobuf-java:4.34.0, but the
19+
// grpc/java plugin still has stale pins at 4.33.5 from the previous
20+
// version's copy.
21+
baseDir := filepath.Join(tmpDir, "plugins", "protocolbuffers", "java", "v34.0")
22+
require.NoError(t, os.MkdirAll(baseDir, 0755))
23+
baseYAML := `version: v1
24+
name: buf.build/protocolbuffers/java
25+
plugin_version: v34.0
26+
output_languages:
27+
- java
28+
registry:
29+
maven:
30+
deps:
31+
- com.google.protobuf:protobuf-java:4.34.0
32+
additional_runtimes:
33+
- name: lite
34+
deps:
35+
- com.google.protobuf:protobuf-javalite:4.34.0
36+
- build.buf:protobuf-javalite:4.34.0
37+
opts: [lite]
38+
`
39+
require.NoError(t, os.WriteFile(filepath.Join(baseDir, "buf.plugin.yaml"), []byte(baseYAML), 0644))
40+
41+
consumerDir := filepath.Join(tmpDir, "plugins", "grpc", "java", "v1.80.0")
42+
require.NoError(t, os.MkdirAll(consumerDir, 0755))
43+
// This has the dep updated to v34.0 but Maven pins still at 4.33.5
44+
// (the old version from the copy step).
45+
consumerYAML := `version: v1
46+
name: buf.build/grpc/java
47+
plugin_version: v1.80.0
48+
source_url: https://github.com/grpc/grpc-java
49+
description: Generates Java client and server stubs for the gRPC framework.
50+
deps:
51+
- plugin: buf.build/protocolbuffers/java:v34.0
52+
output_languages:
53+
- java
54+
spdx_license_id: Apache-2.0
55+
license_url: https://github.com/grpc/grpc-java/blob/v1.80.0/LICENSE
56+
registry:
57+
maven:
58+
deps:
59+
- io.grpc:grpc-core:1.80.0
60+
- io.grpc:grpc-protobuf:1.80.0
61+
- io.grpc:grpc-stub:1.80.0
62+
# Add direct dependency on newer protobuf as gRPC is still on 3.25.8
63+
- com.google.protobuf:protobuf-java:4.33.5
64+
additional_runtimes:
65+
- name: lite
66+
deps:
67+
- io.grpc:grpc-core:1.80.0
68+
- io.grpc:grpc-protobuf-lite:1.80.0
69+
- io.grpc:grpc-stub:1.80.0
70+
# Add direct dependency on newer protobuf as gRPC is still on 3.25.8
71+
- com.google.protobuf:protobuf-javalite:4.33.5
72+
- build.buf:protobuf-javalite:4.33.5
73+
opts: [lite]
74+
`
75+
require.NoError(t, os.WriteFile(filepath.Join(consumerDir, "buf.plugin.yaml"), []byte(consumerYAML), 0644))
76+
77+
pluginsDir := filepath.Join(tmpDir, "plugins")
78+
err := RegenerateMavenDeps(consumerDir, pluginsDir)
79+
require.NoError(t, err)
80+
81+
// Verify buf.plugin.yaml was updated with the correct versions.
82+
updatedYAML, err := os.ReadFile(filepath.Join(consumerDir, "buf.plugin.yaml"))
83+
require.NoError(t, err)
84+
yamlStr := string(updatedYAML)
85+
assert.Contains(t, yamlStr, "com.google.protobuf:protobuf-java:4.34.0")
86+
assert.NotContains(t, yamlStr, "com.google.protobuf:protobuf-java:4.33.5")
87+
assert.Contains(t, yamlStr, "com.google.protobuf:protobuf-javalite:4.34.0")
88+
assert.NotContains(t, yamlStr, "com.google.protobuf:protobuf-javalite:4.33.5")
89+
assert.Contains(t, yamlStr, "build.buf:protobuf-javalite:4.34.0")
90+
assert.NotContains(t, yamlStr, "build.buf:protobuf-javalite:4.33.5")
91+
// grpc deps should be unchanged
92+
assert.Contains(t, yamlStr, "io.grpc:grpc-core:1.80.0")
93+
// Comments should be preserved
94+
assert.Contains(t, yamlStr, "# Add direct dependency on newer protobuf")
95+
96+
// Verify pom.xml has the correct versions.
97+
pomBytes, err := os.ReadFile(filepath.Join(consumerDir, "pom.xml"))
98+
require.NoError(t, err)
99+
var pom pomProject
100+
require.NoError(t, xml.Unmarshal(pomBytes, &pom))
101+
var depVersions []string
102+
for _, dep := range pom.Dependencies {
103+
depVersions = append(depVersions, dep.GroupID+":"+dep.ArtifactID+":"+dep.Version)
104+
}
105+
assert.Contains(t, depVersions, "com.google.protobuf:protobuf-java:4.34.0")
106+
assert.Contains(t, depVersions, "io.grpc:grpc-core:1.80.0")
107+
}
108+
13109
func TestRegenerateMavenDeps(t *testing.T) {
14110
t.Parallel()
15111
tmpDir := t.TempDir()

0 commit comments

Comments
 (0)