From c76661c11a44b8804385311ddafff8458e052e90 Mon Sep 17 00:00:00 2001 From: David Marby Date: Tue, 24 Feb 2026 16:51:30 +0100 Subject: [PATCH 01/12] Add maven-deps packaging stages to Java/Kotlin plugin Dockerfiles --- internal/cmd/fetcher/main.go | 90 ++++ internal/cmd/fetcher/main_test.go | 390 ++++++++++++++++++ internal/cmd/regenerate-maven-poms/main.go | 183 ++++++++ internal/cmd/render-pom/main.go | 52 +++ internal/maven/deps.go | 140 +++++++ internal/maven/pom.go | 73 ++++ internal/maven/pom.xml.gotext | 75 ++++ internal/maven/pom_test.go | 305 ++++++++++++++ plugins/apple/servicetalk/v0.42.63/Dockerfile | 35 ++ .../connect-kotlin/v0.1.10/Dockerfile | 93 +++++ .../bufbuild/validate-java/v1.3.3/Dockerfile | 36 ++ .../v1.2.4/Dockerfile | 61 +++ plugins/connectrpc/kotlin/v0.7.4/Dockerfile | 94 +++++ plugins/grpc/java/v1.79.0/Dockerfile | 51 +++ plugins/grpc/kotlin/v1.5.0/Dockerfile | 103 +++++ plugins/protocolbuffers/java/v33.5/Dockerfile | 31 ++ .../protocolbuffers/kotlin/v33.5/Dockerfile | 73 ++++ 17 files changed, 1885 insertions(+) create mode 100644 internal/cmd/regenerate-maven-poms/main.go create mode 100644 internal/cmd/render-pom/main.go create mode 100644 internal/maven/deps.go create mode 100644 internal/maven/pom.go create mode 100644 internal/maven/pom.xml.gotext create mode 100644 internal/maven/pom_test.go diff --git a/internal/cmd/fetcher/main.go b/internal/cmd/fetcher/main.go index 327c18350..90140111c 100644 --- a/internal/cmd/fetcher/main.go +++ b/internal/cmd/fetcher/main.go @@ -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" ) @@ -131,6 +132,9 @@ func postProcessCreatedPlugins(ctx context.Context, logger *slog.Logger, plugins } for _, plugin := range plugins { newPluginRef := plugin.String() + if err := regenerateMavenDeps(logger, 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) } @@ -273,6 +277,92 @@ func recreateSwiftPackageResolved(ctx context.Context, logger *slog.Logger, plug return nil } +// regenerateMavenDeps regenerates the POM in the Dockerfile's maven-deps stage +// from the plugin's buf.plugin.yaml. This ensures the POM always reflects the +// actual Maven dependencies declared in the config, rather than relying on +// version string replacement which can miss transitive dependency updates. +func regenerateMavenDeps(logger *slog.Logger, plugin createdPlugin) error { + versionDir := filepath.Join(plugin.pluginDir, plugin.newVersion) + yamlPath := filepath.Join(versionDir, "buf.plugin.yaml") + pluginConfig, err := bufremotepluginconfig.ParseConfig(yamlPath) + if err != nil { + return err + } + if pluginConfig.Registry == nil || pluginConfig.Registry.Maven == nil { + return nil // not a Maven plugin + } + // Resolve Maven dependencies from plugin deps (top-level deps stanza) + // and merge them into the plugin's Maven config. This ensures the + // maven-deps Docker stage caches all dependencies needed for offline + // builds, including those from dependent plugins (e.g. Kotlin depending + // on Java brings in build.buf:protobuf-javalite for lite builds). + pluginsDir := filepath.Dir(filepath.Dir(plugin.pluginDir)) + if err := maven.MergeTransitiveDeps(pluginConfig, pluginsDir); err != nil { + return fmt.Errorf("merging dep Maven dependencies: %w", err) + } + maven.DeduplicateAllDeps(pluginConfig.Registry.Maven) + dockerfilePath := filepath.Join(versionDir, "Dockerfile") + dockerfileBytes, err := os.ReadFile(dockerfilePath) + if err != nil { + return err + } + dockerfile := string(dockerfileBytes) + if !strings.Contains(dockerfile, "maven-deps") { + return nil // no maven-deps stage to update + } + pom, err := maven.RenderDockerfilePOM(pluginConfig) + if err != nil { + return fmt.Errorf("rendering POM: %w", err) + } + updated, err := replacePOMInDockerfile(dockerfile, pom) + if err != nil { + return err + } + logger.Info("regenerated maven deps POM", slog.Any("plugin", plugin)) + return os.WriteFile(dockerfilePath, []byte(updated), 0644) +} + +// replacePOMInDockerfile replaces the POM heredoc content between +// "COPY < + temp + temp + 1.0 + + + com.google.protobuf + protobuf-kotlin + 4.33.5 + + + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline + +FROM scratch +COPY --from=build /app . +COPY --from=maven-deps /root/.m2/repository /maven-repository +ENTRYPOINT ["/app"] +` + require.NoError(t, os.WriteFile(filepath.Join(consumerDir, "Dockerfile"), []byte(dockerfile), 0644)) + + // Run regenerateMavenDeps on the consumer plugin + logger := slog.New(slog.NewTextHandler(testWriter{t}, &slog.HandlerOptions{Level: slog.LevelDebug})) + err := regenerateMavenDeps(logger, createdPlugin{ + org: "test", + name: "consumer-plugin", + pluginDir: filepath.Join(tmpDir, "plugins", "test", "consumer-plugin"), + newVersion: "v1.0.0", + }) + require.NoError(t, err) + + // Read the updated Dockerfile and verify it includes deps from base-plugin + updatedBytes, err := os.ReadFile(filepath.Join(consumerDir, "Dockerfile")) + require.NoError(t, err) + updated := string(updatedBytes) + + // Consumer's own deps should be present + assert.Contains(t, updated, "protobuf-kotlin") + assert.Contains(t, updated, "protobuf-kotlin-lite") + + // Base plugin's main deps should be merged in + assert.Contains(t, updated, "protobuf-java") + + // Base plugin's lite runtime deps should be merged into the + // matching lite runtime section + assert.Contains(t, updated, "protobuf-javalite") + assert.Contains(t, updated, "build.buf") +} + +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 TestDeduplicateMavenDeps(t *testing.T) { + t.Parallel() + tests := []struct { + name string + deps []bufremotepluginconfig.MavenDependencyConfig + want []bufremotepluginconfig.MavenDependencyConfig + }{ + { + 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 keeps first", + deps: []bufremotepluginconfig.MavenDependencyConfig{ + {GroupID: "com.example", ArtifactID: "a", Version: "1.0"}, + {GroupID: "com.example", ArtifactID: "a", Version: "2.0"}, + }, + want: []bufremotepluginconfig.MavenDependencyConfig{ + {GroupID: "com.example", ArtifactID: "a", Version: "1.0"}, + }, + }, + { + 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() + got := maven.DeduplicateDeps(tt.deps) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestReplacePOMInDockerfile(t *testing.T) { + t.Parallel() + tests := []struct { + name string + dockerfile string + newPOM string + want string + wantErr string + }{ + { + name: "basic replacement", + dockerfile: `FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps +COPY < + old + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline +`, + newPOM: ` + new + +`, + want: `FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps +COPY < + new + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline +`, + }, + { + name: "new POM without trailing newline", + dockerfile: `FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps +COPY < + old + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline +`, + newPOM: "\n new\n", + want: `FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps +COPY < + new + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline +`, + }, + { + name: "preserves surrounding content", + dockerfile: `# syntax=docker/dockerfile:1.19 +FROM debian:bookworm AS build +RUN echo hello + +FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps +COPY < + old + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline + +FROM scratch +COPY --from=build /app . +COPY --from=maven-deps /root/.m2/repository /maven-repository +ENTRYPOINT ["/app"] +`, + newPOM: "\n replaced\n\n", + want: `# syntax=docker/dockerfile:1.19 +FROM debian:bookworm AS build +RUN echo hello + +FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps +COPY < + replaced + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline + +FROM scratch +COPY --from=build /app . +COPY --from=maven-deps /root/.m2/repository /maven-repository +ENTRYPOINT ["/app"] +`, + }, + { + name: "missing COPY heredoc marker", + dockerfile: "FROM maven:3.9.11 AS maven-deps\nRUN echo hello\n", + newPOM: "", + wantErr: `could not find "COPY < + old + +`, + newPOM: "", + wantErr: "could not find closing EOF for POM heredoc in Dockerfile", + }, + { + name: "nested heredoc before POM EOF", + dockerfile: "FROM maven:3.9.11 AS maven-deps\n" + + "COPY <\n" + + " old\n" + + "\n" + + "EOF\n" + + "RUN cat < /tmp/other.txt\n" + + "some other content\n" + + "EOF\n", + newPOM: "\n new\n\n", + want: "FROM maven:3.9.11 AS maven-deps\n" + + "COPY <\n" + + " new\n" + + "\n" + + "EOF\n" + + "RUN cat < /tmp/other.txt\n" + + "some other content\n" + + "EOF\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := replacePOMInDockerfile(tt.dockerfile, tt.newPOM) + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/cmd/regenerate-maven-poms/main.go b/internal/cmd/regenerate-maven-poms/main.go new file mode 100644 index 000000000..58764a773 --- /dev/null +++ b/internal/cmd/regenerate-maven-poms/main.go @@ -0,0 +1,183 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremotepluginconfig" + "github.com/bufbuild/plugins/internal/maven" +) + +func main() { + if len(os.Args) < 2 { + fmt.Fprintf(os.Stderr, "usage: %s [...]\n", os.Args[0]) + os.Exit(1) + } + + for _, pluginDir := range os.Args[1:] { + if err := regenerateMavenDeps(pluginDir); err != nil { + fmt.Fprintf(os.Stderr, "failed to regenerate %s: %v\n", pluginDir, err) + os.Exit(1) + } + fmt.Printf("regenerated: %s\n", pluginDir) + } +} + +func regenerateMavenDeps(pluginDir string) error { + yamlPath := filepath.Join(pluginDir, "buf.plugin.yaml") + pluginConfig, err := bufremotepluginconfig.ParseConfig(yamlPath) + if err != nil { + return err + } + if pluginConfig.Registry == nil || pluginConfig.Registry.Maven == nil { + return nil // not a Maven plugin + } + + // Merge transitive Maven deps from plugin dependencies and deduplicate. + // 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.MergeTransitiveDeps(pluginConfig, pluginsDir); err != nil { + return fmt.Errorf("merging transitive deps: %w", err) + } + maven.DeduplicateAllDeps(pluginConfig.Registry.Maven) + + pom, err := maven.RenderPOM(pluginConfig) + if err != nil { + return fmt.Errorf("rendering POM: %w", err) + } + + dockerfilePath := filepath.Join(pluginDir, "Dockerfile") + dockerfileBytes, err := os.ReadFile(dockerfilePath) + if err != nil { + return err + } + dockerfile := string(dockerfileBytes) + + var updated string + if strings.Contains(dockerfile, "maven-deps") { + // Update the existing maven-deps stage POM. + updated, err = replacePOM(dockerfile, pom) + if err != nil { + return fmt.Errorf("replacing POM: %w", err) + } + } else { + // Insert a new maven-deps stage. + updated, err = insertMavenDepsStage(dockerfile, pom) + if err != nil { + return fmt.Errorf("inserting maven-deps stage: %w", err) + } + } + + return os.WriteFile(dockerfilePath, []byte(updated), 0644) +} + +// replacePOM replaces the POM XML between COPY < 0 && pomLines[len(pomLines)-1] == "" { + pomLines = pomLines[:len(pomLines)-1] + } + mavenDepsLines := []string{ + "FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps", + "COPY < 0 && strings.TrimSpace(lines[insertAt-1]) == "" { + insertAt-- + } + + // Assemble: [build stage content] + 2 blank lines + maven-deps stage + + // 1 blank line + final stage. + var newLines []string + newLines = append(newLines, lines[:insertAt]...) + newLines = append(newLines, "", "") + newLines = append(newLines, mavenDepsLines...) + newLines = append(newLines, "") + newLines = append(newLines, lines[lastFromIdx:]...) + + // Find the last FROM in the new lines array (the final stage). + finalFromIdx := -1 + for i, line := range newLines { + if isFromLine(line) { + finalFromIdx = i + } + } + + // Insert COPY --from=maven-deps before the first USER/CMD/ENTRYPOINT in + // the final stage. + copyInsertAt := -1 + for i := finalFromIdx + 1; i < len(newLines); i++ { + if isCopyInsertTarget(newLines[i]) { + copyInsertAt = i + break + } + } + + copyLine := "COPY --from=maven-deps /root/.m2/repository /maven-repository" + var finalLines []string + if copyInsertAt < 0 { + finalLines = append(newLines, copyLine) + } else { + finalLines = make([]string, 0, len(newLines)+1) + finalLines = append(finalLines, newLines[:copyInsertAt]...) + finalLines = append(finalLines, copyLine) + finalLines = append(finalLines, newLines[copyInsertAt:]...) + } + + return strings.Join(finalLines, "\n"), nil +} + +func isFromLine(line string) bool { + trimmed := strings.TrimSpace(line) + return strings.HasPrefix(strings.ToUpper(trimmed), "FROM ") +} + +func isCopyInsertTarget(line string) bool { + upper := strings.ToUpper(strings.TrimSpace(line)) + return strings.HasPrefix(upper, "USER ") || + strings.HasPrefix(upper, "CMD ") || + strings.HasPrefix(upper, "CMD[") || + strings.HasPrefix(upper, "ENTRYPOINT ") || + strings.HasPrefix(upper, "ENTRYPOINT[") +} diff --git a/internal/cmd/render-pom/main.go b/internal/cmd/render-pom/main.go new file mode 100644 index 000000000..1f3e188c8 --- /dev/null +++ b/internal/cmd/render-pom/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "context" + "fmt" + "os" + + "buf.build/go/app" + "buf.build/go/app/appcmd" + "github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremotepluginconfig" + "github.com/spf13/pflag" + + "github.com/bufbuild/plugins/internal/maven" +) + +func main() { + appcmd.Main(context.Background(), newCommand("render-pom")) +} + +func newCommand(name string) *appcmd.Command { + cmdFlags := &flags{} + return &appcmd.Command{ + Use: name, + Short: "Renders a pom.xml template to test building Java/Kotlin code using Maven", + BindFlags: cmdFlags.Bind, + Run: func(ctx context.Context, container app.Container) error { + return run(ctx, container, cmdFlags) + }, + } +} + +type flags struct { + pluginPath string +} + +func (f *flags) Bind(flagSet *pflag.FlagSet) { + flagSet.StringVar(&f.pluginPath, "plugin", "", "path to plugin YAML file (passed as context to render template)") + _ = appcmd.MarkFlagRequired(flagSet, "plugin") +} + +func run(_ context.Context, _ app.Container, cmdFlags *flags) error { + pluginConfig, err := bufremotepluginconfig.ParseConfig(cmdFlags.pluginPath) + if err != nil { + return fmt.Errorf("failed to parse config %s: %w", cmdFlags.pluginPath, err) + } + pom, err := maven.RenderPOM(pluginConfig) + if err != nil { + return err + } + _, err = fmt.Fprint(os.Stdout, pom) + return err +} diff --git a/internal/maven/deps.go b/internal/maven/deps.go new file mode 100644 index 000000000..56c969ca1 --- /dev/null +++ b/internal/maven/deps.go @@ -0,0 +1,140 @@ +package maven + +import ( + "fmt" + "log" + "path/filepath" + + "github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremotepluginconfig" +) + +// MergeTransitiveDeps resolves Maven dependencies from the top-level deps +// stanza in the plugin config and merges them into the plugin's Maven +// registry config. Dependencies are resolved transitively so that all +// Maven artifacts needed for offline builds are included in the POM. +func MergeTransitiveDeps( + pluginConfig *bufremotepluginconfig.Config, + pluginsDir string, +) error { + if pluginConfig.Registry == nil || pluginConfig.Registry.Maven == nil { + return nil + } + visited := make(map[string]bool) + return mergeTransitiveDepsRecursive(pluginConfig, pluginsDir, visited) +} + +func mergeTransitiveDepsRecursive( + pluginConfig *bufremotepluginconfig.Config, + pluginsDir string, + visited map[string]bool, +) error { + for _, dep := range pluginConfig.Dependencies { + depKey := dep.IdentityString() + ":" + dep.Version() + if visited[depKey] { + continue + } + visited[depKey] = true + depPath := filepath.Join( + pluginsDir, dep.Owner(), dep.Plugin(), + dep.Version(), "buf.plugin.yaml", + ) + depConfig, err := bufremotepluginconfig.ParseConfig(depPath) + if err != nil { + return fmt.Errorf("loading dep config %s from %s: %w", depKey, depPath, err) + } + // Recursively resolve transitive dependencies first so + // that depConfig.Registry.Maven accumulates the full + // transitive closure before we merge into pluginConfig. + if err := mergeTransitiveDepsRecursive(depConfig, pluginsDir, visited); err != nil { + return err + } + if depConfig.Registry == nil || depConfig.Registry.Maven == nil { + continue + } + depMaven := depConfig.Registry.Maven + pluginConfig.Registry.Maven.Deps = append( + pluginConfig.Registry.Maven.Deps, depMaven.Deps..., + ) + // Merge additional runtimes: for matching runtime names, + // append deps; otherwise add the new runtime entry. + for _, depRuntime := range depMaven.AdditionalRuntimes { + merged := false + for i, runtime := range pluginConfig.Registry.Maven.AdditionalRuntimes { + if runtime.Name == depRuntime.Name { + pluginConfig.Registry.Maven.AdditionalRuntimes[i].Deps = append( + pluginConfig.Registry.Maven.AdditionalRuntimes[i].Deps, + depRuntime.Deps..., + ) + merged = true + break + } + } + if !merged { + pluginConfig.Registry.Maven.AdditionalRuntimes = append( + pluginConfig.Registry.Maven.AdditionalRuntimes, + depRuntime, + ) + } + } + } + return nil +} + +// DeduplicateDeps removes duplicate Maven dependencies by +// groupId:artifactId[:classifier]. The first occurrence wins (parent plugin's +// version takes priority since it was appended first). A warning is logged +// when two entries share the same coordinate but differ in version. +func DeduplicateDeps(deps []bufremotepluginconfig.MavenDependencyConfig) []bufremotepluginconfig.MavenDependencyConfig { + seen := make(map[string]string) // key -> version of first occurrence + var result []bufremotepluginconfig.MavenDependencyConfig + for _, dep := range deps { + key := dep.GroupID + ":" + dep.ArtifactID + if dep.Classifier != "" { + key += ":" + dep.Classifier + } + if existingVersion, ok := seen[key]; ok { + if existingVersion != dep.Version { + log.Printf("WARNING: duplicate Maven dependency %s (keeping %s, discarding %s)", key, existingVersion, dep.Version) + } + continue + } + seen[key] = dep.Version + result = append(result, dep) + } + return result +} + +// DeduplicateAllDeps deduplicates across the main Deps and all +// AdditionalRuntimes Deps using a shared seen set. This ensures the +// flat block in the rendered POM contains no duplicates. +func DeduplicateAllDeps(mavenConfig *bufremotepluginconfig.MavenRegistryConfig) { + if mavenConfig == nil { + return + } + seen := make(map[string]string) + mavenConfig.Deps = deduplicateWithSeen(mavenConfig.Deps, seen) + for i := range mavenConfig.AdditionalRuntimes { + mavenConfig.AdditionalRuntimes[i].Deps = deduplicateWithSeen( + mavenConfig.AdditionalRuntimes[i].Deps, seen, + ) + } +} + +func deduplicateWithSeen(deps []bufremotepluginconfig.MavenDependencyConfig, seen map[string]string) []bufremotepluginconfig.MavenDependencyConfig { + var result []bufremotepluginconfig.MavenDependencyConfig + for _, dep := range deps { + key := dep.GroupID + ":" + dep.ArtifactID + if dep.Classifier != "" { + key += ":" + dep.Classifier + } + if existingVersion, ok := seen[key]; ok { + if existingVersion != dep.Version { + log.Printf("WARNING: duplicate Maven dependency %s (keeping %s, discarding %s)", key, existingVersion, dep.Version) + } + continue + } + seen[key] = dep.Version + result = append(result, dep) + } + return result +} diff --git a/internal/maven/pom.go b/internal/maven/pom.go new file mode 100644 index 000000000..5a70003e9 --- /dev/null +++ b/internal/maven/pom.go @@ -0,0 +1,73 @@ +package maven + +import ( + "bytes" + _ "embed" + "encoding/xml" + "errors" + "fmt" + "io" + "strings" + "text/template" + + "github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremotepluginconfig" +) + +type templateData struct { + *bufremotepluginconfig.Config +} + +var ( + //go:embed pom.xml.gotext + pomTemplateContents string + + pomTemplate = template.Must(template.New("pom.xml").Funcs(template.FuncMap{ + "xml": xmlEscape, + }).Parse(pomTemplateContents)) +) + +// RenderPOM generates a Maven POM XML from a parsed plugin config. +// The POM includes all runtime dependencies, additional runtimes, and +// kotlin-maven-plugin for Kotlin plugins. maven-compiler-plugin and +// maven-source-plugin are bundled in the maven-jdk base image. +func RenderPOM(pluginConfig *bufremotepluginconfig.Config) (string, error) { + if pluginConfig.Registry == nil || pluginConfig.Registry.Maven == nil { + return "", fmt.Errorf("no Maven registry configured for %q", pluginConfig.Name) + } + data := templateData{ + Config: pluginConfig, + } + var buf bytes.Buffer + if err := pomTemplate.Execute(&buf, data); err != nil { + return "", err + } + pom := buf.String() + decoder := xml.NewDecoder(strings.NewReader(pom)) + for { + if _, err := decoder.Token(); err != nil { + if errors.Is(err, io.EOF) { + break + } + return "", fmt.Errorf("generated POM is not well-formed XML: %w", err) + } + } + return pom, nil +} + +// RenderDockerfilePOM generates a simplified POM suitable for inlining in a +// Dockerfile heredoc. It uses temp groupId/artifactId and injects build +// plugins for caching compiler dependencies. +// +// Deprecated: Use RenderPOM instead. This function is kept for compatibility +// but now returns the same output as RenderPOM. +func RenderDockerfilePOM(pluginConfig *bufremotepluginconfig.Config) (string, error) { + return RenderPOM(pluginConfig) +} + +func xmlEscape(raw string) (string, error) { + var b strings.Builder + if err := xml.EscapeText(&b, []byte(raw)); err != nil { + return "", err + } + return b.String(), nil +} diff --git a/internal/maven/pom.xml.gotext b/internal/maven/pom.xml.gotext new file mode 100644 index 000000000..92702a3b8 --- /dev/null +++ b/internal/maven/pom.xml.gotext @@ -0,0 +1,75 @@ + + 4.0.0 + temp + temp + 1.0 + {{- $kotlinCompiler := .Registry.Maven.Compiler.Kotlin }} + + {{- range $dep := .Registry.Maven.Deps }} + + {{ $dep.GroupID | xml }} + {{ $dep.ArtifactID | xml }} + {{ $dep.Version | xml }} + {{- with $dep.Classifier }} + {{ . | xml }} + {{- end }} + {{- with $dep.Extension }} + {{ . | xml }} + {{- end }} + + {{- end }} + {{- range $runtime := .Registry.Maven.AdditionalRuntimes }} + + {{- range $dep := $runtime.Deps }} + + {{ $dep.GroupID | xml }} + {{ $dep.ArtifactID | xml }} + {{ $dep.Version | xml }} + {{- with $dep.Classifier }} + {{ . | xml }} + {{- end }} + {{- with $dep.Extension }} + {{ . | xml }} + {{- end }} + + {{- end }} + {{- end }} + {{- if $kotlinCompiler.Version }} + + + org.jetbrains.kotlin + kotlin-compiler-embeddable + {{ $kotlinCompiler.Version | xml }} + + + org.jetbrains.kotlin + kotlin-scripting-compiler + {{ $kotlinCompiler.Version | xml }} + + {{- end }} + + {{- if $kotlinCompiler.Version }} + + + + org.jetbrains.kotlin + kotlin-maven-plugin + {{- with $kotlinCompiler.Version }} + {{ . | xml }} + {{- end }} + + {{- with $kotlinCompiler.APIVersion }} + {{ . | xml }} + {{- end }} + {{- with $kotlinCompiler.JVMTarget }} + {{ . | xml }} + {{- end }} + {{- with $kotlinCompiler.LanguageVersion }} + {{ . | xml }} + {{- end }} + + + + + {{- end }} + diff --git a/internal/maven/pom_test.go b/internal/maven/pom_test.go new file mode 100644 index 000000000..1928b9bbc --- /dev/null +++ b/internal/maven/pom_test.go @@ -0,0 +1,305 @@ +package maven + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremotepluginconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRenderPOM_BasicJavaPlugin(t *testing.T) { + t.Parallel() + + // Create temporary config file + tmpDir := t.TempDir() + yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") + yamlContent := `version: v1 +name: buf.build/test/plugin +plugin_version: v1.0.0 +output_languages: + - java +registry: + maven: + deps: + - com.google.protobuf:protobuf-java:4.33.5 +` + require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) + + config, err := bufremotepluginconfig.ParseConfig(yamlPath) + require.NoError(t, err) + + pom, err := RenderPOM(config) + require.NoError(t, err) + + assert.Contains(t, pom, "com.google.protobuf") + assert.Contains(t, pom, "protobuf-java") + assert.Contains(t, pom, "4.33.5") +} + +func TestRenderPOM_XMLEscaping(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") + yamlContent := `version: v1 +name: buf.build/test/plugin +plugin_version: v1.0.0 +output_languages: + - java +registry: + maven: + deps: + - com.test:artifact<>&:1.0.0 +` + require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) + + config, err := bufremotepluginconfig.ParseConfig(yamlPath) + require.NoError(t, err) + + pom, err := RenderPOM(config) + require.NoError(t, err) + + assert.Contains(t, pom, "artifact<>&") + assert.NotContains(t, pom, "artifact<>&") +} + +func TestRenderPOM_KotlinCompiler(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") + yamlContent := `version: v1 +name: buf.build/test/kotlin-plugin +plugin_version: v1.0.0 +output_languages: + - kotlin +registry: + maven: + compiler: + kotlin: + version: 1.8.22 + jvm_target: "1.8" + language_version: "1.8" + api_version: "1.8" + deps: [] +` + require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) + + config, err := bufremotepluginconfig.ParseConfig(yamlPath) + require.NoError(t, err) + + pom, err := RenderPOM(config) + require.NoError(t, err) + + assert.Contains(t, pom, "kotlin-maven-plugin") + assert.Contains(t, pom, "1.8.22") + assert.Contains(t, pom, "1.8") + assert.Contains(t, pom, "1.8") + assert.Contains(t, pom, "1.8") +} + +func TestRenderPOM_AdditionalRuntimes(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") + yamlContent := `version: v1 +name: buf.build/test/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 + opts: [lite] +` + require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) + + config, err := bufremotepluginconfig.ParseConfig(yamlPath) + require.NoError(t, err) + + pom, err := RenderPOM(config) + require.NoError(t, err) + + assert.Contains(t, pom, "") + assert.Contains(t, pom, "protobuf-javalite") +} + +func TestRenderPOM_ClassifierAndExtension(t *testing.T) { + t.Parallel() + t.Skip("Classifier/extension format in YAML unknown - no real-world examples in codebase") +} + +func TestRenderPOM_NoMavenConfig(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") + yamlContent := `version: v1 +name: buf.build/test/plugin +plugin_version: v1.0.0 +output_languages: + - go +` + require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) + + config, err := bufremotepluginconfig.ParseConfig(yamlPath) + require.NoError(t, err) + + _, err = RenderPOM(config) + require.Error(t, err) + assert.Contains(t, err.Error(), "no Maven registry configured") +} + +func TestRenderPOM_EmptyDeps(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") + yamlContent := `version: v1 +name: buf.build/test/plugin +plugin_version: v1.0.0 +output_languages: + - java +registry: + maven: + deps: [] +` + require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) + + config, err := bufremotepluginconfig.ParseConfig(yamlPath) + require.NoError(t, err) + + pom, err := RenderPOM(config) + require.NoError(t, err) + + // Should still generate valid POM structure + assert.Contains(t, pom, "4.0.0") + assert.Contains(t, pom, "temp") + assert.Contains(t, pom, "temp") +} + +func TestRenderPOM_MalformedXMLDetected(t *testing.T) { + t.Parallel() + + // A runtime name containing "--" produces an invalid XML comment + // ( is not well-formed XML). + tmpDir := t.TempDir() + yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") + yamlContent := `version: v1 +name: buf.build/test/plugin +plugin_version: v1.0.0 +output_languages: + - java +registry: + maven: + deps: + - com.google.protobuf:protobuf-java:4.33.5 + additional_runtimes: + - name: "bad--name" + deps: + - com.google.protobuf:protobuf-javalite:4.33.5 + opts: [lite] +` + require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) + + config, err := bufremotepluginconfig.ParseConfig(yamlPath) + require.NoError(t, err) + + _, err = RenderPOM(config) + require.Error(t, err) + assert.Contains(t, err.Error(), "generated POM is not well-formed XML") +} + +func TestXMLEscape_SpecialCharacters(t *testing.T) { + t.Parallel() + tests := []struct { + input string + expected string + }{ + {"normal-text", "normal-text"}, + {"", "<tag>"}, + {"a&b", "a&b"}, + {`"quoted"`, ""quoted""}, + {"'single'", "'single'"}, + {"<>&\"'", "<>&"'"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result, err := xmlEscape(tt.input) + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestRenderPOM_KotlinDynamicDependencies(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") + yamlContent := `version: v1 +name: buf.build/test/kotlin-plugin +plugin_version: v1.0.0 +output_languages: + - kotlin +registry: + maven: + compiler: + kotlin: + version: 1.9.0 + deps: [] +` + require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) + + config, err := bufremotepluginconfig.ParseConfig(yamlPath) + require.NoError(t, err) + + pom, err := RenderPOM(config) + require.NoError(t, err) + + // Verify dynamic Kotlin dependencies are included + assert.Contains(t, pom, " + + build.buf + connect-kotlin-google-javalite-ext + 0.1.10 + + + com.google.protobuf + protobuf-kotlin-lite + 3.24.3 + + + com.google.protobuf + protobuf-javalite + 3.24.3 + + + build.buf + protobuf-javalite + 3.24.3 + + + + org.jetbrains.kotlin + kotlin-compiler-embeddable + 1.8.22 + + + org.jetbrains.kotlin + kotlin-scripting-compiler + 1.8.22 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + 1.8.22 + + + + + + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline + FROM gcr.io/distroless/java17-debian11 WORKDIR /app COPY --from=build /app/protoc-gen-connect-kotlin.jar /app +COPY --from=maven-deps /root/.m2/repository /maven-repository CMD ["/app/protoc-gen-connect-kotlin.jar"] diff --git a/plugins/bufbuild/validate-java/v1.3.3/Dockerfile b/plugins/bufbuild/validate-java/v1.3.3/Dockerfile index 3b6ae2357..20b60d9dc 100644 --- a/plugins/bufbuild/validate-java/v1.3.3/Dockerfile +++ b/plugins/bufbuild/validate-java/v1.3.3/Dockerfile @@ -2,8 +2,44 @@ FROM golang:1.26.0-bookworm AS build RUN CGO_ENABLED=0 go install -ldflags "-s -w" -trimpath github.com/envoyproxy/protoc-gen-validate/cmd/protoc-gen-validate-java@v1.3.3 + +FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps +COPY < + 4.0.0 + temp + temp + 1.0 + + + build.buf.protoc-gen-validate + pgv-java-stub + 1.3.3 + + + com.google.protobuf + protobuf-java + 4.33.5 + + + + com.google.protobuf + protobuf-javalite + 4.33.5 + + + build.buf + protobuf-javalite + 4.33.5 + + + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline + FROM scratch COPY --from=build --link /etc/passwd /etc/passwd COPY --from=build --link --chown=root:root /go/bin/protoc-gen-validate-java . +COPY --from=maven-deps /root/.m2/repository /maven-repository USER nobody ENTRYPOINT [ "/protoc-gen-validate-java" ] diff --git a/plugins/community/salesforce-reactive-grpc/v1.2.4/Dockerfile b/plugins/community/salesforce-reactive-grpc/v1.2.4/Dockerfile index e02656e52..e38f2ea57 100644 --- a/plugins/community/salesforce-reactive-grpc/v1.2.4/Dockerfile +++ b/plugins/community/salesforce-reactive-grpc/v1.2.4/Dockerfile @@ -7,8 +7,69 @@ RUN curl -fsSL -o reactor-grpc-protoc.jar https://repo1.maven.org/maven2/com/sal FROM gcr.io/distroless/java21-debian12:latest@sha256:7c9a9a362eadadb308d29b9c7fec2b39e5d5aa21d58837176a2cca50bdd06609 AS base +FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps +COPY < + 4.0.0 + temp + temp + 1.0 + + + com.salesforce.servicelibs + reactor-grpc-stub + 1.2.4 + + + io.projectreactor + reactor-core + 3.5.4 + + + com.google.protobuf + protobuf-java + 4.31.0 + + + io.grpc + grpc-core + 1.73.0 + + + io.grpc + grpc-protobuf + 1.73.0 + + + io.grpc + grpc-stub + 1.73.0 + + + + io.grpc + grpc-protobuf-lite + 1.73.0 + + + com.google.protobuf + protobuf-javalite + 4.31.0 + + + build.buf + protobuf-javalite + 4.31.0 + + + + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline + FROM scratch COPY --from=base --link / / COPY --from=build --link --chmod=0755 --chown=root:root /app/reactor-grpc-protoc.jar . +COPY --from=maven-deps /root/.m2/repository /maven-repository USER nobody ENTRYPOINT [ "/usr/bin/java", "-jar", "/reactor-grpc-protoc.jar"] diff --git a/plugins/connectrpc/kotlin/v0.7.4/Dockerfile b/plugins/connectrpc/kotlin/v0.7.4/Dockerfile index ef473a1e4..82f08b24c 100644 --- a/plugins/connectrpc/kotlin/v0.7.4/Dockerfile +++ b/plugins/connectrpc/kotlin/v0.7.4/Dockerfile @@ -8,8 +8,102 @@ RUN curl -fsSL -o /app/protoc-gen-connect-kotlin.jar https://repo1.maven.org/mav FROM gcr.io/distroless/java21-debian12:latest@sha256:7c05bf8a64ff1a70a16083e9bdd35b463aa0d014c2fc782d31d13ea7a61de633 as base +FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps +COPY < + 4.0.0 + temp + temp + 1.0 + + + com.connectrpc + connect-kotlin + 0.7.4 + + + com.connectrpc + connect-kotlin-google-java-ext + 0.7.4 + + + com.connectrpc + connect-kotlin-okhttp + 0.7.4 + + + com.google.protobuf + protobuf-kotlin + 4.31.1 + + + org.jetbrains.kotlin + kotlin-stdlib + 1.8.22 + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 1.8.22 + + + com.google.protobuf + protobuf-java + 4.31.1 + + + + com.connectrpc + connect-kotlin-google-javalite-ext + 0.7.4 + + + com.google.protobuf + protobuf-kotlin-lite + 4.31.1 + + + com.google.protobuf + protobuf-javalite + 4.31.1 + + + build.buf + protobuf-javalite + 4.31.1 + + + + org.jetbrains.kotlin + kotlin-compiler-embeddable + 2.1.0 + + + org.jetbrains.kotlin + kotlin-scripting-compiler + 2.1.0 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + 2.1.0 + + 1.8 + + + + + + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline + FROM scratch COPY --from=base --link / / COPY --from=build --link --chmod=0755 --chown=root:root /app/protoc-gen-connect-kotlin.jar . +COPY --from=maven-deps /root/.m2/repository /maven-repository USER nobody ENTRYPOINT [ "/usr/bin/java", "-jar", "/protoc-gen-connect-kotlin.jar"] diff --git a/plugins/grpc/java/v1.79.0/Dockerfile b/plugins/grpc/java/v1.79.0/Dockerfile index 5e314866a..4c98dd84b 100644 --- a/plugins/grpc/java/v1.79.0/Dockerfile +++ b/plugins/grpc/java/v1.79.0/Dockerfile @@ -17,8 +17,59 @@ RUN arch=${TARGETARCH}; \ FROM gcr.io/distroless/cc-debian12:latest@sha256:72344f7f909a8bf003c67f55687e6d51a441b49661af8f660aa7b285f00e57df AS base +FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps +COPY < + 4.0.0 + temp + temp + 1.0 + + + io.grpc + grpc-core + 1.79.0 + + + io.grpc + grpc-protobuf + 1.79.0 + + + io.grpc + grpc-stub + 1.79.0 + + + com.google.protobuf + protobuf-java + 4.33.5 + + + + io.grpc + grpc-protobuf-lite + 1.79.0 + + + com.google.protobuf + protobuf-javalite + 4.33.5 + + + build.buf + protobuf-javalite + 4.33.5 + + + + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline + FROM scratch COPY --link --from=base / / COPY --link --from=build --chmod=0755 --chown=root:root /build/protoc-gen-grpc-java . +COPY --from=maven-deps /root/.m2/repository /maven-repository USER nobody ENTRYPOINT [ "/protoc-gen-grpc-java" ] diff --git a/plugins/grpc/kotlin/v1.5.0/Dockerfile b/plugins/grpc/kotlin/v1.5.0/Dockerfile index eed8a60dc..acd946097 100644 --- a/plugins/grpc/kotlin/v1.5.0/Dockerfile +++ b/plugins/grpc/kotlin/v1.5.0/Dockerfile @@ -10,8 +10,111 @@ RUN curl -fsSL -o protoc-gen-grpc-kotlin.jar https://repo1.maven.org/maven2/io/g FROM gcr.io/distroless/java21-debian12:latest@sha256:418b2e2a9e452aa9299511427f2ae404dfc910ecfa78feb53b1c60c22c3b640c AS base +FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps +COPY < + 4.0.0 + temp + temp + 1.0 + + + io.grpc + grpc-kotlin-stub + 1.5.0 + + + org.jetbrains.kotlinx + kotlinx-coroutines-core-jvm + 1.10.2 + + + io.grpc + grpc-core + 1.75.0 + + + io.grpc + grpc-protobuf + 1.75.0 + + + io.grpc + grpc-stub + 1.75.0 + + + com.google.protobuf + protobuf-java + 4.32.0 + + + com.google.protobuf + protobuf-kotlin + 4.32.0 + + + org.jetbrains.kotlin + kotlin-stdlib + 1.8.22 + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 1.8.22 + + + + io.grpc + grpc-protobuf-lite + 1.75.0 + + + com.google.protobuf + protobuf-javalite + 4.32.0 + + + build.buf + protobuf-javalite + 4.32.0 + + + com.google.protobuf + protobuf-kotlin-lite + 4.32.0 + + + + org.jetbrains.kotlin + kotlin-compiler-embeddable + 2.2.20 + + + org.jetbrains.kotlin + kotlin-scripting-compiler + 2.2.20 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + 2.2.20 + + + + + + + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline + FROM scratch COPY --link --from=base / / COPY --link --from=build --chmod=0644 --chown=root:root /build/protoc-gen-grpc-kotlin.jar . +COPY --from=maven-deps /root/.m2/repository /maven-repository USER nobody ENTRYPOINT [ "/usr/bin/java", "-jar", "/protoc-gen-grpc-kotlin.jar" ] diff --git a/plugins/protocolbuffers/java/v33.5/Dockerfile b/plugins/protocolbuffers/java/v33.5/Dockerfile index b857d8dc3..b26c570ee 100644 --- a/plugins/protocolbuffers/java/v33.5/Dockerfile +++ b/plugins/protocolbuffers/java/v33.5/Dockerfile @@ -23,8 +23,39 @@ RUN bazelisk ${BAZEL_OPTS} build '//plugins:protoc-gen-java.stripped' FROM gcr.io/distroless/cc-debian12:latest@sha256:72344f7f909a8bf003c67f55687e6d51a441b49661af8f660aa7b285f00e57df AS base +FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps +COPY < + 4.0.0 + temp + temp + 1.0 + + + com.google.protobuf + protobuf-java + 4.33.5 + + + + com.google.protobuf + protobuf-javalite + 4.33.5 + + + build.buf + protobuf-javalite + 4.33.5 + + + + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline + FROM scratch COPY --from=base --link / / COPY --from=build --link --chmod=0755 /build/bazel-bin/plugins/protoc-gen-java . +COPY --from=maven-deps /root/.m2/repository /maven-repository USER nobody ENTRYPOINT ["/protoc-gen-java"] diff --git a/plugins/protocolbuffers/kotlin/v33.5/Dockerfile b/plugins/protocolbuffers/kotlin/v33.5/Dockerfile index f1c72e8fd..96cfedfbf 100644 --- a/plugins/protocolbuffers/kotlin/v33.5/Dockerfile +++ b/plugins/protocolbuffers/kotlin/v33.5/Dockerfile @@ -23,8 +23,81 @@ RUN bazelisk ${BAZEL_OPTS} build '//plugins:protoc-gen-kotlin.stripped' FROM gcr.io/distroless/cc-debian12:latest@sha256:72344f7f909a8bf003c67f55687e6d51a441b49661af8f660aa7b285f00e57df AS base +FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps +COPY < + 4.0.0 + temp + temp + 1.0 + + + com.google.protobuf + protobuf-kotlin + 4.33.5 + + + org.jetbrains.kotlin + kotlin-stdlib + 1.8.22 + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 1.8.22 + + + com.google.protobuf + protobuf-java + 4.33.5 + + + + com.google.protobuf + protobuf-kotlin-lite + 4.33.5 + + + com.google.protobuf + protobuf-javalite + 4.33.5 + + + build.buf + protobuf-javalite + 4.33.5 + + + + org.jetbrains.kotlin + kotlin-compiler-embeddable + 1.8.22 + + + org.jetbrains.kotlin + kotlin-scripting-compiler + 1.8.22 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + 1.8.22 + + + + + + + +EOF +RUN cd /tmp && mvn -f pom.xml dependency:go-offline + FROM scratch COPY --from=base --link / / COPY --from=build --link --chmod=0755 /build/bazel-bin/plugins/protoc-gen-kotlin . +COPY --from=maven-deps /root/.m2/repository /maven-repository USER nobody ENTRYPOINT ["/protoc-gen-kotlin"] From f7a02aeae640473e894dacc93ce70f90a08815c6 Mon Sep 17 00:00:00 2001 From: David Marby Date: Tue, 24 Feb 2026 17:03:35 +0100 Subject: [PATCH 02/12] Swap regenerate-maven-poms to app cmd --- internal/cmd/regenerate-maven-poms/main.go | 33 +++++++++++++------ .../connect-kotlin/v0.1.10/Dockerfile | 1 + 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/internal/cmd/regenerate-maven-poms/main.go b/internal/cmd/regenerate-maven-poms/main.go index 58764a773..eaa25c030 100644 --- a/internal/cmd/regenerate-maven-poms/main.go +++ b/internal/cmd/regenerate-maven-poms/main.go @@ -1,33 +1,46 @@ package main import ( + "context" "fmt" "os" "path/filepath" "regexp" "strings" + "buf.build/go/app" + "buf.build/go/app/appcmd" "github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremotepluginconfig" "github.com/bufbuild/plugins/internal/maven" ) func main() { - if len(os.Args) < 2 { - fmt.Fprintf(os.Stderr, "usage: %s [...]\n", os.Args[0]) - os.Exit(1) - } + appcmd.Main(context.Background(), newCommand("regenerate-maven-poms")) +} - for _, pluginDir := range os.Args[1:] { - if err := regenerateMavenDeps(pluginDir); err != nil { - fmt.Fprintf(os.Stderr, "failed to regenerate %s: %v\n", pluginDir, err) - os.Exit(1) - } - fmt.Printf("regenerated: %s\n", pluginDir) +func newCommand(name string) *appcmd.Command { + return &appcmd.Command{ + Use: name + " [...]", + 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) + if err := regenerateMavenDeps(pluginDir); err != nil { + return fmt.Errorf("failed to regenerate %s: %w", pluginDir, err) + } + fmt.Fprintf(container.Stdout(), "regenerated: %s\n", pluginDir) + } + return nil + }, } } func regenerateMavenDeps(pluginDir string) error { yamlPath := filepath.Join(pluginDir, "buf.plugin.yaml") + if _, err := os.Stat(yamlPath); err != nil { + return nil // no buf.plugin.yaml, skip + } pluginConfig, err := bufremotepluginconfig.ParseConfig(yamlPath) if err != nil { return err diff --git a/plugins/bufbuild/connect-kotlin/v0.1.10/Dockerfile b/plugins/bufbuild/connect-kotlin/v0.1.10/Dockerfile index 8762ca83a..a0ff3a4d6 100644 --- a/plugins/bufbuild/connect-kotlin/v0.1.10/Dockerfile +++ b/plugins/bufbuild/connect-kotlin/v0.1.10/Dockerfile @@ -94,6 +94,7 @@ COPY < + EOF RUN cd /tmp && mvn -f pom.xml dependency:go-offline From 2f11fe8868394fa7196eb85b1ef86234f303194a Mon Sep 17 00:00:00 2001 From: David Marby Date: Tue, 24 Feb 2026 17:10:33 +0100 Subject: [PATCH 03/12] Re-generate plugins --- plugins/apple/servicetalk/v0.42.63/Dockerfile | 11 +++++++++++ plugins/bufbuild/validate-java/v1.3.3/Dockerfile | 1 + 2 files changed, 12 insertions(+) diff --git a/plugins/apple/servicetalk/v0.42.63/Dockerfile b/plugins/apple/servicetalk/v0.42.63/Dockerfile index 274e1f13e..c626b61ea 100644 --- a/plugins/apple/servicetalk/v0.42.63/Dockerfile +++ b/plugins/apple/servicetalk/v0.42.63/Dockerfile @@ -36,6 +36,17 @@ COPY <protobuf-java 4.33.5 + + + com.google.protobuf + protobuf-javalite + 4.33.5 + + + build.buf + protobuf-javalite + 4.33.5 + diff --git a/plugins/bufbuild/validate-java/v1.3.3/Dockerfile b/plugins/bufbuild/validate-java/v1.3.3/Dockerfile index 20b60d9dc..4792f6eb0 100644 --- a/plugins/bufbuild/validate-java/v1.3.3/Dockerfile +++ b/plugins/bufbuild/validate-java/v1.3.3/Dockerfile @@ -34,6 +34,7 @@ COPY < + EOF RUN cd /tmp && mvn -f pom.xml dependency:go-offline From aee3b1e3ec6f454efe6986900543017ca5a9df75 Mon Sep 17 00:00:00 2001 From: David Marby Date: Tue, 24 Feb 2026 19:41:12 +0100 Subject: [PATCH 04/12] Fix lint failures --- internal/cmd/fetcher/main.go | 55 ++-------------- internal/cmd/fetcher/main_test.go | 19 +++--- internal/cmd/regenerate-maven-poms/main.go | 65 +++++++++---------- internal/cmd/render-pom/main.go | 52 --------------- internal/maven/deps.go | 48 +++++--------- internal/maven/dockerfile.go | 51 +++++++++++++++ internal/maven/pom.go | 10 --- internal/maven/pom_test.go | 1 + plugins/apple/servicetalk/v0.42.63/Dockerfile | 1 - .../connect-kotlin/v0.1.10/Dockerfile | 1 - .../bufbuild/validate-java/v1.3.3/Dockerfile | 1 - .../v1.2.4/Dockerfile | 1 - plugins/connectrpc/kotlin/v0.7.4/Dockerfile | 1 - plugins/grpc/java/v1.79.0/Dockerfile | 1 - plugins/grpc/kotlin/v1.5.0/Dockerfile | 1 - plugins/protocolbuffers/java/v33.5/Dockerfile | 1 - .../protocolbuffers/kotlin/v33.5/Dockerfile | 1 - 17 files changed, 117 insertions(+), 193 deletions(-) delete mode 100644 internal/cmd/render-pom/main.go create mode 100644 internal/maven/dockerfile.go diff --git a/internal/cmd/fetcher/main.go b/internal/cmd/fetcher/main.go index 90140111c..3599f2333 100644 --- a/internal/cmd/fetcher/main.go +++ b/internal/cmd/fetcher/main.go @@ -132,7 +132,7 @@ func postProcessCreatedPlugins(ctx context.Context, logger *slog.Logger, plugins } for _, plugin := range plugins { newPluginRef := plugin.String() - if err := regenerateMavenDeps(logger, plugin); err != nil { + if err := regenerateMavenDeps(ctx, logger, plugin); err != nil { return fmt.Errorf("failed to regenerate maven deps for %s: %w", newPluginRef, err) } if err := runGoModTidy(ctx, logger, plugin); err != nil { @@ -281,7 +281,7 @@ func recreateSwiftPackageResolved(ctx context.Context, logger *slog.Logger, plug // from the plugin's buf.plugin.yaml. This ensures the POM always reflects the // actual Maven dependencies declared in the config, rather than relying on // version string replacement which can miss transitive dependency updates. -func regenerateMavenDeps(logger *slog.Logger, plugin createdPlugin) error { +func regenerateMavenDeps(ctx context.Context, logger *slog.Logger, plugin createdPlugin) error { versionDir := filepath.Join(plugin.pluginDir, plugin.newVersion) yamlPath := filepath.Join(versionDir, "buf.plugin.yaml") pluginConfig, err := bufremotepluginconfig.ParseConfig(yamlPath) @@ -300,7 +300,7 @@ func regenerateMavenDeps(logger *slog.Logger, plugin createdPlugin) error { if err := maven.MergeTransitiveDeps(pluginConfig, pluginsDir); err != nil { return fmt.Errorf("merging dep Maven dependencies: %w", err) } - maven.DeduplicateAllDeps(pluginConfig.Registry.Maven) + maven.DeduplicateAllDeps(ctx, pluginConfig.Registry.Maven) dockerfilePath := filepath.Join(versionDir, "Dockerfile") dockerfileBytes, err := os.ReadFile(dockerfilePath) if err != nil { @@ -310,57 +310,16 @@ func regenerateMavenDeps(logger *slog.Logger, plugin createdPlugin) error { if !strings.Contains(dockerfile, "maven-deps") { return nil // no maven-deps stage to update } - pom, err := maven.RenderDockerfilePOM(pluginConfig) + pom, err := maven.RenderPOM(pluginConfig) if err != nil { return fmt.Errorf("rendering POM: %w", err) } - updated, err := replacePOMInDockerfile(dockerfile, pom) + updated, err := maven.ReplacePOMInDockerfile(dockerfile, pom) if err != nil { return err } - logger.Info("regenerated maven deps POM", slog.Any("plugin", plugin)) - return os.WriteFile(dockerfilePath, []byte(updated), 0644) -} - -// replacePOMInDockerfile replaces the POM heredoc content between -// "COPY < [...]", Short: "Regenerates maven-deps POM and Dockerfile stage for Java/Kotlin plugins", Args: appcmd.MinimumNArgs(1), - Run: func(_ context.Context, container app.Container) error { + Run: func(ctx context.Context, container app.Container) error { for i := range container.NumArgs() { pluginDir := container.Arg(i) - if err := regenerateMavenDeps(pluginDir); err != nil { + if err := regenerateMavenDeps(ctx, pluginDir); err != nil { return fmt.Errorf("failed to regenerate %s: %w", pluginDir, err) } fmt.Fprintf(container.Stdout(), "regenerated: %s\n", pluginDir) @@ -36,9 +38,9 @@ func newCommand(name string) *appcmd.Command { } } -func regenerateMavenDeps(pluginDir string) error { +func regenerateMavenDeps(ctx context.Context, pluginDir string) error { yamlPath := filepath.Join(pluginDir, "buf.plugin.yaml") - if _, err := os.Stat(yamlPath); err != nil { + if !fileExists(yamlPath) { return nil // no buf.plugin.yaml, skip } pluginConfig, err := bufremotepluginconfig.ParseConfig(yamlPath) @@ -56,7 +58,7 @@ func regenerateMavenDeps(pluginDir string) error { if err := maven.MergeTransitiveDeps(pluginConfig, pluginsDir); err != nil { return fmt.Errorf("merging transitive deps: %w", err) } - maven.DeduplicateAllDeps(pluginConfig.Registry.Maven) + maven.DeduplicateAllDeps(ctx, pluginConfig.Registry.Maven) pom, err := maven.RenderPOM(pluginConfig) if err != nil { @@ -72,8 +74,7 @@ func regenerateMavenDeps(pluginDir string) error { var updated string if strings.Contains(dockerfile, "maven-deps") { - // Update the existing maven-deps stage POM. - updated, err = replacePOM(dockerfile, pom) + updated, err = maven.ReplacePOMInDockerfile(dockerfile, pom) if err != nil { return fmt.Errorf("replacing POM: %w", err) } @@ -85,17 +86,7 @@ func regenerateMavenDeps(pluginDir string) error { } } - return os.WriteFile(dockerfilePath, []byte(updated), 0644) -} - -// replacePOM replaces the POM XML between COPY < 0 && pomLines[len(pomLines)-1] == "" { pomLines = pomLines[:len(pomLines)-1] } - mavenDepsLines := []string{ - "FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps", - "COPY < version of first occurrence - var result []bufremotepluginconfig.MavenDependencyConfig - for _, dep := range deps { - key := dep.GroupID + ":" + dep.ArtifactID - if dep.Classifier != "" { - key += ":" + dep.Classifier - } - if existingVersion, ok := seen[key]; ok { - if existingVersion != dep.Version { - log.Printf("WARNING: duplicate Maven dependency %s (keeping %s, discarding %s)", key, existingVersion, dep.Version) - } - continue - } - seen[key] = dep.Version - result = append(result, dep) - } - return result -} - // DeduplicateAllDeps deduplicates across the main Deps and all // AdditionalRuntimes Deps using a shared seen set. This ensures the // flat block in the rendered POM contains no duplicates. -func DeduplicateAllDeps(mavenConfig *bufremotepluginconfig.MavenRegistryConfig) { +func DeduplicateAllDeps( + ctx context.Context, + mavenConfig *bufremotepluginconfig.MavenRegistryConfig, +) { if mavenConfig == nil { return } seen := make(map[string]string) - mavenConfig.Deps = deduplicateWithSeen(mavenConfig.Deps, seen) + mavenConfig.Deps = deduplicateWithSeen(ctx, mavenConfig.Deps, seen) for i := range mavenConfig.AdditionalRuntimes { mavenConfig.AdditionalRuntimes[i].Deps = deduplicateWithSeen( - mavenConfig.AdditionalRuntimes[i].Deps, seen, + ctx, mavenConfig.AdditionalRuntimes[i].Deps, seen, ) } } -func deduplicateWithSeen(deps []bufremotepluginconfig.MavenDependencyConfig, seen map[string]string) []bufremotepluginconfig.MavenDependencyConfig { +func deduplicateWithSeen( + ctx context.Context, + deps []bufremotepluginconfig.MavenDependencyConfig, + seen map[string]string, +) []bufremotepluginconfig.MavenDependencyConfig { var result []bufremotepluginconfig.MavenDependencyConfig for _, dep := range deps { key := dep.GroupID + ":" + dep.ArtifactID @@ -129,7 +113,11 @@ func deduplicateWithSeen(deps []bufremotepluginconfig.MavenDependencyConfig, see } if existingVersion, ok := seen[key]; ok { if existingVersion != dep.Version { - log.Printf("WARNING: duplicate Maven dependency %s (keeping %s, discarding %s)", key, existingVersion, dep.Version) + slog.WarnContext(ctx, "duplicate Maven dependency", + slog.String("key", key), + slog.String("keeping", existingVersion), + slog.String("discarding", dep.Version), + ) } continue } diff --git a/internal/maven/dockerfile.go b/internal/maven/dockerfile.go new file mode 100644 index 000000000..5c60a89f4 --- /dev/null +++ b/internal/maven/dockerfile.go @@ -0,0 +1,51 @@ +package maven + +import ( + "errors" + "fmt" + "strings" +) + +// ReplacePOMInDockerfile replaces the POM heredoc content between +// "COPY < - EOF RUN cd /tmp && mvn -f pom.xml dependency:go-offline diff --git a/plugins/bufbuild/connect-kotlin/v0.1.10/Dockerfile b/plugins/bufbuild/connect-kotlin/v0.1.10/Dockerfile index a0ff3a4d6..8762ca83a 100644 --- a/plugins/bufbuild/connect-kotlin/v0.1.10/Dockerfile +++ b/plugins/bufbuild/connect-kotlin/v0.1.10/Dockerfile @@ -94,7 +94,6 @@ COPY < - EOF RUN cd /tmp && mvn -f pom.xml dependency:go-offline diff --git a/plugins/bufbuild/validate-java/v1.3.3/Dockerfile b/plugins/bufbuild/validate-java/v1.3.3/Dockerfile index 4792f6eb0..20b60d9dc 100644 --- a/plugins/bufbuild/validate-java/v1.3.3/Dockerfile +++ b/plugins/bufbuild/validate-java/v1.3.3/Dockerfile @@ -34,7 +34,6 @@ COPY < - EOF RUN cd /tmp && mvn -f pom.xml dependency:go-offline diff --git a/plugins/community/salesforce-reactive-grpc/v1.2.4/Dockerfile b/plugins/community/salesforce-reactive-grpc/v1.2.4/Dockerfile index e38f2ea57..cd7c8ea4e 100644 --- a/plugins/community/salesforce-reactive-grpc/v1.2.4/Dockerfile +++ b/plugins/community/salesforce-reactive-grpc/v1.2.4/Dockerfile @@ -63,7 +63,6 @@ COPY < - EOF RUN cd /tmp && mvn -f pom.xml dependency:go-offline diff --git a/plugins/connectrpc/kotlin/v0.7.4/Dockerfile b/plugins/connectrpc/kotlin/v0.7.4/Dockerfile index 82f08b24c..68fec8f30 100644 --- a/plugins/connectrpc/kotlin/v0.7.4/Dockerfile +++ b/plugins/connectrpc/kotlin/v0.7.4/Dockerfile @@ -97,7 +97,6 @@ COPY < - EOF RUN cd /tmp && mvn -f pom.xml dependency:go-offline diff --git a/plugins/grpc/java/v1.79.0/Dockerfile b/plugins/grpc/java/v1.79.0/Dockerfile index 4c98dd84b..968653e85 100644 --- a/plugins/grpc/java/v1.79.0/Dockerfile +++ b/plugins/grpc/java/v1.79.0/Dockerfile @@ -63,7 +63,6 @@ COPY < - EOF RUN cd /tmp && mvn -f pom.xml dependency:go-offline diff --git a/plugins/grpc/kotlin/v1.5.0/Dockerfile b/plugins/grpc/kotlin/v1.5.0/Dockerfile index acd946097..8145b41e1 100644 --- a/plugins/grpc/kotlin/v1.5.0/Dockerfile +++ b/plugins/grpc/kotlin/v1.5.0/Dockerfile @@ -108,7 +108,6 @@ COPY < - EOF RUN cd /tmp && mvn -f pom.xml dependency:go-offline diff --git a/plugins/protocolbuffers/java/v33.5/Dockerfile b/plugins/protocolbuffers/java/v33.5/Dockerfile index b26c570ee..49b645a6e 100644 --- a/plugins/protocolbuffers/java/v33.5/Dockerfile +++ b/plugins/protocolbuffers/java/v33.5/Dockerfile @@ -49,7 +49,6 @@ COPY < - EOF RUN cd /tmp && mvn -f pom.xml dependency:go-offline diff --git a/plugins/protocolbuffers/kotlin/v33.5/Dockerfile b/plugins/protocolbuffers/kotlin/v33.5/Dockerfile index 96cfedfbf..2cb1df3ed 100644 --- a/plugins/protocolbuffers/kotlin/v33.5/Dockerfile +++ b/plugins/protocolbuffers/kotlin/v33.5/Dockerfile @@ -91,7 +91,6 @@ COPY < - EOF RUN cd /tmp && mvn -f pom.xml dependency:go-offline From 9ca7b30d670e296dc200676323b7fe38252e4a80 Mon Sep 17 00:00:00 2001 From: David Marby Date: Wed, 25 Feb 2026 19:44:11 +0100 Subject: [PATCH 05/12] Swap to table driven tests, and parse xml instead of just string matching --- internal/maven/pom_test.go | 371 +++++++++++++++++-------------------- 1 file changed, 168 insertions(+), 203 deletions(-) diff --git a/internal/maven/pom_test.go b/internal/maven/pom_test.go index 6f69807b4..4ff7b3201 100644 --- a/internal/maven/pom_test.go +++ b/internal/maven/pom_test.go @@ -1,6 +1,7 @@ package maven import ( + "encoding/xml" "os" "path/filepath" "strings" @@ -11,13 +12,56 @@ import ( "github.com/stretchr/testify/require" ) -func TestRenderPOM_BasicJavaPlugin(t *testing.T) { - t.Parallel() +// pomProject mirrors the Maven POM structure for test assertions. +type pomProject struct { + XMLName xml.Name `xml:"project"` + ModelVersion string `xml:"modelVersion"` + GroupID string `xml:"groupId"` + ArtifactID string `xml:"artifactId"` + Version string `xml:"version"` + Dependencies []pomDependency `xml:"dependencies>dependency"` + Build *pomBuild `xml:"build"` +} + +type pomDependency struct { + GroupID string `xml:"groupId"` + ArtifactID string `xml:"artifactId"` + Version string `xml:"version"` + Classifier string `xml:"classifier"` + Type string `xml:"type"` +} + +type pomBuild struct { + Plugins []pomPlugin `xml:"plugins>plugin"` +} + +type pomPlugin struct { + GroupID string `xml:"groupId"` + ArtifactID string `xml:"artifactId"` + Version string `xml:"version"` + Configuration *pomConfiguration `xml:"configuration"` +} + +type pomConfiguration struct { + APIVersion string `xml:"apiVersion"` + JVMTarget string `xml:"jvmTarget"` + LanguageVersion string `xml:"languageVersion"` +} - // Create temporary config file - tmpDir := t.TempDir() - yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") - yamlContent := `version: v1 +func TestRenderPOM(t *testing.T) { + t.Parallel() + tests := []struct { + name string + yaml string + wantErr string + // check runs assertions against the parsed POM. XML comments + // are not preserved by encoding/xml, so rawPOM is provided + // for comment checks. + check func(t *testing.T, p pomProject, rawPOM string) + }{ + { + name: "basic Java plugin", + yaml: `version: v1 name: buf.build/test/plugin plugin_version: v1.0.0 output_languages: @@ -26,26 +70,18 @@ registry: maven: deps: - com.google.protobuf:protobuf-java:4.33.5 -` - require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) - - config, err := bufremotepluginconfig.ParseConfig(yamlPath) - require.NoError(t, err) - - pom, err := RenderPOM(config) - require.NoError(t, err) - - assert.Contains(t, pom, "com.google.protobuf") - assert.Contains(t, pom, "protobuf-java") - assert.Contains(t, pom, "4.33.5") -} - -func TestRenderPOM_XMLEscaping(t *testing.T) { - t.Parallel() - - tmpDir := t.TempDir() - yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") - yamlContent := `version: v1 +`, + check: func(t *testing.T, p pomProject, _ string) { //nolint:thelper + require.Len(t, p.Dependencies, 1) + dep := p.Dependencies[0] + assert.Equal(t, "com.google.protobuf", dep.GroupID) + assert.Equal(t, "protobuf-java", dep.ArtifactID) + assert.Equal(t, "4.33.5", dep.Version) + }, + }, + { + name: "XML escaping round-trips correctly", + yaml: `version: v1 name: buf.build/test/plugin plugin_version: v1.0.0 output_languages: @@ -54,25 +90,17 @@ registry: maven: deps: - com.test:artifact<>&:1.0.0 -` - require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) - - config, err := bufremotepluginconfig.ParseConfig(yamlPath) - require.NoError(t, err) - - pom, err := RenderPOM(config) - require.NoError(t, err) - - assert.Contains(t, pom, "artifact<>&") - assert.NotContains(t, pom, "artifact<>&") -} - -func TestRenderPOM_KotlinCompiler(t *testing.T) { - t.Parallel() - - tmpDir := t.TempDir() - yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") - yamlContent := `version: v1 +`, + check: func(t *testing.T, p pomProject, _ string) { //nolint:thelper + require.Len(t, p.Dependencies, 1) + // encoding/xml decodes entities, so the parsed value + // should match the original unescaped input. + assert.Equal(t, "artifact<>&", p.Dependencies[0].ArtifactID) + }, + }, + { + name: "Kotlin compiler plugin configuration", + yaml: `version: v1 name: buf.build/test/kotlin-plugin plugin_version: v1.0.0 output_languages: @@ -86,28 +114,23 @@ registry: language_version: "1.8" api_version: "1.8" deps: [] -` - require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) - - config, err := bufremotepluginconfig.ParseConfig(yamlPath) - require.NoError(t, err) - - pom, err := RenderPOM(config) - require.NoError(t, err) - - assert.Contains(t, pom, "kotlin-maven-plugin") - assert.Contains(t, pom, "1.8.22") - assert.Contains(t, pom, "1.8") - assert.Contains(t, pom, "1.8") - assert.Contains(t, pom, "1.8") -} - -func TestRenderPOM_AdditionalRuntimes(t *testing.T) { - t.Parallel() - - tmpDir := t.TempDir() - yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") - yamlContent := `version: v1 +`, + check: func(t *testing.T, p pomProject, _ string) { //nolint:thelper + require.NotNil(t, p.Build) + require.Len(t, p.Build.Plugins, 1) + plugin := p.Build.Plugins[0] + assert.Equal(t, "org.jetbrains.kotlin", plugin.GroupID) + assert.Equal(t, "kotlin-maven-plugin", plugin.ArtifactID) + assert.Equal(t, "1.8.22", plugin.Version) + require.NotNil(t, plugin.Configuration) + assert.Equal(t, "1.8", plugin.Configuration.JVMTarget) + assert.Equal(t, "1.8", plugin.Configuration.LanguageVersion) + assert.Equal(t, "1.8", plugin.Configuration.APIVersion) + }, + }, + { + name: "additional runtimes", + yaml: `version: v1 name: buf.build/test/plugin plugin_version: v1.0.0 output_languages: @@ -121,51 +144,28 @@ registry: deps: - com.google.protobuf:protobuf-javalite:4.33.5 opts: [lite] -` - require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) - - config, err := bufremotepluginconfig.ParseConfig(yamlPath) - require.NoError(t, err) - - pom, err := RenderPOM(config) - require.NoError(t, err) - - assert.Contains(t, pom, "") - assert.Contains(t, pom, "protobuf-javalite") -} - -func TestRenderPOM_ClassifierAndExtension(t *testing.T) { - t.Parallel() - t.Skip("Classifier/extension format in YAML unknown - no real-world examples in codebase") -} - -func TestRenderPOM_NoMavenConfig(t *testing.T) { - t.Parallel() - - tmpDir := t.TempDir() - yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") - yamlContent := `version: v1 +`, + check: func(t *testing.T, p pomProject, rawPOM string) { //nolint:thelper + require.Len(t, p.Dependencies, 2) + assert.Equal(t, "protobuf-java", p.Dependencies[0].ArtifactID) + assert.Equal(t, "protobuf-javalite", p.Dependencies[1].ArtifactID) + // XML comments are not preserved by encoding/xml. + assert.Contains(t, rawPOM, "") + }, + }, + { + name: "no Maven config returns error", + yaml: `version: v1 name: buf.build/test/plugin plugin_version: v1.0.0 output_languages: - go -` - require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) - - config, err := bufremotepluginconfig.ParseConfig(yamlPath) - require.NoError(t, err) - - _, err = RenderPOM(config) - require.Error(t, err) - assert.Contains(t, err.Error(), "no Maven registry configured") -} - -func TestRenderPOM_EmptyDeps(t *testing.T) { - t.Parallel() - - tmpDir := t.TempDir() - yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") - yamlContent := `version: v1 +`, + wantErr: "no Maven registry configured", + }, + { + name: "empty deps renders valid structure", + yaml: `version: v1 name: buf.build/test/plugin plugin_version: v1.0.0 output_languages: @@ -173,29 +173,18 @@ output_languages: registry: maven: deps: [] -` - require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) - - config, err := bufremotepluginconfig.ParseConfig(yamlPath) - require.NoError(t, err) - - pom, err := RenderPOM(config) - require.NoError(t, err) - - // Should still generate valid POM structure - assert.Contains(t, pom, "4.0.0") - assert.Contains(t, pom, "temp") - assert.Contains(t, pom, "temp") -} - -func TestRenderPOM_MalformedXMLDetected(t *testing.T) { - t.Parallel() - - // A runtime name containing "--" produces an invalid XML comment - // ( is not well-formed XML). - tmpDir := t.TempDir() - yamlPath := filepath.Join(tmpDir, "buf.plugin.yaml") - yamlContent := `version: v1 +`, + check: func(t *testing.T, p pomProject, _ string) { //nolint:thelper + assert.Equal(t, "4.0.0", p.ModelVersion) + assert.Equal(t, "temp", p.GroupID) + assert.Equal(t, "temp", p.ArtifactID) + assert.Equal(t, "1.0", p.Version) + assert.Empty(t, p.Dependencies) + }, + }, + { + name: "malformed XML detected", + yaml: `version: v1 name: buf.build/test/plugin plugin_version: v1.0.0 output_languages: @@ -209,18 +198,56 @@ registry: deps: - com.google.protobuf:protobuf-javalite:4.33.5 opts: [lite] -` - require.NoError(t, os.WriteFile(yamlPath, []byte(yamlContent), 0644)) - - config, err := bufremotepluginconfig.ParseConfig(yamlPath) - require.NoError(t, err) - - _, err = RenderPOM(config) - require.Error(t, err) - assert.Contains(t, err.Error(), "generated POM is not well-formed XML") +`, + wantErr: "generated POM is not well-formed XML", + }, + { + name: "Kotlin dynamic dependencies", + yaml: `version: v1 +name: buf.build/test/kotlin-plugin +plugin_version: v1.0.0 +output_languages: + - kotlin +registry: + maven: + compiler: + kotlin: + version: 1.9.0 + deps: [] +`, + check: func(t *testing.T, p pomProject, rawPOM string) { //nolint:thelper + require.Len(t, p.Dependencies, 2) + assert.Equal(t, "kotlin-compiler-embeddable", p.Dependencies[0].ArtifactID) + assert.Equal(t, "1.9.0", p.Dependencies[0].Version) + assert.Equal(t, "kotlin-scripting-compiler", p.Dependencies[1].ArtifactID) + assert.Equal(t, "1.9.0", p.Dependencies[1].Version) + assert.Contains(t, rawPOM, " - - org.jetbrains.kotlin - kotlin-compiler-embeddable - {{ $kotlinCompiler.Version | xml }} - - - org.jetbrains.kotlin - kotlin-scripting-compiler - {{ $kotlinCompiler.Version | xml }} - - {{- end }} {{- if $kotlinCompiler.Version }} diff --git a/internal/maven/pom_test.go b/internal/maven/pom_test.go index 4ff7b3201..2e9dd63d0 100644 --- a/internal/maven/pom_test.go +++ b/internal/maven/pom_test.go @@ -202,7 +202,7 @@ registry: wantErr: "generated POM is not well-formed XML", }, { - name: "Kotlin dynamic dependencies", + name: "Kotlin plugin with no explicit deps", yaml: `version: v1 name: buf.build/test/kotlin-plugin plugin_version: v1.0.0 @@ -215,13 +215,12 @@ registry: version: 1.9.0 deps: [] `, - check: func(t *testing.T, p pomProject, rawPOM string) { //nolint:thelper - require.Len(t, p.Dependencies, 2) - assert.Equal(t, "kotlin-compiler-embeddable", p.Dependencies[0].ArtifactID) - assert.Equal(t, "1.9.0", p.Dependencies[0].Version) - assert.Equal(t, "kotlin-scripting-compiler", p.Dependencies[1].ArtifactID) - assert.Equal(t, "1.9.0", p.Dependencies[1].Version) - assert.Contains(t, rawPOM, " - - org.jetbrains.kotlin - kotlin-compiler-embeddable - 1.8.22 - - - org.jetbrains.kotlin - kotlin-scripting-compiler - 1.8.22 - diff --git a/plugins/connectrpc/kotlin/v0.7.4/Dockerfile b/plugins/connectrpc/kotlin/v0.7.4/Dockerfile index 68fec8f30..60a7f688d 100644 --- a/plugins/connectrpc/kotlin/v0.7.4/Dockerfile +++ b/plugins/connectrpc/kotlin/v0.7.4/Dockerfile @@ -72,17 +72,6 @@ COPY <protobuf-javalite 4.31.1 - - - org.jetbrains.kotlin - kotlin-compiler-embeddable - 2.1.0 - - - org.jetbrains.kotlin - kotlin-scripting-compiler - 2.1.0 - diff --git a/plugins/grpc/kotlin/v1.5.0/Dockerfile b/plugins/grpc/kotlin/v1.5.0/Dockerfile index 8145b41e1..294916a70 100644 --- a/plugins/grpc/kotlin/v1.5.0/Dockerfile +++ b/plugins/grpc/kotlin/v1.5.0/Dockerfile @@ -84,17 +84,6 @@ COPY <protobuf-kotlin-lite 4.32.0 - - - org.jetbrains.kotlin - kotlin-compiler-embeddable - 2.2.20 - - - org.jetbrains.kotlin - kotlin-scripting-compiler - 2.2.20 - diff --git a/plugins/protocolbuffers/kotlin/v33.5/Dockerfile b/plugins/protocolbuffers/kotlin/v33.5/Dockerfile index 2cb1df3ed..0d6b6dcac 100644 --- a/plugins/protocolbuffers/kotlin/v33.5/Dockerfile +++ b/plugins/protocolbuffers/kotlin/v33.5/Dockerfile @@ -67,17 +67,6 @@ COPY <protobuf-javalite 4.33.5 - - - org.jetbrains.kotlin - kotlin-compiler-embeddable - 1.8.22 - - - org.jetbrains.kotlin - kotlin-scripting-compiler - 1.8.22 - From 32ab4662448b44fac765bc24a3df3d704f2f3c58 Mon Sep 17 00:00:00 2001 From: David Marby Date: Thu, 26 Feb 2026 19:51:20 +0100 Subject: [PATCH 09/12] Adress review comments --- internal/cmd/fetcher/main.go | 49 +--- internal/cmd/fetcher/main_test.go | 209 +++--------------- internal/cmd/regenerate-maven-poms/main.go | 162 +------------- internal/maven/dockerfile.go | 139 ++++++++---- internal/maven/pom_test.go | 23 -- internal/maven/regenerate.go | 55 +++++ plugins/apple/servicetalk/v0.42.63/Dockerfile | 44 +--- plugins/apple/servicetalk/v0.42.63/pom.xml | 39 ++++ .../connect-kotlin/v0.1.10/Dockerfile | 81 +------ .../bufbuild/connect-kotlin/v0.1.10/pom.xml | 75 +++++++ .../bufbuild/validate-java/v1.3.3/Dockerfile | 35 +-- plugins/bufbuild/validate-java/v1.3.3/pom.xml | 29 +++ .../v1.2.4/Dockerfile | 59 +---- .../salesforce-reactive-grpc/v1.2.4/pom.xml | 54 +++++ plugins/connectrpc/kotlin/v0.7.4/Dockerfile | 81 +------ plugins/connectrpc/kotlin/v0.7.4/pom.xml | 76 +++++++ plugins/grpc/java/v1.79.0/Dockerfile | 49 +--- plugins/grpc/java/v1.79.0/pom.xml | 44 ++++ plugins/grpc/kotlin/v1.5.0/Dockerfile | 90 +------- plugins/grpc/kotlin/v1.5.0/pom.xml | 85 +++++++ plugins/protocolbuffers/java/v33.5/Dockerfile | 29 +-- plugins/protocolbuffers/java/v33.5/pom.xml | 24 ++ .../protocolbuffers/kotlin/v33.5/Dockerfile | 60 +---- plugins/protocolbuffers/kotlin/v33.5/pom.xml | 55 +++++ 24 files changed, 698 insertions(+), 948 deletions(-) create mode 100644 internal/maven/regenerate.go create mode 100644 plugins/apple/servicetalk/v0.42.63/pom.xml create mode 100644 plugins/bufbuild/connect-kotlin/v0.1.10/pom.xml create mode 100644 plugins/bufbuild/validate-java/v1.3.3/pom.xml create mode 100644 plugins/community/salesforce-reactive-grpc/v1.2.4/pom.xml create mode 100644 plugins/connectrpc/kotlin/v0.7.4/pom.xml create mode 100644 plugins/grpc/java/v1.79.0/pom.xml create mode 100644 plugins/grpc/kotlin/v1.5.0/pom.xml create mode 100644 plugins/protocolbuffers/java/v33.5/pom.xml create mode 100644 plugins/protocolbuffers/kotlin/v33.5/pom.xml diff --git a/internal/cmd/fetcher/main.go b/internal/cmd/fetcher/main.go index 477864846..d05beddfa 100644 --- a/internal/cmd/fetcher/main.go +++ b/internal/cmd/fetcher/main.go @@ -132,7 +132,7 @@ func postProcessCreatedPlugins(ctx context.Context, logger *slog.Logger, plugins } for _, plugin := range plugins { newPluginRef := plugin.String() - if err := regenerateMavenDeps(ctx, logger, plugin); err != nil { + 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 { @@ -277,51 +277,12 @@ func recreateSwiftPackageResolved(ctx context.Context, logger *slog.Logger, plug return nil } -// regenerateMavenDeps regenerates the POM in the Dockerfile's maven-deps stage -// from the plugin's buf.plugin.yaml. This ensures the POM always reflects the -// actual Maven dependencies declared in the config, rather than relying on -// version string replacement which can miss transitive dependency updates. -func regenerateMavenDeps(ctx context.Context, logger *slog.Logger, plugin createdPlugin) error { +// 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) - yamlPath := filepath.Join(versionDir, "buf.plugin.yaml") - pluginConfig, err := bufremotepluginconfig.ParseConfig(yamlPath) - if err != nil { - return err - } - if pluginConfig.Registry == nil || pluginConfig.Registry.Maven == nil { - return nil // not a Maven plugin - } - // Resolve Maven dependencies from plugin deps (top-level deps stanza) - // and merge them into the plugin's Maven config. This ensures the - // maven-deps Docker stage caches all dependencies needed for offline - // builds, including those from dependent plugins (e.g. Kotlin depending - // on Java brings in build.buf:protobuf-javalite for lite builds). pluginsDir := filepath.Dir(filepath.Dir(plugin.pluginDir)) - if err := maven.MergeTransitiveDeps(pluginConfig, pluginsDir); err != nil { - return fmt.Errorf("merging dep Maven dependencies: %w", err) - } - if err := maven.DeduplicateAllDeps(pluginConfig.Registry.Maven); err != nil { - return fmt.Errorf("deduplicating deps: %w", err) - } - dockerfilePath := filepath.Join(versionDir, "Dockerfile") - dockerfileBytes, err := os.ReadFile(dockerfilePath) - if err != nil { - return err - } - dockerfile := string(dockerfileBytes) - if !strings.Contains(dockerfile, "maven-deps") { - return nil // no maven-deps stage to update - } - pom, err := maven.RenderPOM(pluginConfig) - if err != nil { - return fmt.Errorf("rendering POM: %w", err) - } - updated, err := maven.ReplacePOMInDockerfile(dockerfile, pom) - if err != nil { - return err - } - logger.InfoContext(ctx, "regenerated maven deps POM", slog.Any("plugin", plugin)) - return os.WriteFile(dockerfilePath, []byte(updated), 0644) //nolint:gosec // Dockerfiles should be world-readable. + return maven.RegenerateMavenDeps(versionDir, pluginsDir) } // runPluginTests runs 'make test PLUGINS="org/name:v"' in order to generate plugin.sum files. diff --git a/internal/cmd/fetcher/main_test.go b/internal/cmd/fetcher/main_test.go index 344f2a96f..9630c0a8f 100644 --- a/internal/cmd/fetcher/main_test.go +++ b/internal/cmd/fetcher/main_test.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/xml" "log/slog" "os" "path/filepath" @@ -415,33 +416,14 @@ registry: FROM debian:bookworm AS build RUN echo hello -FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - temp - temp - 1.0 - - - com.google.protobuf - protobuf-kotlin - 4.33.5 - - - -EOF -RUN cd /tmp && mvn -f pom.xml dependency:go-offline - FROM scratch COPY --from=build /app . -COPY --from=maven-deps /root/.m2/repository /maven-repository ENTRYPOINT ["/app"] ` require.NoError(t, os.WriteFile(filepath.Join(consumerDir, "Dockerfile"), []byte(dockerfile), 0644)) // Run regenerateMavenDeps on the consumer plugin - logger := slog.New(slog.NewTextHandler(testWriter{t}, &slog.HandlerOptions{Level: slog.LevelDebug})) - err := regenerateMavenDeps(t.Context(), logger, createdPlugin{ + err := regenerateMavenDeps(createdPlugin{ org: "test", name: "consumer-plugin", pluginDir: filepath.Join(tmpDir, "plugins", "test", "consumer-plugin"), @@ -449,22 +431,30 @@ ENTRYPOINT ["/app"] }) require.NoError(t, err) - // Read the updated Dockerfile and verify it includes deps from base-plugin - updatedBytes, err := os.ReadFile(filepath.Join(consumerDir, "Dockerfile")) + // Verify the maven-deps stage was inserted into the Dockerfile. + dockerfileBytes, err := os.ReadFile(filepath.Join(consumerDir, "Dockerfile")) require.NoError(t, err) - updated := string(updatedBytes) - - // Consumer's own deps should be present - assert.Contains(t, updated, "protobuf-kotlin") - assert.Contains(t, updated, "protobuf-kotlin-lite") - - // Base plugin's main deps should be merged in - assert.Contains(t, updated, "protobuf-java") + 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, updated, "protobuf-javalite") - assert.Contains(t, updated, "build.buf") + // 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) { @@ -612,147 +602,18 @@ func TestDeduplicateAllDeps(t *testing.T) { } } -func TestReplacePOMInDockerfile(t *testing.T) { - t.Parallel() - tests := []struct { - name string - dockerfile string - newPOM string - want string - wantErr string - }{ - { - name: "basic replacement", - dockerfile: `FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - old - -EOF -RUN cd /tmp && mvn -f pom.xml dependency:go-offline -`, - newPOM: ` - new - -`, - want: `FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - new - -EOF -RUN cd /tmp && mvn -f pom.xml dependency:go-offline -`, - }, - { - name: "new POM without trailing newline", - dockerfile: `FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - old - -EOF -RUN cd /tmp && mvn -f pom.xml dependency:go-offline -`, - newPOM: "\n new\n", - want: `FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - new - -EOF -RUN cd /tmp && mvn -f pom.xml dependency:go-offline -`, - }, - { - name: "preserves surrounding content", - dockerfile: `# syntax=docker/dockerfile:1.19 -FROM debian:bookworm AS build -RUN echo hello - -FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - old - -EOF -RUN cd /tmp && mvn -f pom.xml dependency:go-offline - -FROM scratch -COPY --from=build /app . -COPY --from=maven-deps /root/.m2/repository /maven-repository -ENTRYPOINT ["/app"] -`, - newPOM: "\n replaced\n\n", - want: `# syntax=docker/dockerfile:1.19 -FROM debian:bookworm AS build -RUN echo hello +// testPOMProject mirrors the Maven POM structure for test assertions. +type testPOMProject struct { + XMLName xml.Name `xml:"project"` + Dependencies []testDep `xml:"dependencies>dependency"` +} -FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - replaced - -EOF -RUN cd /tmp && mvn -f pom.xml dependency:go-offline +type testDep struct { + GroupID string `xml:"groupId"` + ArtifactID string `xml:"artifactId"` + Version string `xml:"version"` +} -FROM scratch -COPY --from=build /app . -COPY --from=maven-deps /root/.m2/repository /maven-repository -ENTRYPOINT ["/app"] -`, - }, - { - name: "missing COPY heredoc marker", - dockerfile: "FROM maven:3.9.11 AS maven-deps\nRUN echo hello\n", - newPOM: "", - wantErr: `could not find "COPY < - old - -`, - newPOM: "", - wantErr: "could not find closing EOF for POM heredoc in Dockerfile", - }, - { - name: "nested heredoc before POM EOF", - dockerfile: "FROM maven:3.9.11 AS maven-deps\n" + - "COPY <\n" + - " old\n" + - "\n" + - "EOF\n" + - "RUN cat < /tmp/other.txt\n" + - "some other content\n" + - "EOF\n", - newPOM: "\n new\n\n", - want: "FROM maven:3.9.11 AS maven-deps\n" + - "COPY <\n" + - " new\n" + - "\n" + - "EOF\n" + - "RUN cat < /tmp/other.txt\n" + - "some other content\n" + - "EOF\n", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, err := maven.ReplacePOMInDockerfile(tt.dockerfile, tt.newPOM) - if tt.wantErr != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantErr) - return - } - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } +func (d testDep) String() string { + return d.GroupID + ":" + d.ArtifactID + ":" + d.Version } diff --git a/internal/cmd/regenerate-maven-poms/main.go b/internal/cmd/regenerate-maven-poms/main.go index 24b145cb7..13c061629 100644 --- a/internal/cmd/regenerate-maven-poms/main.go +++ b/internal/cmd/regenerate-maven-poms/main.go @@ -2,16 +2,11 @@ package main import ( "context" - "errors" "fmt" - "os" "path/filepath" - "slices" - "strings" "buf.build/go/app" "buf.build/go/app/appcmd" - "github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremotepluginconfig" "github.com/bufbuild/plugins/internal/maven" ) @@ -28,7 +23,10 @@ func newCommand(name string) *appcmd.Command { Run: func(_ context.Context, container app.Container) error { for i := range container.NumArgs() { pluginDir := container.Arg(i) - if err := regenerateMavenDeps(pluginDir); err != nil { + // 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) @@ -37,155 +35,3 @@ func newCommand(name string) *appcmd.Command { }, } } - -func regenerateMavenDeps(pluginDir string) error { - yamlPath := filepath.Join(pluginDir, "buf.plugin.yaml") - if !fileExists(yamlPath) { - return nil // no buf.plugin.yaml, skip - } - pluginConfig, err := bufremotepluginconfig.ParseConfig(yamlPath) - if err != nil { - return err - } - if pluginConfig.Registry == nil || pluginConfig.Registry.Maven == nil { - return nil // not a Maven plugin - } - - // Merge transitive Maven deps from plugin dependencies and deduplicate. - // 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.MergeTransitiveDeps(pluginConfig, pluginsDir); err != nil { - return fmt.Errorf("merging transitive deps: %w", err) - } - if err := maven.DeduplicateAllDeps(pluginConfig.Registry.Maven); err != nil { - return fmt.Errorf("deduplicating deps: %w", err) - } - - pom, err := maven.RenderPOM(pluginConfig) - if err != nil { - return fmt.Errorf("rendering POM: %w", err) - } - - dockerfilePath := filepath.Join(pluginDir, "Dockerfile") - dockerfileBytes, err := os.ReadFile(dockerfilePath) - if err != nil { - return err - } - dockerfile := string(dockerfileBytes) - - var updated string - if strings.Contains(dockerfile, "maven-deps") { - updated, err = maven.ReplacePOMInDockerfile(dockerfile, pom) - if err != nil { - return fmt.Errorf("replacing POM: %w", err) - } - } else { - // Insert a new maven-deps stage. - updated, err = insertMavenDepsStage(dockerfile, pom) - if err != nil { - return fmt.Errorf("inserting maven-deps stage: %w", err) - } - } - - return os.WriteFile(dockerfilePath, []byte(updated), 0644) //nolint:gosec // Dockerfiles should be world-readable. -} - -// insertMavenDepsStage inserts a new maven-deps stage into a Dockerfile that -// does not have one. The stage is inserted before the final FROM line, and a -// COPY --from=maven-deps line is added in the final stage before the first -// USER, CMD, or ENTRYPOINT directive. -func insertMavenDepsStage(dockerfile, pom string) (string, error) { - lines := strings.Split(dockerfile, "\n") - - // Find the index of the last FROM line (the final stage). - lastFromIdx := -1 - for i, line := range lines { - if isFromLine(line) { - lastFromIdx = i - } - } - if lastFromIdx < 0 { - return "", errors.New("no FROM line found in Dockerfile") - } - - // Build the maven-deps stage lines. The POM ends with "\n\n" from the - // template; remove one trailing empty string so there is exactly one - // blank line before EOF in the heredoc. - pomLines := strings.Split(pom, "\n") - if len(pomLines) > 0 && pomLines[len(pomLines)-1] == "" { - pomLines = pomLines[:len(pomLines)-1] - } - mavenDepsLines := slices.Concat( - []string{ - "FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps", - "COPY < 0 && strings.TrimSpace(lines[insertAt-1]) == "" { - insertAt-- - } - - // Assemble: [build stage content] + 2 blank lines + maven-deps stage + - // 1 blank line + final stage. - var newLines []string - newLines = append(newLines, lines[:insertAt]...) - newLines = append(newLines, "", "") - newLines = append(newLines, mavenDepsLines...) - newLines = append(newLines, "") - newLines = append(newLines, lines[lastFromIdx:]...) - - // Find the last FROM in the new lines array (the final stage). - finalFromIdx := -1 - for i, line := range newLines { - if isFromLine(line) { - finalFromIdx = i - } - } - - // Insert COPY --from=maven-deps before the first USER/CMD/ENTRYPOINT in - // the final stage. - copyInsertAt := -1 - for i := finalFromIdx + 1; i < len(newLines); i++ { - if isCopyInsertTarget(newLines[i]) { - copyInsertAt = i - break - } - } - - copyLine := "COPY --from=maven-deps /root/.m2/repository /maven-repository" - if copyInsertAt < 0 { - newLines = append(newLines, copyLine) - return strings.Join(newLines, "\n"), nil - } - finalLines := slices.Concat(newLines[:copyInsertAt], []string{copyLine}, newLines[copyInsertAt:]) - return strings.Join(finalLines, "\n"), nil -} - -func isFromLine(line string) bool { - trimmed := strings.TrimSpace(line) - return strings.HasPrefix(strings.ToUpper(trimmed), "FROM ") -} - -func isCopyInsertTarget(line string) bool { - upper := strings.ToUpper(strings.TrimSpace(line)) - return strings.HasPrefix(upper, "USER ") || - strings.HasPrefix(upper, "CMD ") || - strings.HasPrefix(upper, "CMD[") || - strings.HasPrefix(upper, "ENTRYPOINT ") || - strings.HasPrefix(upper, "ENTRYPOINT[") -} - -func fileExists(path string) bool { - _, err := os.Stat(path) - return err == nil -} diff --git a/internal/maven/dockerfile.go b/internal/maven/dockerfile.go index 5c60a89f4..b47a4e64d 100644 --- a/internal/maven/dockerfile.go +++ b/internal/maven/dockerfile.go @@ -2,50 +2,111 @@ package maven import ( "errors" - "fmt" + "slices" "strings" ) -// ReplacePOMInDockerfile replaces the POM heredoc content between -// "COPY < 0 && strings.TrimSpace(lines[insertAt-1]) == "" { + insertAt-- } - var sb strings.Builder - sb.WriteString(dockerfile[:contentStart]) - sb.WriteString(newPOM) - if !strings.HasSuffix(newPOM, "\n") { - sb.WriteByte('\n') + + // Assemble: [build stage content] + blank line + + // maven-deps stage + blank line + final stage. + var newLines []string + newLines = append(newLines, lines[:insertAt]...) + newLines = append(newLines, "") + newLines = append(newLines, mavenDepsLines...) + newLines = append(newLines, "") + newLines = append(newLines, lines[lastFromIdx:]...) + + // Find the last FROM in the new lines array (the final stage). + finalFromIdx := -1 + for i, line := range newLines { + if isFromLine(line) { + finalFromIdx = i + } } - sb.WriteString(dockerfile[eofIdx:]) - return sb.String(), nil + + // Insert COPY --from=maven-deps before the first + // USER/CMD/ENTRYPOINT in the final stage. + copyInsertAt := -1 + for i := finalFromIdx + 1; i < len(newLines); i++ { + if isCopyInsertTarget(newLines[i]) { + copyInsertAt = i + break + } + } + + copyLine := "COPY --from=maven-deps /root/.m2/repository /maven-repository" + if copyInsertAt < 0 { + newLines = append(newLines, copyLine) + return strings.Join(newLines, "\n"), nil + } + finalLines := slices.Concat( + newLines[:copyInsertAt], + []string{copyLine}, + newLines[copyInsertAt:], + ) + return strings.Join(finalLines, "\n"), nil +} + +func isFromLine(line string) bool { + trimmed := strings.TrimSpace(line) + return strings.HasPrefix(strings.ToUpper(trimmed), "FROM ") +} + +func isMavenDepsFromLine(line string) bool { + trimmed := strings.TrimSpace(line) + upper := strings.ToUpper(trimmed) + return strings.HasPrefix(upper, "FROM ") && strings.Contains(strings.ToLower(trimmed), "as maven-deps") +} + +func isCopyInsertTarget(line string) bool { + upper := strings.ToUpper(strings.TrimSpace(line)) + return strings.HasPrefix(upper, "USER ") || + strings.HasPrefix(upper, "CMD ") || + strings.HasPrefix(upper, "CMD[") || + strings.HasPrefix(upper, "ENTRYPOINT ") || + strings.HasPrefix(upper, "ENTRYPOINT[") } diff --git a/internal/maven/pom_test.go b/internal/maven/pom_test.go index 2e9dd63d0..7370b8f05 100644 --- a/internal/maven/pom_test.go +++ b/internal/maven/pom_test.go @@ -79,25 +79,6 @@ registry: assert.Equal(t, "4.33.5", dep.Version) }, }, - { - name: "XML escaping round-trips correctly", - yaml: `version: v1 -name: buf.build/test/plugin -plugin_version: v1.0.0 -output_languages: - - java -registry: - maven: - deps: - - com.test:artifact<>&:1.0.0 -`, - check: func(t *testing.T, p pomProject, _ string) { //nolint:thelper - require.Len(t, p.Dependencies, 1) - // encoding/xml decodes entities, so the parsed value - // should match the original unescaped input. - assert.Equal(t, "artifact<>&", p.Dependencies[0].ArtifactID) - }, - }, { name: "Kotlin compiler plugin configuration", yaml: `version: v1 @@ -175,10 +156,6 @@ registry: deps: [] `, check: func(t *testing.T, p pomProject, _ string) { //nolint:thelper - assert.Equal(t, "4.0.0", p.ModelVersion) - assert.Equal(t, "temp", p.GroupID) - assert.Equal(t, "temp", p.ArtifactID) - assert.Equal(t, "1.0", p.Version) assert.Empty(t, p.Dependencies) }, }, diff --git a/internal/maven/regenerate.go b/internal/maven/regenerate.go new file mode 100644 index 000000000..d8392f98d --- /dev/null +++ b/internal/maven/regenerate.go @@ -0,0 +1,55 @@ +package maven + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremotepluginconfig" +) + +// RegenerateMavenDeps processes a Maven plugin version directory by +// merging transitive deps, deduplicating, rendering POM to a pom.xml +// file, and ensuring the Dockerfile has an up-to-date maven-deps +// stage. Returns nil without changes if the plugin has no Maven +// registry config. +func RegenerateMavenDeps(pluginVersionDir, pluginsDir string) error { + yamlPath := filepath.Join(pluginVersionDir, "buf.plugin.yaml") + if _, err := os.Stat(yamlPath); errors.Is(err, os.ErrNotExist) { + return nil + } else if err != nil { + return err + } + pluginConfig, err := bufremotepluginconfig.ParseConfig(yamlPath) + if err != nil { + return err + } + if pluginConfig.Registry == nil || pluginConfig.Registry.Maven == nil { + return nil + } + if err := MergeTransitiveDeps(pluginConfig, pluginsDir); err != nil { + return fmt.Errorf("merging transitive deps: %w", err) + } + if err := DeduplicateAllDeps(pluginConfig.Registry.Maven); err != nil { + return fmt.Errorf("deduplicating deps: %w", err) + } + pom, err := RenderPOM(pluginConfig) + if err != nil { + return fmt.Errorf("rendering POM: %w", err) + } + pomPath := filepath.Join(pluginVersionDir, "pom.xml") + if err := os.WriteFile(pomPath, []byte(pom), 0644); err != nil { //nolint:gosec + return fmt.Errorf("writing pom.xml: %w", err) + } + dockerfilePath := filepath.Join(pluginVersionDir, "Dockerfile") + dockerfileBytes, err := os.ReadFile(dockerfilePath) + if err != nil { + return err + } + updated, err := EnsureMavenDepsStage(string(dockerfileBytes)) + if err != nil { + return fmt.Errorf("ensuring maven-deps stage: %w", err) + } + return os.WriteFile(dockerfilePath, []byte(updated), 0644) //nolint:gosec +} diff --git a/plugins/apple/servicetalk/v0.42.63/Dockerfile b/plugins/apple/servicetalk/v0.42.63/Dockerfile index a2a643096..bf51e0b77 100644 --- a/plugins/apple/servicetalk/v0.42.63/Dockerfile +++ b/plugins/apple/servicetalk/v0.42.63/Dockerfile @@ -8,48 +8,8 @@ RUN curl -fsSL -o servicetalk-grpc-protoc.jar https://repo1.maven.org/maven2/io/ FROM gcr.io/distroless/java21-debian12:latest@sha256:f34fd3e4e2d7a246d764d0614f5e6ffb3a735930723fac4cfc25a72798950262 AS base -FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - 4.0.0 - temp - temp - 1.0 - - - io.servicetalk - servicetalk-data-protobuf - 0.42.63 - - - io.servicetalk - servicetalk-grpc-api - 0.42.63 - - - io.servicetalk - servicetalk-grpc-protobuf - 0.42.63 - - - com.google.protobuf - protobuf-java - 4.33.5 - - - - com.google.protobuf - protobuf-javalite - 4.33.5 - - - build.buf - protobuf-javalite - 4.33.5 - - - -EOF +FROM maven:3.9.11-eclipse-temurin-21 AS maven-deps +COPY pom.xml /tmp/pom.xml RUN cd /tmp && mvn -f pom.xml dependency:go-offline FROM scratch diff --git a/plugins/apple/servicetalk/v0.42.63/pom.xml b/plugins/apple/servicetalk/v0.42.63/pom.xml new file mode 100644 index 000000000..783a521b2 --- /dev/null +++ b/plugins/apple/servicetalk/v0.42.63/pom.xml @@ -0,0 +1,39 @@ + + 4.0.0 + temp + temp + 1.0 + + + io.servicetalk + servicetalk-data-protobuf + 0.42.63 + + + io.servicetalk + servicetalk-grpc-api + 0.42.63 + + + io.servicetalk + servicetalk-grpc-protobuf + 0.42.63 + + + com.google.protobuf + protobuf-java + 4.33.5 + + + + com.google.protobuf + protobuf-javalite + 4.33.5 + + + build.buf + protobuf-javalite + 4.33.5 + + + diff --git a/plugins/bufbuild/connect-kotlin/v0.1.10/Dockerfile b/plugins/bufbuild/connect-kotlin/v0.1.10/Dockerfile index 88b862609..c4cdfbce7 100644 --- a/plugins/bufbuild/connect-kotlin/v0.1.10/Dockerfile +++ b/plugins/bufbuild/connect-kotlin/v0.1.10/Dockerfile @@ -5,85 +5,8 @@ RUN apt-get update \ WORKDIR /app RUN curl -fsSL -o /app/protoc-gen-connect-kotlin.jar https://repo1.maven.org/maven2/build/buf/protoc-gen-connect-kotlin/0.1.10/protoc-gen-connect-kotlin-0.1.10.jar - -FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - 4.0.0 - temp - temp - 1.0 - - - build.buf - connect-kotlin - 0.1.10 - - - build.buf - connect-kotlin-google-java-ext - 0.1.10 - - - build.buf - connect-kotlin-okhttp - 0.1.10 - - - com.google.protobuf - protobuf-kotlin - 3.24.3 - - - org.jetbrains.kotlin - kotlin-stdlib - 1.8.22 - - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - 1.8.22 - - - com.google.protobuf - protobuf-java - 3.24.3 - - - - build.buf - connect-kotlin-google-javalite-ext - 0.1.10 - - - com.google.protobuf - protobuf-kotlin-lite - 3.24.3 - - - com.google.protobuf - protobuf-javalite - 3.24.3 - - - build.buf - protobuf-javalite - 3.24.3 - - - - - - org.jetbrains.kotlin - kotlin-maven-plugin - 1.8.22 - - - - - - -EOF +FROM maven:3.9.11-eclipse-temurin-21 AS maven-deps +COPY pom.xml /tmp/pom.xml RUN cd /tmp && mvn -f pom.xml dependency:go-offline FROM gcr.io/distroless/java17-debian11 diff --git a/plugins/bufbuild/connect-kotlin/v0.1.10/pom.xml b/plugins/bufbuild/connect-kotlin/v0.1.10/pom.xml new file mode 100644 index 000000000..0cf0763c4 --- /dev/null +++ b/plugins/bufbuild/connect-kotlin/v0.1.10/pom.xml @@ -0,0 +1,75 @@ + + 4.0.0 + temp + temp + 1.0 + + + build.buf + connect-kotlin + 0.1.10 + + + build.buf + connect-kotlin-google-java-ext + 0.1.10 + + + build.buf + connect-kotlin-okhttp + 0.1.10 + + + com.google.protobuf + protobuf-kotlin + 3.24.3 + + + org.jetbrains.kotlin + kotlin-stdlib + 1.8.22 + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 1.8.22 + + + com.google.protobuf + protobuf-java + 3.24.3 + + + + build.buf + connect-kotlin-google-javalite-ext + 0.1.10 + + + com.google.protobuf + protobuf-kotlin-lite + 3.24.3 + + + com.google.protobuf + protobuf-javalite + 3.24.3 + + + build.buf + protobuf-javalite + 3.24.3 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + 1.8.22 + + + + + + diff --git a/plugins/bufbuild/validate-java/v1.3.3/Dockerfile b/plugins/bufbuild/validate-java/v1.3.3/Dockerfile index 20b60d9dc..bad885156 100644 --- a/plugins/bufbuild/validate-java/v1.3.3/Dockerfile +++ b/plugins/bufbuild/validate-java/v1.3.3/Dockerfile @@ -2,39 +2,8 @@ FROM golang:1.26.0-bookworm AS build RUN CGO_ENABLED=0 go install -ldflags "-s -w" -trimpath github.com/envoyproxy/protoc-gen-validate/cmd/protoc-gen-validate-java@v1.3.3 - -FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - 4.0.0 - temp - temp - 1.0 - - - build.buf.protoc-gen-validate - pgv-java-stub - 1.3.3 - - - com.google.protobuf - protobuf-java - 4.33.5 - - - - com.google.protobuf - protobuf-javalite - 4.33.5 - - - build.buf - protobuf-javalite - 4.33.5 - - - -EOF +FROM maven:3.9.11-eclipse-temurin-21 AS maven-deps +COPY pom.xml /tmp/pom.xml RUN cd /tmp && mvn -f pom.xml dependency:go-offline FROM scratch diff --git a/plugins/bufbuild/validate-java/v1.3.3/pom.xml b/plugins/bufbuild/validate-java/v1.3.3/pom.xml new file mode 100644 index 000000000..288dddd38 --- /dev/null +++ b/plugins/bufbuild/validate-java/v1.3.3/pom.xml @@ -0,0 +1,29 @@ + + 4.0.0 + temp + temp + 1.0 + + + build.buf.protoc-gen-validate + pgv-java-stub + 1.3.3 + + + com.google.protobuf + protobuf-java + 4.33.5 + + + + com.google.protobuf + protobuf-javalite + 4.33.5 + + + build.buf + protobuf-javalite + 4.33.5 + + + diff --git a/plugins/community/salesforce-reactive-grpc/v1.2.4/Dockerfile b/plugins/community/salesforce-reactive-grpc/v1.2.4/Dockerfile index cd7c8ea4e..7782a7695 100644 --- a/plugins/community/salesforce-reactive-grpc/v1.2.4/Dockerfile +++ b/plugins/community/salesforce-reactive-grpc/v1.2.4/Dockerfile @@ -7,63 +7,8 @@ RUN curl -fsSL -o reactor-grpc-protoc.jar https://repo1.maven.org/maven2/com/sal FROM gcr.io/distroless/java21-debian12:latest@sha256:7c9a9a362eadadb308d29b9c7fec2b39e5d5aa21d58837176a2cca50bdd06609 AS base -FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - 4.0.0 - temp - temp - 1.0 - - - com.salesforce.servicelibs - reactor-grpc-stub - 1.2.4 - - - io.projectreactor - reactor-core - 3.5.4 - - - com.google.protobuf - protobuf-java - 4.31.0 - - - io.grpc - grpc-core - 1.73.0 - - - io.grpc - grpc-protobuf - 1.73.0 - - - io.grpc - grpc-stub - 1.73.0 - - - - io.grpc - grpc-protobuf-lite - 1.73.0 - - - com.google.protobuf - protobuf-javalite - 4.31.0 - - - build.buf - protobuf-javalite - 4.31.0 - - - -EOF +FROM maven:3.9.11-eclipse-temurin-21 AS maven-deps +COPY pom.xml /tmp/pom.xml RUN cd /tmp && mvn -f pom.xml dependency:go-offline FROM scratch diff --git a/plugins/community/salesforce-reactive-grpc/v1.2.4/pom.xml b/plugins/community/salesforce-reactive-grpc/v1.2.4/pom.xml new file mode 100644 index 000000000..4ad758518 --- /dev/null +++ b/plugins/community/salesforce-reactive-grpc/v1.2.4/pom.xml @@ -0,0 +1,54 @@ + + 4.0.0 + temp + temp + 1.0 + + + com.salesforce.servicelibs + reactor-grpc-stub + 1.2.4 + + + io.projectreactor + reactor-core + 3.5.4 + + + com.google.protobuf + protobuf-java + 4.31.0 + + + io.grpc + grpc-core + 1.73.0 + + + io.grpc + grpc-protobuf + 1.73.0 + + + io.grpc + grpc-stub + 1.73.0 + + + + io.grpc + grpc-protobuf-lite + 1.73.0 + + + com.google.protobuf + protobuf-javalite + 4.31.0 + + + build.buf + protobuf-javalite + 4.31.0 + + + diff --git a/plugins/connectrpc/kotlin/v0.7.4/Dockerfile b/plugins/connectrpc/kotlin/v0.7.4/Dockerfile index 60a7f688d..22137d55d 100644 --- a/plugins/connectrpc/kotlin/v0.7.4/Dockerfile +++ b/plugins/connectrpc/kotlin/v0.7.4/Dockerfile @@ -8,85 +8,8 @@ RUN curl -fsSL -o /app/protoc-gen-connect-kotlin.jar https://repo1.maven.org/mav FROM gcr.io/distroless/java21-debian12:latest@sha256:7c05bf8a64ff1a70a16083e9bdd35b463aa0d014c2fc782d31d13ea7a61de633 as base -FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - 4.0.0 - temp - temp - 1.0 - - - com.connectrpc - connect-kotlin - 0.7.4 - - - com.connectrpc - connect-kotlin-google-java-ext - 0.7.4 - - - com.connectrpc - connect-kotlin-okhttp - 0.7.4 - - - com.google.protobuf - protobuf-kotlin - 4.31.1 - - - org.jetbrains.kotlin - kotlin-stdlib - 1.8.22 - - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - 1.8.22 - - - com.google.protobuf - protobuf-java - 4.31.1 - - - - com.connectrpc - connect-kotlin-google-javalite-ext - 0.7.4 - - - com.google.protobuf - protobuf-kotlin-lite - 4.31.1 - - - com.google.protobuf - protobuf-javalite - 4.31.1 - - - build.buf - protobuf-javalite - 4.31.1 - - - - - - org.jetbrains.kotlin - kotlin-maven-plugin - 2.1.0 - - 1.8 - - - - - -EOF +FROM maven:3.9.11-eclipse-temurin-21 AS maven-deps +COPY pom.xml /tmp/pom.xml RUN cd /tmp && mvn -f pom.xml dependency:go-offline FROM scratch diff --git a/plugins/connectrpc/kotlin/v0.7.4/pom.xml b/plugins/connectrpc/kotlin/v0.7.4/pom.xml new file mode 100644 index 000000000..b9d6a8bfe --- /dev/null +++ b/plugins/connectrpc/kotlin/v0.7.4/pom.xml @@ -0,0 +1,76 @@ + + 4.0.0 + temp + temp + 1.0 + + + com.connectrpc + connect-kotlin + 0.7.4 + + + com.connectrpc + connect-kotlin-google-java-ext + 0.7.4 + + + com.connectrpc + connect-kotlin-okhttp + 0.7.4 + + + com.google.protobuf + protobuf-kotlin + 4.31.1 + + + org.jetbrains.kotlin + kotlin-stdlib + 1.8.22 + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 1.8.22 + + + com.google.protobuf + protobuf-java + 4.31.1 + + + + com.connectrpc + connect-kotlin-google-javalite-ext + 0.7.4 + + + com.google.protobuf + protobuf-kotlin-lite + 4.31.1 + + + com.google.protobuf + protobuf-javalite + 4.31.1 + + + build.buf + protobuf-javalite + 4.31.1 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + 2.1.0 + + 1.8 + + + + + diff --git a/plugins/grpc/java/v1.79.0/Dockerfile b/plugins/grpc/java/v1.79.0/Dockerfile index 968653e85..a933e7d21 100644 --- a/plugins/grpc/java/v1.79.0/Dockerfile +++ b/plugins/grpc/java/v1.79.0/Dockerfile @@ -17,53 +17,8 @@ RUN arch=${TARGETARCH}; \ FROM gcr.io/distroless/cc-debian12:latest@sha256:72344f7f909a8bf003c67f55687e6d51a441b49661af8f660aa7b285f00e57df AS base -FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - 4.0.0 - temp - temp - 1.0 - - - io.grpc - grpc-core - 1.79.0 - - - io.grpc - grpc-protobuf - 1.79.0 - - - io.grpc - grpc-stub - 1.79.0 - - - com.google.protobuf - protobuf-java - 4.33.5 - - - - io.grpc - grpc-protobuf-lite - 1.79.0 - - - com.google.protobuf - protobuf-javalite - 4.33.5 - - - build.buf - protobuf-javalite - 4.33.5 - - - -EOF +FROM maven:3.9.11-eclipse-temurin-21 AS maven-deps +COPY pom.xml /tmp/pom.xml RUN cd /tmp && mvn -f pom.xml dependency:go-offline FROM scratch diff --git a/plugins/grpc/java/v1.79.0/pom.xml b/plugins/grpc/java/v1.79.0/pom.xml new file mode 100644 index 000000000..bb06ec6c0 --- /dev/null +++ b/plugins/grpc/java/v1.79.0/pom.xml @@ -0,0 +1,44 @@ + + 4.0.0 + temp + temp + 1.0 + + + io.grpc + grpc-core + 1.79.0 + + + io.grpc + grpc-protobuf + 1.79.0 + + + io.grpc + grpc-stub + 1.79.0 + + + com.google.protobuf + protobuf-java + 4.33.5 + + + + io.grpc + grpc-protobuf-lite + 1.79.0 + + + com.google.protobuf + protobuf-javalite + 4.33.5 + + + build.buf + protobuf-javalite + 4.33.5 + + + diff --git a/plugins/grpc/kotlin/v1.5.0/Dockerfile b/plugins/grpc/kotlin/v1.5.0/Dockerfile index 294916a70..d74acfe39 100644 --- a/plugins/grpc/kotlin/v1.5.0/Dockerfile +++ b/plugins/grpc/kotlin/v1.5.0/Dockerfile @@ -10,94 +10,8 @@ RUN curl -fsSL -o protoc-gen-grpc-kotlin.jar https://repo1.maven.org/maven2/io/g FROM gcr.io/distroless/java21-debian12:latest@sha256:418b2e2a9e452aa9299511427f2ae404dfc910ecfa78feb53b1c60c22c3b640c AS base -FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - 4.0.0 - temp - temp - 1.0 - - - io.grpc - grpc-kotlin-stub - 1.5.0 - - - org.jetbrains.kotlinx - kotlinx-coroutines-core-jvm - 1.10.2 - - - io.grpc - grpc-core - 1.75.0 - - - io.grpc - grpc-protobuf - 1.75.0 - - - io.grpc - grpc-stub - 1.75.0 - - - com.google.protobuf - protobuf-java - 4.32.0 - - - com.google.protobuf - protobuf-kotlin - 4.32.0 - - - org.jetbrains.kotlin - kotlin-stdlib - 1.8.22 - - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - 1.8.22 - - - - io.grpc - grpc-protobuf-lite - 1.75.0 - - - com.google.protobuf - protobuf-javalite - 4.32.0 - - - build.buf - protobuf-javalite - 4.32.0 - - - com.google.protobuf - protobuf-kotlin-lite - 4.32.0 - - - - - - org.jetbrains.kotlin - kotlin-maven-plugin - 2.2.20 - - - - - - -EOF +FROM maven:3.9.11-eclipse-temurin-21 AS maven-deps +COPY pom.xml /tmp/pom.xml RUN cd /tmp && mvn -f pom.xml dependency:go-offline FROM scratch diff --git a/plugins/grpc/kotlin/v1.5.0/pom.xml b/plugins/grpc/kotlin/v1.5.0/pom.xml new file mode 100644 index 000000000..6e18f9727 --- /dev/null +++ b/plugins/grpc/kotlin/v1.5.0/pom.xml @@ -0,0 +1,85 @@ + + 4.0.0 + temp + temp + 1.0 + + + io.grpc + grpc-kotlin-stub + 1.5.0 + + + org.jetbrains.kotlinx + kotlinx-coroutines-core-jvm + 1.10.2 + + + io.grpc + grpc-core + 1.75.0 + + + io.grpc + grpc-protobuf + 1.75.0 + + + io.grpc + grpc-stub + 1.75.0 + + + com.google.protobuf + protobuf-java + 4.32.0 + + + com.google.protobuf + protobuf-kotlin + 4.32.0 + + + org.jetbrains.kotlin + kotlin-stdlib + 1.8.22 + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 1.8.22 + + + + io.grpc + grpc-protobuf-lite + 1.75.0 + + + com.google.protobuf + protobuf-javalite + 4.32.0 + + + build.buf + protobuf-javalite + 4.32.0 + + + com.google.protobuf + protobuf-kotlin-lite + 4.32.0 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + 2.2.20 + + + + + + diff --git a/plugins/protocolbuffers/java/v33.5/Dockerfile b/plugins/protocolbuffers/java/v33.5/Dockerfile index 49b645a6e..06b3381cb 100644 --- a/plugins/protocolbuffers/java/v33.5/Dockerfile +++ b/plugins/protocolbuffers/java/v33.5/Dockerfile @@ -23,33 +23,8 @@ RUN bazelisk ${BAZEL_OPTS} build '//plugins:protoc-gen-java.stripped' FROM gcr.io/distroless/cc-debian12:latest@sha256:72344f7f909a8bf003c67f55687e6d51a441b49661af8f660aa7b285f00e57df AS base -FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - 4.0.0 - temp - temp - 1.0 - - - com.google.protobuf - protobuf-java - 4.33.5 - - - - com.google.protobuf - protobuf-javalite - 4.33.5 - - - build.buf - protobuf-javalite - 4.33.5 - - - -EOF +FROM maven:3.9.11-eclipse-temurin-21 AS maven-deps +COPY pom.xml /tmp/pom.xml RUN cd /tmp && mvn -f pom.xml dependency:go-offline FROM scratch diff --git a/plugins/protocolbuffers/java/v33.5/pom.xml b/plugins/protocolbuffers/java/v33.5/pom.xml new file mode 100644 index 000000000..09e0eca76 --- /dev/null +++ b/plugins/protocolbuffers/java/v33.5/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + temp + temp + 1.0 + + + com.google.protobuf + protobuf-java + 4.33.5 + + + + com.google.protobuf + protobuf-javalite + 4.33.5 + + + build.buf + protobuf-javalite + 4.33.5 + + + diff --git a/plugins/protocolbuffers/kotlin/v33.5/Dockerfile b/plugins/protocolbuffers/kotlin/v33.5/Dockerfile index 0d6b6dcac..82e22f49f 100644 --- a/plugins/protocolbuffers/kotlin/v33.5/Dockerfile +++ b/plugins/protocolbuffers/kotlin/v33.5/Dockerfile @@ -23,64 +23,8 @@ RUN bazelisk ${BAZEL_OPTS} build '//plugins:protoc-gen-kotlin.stripped' FROM gcr.io/distroless/cc-debian12:latest@sha256:72344f7f909a8bf003c67f55687e6d51a441b49661af8f660aa7b285f00e57df AS base -FROM maven:3.9.11-eclipse-temurin-25 AS maven-deps -COPY < - 4.0.0 - temp - temp - 1.0 - - - com.google.protobuf - protobuf-kotlin - 4.33.5 - - - org.jetbrains.kotlin - kotlin-stdlib - 1.8.22 - - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - 1.8.22 - - - com.google.protobuf - protobuf-java - 4.33.5 - - - - com.google.protobuf - protobuf-kotlin-lite - 4.33.5 - - - com.google.protobuf - protobuf-javalite - 4.33.5 - - - build.buf - protobuf-javalite - 4.33.5 - - - - - - org.jetbrains.kotlin - kotlin-maven-plugin - 1.8.22 - - - - - - -EOF +FROM maven:3.9.11-eclipse-temurin-21 AS maven-deps +COPY pom.xml /tmp/pom.xml RUN cd /tmp && mvn -f pom.xml dependency:go-offline FROM scratch diff --git a/plugins/protocolbuffers/kotlin/v33.5/pom.xml b/plugins/protocolbuffers/kotlin/v33.5/pom.xml new file mode 100644 index 000000000..ca57d2f7f --- /dev/null +++ b/plugins/protocolbuffers/kotlin/v33.5/pom.xml @@ -0,0 +1,55 @@ + + 4.0.0 + temp + temp + 1.0 + + + com.google.protobuf + protobuf-kotlin + 4.33.5 + + + org.jetbrains.kotlin + kotlin-stdlib + 1.8.22 + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 1.8.22 + + + com.google.protobuf + protobuf-java + 4.33.5 + + + + com.google.protobuf + protobuf-kotlin-lite + 4.33.5 + + + com.google.protobuf + protobuf-javalite + 4.33.5 + + + build.buf + protobuf-javalite + 4.33.5 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + 1.8.22 + + + + + + From 0a322947945a3678698045aac4488e7a294dee45 Mon Sep 17 00:00:00 2001 From: David Marby Date: Thu, 26 Feb 2026 20:10:47 +0100 Subject: [PATCH 10/12] Allow pom.xml in .dockerignore for maven plugins The maven-deps stage uses COPY pom.xml /tmp/pom.xml, but the .dockerignore excluded everything except the Dockerfile from the build context. Add !pom.xml to any .dockerignore when regenerating maven deps so the build context includes the POM file. --- internal/maven/regenerate.go | 30 ++++++++++++++++++- .../apple/servicetalk/v0.42.63/.dockerignore | 1 + .../connect-kotlin/v0.1.10/.dockerignore | 1 + .../validate-java/v1.3.3/.dockerignore | 1 + .../v1.2.4/.dockerignore | 1 + .../connectrpc/kotlin/v0.7.4/.dockerignore | 1 + plugins/grpc/java/v1.79.0/.dockerignore | 1 + plugins/grpc/kotlin/v1.5.0/.dockerignore | 1 + .../protocolbuffers/java/v33.5/.dockerignore | 1 + .../kotlin/v33.5/.dockerignore | 1 + 10 files changed, 38 insertions(+), 1 deletion(-) diff --git a/internal/maven/regenerate.go b/internal/maven/regenerate.go index d8392f98d..6cb42c5e9 100644 --- a/internal/maven/regenerate.go +++ b/internal/maven/regenerate.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremotepluginconfig" ) @@ -51,5 +52,32 @@ func RegenerateMavenDeps(pluginVersionDir, pluginsDir string) error { if err != nil { return fmt.Errorf("ensuring maven-deps stage: %w", err) } - return os.WriteFile(dockerfilePath, []byte(updated), 0644) //nolint:gosec + if err := os.WriteFile(dockerfilePath, []byte(updated), 0644); err != nil { //nolint:gosec + return fmt.Errorf("writing Dockerfile: %w", err) + } + dockerignorePath := filepath.Join(pluginVersionDir, ".dockerignore") + if err := ensureDockerignoreAllowsPOM(dockerignorePath); err != nil { + return fmt.Errorf("updating .dockerignore: %w", err) + } + return nil +} + +// ensureDockerignoreAllowsPOM adds "!pom.xml" to the .dockerignore if it +// exists and doesn't already allow pom.xml. The pom.xml must be present in +// the build context for the maven-deps COPY instruction to succeed. +func ensureDockerignoreAllowsPOM(path string) error { + content, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + return nil + } else if err != nil { + return err + } + const pomRule = "!pom.xml" + for _, line := range strings.Split(string(content), "\n") { + if strings.TrimSpace(line) == pomRule { + return nil + } + } + updated := strings.TrimRight(string(content), "\n") + "\n" + pomRule + "\n" + return os.WriteFile(path, []byte(updated), 0644) //nolint:gosec } diff --git a/plugins/apple/servicetalk/v0.42.63/.dockerignore b/plugins/apple/servicetalk/v0.42.63/.dockerignore index 5d0f124ff..7ff6309df 100644 --- a/plugins/apple/servicetalk/v0.42.63/.dockerignore +++ b/plugins/apple/servicetalk/v0.42.63/.dockerignore @@ -1,2 +1,3 @@ * !Dockerfile +!pom.xml diff --git a/plugins/bufbuild/connect-kotlin/v0.1.10/.dockerignore b/plugins/bufbuild/connect-kotlin/v0.1.10/.dockerignore index 5d0f124ff..7ff6309df 100644 --- a/plugins/bufbuild/connect-kotlin/v0.1.10/.dockerignore +++ b/plugins/bufbuild/connect-kotlin/v0.1.10/.dockerignore @@ -1,2 +1,3 @@ * !Dockerfile +!pom.xml diff --git a/plugins/bufbuild/validate-java/v1.3.3/.dockerignore b/plugins/bufbuild/validate-java/v1.3.3/.dockerignore index 5d0f124ff..7ff6309df 100644 --- a/plugins/bufbuild/validate-java/v1.3.3/.dockerignore +++ b/plugins/bufbuild/validate-java/v1.3.3/.dockerignore @@ -1,2 +1,3 @@ * !Dockerfile +!pom.xml diff --git a/plugins/community/salesforce-reactive-grpc/v1.2.4/.dockerignore b/plugins/community/salesforce-reactive-grpc/v1.2.4/.dockerignore index 5d0f124ff..7ff6309df 100644 --- a/plugins/community/salesforce-reactive-grpc/v1.2.4/.dockerignore +++ b/plugins/community/salesforce-reactive-grpc/v1.2.4/.dockerignore @@ -1,2 +1,3 @@ * !Dockerfile +!pom.xml diff --git a/plugins/connectrpc/kotlin/v0.7.4/.dockerignore b/plugins/connectrpc/kotlin/v0.7.4/.dockerignore index 5d0f124ff..7ff6309df 100644 --- a/plugins/connectrpc/kotlin/v0.7.4/.dockerignore +++ b/plugins/connectrpc/kotlin/v0.7.4/.dockerignore @@ -1,2 +1,3 @@ * !Dockerfile +!pom.xml diff --git a/plugins/grpc/java/v1.79.0/.dockerignore b/plugins/grpc/java/v1.79.0/.dockerignore index 5d0f124ff..7ff6309df 100644 --- a/plugins/grpc/java/v1.79.0/.dockerignore +++ b/plugins/grpc/java/v1.79.0/.dockerignore @@ -1,2 +1,3 @@ * !Dockerfile +!pom.xml diff --git a/plugins/grpc/kotlin/v1.5.0/.dockerignore b/plugins/grpc/kotlin/v1.5.0/.dockerignore index 5d0f124ff..7ff6309df 100644 --- a/plugins/grpc/kotlin/v1.5.0/.dockerignore +++ b/plugins/grpc/kotlin/v1.5.0/.dockerignore @@ -1,2 +1,3 @@ * !Dockerfile +!pom.xml diff --git a/plugins/protocolbuffers/java/v33.5/.dockerignore b/plugins/protocolbuffers/java/v33.5/.dockerignore index 9612bcbe0..f93a3ef18 100644 --- a/plugins/protocolbuffers/java/v33.5/.dockerignore +++ b/plugins/protocolbuffers/java/v33.5/.dockerignore @@ -2,3 +2,4 @@ !BUILD !Dockerfile !java.cc +!pom.xml diff --git a/plugins/protocolbuffers/kotlin/v33.5/.dockerignore b/plugins/protocolbuffers/kotlin/v33.5/.dockerignore index d02a40d82..c881939e8 100644 --- a/plugins/protocolbuffers/kotlin/v33.5/.dockerignore +++ b/plugins/protocolbuffers/kotlin/v33.5/.dockerignore @@ -2,3 +2,4 @@ !BUILD !Dockerfile !kotlin.cc +!pom.xml From daae4013b9e3b163b1e7ed977cfdffeb1c24c8e6 Mon Sep 17 00:00:00 2001 From: David Marby Date: Thu, 26 Feb 2026 20:27:32 +0100 Subject: [PATCH 11/12] Use SplitSeq instead of Split in range loop --- internal/maven/regenerate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/maven/regenerate.go b/internal/maven/regenerate.go index 6cb42c5e9..ea5d4f577 100644 --- a/internal/maven/regenerate.go +++ b/internal/maven/regenerate.go @@ -73,7 +73,7 @@ func ensureDockerignoreAllowsPOM(path string) error { return err } const pomRule = "!pom.xml" - for _, line := range strings.Split(string(content), "\n") { + for line := range strings.SplitSeq(string(content), "\n") { if strings.TrimSpace(line) == pomRule { return nil } From 6af799eec79533c9e3d59b7d08e9f6ae0747f32e Mon Sep 17 00:00:00 2001 From: David Marby Date: Thu, 26 Feb 2026 21:52:48 +0100 Subject: [PATCH 12/12] Address PR feedback --- internal/maven/pom_test.go | 23 ----------------------- internal/maven/regenerate.go | 5 ----- 2 files changed, 28 deletions(-) diff --git a/internal/maven/pom_test.go b/internal/maven/pom_test.go index 7370b8f05..dce76a13b 100644 --- a/internal/maven/pom_test.go +++ b/internal/maven/pom_test.go @@ -222,26 +222,3 @@ registry: }) } } - -func TestXMLEscape(t *testing.T) { - t.Parallel() - tests := []struct { - input string - expected string - }{ - {"normal-text", "normal-text"}, - {"", "<tag>"}, - {"a&b", "a&b"}, - {`"quoted"`, ""quoted""}, - {"'single'", "'single'"}, - {"<>&\"'", "<>&"'"}, - } - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - t.Parallel() - result, err := xmlEscape(tt.input) - require.NoError(t, err) - assert.Equal(t, tt.expected, result) - }) - } -} diff --git a/internal/maven/regenerate.go b/internal/maven/regenerate.go index ea5d4f577..5e3bff3ca 100644 --- a/internal/maven/regenerate.go +++ b/internal/maven/regenerate.go @@ -17,11 +17,6 @@ import ( // registry config. func RegenerateMavenDeps(pluginVersionDir, pluginsDir string) error { yamlPath := filepath.Join(pluginVersionDir, "buf.plugin.yaml") - if _, err := os.Stat(yamlPath); errors.Is(err, os.ErrNotExist) { - return nil - } else if err != nil { - return err - } pluginConfig, err := bufremotepluginconfig.ParseConfig(yamlPath) if err != nil { return err