Skip to content

Commit f851caf

Browse files
authored
feat(internal/librarian/java): generate poms for proto- and grpc- modules if not exist (#4925)
If a pom.xml is not already present for proto- and grpc- modules, generate one from template. Fix #4919
1 parent 70e398c commit f851caf

File tree

11 files changed

+528
-30
lines changed

11 files changed

+528
-30
lines changed

internal/librarian/java/generate.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ func generateAPI(ctx context.Context, cfg *config.Config, api *config.API, libra
8686
bomVersion = library.Java.LibrariesBomVersion
8787
}
8888
p := postProcessParams{
89+
cfg: cfg,
90+
library: library,
8991
outDir: outdir,
9092
libraryName: library.Name,
9193
libraryVersion: library.Version,
@@ -152,6 +154,9 @@ func generateAPI(ctx context.Context, cfg *config.Config, api *config.API, libra
152154
}
153155

154156
func deriveDistributionName(library *config.Library) string {
157+
if library.Java != nil && library.Java.DistributionNameOverride != "" {
158+
return library.Java.DistributionNameOverride
159+
}
155160
groupID := "com.google.cloud"
156161
if library.Java != nil && library.Java.GroupID != "" {
157162
groupID = library.Java.GroupID

internal/librarian/java/generate_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ func TestDeriveDistributionName(t *testing.T) {
151151
},
152152
want: "com.custom:google-cloud-secretmanager",
153153
},
154+
{
155+
name: "distributionName override",
156+
library: &config.Library{
157+
Name: "secretmanager",
158+
Java: &config.JavaModule{DistributionNameOverride: "com.google.cloud:google-cloud-secretmanager-v1"},
159+
},
160+
want: "com.google.cloud:google-cloud-secretmanager-v1",
161+
},
154162
{
155163
name: "library name already has prefix",
156164
library: &config.Library{Name: "google-cloud-secretmanager"},

internal/librarian/java/pom.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package java
16+
17+
import (
18+
"fmt"
19+
"os"
20+
"path/filepath"
21+
"strings"
22+
23+
"github.com/googleapis/librarian/internal/config"
24+
"github.com/googleapis/librarian/internal/serviceconfig"
25+
)
26+
27+
const (
28+
protoPomTemplateName = "proto_pom.xml.tmpl"
29+
grpcPomTemplateName = "grpc_pom.xml.tmpl"
30+
grcpProtoGroupID = "com.google.api.grpc"
31+
)
32+
33+
// grpcProtoPomData holds the data for rendering POM templates.
34+
type grpcProtoPomData struct {
35+
Proto coordinates
36+
Grpc coordinates
37+
Parent coordinates
38+
Version string
39+
MainArtifactID string
40+
}
41+
42+
type coordinates struct {
43+
GroupID string
44+
ArtifactID string
45+
}
46+
47+
// javaModule represents a Maven module and its POM generation state.
48+
type javaModule struct {
49+
artifactID string
50+
dir string
51+
isMissing bool
52+
data grpcProtoPomData
53+
template string
54+
}
55+
56+
// generatePomsIfMissing generates missing proto-* and grpc-* POMs.
57+
func generatePomsIfMissing(library *config.Library, libraryDir, googleapisDir string) error {
58+
modules, err := collectModules(library, libraryDir, googleapisDir)
59+
if err != nil {
60+
return err
61+
}
62+
for _, m := range modules {
63+
if !m.isMissing {
64+
continue
65+
}
66+
if err := writePom(filepath.Join(m.dir, "pom.xml"), m.template, m.data); err != nil {
67+
return fmt.Errorf("failed to generate %s: %w", m.artifactID, err)
68+
}
69+
}
70+
return nil
71+
}
72+
73+
// collectModules identifies all expected proto-* and grpc-* modules
74+
// for the given library based on its configuration and checks a pom.xml presence
75+
// on the filesystem.
76+
//
77+
// All expected modules are collected (even if they exist) because the client
78+
// module's POM requires a full list of all proto and gRPC dependencies
79+
// to ensure its dependency list is fully synchronized.
80+
func collectModules(library *config.Library, libraryDir, googleapisDir string) ([]javaModule, error) {
81+
distName := deriveDistributionName(library)
82+
parts := strings.SplitN(distName, ":", 2)
83+
if len(parts) != 2 {
84+
return nil, fmt.Errorf("invalid distribution name %q: expected format groupID:artifactID", distName)
85+
}
86+
gapicGroupID := parts[0]
87+
gapicArtifactID := parts[1]
88+
89+
var modules []javaModule
90+
for _, api := range library.APIs {
91+
version := serviceconfig.ExtractVersion(api.Path)
92+
if version == "" {
93+
return nil, fmt.Errorf("failed to extract version from API path %q", api.Path)
94+
}
95+
96+
names := deriveModuleNames(gapicArtifactID, version)
97+
98+
apiCfg, err := serviceconfig.Find(googleapisDir, api.Path, config.LanguageJava)
99+
if err != nil {
100+
return nil, fmt.Errorf("failed to find api config for %s: %w", api.Path, err)
101+
}
102+
transport := apiCfg.Transport(config.LanguageJava)
103+
104+
data := grpcProtoPomData{
105+
Proto: coordinates{
106+
GroupID: grcpProtoGroupID,
107+
ArtifactID: names.proto,
108+
},
109+
Grpc: coordinates{
110+
GroupID: grcpProtoGroupID,
111+
ArtifactID: names.grpc,
112+
},
113+
Parent: coordinates{
114+
GroupID: gapicGroupID,
115+
ArtifactID: fmt.Sprintf("%s-parent", gapicArtifactID),
116+
},
117+
MainArtifactID: gapicArtifactID,
118+
Version: library.Version,
119+
}
120+
121+
// Proto module
122+
protoDir := filepath.Join(libraryDir, names.proto)
123+
isProtoMissing, err := isPomMissing(protoDir)
124+
if err != nil {
125+
return nil, err
126+
}
127+
modules = append(modules, javaModule{
128+
artifactID: names.proto,
129+
dir: protoDir,
130+
isMissing: isProtoMissing,
131+
data: data,
132+
template: protoPomTemplateName,
133+
})
134+
135+
// gRPC module
136+
if transport == serviceconfig.GRPC || transport == serviceconfig.GRPCRest {
137+
grpcDir := filepath.Join(libraryDir, names.grpc)
138+
isGrpcMissing, err := isPomMissing(grpcDir)
139+
if err != nil {
140+
return nil, err
141+
}
142+
modules = append(modules, javaModule{
143+
artifactID: names.grpc,
144+
dir: grpcDir,
145+
isMissing: isGrpcMissing,
146+
data: data,
147+
template: grpcPomTemplateName,
148+
})
149+
}
150+
}
151+
return modules, nil
152+
}
153+
154+
func isPomMissing(dir string) (bool, error) {
155+
pomPath := filepath.Join(dir, "pom.xml")
156+
if _, err := os.Stat(pomPath); err == nil {
157+
return false, nil
158+
}
159+
if _, err := os.Stat(dir); os.IsNotExist(err) {
160+
return false, fmt.Errorf("target directory %s does not exist: %w", dir, err)
161+
}
162+
return true, nil
163+
}
164+
165+
func writePom(pomPath, templateName string, data any) (err error) {
166+
f, err := os.Create(pomPath)
167+
if err != nil {
168+
return fmt.Errorf("failed to create %s: %w", pomPath, err)
169+
}
170+
defer func() {
171+
cerr := f.Close()
172+
if err == nil {
173+
err = cerr
174+
}
175+
}()
176+
if terr := templates.ExecuteTemplate(f, templateName, data); terr != nil {
177+
return fmt.Errorf("failed to execute template %s: %w", templateName, terr)
178+
}
179+
return nil
180+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package java
16+
17+
import (
18+
"errors"
19+
"flag"
20+
"os"
21+
"path/filepath"
22+
"testing"
23+
24+
"github.com/google/go-cmp/cmp"
25+
"github.com/googleapis/librarian/internal/config"
26+
)
27+
28+
// update is used to refresh the golden files in testdata/ when template
29+
// changes result in intentional output differences.
30+
// Usage: go test ./internal/librarian/java -v -update.
31+
var update = flag.Bool("update", false, "update golden files")
32+
33+
func TestSyncPoms_Golden(t *testing.T) {
34+
googleapisDir, err := filepath.Abs("../../testdata/googleapis")
35+
if err != nil {
36+
t.Fatal(err)
37+
}
38+
testdataDir := filepath.Join("testdata", "syncpoms", "secretmanager-v1")
39+
library := &config.Library{
40+
Name: "secretmanager",
41+
Version: "1.2.3",
42+
APIs: []*config.API{
43+
{Path: "google/cloud/secretmanager/v1"},
44+
},
45+
}
46+
tmpDir := t.TempDir()
47+
// Pre-create the directories that syncPoms expects to exist.
48+
protoArtifactID := "proto-google-cloud-secretmanager-v1"
49+
grpcArtifactID := "grpc-google-cloud-secretmanager-v1"
50+
if err := os.MkdirAll(filepath.Join(tmpDir, protoArtifactID), 0755); err != nil {
51+
t.Fatal(err)
52+
}
53+
if err := os.MkdirAll(filepath.Join(tmpDir, grpcArtifactID), 0755); err != nil {
54+
t.Fatal(err)
55+
}
56+
if err := generatePomsIfMissing(library, tmpDir, googleapisDir); err != nil {
57+
t.Fatal(err)
58+
}
59+
artifacts := []string{protoArtifactID, grpcArtifactID}
60+
for _, artifact := range artifacts {
61+
gotPath := filepath.Join(tmpDir, artifact, "pom.xml")
62+
got, err := os.ReadFile(gotPath)
63+
if err != nil {
64+
t.Fatal(err)
65+
}
66+
goldenPath := filepath.Join(testdataDir, artifact, "pom.xml")
67+
if *update {
68+
if err := os.MkdirAll(filepath.Dir(goldenPath), 0755); err != nil {
69+
t.Fatal(err)
70+
}
71+
if err := os.WriteFile(goldenPath, got, 0644); err != nil {
72+
t.Fatal(err)
73+
}
74+
}
75+
want, err := os.ReadFile(goldenPath)
76+
if err != nil {
77+
t.Fatal(err)
78+
}
79+
if diff := cmp.Diff(string(want), string(got)); diff != "" {
80+
t.Errorf("mismatch in %s (-want +got):\n%s\n\nHint: run 'go test ./internal/librarian/java -v -update' to update golden files.", artifact, diff)
81+
}
82+
}
83+
}
84+
85+
func TestCollectModules_Error(t *testing.T) {
86+
for _, test := range []struct {
87+
name string
88+
library *config.Library
89+
}{
90+
{
91+
name: "invalid distribution name",
92+
library: &config.Library{
93+
Java: &config.JavaModule{
94+
DistributionNameOverride: "invalid-name",
95+
},
96+
},
97+
},
98+
{
99+
name: "failed to find api config",
100+
library: &config.Library{
101+
APIs: []*config.API{
102+
{Path: "google/ads/unrecognized/v1"},
103+
},
104+
},
105+
},
106+
} {
107+
t.Run(test.name, func(t *testing.T) {
108+
if _, err := collectModules(test.library, t.TempDir(), "/nonexistent"); err == nil {
109+
t.Error("collectModules() error = nil, want non-nil")
110+
}
111+
})
112+
}
113+
}
114+
115+
func TestIsPomMissing(t *testing.T) {
116+
for _, test := range []struct {
117+
name string
118+
setup func(t *testing.T) string
119+
want bool
120+
}{
121+
{
122+
name: "pom exists",
123+
setup: func(t *testing.T) string {
124+
dir := t.TempDir()
125+
if err := os.WriteFile(filepath.Join(dir, "pom.xml"), []byte("content"), 0644); err != nil {
126+
t.Fatal(err)
127+
}
128+
return dir
129+
},
130+
},
131+
{
132+
name: "pom missing",
133+
setup: func(t *testing.T) string {
134+
return t.TempDir()
135+
},
136+
want: true,
137+
},
138+
} {
139+
t.Run(test.name, func(t *testing.T) {
140+
dir := test.setup(t)
141+
got, err := isPomMissing(dir)
142+
if err != nil {
143+
t.Fatal(err)
144+
}
145+
if got != test.want {
146+
t.Errorf("isPomMissing(%q) = %v, want %v", dir, got, test.want)
147+
}
148+
})
149+
}
150+
}
151+
152+
func TestIsPomMissing_DirMissingError(t *testing.T) {
153+
dir := filepath.Join(t.TempDir(), "nonexistent")
154+
_, err := isPomMissing(dir)
155+
if !errors.Is(err, os.ErrNotExist) {
156+
t.Errorf("isPomMissing(%q) error = %v, want %v", dir, err, os.ErrNotExist)
157+
}
158+
}

0 commit comments

Comments
 (0)