@@ -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.
1520func 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+ }
0 commit comments