Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go.work
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
go 1.24.7

use ./internal/librariangen
Comment thread
suztomo marked this conversation as resolved.
1 change: 1 addition & 0 deletions internal/librariangen/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
workspace/
5 changes: 5 additions & 0 deletions internal/librariangen/bazel/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

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

// GapicYAML is the GAPIC config file in the API version directory in googleapis.
func (c *Config) GapicYAML() string { return c.gapicYAML }

// ServiceYAML is the client config file in the API version directory in googleapis.
func (c *Config) ServiceYAML() string { return c.serviceYAML }

Expand Down Expand Up @@ -81,6 +85,7 @@ func Parse(dir string) (*Config, error) {
if c.restNumericEnums, err = findBool(gapicLibraryBlock, "rest_numeric_enums"); err != nil {
return nil, fmt.Errorf("librariangen: failed to parse BUILD.bazel file %s: %w", fp, err)
}
c.gapicYAML = strings.TrimPrefix(findString(gapicLibraryBlock, "gapic_yaml"), ":")
}
if err := c.Validate(); err != nil {
return nil, fmt.Errorf("librariangen: invalid bazel config in %s: %w", dir, err)
Expand Down
15 changes: 13 additions & 2 deletions internal/librariangen/bazel/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ java_grpc_library(
java_gapic_library(
name = "asset_java_gapic",
srcs = [":asset_proto_with_info"],
gapic_yaml = "cloudasset_gapic.yaml",
grpc_service_config = "cloudasset_grpc_service_config.json",
rest_numeric_enums = True,
service_yaml = "cloudasset_v1.yaml",
Expand Down Expand Up @@ -67,6 +68,11 @@ java_gapic_library(
t.Errorf("ServiceYAML() = %q; want %q", got.ServiceYAML(), want)
}
})
t.Run("GapicYAML", func(t *testing.T) {
if want := "cloudasset_gapic.yaml"; got.GapicYAML() != want {
t.Errorf("GapicYAML() = %q; want %q", got.GapicYAML(), want)
}
})
t.Run("GRPCServiceConfig", func(t *testing.T) {
if want := "cloudasset_grpc_service_config.json"; got.GRPCServiceConfig() != want {
t.Errorf("GRPCServiceConfig() = %q; want %q", got.GRPCServiceConfig(), want)
Expand All @@ -84,7 +90,7 @@ java_gapic_library(
})
}

func TestParse_serviceConfigIsTarget(t *testing.T) {
func TestParse_configIsTarget(t *testing.T) {
content := `
java_grpc_library(
name = "asset_java_grpc",
Expand All @@ -95,6 +101,7 @@ java_grpc_library(
java_gapic_library(
name = "asset_java_gapic",
srcs = [":asset_proto_with_info"],
gapic_yaml = ":cloudasset_gapic.yaml",
grpc_service_config = "cloudasset_grpc_service_config.json",
rest_numeric_enums = True,
service_yaml = ":cloudasset_v1.yaml",
Expand Down Expand Up @@ -124,6 +131,9 @@ java_gapic_library(
if want := "cloudasset_v1.yaml"; got.ServiceYAML() != want {
t.Errorf("ServiceYAML() = %q; want %q", got.ServiceYAML(), want)
}
if want := "cloudasset_gapic.yaml"; got.GapicYAML() != want {
t.Errorf("GapicYAML() = %q; want %q", got.GapicYAML(), want)
}
}

func TestConfig_Validate(t *testing.T) {
Expand All @@ -136,6 +146,7 @@ func TestConfig_Validate(t *testing.T) {
name: "valid GAPIC",
cfg: &Config{
hasGAPIC: true,
gapicYAML: "a",
serviceYAML: "b",
grpcServiceConfig: "c",
transport: "d",
Expand All @@ -149,7 +160,7 @@ func TestConfig_Validate(t *testing.T) {
},
{
name: "gRPC service config and transport are optional",
cfg: &Config{hasGAPIC: true, serviceYAML: "b"},
cfg: &Config{hasGAPIC: true, serviceYAML: "b", gapicYAML: "a"},
wantErr: false,
},
{
Expand Down
258 changes: 258 additions & 0 deletions internal/librariangen/generate/generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package generate

import (
"archive/zip"
"context"
"errors"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"

"cloud.google.com/java/internal/librariangen/bazel"
"cloud.google.com/java/internal/librariangen/execv"
"cloud.google.com/java/internal/librariangen/protoc"
"cloud.google.com/java/internal/librariangen/request"
)

// Test substitution vars.
var (
bazelParse = bazel.Parse
execvRun = execv.Run
requestParse = request.ParseLibrary
protocBuild = protoc.Build
)

// Config holds the internal librariangen configuration for the generate command.
type Config struct {
// LibrarianDir is the path to the librarian-tool input directory.
// It is expected to contain the generate-request.json file.
LibrarianDir string
// InputDir is the path to the .librarian/generator-input directory from the
// language repository.
InputDir string
// OutputDir is the path to the empty directory where librariangen writes
// its output.
OutputDir string
// SourceDir is the path to a complete checkout of the googleapis repository.
SourceDir string
}

// Validate ensures that the configuration is valid.
func (c *Config) Validate() error {
if c.LibrarianDir == "" {
return errors.New("librariangen: librarian directory must be set")
}
if c.InputDir == "" {
return errors.New("librariangen: input directory must be set")
}
if c.OutputDir == "" {
return errors.New("librariangen: output directory must be set")
}
if c.SourceDir == "" {
return errors.New("librariangen: source directory must be set")
}
return nil
}

// Generate is the main entrypoint for the `generate` command. It orchestrates
// the entire generation process.
func Generate(ctx context.Context, cfg *Config) error {
if err := cfg.Validate(); err != nil {
return fmt.Errorf("librariangen: invalid configuration: %w", err)
}
slog.Debug("librariangen: generate command started")
defer cleanupIntermediateFiles(cfg.OutputDir)

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

if err := invokeProtoc(ctx, cfg, generateReq); err != nil {
return fmt.Errorf("librariangen: gapic generation failed: %w", err)
}

// Unzip the generated zip file.
zipPath := filepath.Join(cfg.OutputDir, "java_gapic.zip")
if err := unzip(zipPath, cfg.OutputDir); err != nil {
return fmt.Errorf("librariangen: failed to unzip %s: %w", zipPath, err)
}

// Unzip the inner temp-codegen.srcjar.
srcjarPath := filepath.Join(cfg.OutputDir, "temp-codegen.srcjar")
srcjarDest := filepath.Join(cfg.OutputDir, "java_gapic_srcjar")
if err := unzip(srcjarPath, srcjarDest); err != nil {
return fmt.Errorf("librariangen: failed to unzip %s: %w", srcjarPath, err)
}

if err := restructureOutput(cfg.OutputDir, generateReq.ID); err != nil {
return fmt.Errorf("librariangen: failed to restructure output: %w", err)
}

slog.Debug("librariangen: generate command finished")
return nil
}

// invokeProtoc handles the protoc GAPIC generation logic for the 'generate' CLI command.
// It reads a request file, and for each API specified, it invokes protoc
// to generate the client library. It returns the module path and the path to the service YAML.
func invokeProtoc(ctx context.Context, cfg *Config, generateReq *request.Library) error {
for _, api := range generateReq.APIs {
apiServiceDir := filepath.Join(cfg.SourceDir, api.Path)
slog.Info("processing api", "service_dir", apiServiceDir)
bazelConfig, err := bazelParse(apiServiceDir)
if err != nil {
return fmt.Errorf("librariangen: failed to parse BUILD.bazel for %s: %w", apiServiceDir, err)
}
args, err := protocBuild(apiServiceDir, bazelConfig, cfg.SourceDir, cfg.OutputDir)
if err != nil {
return fmt.Errorf("librariangen: failed to build protoc command for api %q in library %q: %w", api.Path, generateReq.ID, err)
}
if err := execvRun(ctx, args, cfg.OutputDir); err != nil {
return fmt.Errorf("librariangen: protoc failed for api %q in library %q: %w", api.Path, generateReq.ID, err)
}
}
return nil
}

// readGenerateReq reads generate-request.json from the librarian-tool input directory.
// The request file tells librariangen which library and APIs to generate.
// It is prepared by the Librarian tool and mounted at /librarian.
func readGenerateReq(librarianDir string) (*request.Library, error) {
reqPath := filepath.Join(librarianDir, "generate-request.json")
slog.Debug("librariangen: reading generate request", "path", reqPath)

generateReq, err := requestParse(reqPath)
if err != nil {
return nil, err
}
slog.Debug("librariangen: successfully unmarshalled request", "library_id", generateReq.ID)
return generateReq, nil
}

// moveFiles moves all files (and directories) from sourceDir to targetDir.
func moveFiles(sourceDir, targetDir string) error {
files, err := os.ReadDir(sourceDir)
if err != nil {
return fmt.Errorf("librariangen: failed to read dir %s: %w", sourceDir, err)
}
for _, f := range files {
oldPath := filepath.Join(sourceDir, f.Name())
newPath := filepath.Join(targetDir, f.Name())
slog.Debug("librariangen: moving file", "from", oldPath, "to", newPath)
if err := os.Rename(oldPath, newPath); err != nil {
return fmt.Errorf("librariangen: failed to move %s to %s: %w", oldPath, newPath, err)
}
}
return nil
}

func restructureOutput(outputDir, libraryID string) error {
slog.Debug("librariangen: restructuring output directory", "dir", outputDir)

// Define source and destination directories.
gapicSrcDir := filepath.Join(outputDir, "java_gapic_srcjar", "src", "main", "java")
gapicTestDir := filepath.Join(outputDir, "java_gapic_srcjar", "src", "test", "java")
protoSrcDir := filepath.Join(outputDir, "com")
samplesDir := filepath.Join(outputDir, "java_gapic_srcjar", "samples", "snippets")

gapicDestDir := filepath.Join(outputDir, fmt.Sprintf("google-cloud-%s", libraryID), "src", "main", "java")
gapicTestDestDir := filepath.Join(outputDir, fmt.Sprintf("google-cloud-%s", libraryID), "src", "test", "java")
protoDestDir := filepath.Join(outputDir, fmt.Sprintf("proto-google-cloud-%s-v1", libraryID), "src", "main", "java")
samplesDestDir := filepath.Join(outputDir, "samples", "snippets")

// Create destination directories.
destDirs := []string{gapicDestDir, gapicTestDestDir, protoDestDir, samplesDestDir}
for _, dir := range destDirs {
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
}

// Move files.
moves := map[string]string{
gapicSrcDir: gapicDestDir,
gapicTestDir: gapicTestDestDir,
protoSrcDir: protoDestDir,
samplesDir: samplesDestDir,
}
for src, dest := range moves {
if err := moveFiles(src, dest); err != nil {
return err
}
}

return nil
}

func cleanupIntermediateFiles(outputDir string) {
slog.Debug("librariangen: cleaning up intermediate files", "dir", outputDir)
filesToRemove := []string{
"java_gapic_srcjar",
"com",
"java_gapic.zip",
"temp-codegen.srcjar",
}
for _, file := range filesToRemove {
path := filepath.Join(outputDir, file)
if err := os.RemoveAll(path); err != nil {
slog.Error("librariangen: failed to clean up intermediate file", "path", path, "error", err)
}
}
}

func unzip(src, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()

for _, f := range r.File {
fpath := filepath.Join(dest, f.Name)
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
continue
}

if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return err
}

outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}

rc, err := f.Open()
if err != nil {
return err
}

_, err = io.Copy(outFile, rc)

outFile.Close()
rc.Close()

if err != nil {
return err
}
Comment thread
meltsufin marked this conversation as resolved.
}
Comment thread Fixed
return nil
}
Loading
Loading