Skip to content
Merged
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
12 changes: 12 additions & 0 deletions internal/cmd/fetcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/bufbuild/plugins/internal/docker"
"github.com/bufbuild/plugins/internal/fetchclient"
"github.com/bufbuild/plugins/internal/maven"
"github.com/bufbuild/plugins/internal/plugin"
"github.com/bufbuild/plugins/internal/source"
)
Expand Down Expand Up @@ -131,6 +132,9 @@ func postProcessCreatedPlugins(ctx context.Context, logger *slog.Logger, plugins
}
for _, plugin := range plugins {
newPluginRef := plugin.String()
if err := regenerateMavenDeps(plugin); err != nil {
return fmt.Errorf("failed to regenerate maven deps for %s: %w", newPluginRef, err)
}
if err := runGoModTidy(ctx, logger, plugin); err != nil {
return fmt.Errorf("failed to run go mod tidy for %s: %w", newPluginRef, err)
}
Expand Down Expand Up @@ -273,6 +277,14 @@ func recreateSwiftPackageResolved(ctx context.Context, logger *slog.Logger, plug
return nil
}

// regenerateMavenDeps regenerates the pom.xml and Dockerfile's maven-deps
// stage from the plugin's buf.plugin.yaml.
func regenerateMavenDeps(plugin createdPlugin) error {
versionDir := filepath.Join(plugin.pluginDir, plugin.newVersion)
pluginsDir := filepath.Dir(filepath.Dir(plugin.pluginDir))
return maven.RegenerateMavenDeps(versionDir, pluginsDir)
}

// runPluginTests runs 'make test PLUGINS="org/name:v<new>"' in order to generate plugin.sum files.
func runPluginTests(ctx context.Context, logger *slog.Logger, plugins []createdPlugin) error {
pluginsEnv := make([]string, 0, len(plugins))
Expand Down
259 changes: 259 additions & 0 deletions internal/cmd/fetcher/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"encoding/xml"
"log/slog"
"os"
"path/filepath"
Expand All @@ -15,6 +16,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/bufbuild/plugins/internal/maven"
"github.com/bufbuild/plugins/internal/source"
)

Expand Down Expand Up @@ -358,3 +360,260 @@ func newTestContainer(t *testing.T, root string) appext.Container {
logger := slog.New(slog.NewTextHandler(testWriter{t}, &slog.HandlerOptions{Level: slog.LevelDebug}))
return appext.NewContainer(nameContainer, logger)
}

func TestRegenerateMavenDepsWithPluginDeps(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()

// Setup: base-plugin has Maven deps including an additional lite runtime.
baseDir := filepath.Join(tmpDir, "plugins", "test", "base-plugin", "v1.0.0")
require.NoError(t, os.MkdirAll(baseDir, 0755))
baseYAML := `version: v1
name: buf.build/test/base-plugin
plugin_version: v1.0.0
output_languages:
- java
registry:
maven:
deps:
- com.google.protobuf:protobuf-java:4.33.5
additional_runtimes:
- name: lite
deps:
- com.google.protobuf:protobuf-javalite:4.33.5
- build.buf:protobuf-javalite:4.33.5
opts: [lite]
`
require.NoError(t, os.WriteFile(filepath.Join(baseDir, "buf.plugin.yaml"), []byte(baseYAML), 0644))

// Setup: consumer-plugin depends on base-plugin and has its own Maven
// deps plus a lite runtime. The Dockerfile has a maven-deps stage.
consumerDir := filepath.Join(tmpDir, "plugins", "test", "consumer-plugin", "v1.0.0")
require.NoError(t, os.MkdirAll(consumerDir, 0755))
consumerYAML := `version: v1
name: buf.build/test/consumer-plugin
plugin_version: v1.0.0
deps:
- plugin: buf.build/test/base-plugin:v1.0.0
output_languages:
- kotlin
registry:
maven:
compiler:
kotlin:
version: 1.8.22
deps:
- com.google.protobuf:protobuf-kotlin:4.33.5
additional_runtimes:
- name: lite
deps:
- com.google.protobuf:protobuf-kotlin-lite:4.33.5
opts: [lite]
`
require.NoError(t, os.WriteFile(filepath.Join(consumerDir, "buf.plugin.yaml"), []byte(consumerYAML), 0644))

dockerfile := `# syntax=docker/dockerfile:1.19
FROM debian:bookworm AS build
RUN echo hello

FROM scratch
COPY --from=build /app .
ENTRYPOINT ["/app"]
`
require.NoError(t, os.WriteFile(filepath.Join(consumerDir, "Dockerfile"), []byte(dockerfile), 0644))

// Run regenerateMavenDeps on the consumer plugin
err := regenerateMavenDeps(createdPlugin{
org: "test",
name: "consumer-plugin",
pluginDir: filepath.Join(tmpDir, "plugins", "test", "consumer-plugin"),
newVersion: "v1.0.0",
})
require.NoError(t, err)

// Verify the maven-deps stage was inserted into the Dockerfile.
dockerfileBytes, err := os.ReadFile(filepath.Join(consumerDir, "Dockerfile"))
require.NoError(t, err)
assert.Contains(t, string(dockerfileBytes), "FROM "+maven.MavenImage+" AS maven-deps")
assert.Contains(t, string(dockerfileBytes), "COPY --from=maven-deps /root/.m2/repository /maven-repository")

// Read and parse pom.xml to verify deps include versions.
pomBytes, err := os.ReadFile(filepath.Join(consumerDir, "pom.xml"))
require.NoError(t, err)
var pom testPOMProject
require.NoError(t, xml.Unmarshal(pomBytes, &pom))
var depVersions []string
for _, dep := range pom.Dependencies {
depVersions = append(depVersions, dep.String())
}
// Consumer's own deps should be present.
assert.Contains(t, depVersions, "com.google.protobuf:protobuf-kotlin:4.33.5")
assert.Contains(t, depVersions, "com.google.protobuf:protobuf-kotlin-lite:4.33.5")
// Base plugin's main deps should be merged in.
assert.Contains(t, depVersions, "com.google.protobuf:protobuf-java:4.33.5")
// Base plugin's lite runtime deps should be merged into the
// matching lite runtime section.
assert.Contains(t, depVersions, "com.google.protobuf:protobuf-javalite:4.33.5")
assert.Contains(t, depVersions, "build.buf:protobuf-javalite:4.33.5")
}

func TestMergeDepsMavenDepsTransitive(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()

// Setup: grandparent -> parent -> child chain
grandparentDir := filepath.Join(tmpDir, "plugins", "test", "grandparent", "v1.0.0")
require.NoError(t, os.MkdirAll(grandparentDir, 0755))
grandparentYAML := `version: v1
name: buf.build/test/grandparent
plugin_version: v1.0.0
output_languages:
- java
registry:
maven:
deps:
- org.example:grandparent-dep:1.0.0
`
require.NoError(t, os.WriteFile(filepath.Join(grandparentDir, "buf.plugin.yaml"), []byte(grandparentYAML), 0644))

parentDir := filepath.Join(tmpDir, "plugins", "test", "parent", "v1.0.0")
require.NoError(t, os.MkdirAll(parentDir, 0755))
parentYAML := `version: v1
name: buf.build/test/parent
plugin_version: v1.0.0
deps:
- plugin: buf.build/test/grandparent:v1.0.0
output_languages:
- java
registry:
maven:
deps:
- org.example:parent-dep:1.0.0
`
require.NoError(t, os.WriteFile(filepath.Join(parentDir, "buf.plugin.yaml"), []byte(parentYAML), 0644))

childDir := filepath.Join(tmpDir, "plugins", "test", "child", "v1.0.0")
require.NoError(t, os.MkdirAll(childDir, 0755))
childYAML := `version: v1
name: buf.build/test/child
plugin_version: v1.0.0
deps:
- plugin: buf.build/test/parent:v1.0.0
output_languages:
- java
registry:
maven:
deps:
- org.example:child-dep:1.0.0
`
require.NoError(t, os.WriteFile(filepath.Join(childDir, "buf.plugin.yaml"), []byte(childYAML), 0644))

// Parse the child plugin config
childConfig, err := bufremotepluginconfig.ParseConfig(
filepath.Join(childDir, "buf.plugin.yaml"),
)
require.NoError(t, err)

// Merge transitive deps
pluginsDir := filepath.Join(tmpDir, "plugins")
err = maven.MergeTransitiveDeps(childConfig, pluginsDir)
require.NoError(t, err)

// Child should now have all three deps: child-dep, parent-dep,
// grandparent-dep (transitive through parent)
var artifactIDs []string
for _, dep := range childConfig.Registry.Maven.Deps {
artifactIDs = append(artifactIDs, dep.ArtifactID)
}
assert.Contains(t, artifactIDs, "child-dep")
assert.Contains(t, artifactIDs, "parent-dep")
assert.Contains(t, artifactIDs, "grandparent-dep")
}

func TestDeduplicateAllDeps(t *testing.T) {
t.Parallel()
tests := []struct {
name string
deps []bufremotepluginconfig.MavenDependencyConfig
want []bufremotepluginconfig.MavenDependencyConfig
wantErr string
}{
{
name: "no duplicates",
deps: []bufremotepluginconfig.MavenDependencyConfig{
{GroupID: "com.example", ArtifactID: "a", Version: "1.0"},
{GroupID: "com.example", ArtifactID: "b", Version: "1.0"},
},
want: []bufremotepluginconfig.MavenDependencyConfig{
{GroupID: "com.example", ArtifactID: "a", Version: "1.0"},
{GroupID: "com.example", ArtifactID: "b", Version: "1.0"},
},
},
{
name: "exact duplicate removed",
deps: []bufremotepluginconfig.MavenDependencyConfig{
{GroupID: "com.example", ArtifactID: "a", Version: "1.0"},
{GroupID: "com.example", ArtifactID: "a", Version: "1.0"},
},
want: []bufremotepluginconfig.MavenDependencyConfig{
{GroupID: "com.example", ArtifactID: "a", Version: "1.0"},
},
},
{
name: "version conflict returns error",
deps: []bufremotepluginconfig.MavenDependencyConfig{
{GroupID: "com.example", ArtifactID: "a", Version: "1.0"},
{GroupID: "com.example", ArtifactID: "a", Version: "2.0"},
},
wantErr: "duplicate Maven dependency com.example:a with conflicting versions",
},
{
name: "different classifiers are distinct",
deps: []bufremotepluginconfig.MavenDependencyConfig{
{GroupID: "com.example", ArtifactID: "a", Version: "1.0", Classifier: "sources"},
{GroupID: "com.example", ArtifactID: "a", Version: "1.0"},
},
want: []bufremotepluginconfig.MavenDependencyConfig{
{GroupID: "com.example", ArtifactID: "a", Version: "1.0", Classifier: "sources"},
{GroupID: "com.example", ArtifactID: "a", Version: "1.0"},
},
},
{
name: "nil input",
deps: nil,
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
config := &bufremotepluginconfig.MavenRegistryConfig{
Deps: tt.deps,
}
err := maven.DeduplicateAllDeps(config)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, config.Deps)
})
}
}

