Skip to content

Commit d585c36

Browse files
committed
feat(librariangen): add generate package
Based on https://github.com/googleapis/google-cloud-go/tree/main/internal/librariangen/generate with adaptation for Java. Currently it's just the scaffolding and more work is need to generate a usable GAPIC library. The `generate` package contains the core logic for the generation process, including: - Reading and parsing the `generate-request.json` from librarian. - Parsing `BUILD.bazel` files in the googleapis repository to extract GAPIC configuration. - Building and executing `protoc` with the `gapic-generator-java` plugin. - Unzipping and restructuring the generated files into the final library layout. A `run-generate-library.sh` script is included for local development and end-to-end testing of the generation process. Additionally, a `go.work` file has been added to the root of the repository to support the multi-module workspace structure.
1 parent feabef3 commit d585c36

File tree

10 files changed

+930
-4
lines changed

10 files changed

+930
-4
lines changed

go.work

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
go 1.24.7
2+
3+
use ./internal/librariangen

internal/librariangen/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
workspace/

internal/librariangen/bazel/parser.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828

2929
// Config holds configuration extracted from a googleapis BUILD.bazel file.
3030
type Config struct {
31+
gapicYAML string
3132
grpcServiceConfig string
3233
restNumericEnums bool
3334
serviceYAML string
@@ -38,6 +39,9 @@ type Config struct {
3839
// HasGAPIC indicates whether the GAPIC generator should be run.
3940
func (c *Config) HasGAPIC() bool { return c.hasGAPIC }
4041

42+
// GapicYAML is the GAPIC config file in the API version directory in googleapis.
43+
func (c *Config) GapicYAML() string { return c.gapicYAML }
44+
4145
// ServiceYAML is the client config file in the API version directory in googleapis.
4246
func (c *Config) ServiceYAML() string { return c.serviceYAML }
4347

@@ -81,6 +85,7 @@ func Parse(dir string) (*Config, error) {
8185
if c.restNumericEnums, err = findBool(gapicLibraryBlock, "rest_numeric_enums"); err != nil {
8286
return nil, fmt.Errorf("librariangen: failed to parse BUILD.bazel file %s: %w", fp, err)
8387
}
88+
c.gapicYAML = strings.TrimPrefix(findString(gapicLibraryBlock, "gapic_yaml"), ":")
8489
}
8590
if err := c.Validate(); err != nil {
8691
return nil, fmt.Errorf("librariangen: invalid bazel config in %s: %w", dir, err)

internal/librariangen/bazel/parser_test.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ java_grpc_library(
3131
java_gapic_library(
3232
name = "asset_java_gapic",
3333
srcs = [":asset_proto_with_info"],
34+
gapic_yaml = "cloudasset_gapic.yaml",
3435
grpc_service_config = "cloudasset_grpc_service_config.json",
3536
rest_numeric_enums = True,
3637
service_yaml = "cloudasset_v1.yaml",
@@ -67,6 +68,11 @@ java_gapic_library(
6768
t.Errorf("ServiceYAML() = %q; want %q", got.ServiceYAML(), want)
6869
}
6970
})
71+
t.Run("GapicYAML", func(t *testing.T) {
72+
if want := "cloudasset_gapic.yaml"; got.GapicYAML() != want {
73+
t.Errorf("GapicYAML() = %q; want %q", got.GapicYAML(), want)
74+
}
75+
})
7076
t.Run("GRPCServiceConfig", func(t *testing.T) {
7177
if want := "cloudasset_grpc_service_config.json"; got.GRPCServiceConfig() != want {
7278
t.Errorf("GRPCServiceConfig() = %q; want %q", got.GRPCServiceConfig(), want)
@@ -84,7 +90,7 @@ java_gapic_library(
8490
})
8591
}
8692

87-
func TestParse_serviceConfigIsTarget(t *testing.T) {
93+
func TestParse_configIsTarget(t *testing.T) {
8894
content := `
8995
java_grpc_library(
9096
name = "asset_java_grpc",
@@ -95,6 +101,7 @@ java_grpc_library(
95101
java_gapic_library(
96102
name = "asset_java_gapic",
97103
srcs = [":asset_proto_with_info"],
104+
gapic_yaml = ":cloudasset_gapic.yaml",
98105
grpc_service_config = "cloudasset_grpc_service_config.json",
99106
rest_numeric_enums = True,
100107
service_yaml = ":cloudasset_v1.yaml",
@@ -124,6 +131,9 @@ java_gapic_library(
124131
if want := "cloudasset_v1.yaml"; got.ServiceYAML() != want {
125132
t.Errorf("ServiceYAML() = %q; want %q", got.ServiceYAML(), want)
126133
}
134+
if want := "cloudasset_gapic.yaml"; got.GapicYAML() != want {
135+
t.Errorf("GapicYAML() = %q; want %q", got.GapicYAML(), want)
136+
}
127137
}
128138

129139
func TestConfig_Validate(t *testing.T) {
@@ -136,6 +146,7 @@ func TestConfig_Validate(t *testing.T) {
136146
name: "valid GAPIC",
137147
cfg: &Config{
138148
hasGAPIC: true,
149+
gapicYAML: "a",
139150
serviceYAML: "b",
140151
grpcServiceConfig: "c",
141152
transport: "d",
@@ -149,7 +160,7 @@ func TestConfig_Validate(t *testing.T) {
149160
},
150161
{
151162
name: "gRPC service config and transport are optional",
152-
cfg: &Config{hasGAPIC: true, serviceYAML: "b"},
163+
cfg: &Config{hasGAPIC: true, serviceYAML: "b", gapicYAML: "a"},
153164
wantErr: false,
154165
},
155166
{
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
// Copyright 2025 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+
// http://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 generate
16+
17+
import (
18+
"archive/zip"
19+
"context"
20+
"errors"
21+
"fmt"
22+
"io"
23+
"log/slog"
24+
"os"
25+
"path/filepath"
26+
27+
"cloud.google.com/java/internal/librariangen/bazel"
28+
"cloud.google.com/java/internal/librariangen/execv"
29+
"cloud.google.com/java/internal/librariangen/protoc"
30+
"cloud.google.com/java/internal/librariangen/request"
31+
)
32+
33+
// Test substitution vars.
34+
var (
35+
bazelParse = bazel.Parse
36+
execvRun = execv.Run
37+
requestParse = request.ParseLibrary
38+
protocBuild = protoc.Build
39+
)
40+
41+
// Config holds the internal librariangen configuration for the generate command.
42+
type Config struct {
43+
// LibrarianDir is the path to the librarian-tool input directory.
44+
// It is expected to contain the generate-request.json file.
45+
LibrarianDir string
46+
// InputDir is the path to the .librarian/generator-input directory from the
47+
// language repository.
48+
InputDir string
49+
// OutputDir is the path to the empty directory where librariangen writes
50+
// its output.
51+
OutputDir string
52+
// SourceDir is the path to a complete checkout of the googleapis repository.
53+
SourceDir string
54+
}
55+
56+
// Validate ensures that the configuration is valid.
57+
func (c *Config) Validate() error {
58+
if c.LibrarianDir == "" {
59+
return errors.New("librariangen: librarian directory must be set")
60+
}
61+
if c.InputDir == "" {
62+
return errors.New("librariangen: input directory must be set")
63+
}
64+
if c.OutputDir == "" {
65+
return errors.New("librariangen: output directory must be set")
66+
}
67+
if c.SourceDir == "" {
68+
return errors.New("librariangen: source directory must be set")
69+
}
70+
return nil
71+
}
72+
73+
// Generate is the main entrypoint for the `generate` command. It orchestrates
74+
// the entire generation process.
75+
func Generate(ctx context.Context, cfg *Config) error {
76+
if err := cfg.Validate(); err != nil {
77+
return fmt.Errorf("librariangen: invalid configuration: %w", err)
78+
}
79+
slog.Debug("librariangen: generate command started")
80+
defer cleanupIntermediateFiles(cfg.OutputDir)
81+
82+
generateReq, err := readGenerateReq(cfg.LibrarianDir)
83+
if err != nil {
84+
return fmt.Errorf("librariangen: failed to read request: %w", err)
85+
}
86+
87+
if err := invokeProtoc(ctx, cfg, generateReq); err != nil {
88+
return fmt.Errorf("librariangen: gapic generation failed: %w", err)
89+
}
90+
91+
// Unzip the generated zip file.
92+
zipPath := filepath.Join(cfg.OutputDir, "java_gapic.zip")
93+
if err := unzip(zipPath, cfg.OutputDir); err != nil {
94+
return fmt.Errorf("librariangen: failed to unzip %s: %w", zipPath, err)
95+
}
96+
97+
// Unzip the inner temp-codegen.srcjar.
98+
srcjarPath := filepath.Join(cfg.OutputDir, "temp-codegen.srcjar")
99+
srcjarDest := filepath.Join(cfg.OutputDir, "java_gapic_srcjar")
100+
if err := unzip(srcjarPath, srcjarDest); err != nil {
101+
return fmt.Errorf("librariangen: failed to unzip %s: %w", srcjarPath, err)
102+
}
103+
104+
if err := restructureOutput(cfg.OutputDir, generateReq.ID); err != nil {
105+
return fmt.Errorf("librariangen: failed to restructure output: %w", err)
106+
}
107+
108+
slog.Debug("librariangen: generate command finished")
109+
return nil
110+
}
111+
112+
// invokeProtoc handles the protoc GAPIC generation logic for the 'generate' CLI command.
113+
// It reads a request file, and for each API specified, it invokes protoc
114+
// to generate the client library. It returns the module path and the path to the service YAML.
115+
func invokeProtoc(ctx context.Context, cfg *Config, generateReq *request.Library) error {
116+
for _, api := range generateReq.APIs {
117+
apiServiceDir := filepath.Join(cfg.SourceDir, api.Path)
118+
slog.Info("processing api", "service_dir", apiServiceDir)
119+
bazelConfig, err := bazelParse(apiServiceDir)
120+
if err != nil {
121+
return fmt.Errorf("librariangen: failed to parse BUILD.bazel for %s: %w", apiServiceDir, err)
122+
}
123+
args, err := protocBuild(apiServiceDir, bazelConfig, cfg.SourceDir, cfg.OutputDir)
124+
if err != nil {
125+
return fmt.Errorf("librariangen: failed to build protoc command for api %q in library %q: %w", api.Path, generateReq.ID, err)
126+
}
127+
if err := execvRun(ctx, args, cfg.OutputDir); err != nil {
128+
return fmt.Errorf("librariangen: protoc failed for api %q in library %q: %w", api.Path, generateReq.ID, err)
129+
}
130+
}
131+
return nil
132+
}
133+
134+
// readGenerateReq reads generate-request.json from the librarian-tool input directory.
135+
// The request file tells librariangen which library and APIs to generate.
136+
// It is prepared by the Librarian tool and mounted at /librarian.
137+
func readGenerateReq(librarianDir string) (*request.Library, error) {
138+
reqPath := filepath.Join(librarianDir, "generate-request.json")
139+
slog.Debug("librariangen: reading generate request", "path", reqPath)
140+
141+
generateReq, err := requestParse(reqPath)
142+
if err != nil {
143+
return nil, err
144+
}
145+
slog.Debug("librariangen: successfully unmarshalled request", "library_id", generateReq.ID)
146+
return generateReq, nil
147+
}
148+
149+
// moveFiles moves all files (and directories) from sourceDir to targetDir.
150+
func moveFiles(sourceDir, targetDir string) error {
151+
files, err := os.ReadDir(sourceDir)
152+
if err != nil {
153+
return fmt.Errorf("librariangen: failed to read dir %s: %w", sourceDir, err)
154+
}
155+
for _, f := range files {
156+
oldPath := filepath.Join(sourceDir, f.Name())
157+
newPath := filepath.Join(targetDir, f.Name())
158+
slog.Debug("librariangen: moving file", "from", oldPath, "to", newPath)
159+
if err := os.Rename(oldPath, newPath); err != nil {
160+
return fmt.Errorf("librariangen: failed to move %s to %s: %w", oldPath, newPath, err)
161+
}
162+
}
163+
return nil
164+
}
165+
166+
func restructureOutput(outputDir, libraryID string) error {
167+
slog.Debug("librariangen: restructuring output directory", "dir", outputDir)
168+
169+
// Define source and destination directories.
170+
gapicSrcDir := filepath.Join(outputDir, "java_gapic_srcjar", "src", "main", "java")
171+
gapicTestDir := filepath.Join(outputDir, "java_gapic_srcjar", "src", "test", "java")
172+
protoSrcDir := filepath.Join(outputDir, "com")
173+
samplesDir := filepath.Join(outputDir, "java_gapic_srcjar", "samples", "snippets")
174+
175+
gapicDestDir := filepath.Join(outputDir, fmt.Sprintf("google-cloud-%s", libraryID), "src", "main", "java")
176+
gapicTestDestDir := filepath.Join(outputDir, fmt.Sprintf("google-cloud-%s", libraryID), "src", "test", "java")
177+
protoDestDir := filepath.Join(outputDir, fmt.Sprintf("proto-google-cloud-%s-v1", libraryID), "src", "main", "java")
178+
samplesDestDir := filepath.Join(outputDir, "samples", "snippets")
179+
180+
// Create destination directories.
181+
destDirs := []string{gapicDestDir, gapicTestDestDir, protoDestDir, samplesDestDir}
182+
for _, dir := range destDirs {
183+
if err := os.MkdirAll(dir, 0755); err != nil {
184+
return err
185+
}
186+
}
187+
188+
// Move files.
189+
moves := map[string]string{
190+
gapicSrcDir: gapicDestDir,
191+
gapicTestDir: gapicTestDestDir,
192+
protoSrcDir: protoDestDir,
193+
samplesDir: samplesDestDir,
194+
}
195+
for src, dest := range moves {
196+
if err := moveFiles(src, dest); err != nil {
197+
return err
198+
}
199+
}
200+
201+
return nil
202+
}
203+
204+
func cleanupIntermediateFiles(outputDir string) {
205+
slog.Debug("librariangen: cleaning up intermediate files", "dir", outputDir)
206+
filesToRemove := []string{
207+
"java_gapic_srcjar",
208+
"com",
209+
"java_gapic.zip",
210+
"temp-codegen.srcjar",
211+
}
212+
for _, file := range filesToRemove {
213+
path := filepath.Join(outputDir, file)
214+
if err := os.RemoveAll(path); err != nil {
215+
slog.Error("librariangen: failed to clean up intermediate file", "path", path, "error", err)
216+
}
217+
}
218+
}
219+
220+
func unzip(src, dest string) error {
221+
r, err := zip.OpenReader(src)
222+
if err != nil {
223+
return err
224+
}
225+
defer r.Close()
226+
227+
for _, f := range r.File {
228+
fpath := filepath.Join(dest, f.Name)
229+
if f.FileInfo().IsDir() {
230+
os.MkdirAll(fpath, os.ModePerm)
231+
continue
232+
}
233+
234+
if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
235+
return err
236+
}
237+
238+
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
239+
if err != nil {
240+
return err
241+
}
242+
243+
rc, err := f.Open()
244+
if err != nil {
245+
return err
246+
}
247+
248+
_, err = io.Copy(outFile, rc)
249+
250+
outFile.Close()
251+
rc.Close()
252+
253+
if err != nil {
254+
return err
255+
}
256+
}
257+
return nil
258+
}

0 commit comments

Comments
 (0)