Skip to content

Commit c8a914a

Browse files
committed
feat(librariangen): generate grpc stubs and resource helpers
1 parent c86b4ea commit c8a914a

File tree

5 files changed

+162
-107
lines changed

5 files changed

+162
-107
lines changed

internal/librariangen/generate/generator.go

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -78,26 +78,25 @@ func Generate(ctx context.Context, cfg *Config) error {
7878
return fmt.Errorf("librariangen: invalid configuration: %w", err)
7979
}
8080
slog.Debug("librariangen: generate command started")
81-
defer cleanupIntermediateFiles(cfg.OutputDir)
81+
outputConfig := &protoc.OutputConfig{
82+
GAPICDir: filepath.Join(cfg.OutputDir, "gapic"),
83+
GRPCDir: filepath.Join(cfg.OutputDir, "grpc"),
84+
ProtoDir: filepath.Join(cfg.OutputDir, "proto"),
85+
}
86+
defer cleanupIntermediateFiles(outputConfig)
8287

8388
generateReq, err := readGenerateReq(cfg.LibrarianDir)
8489
if err != nil {
8590
return fmt.Errorf("librariangen: failed to read request: %w", err)
8691
}
8792

88-
if err := invokeProtoc(ctx, cfg, generateReq); err != nil {
93+
if err := invokeProtoc(ctx, cfg, generateReq, outputConfig); err != nil {
8994
return fmt.Errorf("librariangen: gapic generation failed: %w", err)
9095
}
9196

92-
// Unzip the generated zip file.
93-
zipPath := filepath.Join(cfg.OutputDir, "java_gapic.zip")
94-
if err := unzip(zipPath, cfg.OutputDir); err != nil {
95-
return fmt.Errorf("librariangen: failed to unzip %s: %w", zipPath, err)
96-
}
97-
98-
// Unzip the inner temp-codegen.srcjar.
99-
srcjarPath := filepath.Join(cfg.OutputDir, "temp-codegen.srcjar")
100-
srcjarDest := filepath.Join(cfg.OutputDir, "java_gapic_srcjar")
97+
// Unzip the temp-codegen.srcjar.
98+
srcjarPath := filepath.Join(outputConfig.GAPICDir, "temp-codegen.srcjar")
99+
srcjarDest := outputConfig.GAPICDir
101100
if err := unzip(srcjarPath, srcjarDest); err != nil {
102101
return fmt.Errorf("librariangen: failed to unzip %s: %w", srcjarPath, err)
103102
}
@@ -113,18 +112,26 @@ func Generate(ctx context.Context, cfg *Config) error {
113112
// invokeProtoc handles the protoc GAPIC generation logic for the 'generate' CLI command.
114113
// It reads a request file, and for each API specified, it invokes protoc
115114
// to generate the client library. It returns the module path and the path to the service YAML.
116-
func invokeProtoc(ctx context.Context, cfg *Config, generateReq *message.Library) error {
115+
func invokeProtoc(ctx context.Context, cfg *Config, generateReq *message.Library, outputConfig *protoc.OutputConfig) error {
117116
for _, api := range generateReq.APIs {
118117
apiServiceDir := filepath.Join(cfg.SourceDir, api.Path)
119118
slog.Info("processing api", "service_dir", apiServiceDir)
120119
bazelConfig, err := bazelParse(apiServiceDir)
121120
if err != nil {
122121
return fmt.Errorf("librariangen: failed to parse BUILD.bazel for %s: %w", apiServiceDir, err)
123122
}
124-
args, err := protocBuild(apiServiceDir, bazelConfig, cfg.SourceDir, cfg.OutputDir)
123+
args, err := protocBuild(apiServiceDir, bazelConfig, cfg.SourceDir, outputConfig)
125124
if err != nil {
126125
return fmt.Errorf("librariangen: failed to build protoc command for api %q in library %q: %w", api.Path, generateReq.ID, err)
127126
}
127+
128+
// Create protoc output directories.
129+
for _, dir := range []string{outputConfig.ProtoDir, outputConfig.GRPCDir, outputConfig.GAPICDir} {
130+
if err := os.MkdirAll(dir, 0755); err != nil {
131+
return err
132+
}
133+
}
134+
128135
if err := execvRun(ctx, args, cfg.OutputDir); err != nil {
129136
return fmt.Errorf("librariangen: protoc failed for api %q in library %q: %w", api.Path, generateReq.ID, err)
130137
}
@@ -168,29 +175,43 @@ func restructureOutput(outputDir, libraryID string) error {
168175
slog.Debug("librariangen: restructuring output directory", "dir", outputDir)
169176

170177
// Define source and destination directories.
171-
gapicSrcDir := filepath.Join(outputDir, "java_gapic_srcjar", "src", "main", "java")
172-
gapicTestDir := filepath.Join(outputDir, "java_gapic_srcjar", "src", "test", "java")
173-
protoSrcDir := filepath.Join(outputDir, "com")
174-
samplesDir := filepath.Join(outputDir, "java_gapic_srcjar", "samples", "snippets")
175-
178+
gapicSrcDir := filepath.Join(outputDir, "gapic", "src", "main", "java")
179+
gapicTestDir := filepath.Join(outputDir, "gapic", "src", "test", "java")
180+
protoSrcDir := filepath.Join(outputDir, "proto")
181+
resourceNameSrcDir := filepath.Join(outputDir, "gapic", "proto", "src", "main", "java")
182+
grpcSrcDir := filepath.Join(outputDir, "grpc")
183+
samplesDir := filepath.Join(outputDir, "gapic", "samples", "snippets")
184+
185+
// TODO(meltsufin): currently we assume we have a single API variant v1
176186
gapicDestDir := filepath.Join(outputDir, fmt.Sprintf("google-cloud-%s", libraryID), "src", "main", "java")
177187
gapicTestDestDir := filepath.Join(outputDir, fmt.Sprintf("google-cloud-%s", libraryID), "src", "test", "java")
178188
protoDestDir := filepath.Join(outputDir, fmt.Sprintf("proto-google-cloud-%s-v1", libraryID), "src", "main", "java")
189+
resourceNameDestDir := filepath.Join(outputDir, fmt.Sprintf("proto-google-cloud-%s-v1", libraryID), "src", "main", "java")
190+
grpcDestDir := filepath.Join(outputDir, fmt.Sprintf("grpc-google-cloud-%s-v1", libraryID), "src", "main", "java")
179191
samplesDestDir := filepath.Join(outputDir, "samples", "snippets")
180192

181193
// Create destination directories.
182-
destDirs := []string{gapicDestDir, gapicTestDestDir, protoDestDir, samplesDestDir}
194+
destDirs := []string{gapicDestDir, gapicTestDestDir, protoDestDir, samplesDestDir, grpcDestDir}
183195
for _, dir := range destDirs {
184196
if err := os.MkdirAll(dir, 0755); err != nil {
185197
return err
186198
}
187199
}
188200

189-
// Move files.
201+
// The resource name directory is not created if there are no resource names
202+
// to generate. We create it here to avoid errors later.
203+
if _, err := os.Stat(resourceNameSrcDir); os.IsNotExist(err) {
204+
if err := os.MkdirAll(resourceNameSrcDir, 0755); err != nil {
205+
return err
206+
}
207+
}
208+
209+
// Move files that won't have conflicts.
190210
moves := map[string]string{
191211
gapicSrcDir: gapicDestDir,
192212
gapicTestDir: gapicTestDestDir,
193213
protoSrcDir: protoDestDir,
214+
grpcSrcDir: grpcDestDir,
194215
samplesDir: samplesDestDir,
195216
}
196217
for src, dest := range moves {
@@ -199,19 +220,46 @@ func restructureOutput(outputDir, libraryID string) error {
199220
}
200221
}
201222

223+
// Merge the resource name files into the proto destination.
224+
if err := copyAndMerge(resourceNameSrcDir, resourceNameDestDir); err != nil {
225+
return err
226+
}
227+
202228
return nil
203229
}
204230

205-
func cleanupIntermediateFiles(outputDir string) {
206-
slog.Debug("librariangen: cleaning up intermediate files", "dir", outputDir)
207-
filesToRemove := []string{
208-
"java_gapic_srcjar",
209-
"com",
210-
"java_gapic.zip",
211-
"temp-codegen.srcjar",
231+
// copyAndMerge recursively copies the contents of src to dest, merging directories.
232+
func copyAndMerge(src, dest string) error {
233+
entries, err := os.ReadDir(src)
234+
if os.IsNotExist(err) {
235+
return nil
236+
}
237+
if err != nil {
238+
return err
212239
}
213-
for _, file := range filesToRemove {
214-
path := filepath.Join(outputDir, file)
240+
241+
for _, entry := range entries {
242+
srcPath := filepath.Join(src, entry.Name())
243+
destPath := filepath.Join(dest, entry.Name())
244+
if entry.IsDir() {
245+
if err := os.MkdirAll(destPath, 0755); err != nil {
246+
return err
247+
}
248+
if err := copyAndMerge(srcPath, destPath); err != nil {
249+
return err
250+
}
251+
} else {
252+
if err := os.Rename(srcPath, destPath); err != nil {
253+
return fmt.Errorf("librariangen: failed to move %s to %s: %w", srcPath, destPath, err)
254+
}
255+
}
256+
}
257+
return nil
258+
}
259+
260+
func cleanupIntermediateFiles(outputConfig *protoc.OutputConfig) {
261+
slog.Debug("librariangen: cleaning up intermediate files")
262+
for _, path := range []string{outputConfig.GAPICDir, outputConfig.GRPCDir, outputConfig.ProtoDir} {
215263
if err := os.RemoveAll(path); err != nil {
216264
slog.Error("librariangen: failed to clean up intermediate file", "path", path, "error", err)
217265
}

internal/librariangen/generate/generator_test.go

Lines changed: 54 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
"path/filepath"
2525
"strings"
2626
"testing"
27+
28+
"cloud.google.com/java/internal/librariangen/protoc"
2729
)
2830

2931
// testEnv encapsulates a temporary test environment.
@@ -110,40 +112,15 @@ func createFakeZip(t *testing.T, path string) {
110112
zipWriter := zip.NewWriter(newZipFile)
111113
defer zipWriter.Close()
112114

113-
// Create a temporary empty zip file to be included in the main zip file.
114-
tmpfile, err := os.CreateTemp("", "temp-zip-*.zip")
115-
if err != nil {
116-
t.Fatalf("failed to create temp file: %v", err)
117-
}
118-
defer os.Remove(tmpfile.Name())
119-
120-
tempZipWriter := zip.NewWriter(tmpfile)
121-
// Add the src/main/java directory to the inner zip file.
122-
_, err = tempZipWriter.Create("src/main/java/")
115+
// Add the src/main/java directory to the zip file.
116+
_, err = zipWriter.Create("src/main/java/")
123117
if err != nil {
124118
t.Fatalf("failed to create directory in zip: %v", err)
125119
}
126-
_, err = tempZipWriter.Create("src/test/java/")
120+
_, err = zipWriter.Create("src/test/java/")
127121
if err != nil {
128122
t.Fatalf("failed to create directory in zip: %v", err)
129123
}
130-
tempZipWriter.Close()
131-
132-
// Read the content of the temporary zip file.
133-
zipBytes, err := os.ReadFile(tmpfile.Name())
134-
if err != nil {
135-
t.Fatalf("failed to read temp zip file: %v", err)
136-
}
137-
138-
// Add the temporary zip file to the main zip file as temp-codegen.srcjar.
139-
w, err := zipWriter.Create("temp-codegen.srcjar")
140-
if err != nil {
141-
t.Fatalf("failed to create empty file in zip: %v", err)
142-
}
143-
_, err = w.Write(zipBytes)
144-
if err != nil {
145-
t.Fatalf("failed to write content to zip: %v", err)
146-
}
147124
}
148125

149126
func TestGenerate(t *testing.T) {
@@ -255,12 +232,19 @@ java_gapic_library(
255232
}
256233
if tt.protocErr == nil && tt.name != "unzip fails" {
257234
// Simulate protoc creating the zip file.
258-
createFakeZip(t, filepath.Join(e.outputDir, "java_gapic.zip"))
235+
zipPath := filepath.Join(e.outputDir, "gapic", "temp-codegen.srcjar")
236+
if err := os.MkdirAll(filepath.Dir(zipPath), 0755); err != nil {
237+
t.Fatalf("failed to create directory: %v", err)
238+
}
239+
createFakeZip(t, zipPath)
259240
// Create the directory that is expected by restructureOutput.
260-
if err := os.MkdirAll(filepath.Join(e.outputDir, "com"), 0755); err != nil {
241+
if err := os.MkdirAll(filepath.Join(e.outputDir, "gapic", "src", "main", "java"), 0755); err != nil {
242+
t.Fatalf("failed to create directory: %v", err)
243+
}
244+
if err := os.MkdirAll(filepath.Join(e.outputDir, "gapic", "src", "test", "java"), 0755); err != nil {
261245
t.Fatalf("failed to create directory: %v", err)
262246
}
263-
if err := os.MkdirAll(filepath.Join(e.outputDir, "java_gapic_srcjar", "samples", "snippets"), 0755); err != nil {
247+
if err := os.MkdirAll(filepath.Join(e.outputDir, "gapic", "samples", "snippets"), 0755); err != nil {
264248
t.Fatalf("failed to create directory: %v", err)
265249
}
266250
}
@@ -348,48 +332,45 @@ func TestConfig_Validate(t *testing.T) {
348332
func TestRestructureOutput(t *testing.T) {
349333
e := newTestEnv(t)
350334
defer e.cleanup(t)
351-
// Create dummy files and directories to be restructured.
352-
if err := os.MkdirAll(filepath.Join(e.outputDir, "java_gapic_srcjar", "src", "main", "java", "com"), 0755); err != nil {
353-
t.Fatal(err)
354-
}
355-
if err := os.WriteFile(filepath.Join(e.outputDir, "java_gapic_srcjar", "src", "main", "java", "com", "foo.java"), nil, 0644); err != nil {
356-
t.Fatal(err)
357-
}
358-
if err := os.MkdirAll(filepath.Join(e.outputDir, "java_gapic_srcjar", "src", "test", "java", "com"), 0755); err != nil {
359-
t.Fatal(err)
360-
}
361-
if err := os.WriteFile(filepath.Join(e.outputDir, "java_gapic_srcjar", "src", "test", "java", "com", "foo_test.java"), nil, 0644); err != nil {
362-
t.Fatal(err)
363-
}
364-
if err := os.MkdirAll(filepath.Join(e.outputDir, "com"), 0755); err != nil {
365-
t.Fatal(err)
366-
}
367-
if err := os.WriteFile(filepath.Join(e.outputDir, "com", "bar.proto"), nil, 0644); err != nil {
368-
t.Fatal(err)
369-
}
370-
if err := os.MkdirAll(filepath.Join(e.outputDir, "java_gapic_srcjar", "samples", "snippets", "com"), 0755); err != nil {
371-
t.Fatal(err)
372-
}
373-
if err := os.WriteFile(filepath.Join(e.outputDir, "java_gapic_srcjar", "samples", "snippets", "com", "baz.java"), nil, 0644); err != nil {
374-
t.Fatal(err)
335+
336+
// 1. Setup: Create all the source directories and dummy files.
337+
sourceFiles := map[string]string{
338+
"gapic/src/main/java/com/google/foo.java": "",
339+
"gapic/src/test/java/com/google/foo_test.java": "",
340+
"proto/com/google/bar.proto": "",
341+
"grpc/com/google/bar_grpc.java": "",
342+
"gapic/samples/snippets/com/google/baz.java": "",
343+
"gapic/proto/src/main/java/com/google/resname.java": "",
344+
}
345+
for path, content := range sourceFiles {
346+
fullPath := filepath.Join(e.outputDir, path)
347+
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
348+
t.Fatalf("failed to create source directory for %s: %v", path, err)
349+
}
350+
if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil {
351+
t.Fatalf("failed to write source file for %s: %v", path, err)
352+
}
375353
}
376354

355+
// 2. Execute: Call the function under test.
377356
if err := restructureOutput(e.outputDir, "my-library"); err != nil {
378357
t.Fatalf("restructureOutput() failed: %v", err)
379358
}
380359

381-
// Check that the files were moved to the correct locations.
382-
if _, err := os.Stat(filepath.Join(e.outputDir, "google-cloud-my-library", "src", "main", "java", "com", "foo.java")); err != nil {
383-
t.Errorf("file not moved to main: %v", err)
384-
}
385-
if _, err := os.Stat(filepath.Join(e.outputDir, "google-cloud-my-library", "src", "test", "java", "com", "foo_test.java")); err != nil {
386-
t.Errorf("file not moved to test: %v", err)
360+
// 3. Verify: Check that all files were moved to their expected destinations.
361+
expectedFiles := []string{
362+
"google-cloud-my-library/src/main/java/com/google/foo.java",
363+
"google-cloud-my-library/src/test/java/com/google/foo_test.java",
364+
"proto-google-cloud-my-library-v1/src/main/java/com/google/bar.proto",
365+
"grpc-google-cloud-my-library-v1/src/main/java/com/google/bar_grpc.java",
366+
"samples/snippets/com/google/baz.java",
367+
"proto-google-cloud-my-library-v1/src/main/java/com/google/resname.java",
387368
}
388-
if _, err := os.Stat(filepath.Join(e.outputDir, "proto-google-cloud-my-library-v1", "src", "main", "java", "bar.proto")); err != nil {
389-
t.Errorf("file not moved to proto: %v", err)
390-
}
391-
if _, err := os.Stat(filepath.Join(e.outputDir, "samples", "snippets", "com", "baz.java")); err != nil {
392-
t.Errorf("file not moved to samples: %v", err)
369+
for _, path := range expectedFiles {
370+
fullPath := filepath.Join(e.outputDir, path)
371+
if _, err := os.Stat(fullPath); err != nil {
372+
t.Errorf("expected file not found at %s: %v", fullPath, err)
373+
}
393374
}
394375
}
395376

@@ -502,7 +483,7 @@ func TestCleanupIntermediateFiles(t *testing.T) {
502483
defer e.cleanup(t)
503484

504485
// Create a file that cannot be deleted.
505-
protectedDir := filepath.Join(e.outputDir, "com")
486+
protectedDir := filepath.Join(e.outputDir, "proto")
506487
if err := os.Mkdir(protectedDir, 0755); err != nil {
507488
t.Fatalf("failed to create protected dir: %v", err)
508489
}
@@ -515,7 +496,12 @@ func TestCleanupIntermediateFiles(t *testing.T) {
515496
}
516497
defer os.Chmod(protectedDir, 0755) // Restore permissions for cleanup.
517498

518-
cleanupIntermediateFiles(e.outputDir)
499+
outputConfig := &protoc.OutputConfig{
500+
GAPICDir: filepath.Join(e.outputDir, "gapic"),
501+
GRPCDir: filepath.Join(e.outputDir, "grpc"),
502+
ProtoDir: protectedDir,
503+
}
504+
cleanupIntermediateFiles(outputConfig)
519505

520506
if !strings.Contains(buf.String(), "failed to clean up intermediate file") {
521507
t.Errorf("cleanupIntermediateFiles() should log an error on failure, but did not. Log: %s", buf.String())

internal/librariangen/protoc/protoc.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"os"
2020
"path/filepath"
2121
"strings"
22-
2322
)
2423

2524
// ConfigProvider is an interface that describes the configuration needed
@@ -34,8 +33,15 @@ type ConfigProvider interface {
3433
HasGAPIC() bool
3534
}
3635

36+
// OutputConfig provides paths to directories to be used for protoc output.
37+
type OutputConfig struct {
38+
GAPICDir string
39+
GRPCDir string
40+
ProtoDir string
41+
}
42+
3743
// Build constructs the full protoc command arguments for a given API.
38-
func Build(apiServiceDir string, config ConfigProvider, sourceDir, outputDir string) ([]string, error) {
44+
func Build(apiServiceDir string, config ConfigProvider, sourceDir string, outputConfig *OutputConfig) ([]string, error) {
3945
// Gather all .proto files in the API's source directory.
4046
entries, err := os.ReadDir(apiServiceDir)
4147
if err != nil {
@@ -78,9 +84,12 @@ func Build(apiServiceDir string, config ConfigProvider, sourceDir, outputDir str
7884
"--experimental_allow_proto3_optional",
7985
}
8086

81-
args = append(args, fmt.Sprintf("--java_out=%s", outputDir))
87+
args = append(args, fmt.Sprintf("--java_out=%s", outputConfig.ProtoDir))
88+
if config.Transport() != "" && config.Transport() != "rest" {
89+
args = append(args, fmt.Sprintf("--java_grpc_out=%s", outputConfig.GRPCDir))
90+
}
8291
if config.HasGAPIC() {
83-
args = append(args, fmt.Sprintf("--java_gapic_out=metadata:%s", filepath.Join(outputDir, "java_gapic.zip")))
92+
args = append(args, fmt.Sprintf("--java_gapic_out=metadata:%s", outputConfig.GAPICDir))
8493

8594
if len(gapicOpts) > 0 {
8695
args = append(args, "--java_gapic_opt="+strings.Join(gapicOpts, ","))

0 commit comments

Comments
 (0)