// testPOMProject mirrors the Maven POM structure for test assertions.
type testPOMProject struct {
XMLName xml.Name `xml:"project"`
Dependencies []testDep `xml:"dependencies>dependency"`
}

type testDep struct {
GroupID string `xml:"groupId"`
ArtifactID string `xml:"artifactId"`
Version string `xml:"version"`
}

func (d testDep) String() string {
return d.GroupID + ":" + d.ArtifactID + ":" + d.Version
}
37 changes: 37 additions & 0 deletions internal/cmd/regenerate-maven-poms/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"context"
"fmt"
"path/filepath"

"buf.build/go/app"
"buf.build/go/app/appcmd"

"github.com/bufbuild/plugins/internal/maven"
)

func main() {
appcmd.Main(context.Background(), newCommand("regenerate-maven-poms"))
}

func newCommand(name string) *appcmd.Command {
return &appcmd.Command{
Use: name + " <plugin-dir> [<plugin-dir>...]",
Short: "Regenerates maven-deps POM and Dockerfile stage for Java/Kotlin plugins",
Args: appcmd.MinimumNArgs(1),
Run: func(_ context.Context, container app.Container) error {
for i := range container.NumArgs() {
pluginDir := container.Arg(i)
// pluginDir is e.g. plugins/org/name/version, so
// plugins root is 3 levels up.
pluginsDir := filepath.Dir(filepath.Dir(filepath.Dir(pluginDir)))
if err := maven.RegenerateMavenDeps(pluginDir, pluginsDir); err != nil {
return fmt.Errorf("failed to regenerate %s: %w", pluginDir, err)
}
fmt.Fprintf(container.Stdout(), "regenerated: %s\n", pluginDir)
}
return nil
},
}
}
Loading