From 676031a691597ef680999e40717a47c876058e2c Mon Sep 17 00:00:00 2001 From: Mike Eltsufin Date: Tue, 7 Oct 2025 19:37:26 +0000 Subject: [PATCH 1/3] feat: initial librariangen CLI and Docker support for Java --- .github/workflows/librariangen-ci.yml | 49 ++++++++++++++ internal/librariangen/Dockerfile | 65 ++++++++++++++++++ internal/librariangen/README.md | 3 + internal/librariangen/go.mod | 3 + internal/librariangen/main.go | 85 ++++++++++++++++++++++++ internal/librariangen/main_test.go | 94 +++++++++++++++++++++++++++ 6 files changed, 299 insertions(+) create mode 100644 .github/workflows/librariangen-ci.yml create mode 100644 internal/librariangen/Dockerfile create mode 100644 internal/librariangen/README.md create mode 100644 internal/librariangen/go.mod create mode 100644 internal/librariangen/main.go create mode 100644 internal/librariangen/main_test.go diff --git a/.github/workflows/librariangen-ci.yml b/.github/workflows/librariangen-ci.yml new file mode 100644 index 0000000000..190477ad37 --- /dev/null +++ b/.github/workflows/librariangen-ci.yml @@ -0,0 +1,49 @@ +name: librariangen CI + +on: + push: + branches: [ main ] + paths: + - 'internal/librariangen/**' + pull_request: + paths: + - 'internal/librariangen/**' +permissions: + contents: read + +jobs: + test-docker-image: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + + - name: Display Go version + run: go version + + - name: Run Go tests + run: go test ./... + working-directory: internal/librariangen + + - name: Build Docker image + run: docker build -t librariangen-test ./internal/librariangen + + - name: Run version command + id: version + run: | + output=$(docker run --rm -e GOOGLE_SDK_JAVA_LOGGING_LEVEL=quiet librariangen-test --version) + echo "version_output=$output" >> $GITHUB_OUTPUT + + - name: Verify version + run: | + version_output="${{ steps.version.outputs.version_output }}" + if [[ ! "$version_output" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Version format is incorrect. Got: $version_output" + exit 1 + fi + echo "Version check passed. Version is $version_output" diff --git a/internal/librariangen/Dockerfile b/internal/librariangen/Dockerfile new file mode 100644 index 0000000000..85fa27d579 --- /dev/null +++ b/internal/librariangen/Dockerfile @@ -0,0 +1,65 @@ +# 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. + +# This Dockerfile creates a MOSS-compliant image for the Go librariangen, +# which is invoked by the Librarian tool. It uses a multi-stage build to +# create a minimal final image. + +# --- Builder Stage --- +# This stage builds the librariangen binary using the MOSS-compliant base image. +FROM marketplace.gcr.io/google/debian12:latest AS builder + +# Set environment variables for tool versions for easy updates. +ENV GO_VERSION=1.24.0 + +# Install build dependencies. +RUN apt-get update && \ + apt-get install -y \ + build-essential \ + ca-certificates \ + curl \ + wget && \ + rm -rf /var/lib/apt/lists/* + +# Install the specific Go version required for compatibility. +RUN wget https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz -O go.tar.gz && \ + tar -C /usr/local -xzf go.tar.gz && \ + rm go.tar.gz +ENV PATH=/usr/local/go/bin:$PATH +RUN go version + +WORKDIR /src + +# Copy go.mod and go.sum to download dependencies before copying all the source. +# This allows Docker to cache the dependencies layer. +# COPY go.mod go.sum ./ +# RUN go mod download + +# Copy all source code. +COPY . . + +# Build the librariangen binary. +RUN CGO_ENABLED=0 GOOS=linux go build -v -o /librariangen . + +# --- Final Stage --- +# This stage creates the final, minimal image with the compiled binary and +# all required runtime dependencies pinned to specific versions for compatibility. +FROM marketplace.gcr.io/google/debian12:latest + +# Copy the compiled librariangen binary from the builder stage. +COPY --from=builder /librariangen /usr/local/bin/librariangen + +# Set the entrypoint for the container to run the compiled librariangen. +# The Librarian will provide commands like 'generate' as arguments. +ENTRYPOINT [ "/usr/local/bin/librariangen" ] \ No newline at end of file diff --git a/internal/librariangen/README.md b/internal/librariangen/README.md new file mode 100644 index 0000000000..b310d5567c --- /dev/null +++ b/internal/librariangen/README.md @@ -0,0 +1,3 @@ +# Java GAPIC Generator for Librarian (librariangen) + +This directory contains the source code for `librariangen`, a containerized Go application that serves as the Java-specific code generator within the Librarian pipeline. Its responsibility is to generate release-ready Java GAPIC client libraries from `googleapis` API definitions, replacing the legacy Java hermetic code generation toolchain. diff --git a/internal/librariangen/go.mod b/internal/librariangen/go.mod new file mode 100644 index 0000000000..52d7daf52b --- /dev/null +++ b/internal/librariangen/go.mod @@ -0,0 +1,3 @@ +module cloud.google.com/java/internal/librariangen + +go 1.24.4 diff --git a/internal/librariangen/main.go b/internal/librariangen/main.go new file mode 100644 index 0000000000..8367ebe9cd --- /dev/null +++ b/internal/librariangen/main.go @@ -0,0 +1,85 @@ +// 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 main + +import ( + "context" + "errors" + "fmt" + "log/slog" + "os" + "strings" +) + +const version = "0.1.0" + +// main is the entrypoint for the librariangen CLI. +func main() { + logLevel := slog.LevelInfo + switch os.Getenv("GOOGLE_SDK_JAVA_LOGGING_LEVEL") { + case "debug": + logLevel = slog.LevelDebug + case "quiet": + logLevel = slog.LevelError + 1 + } + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: logLevel, + }))) + slog.Info("librariangen: invoked", "args", os.Args) + if err := run(context.Background(), os.Args[1:]); err != nil { + slog.Error("librariangen: failed", "error", err) + os.Exit(1) + } + slog.Info("librariangen: finished successfully") +} + +// run executes the appropriate command based on the CLI's invocation arguments. +// The idiomatic structure is `librariangen [command] [flags]`. +func run(ctx context.Context, args []string) error { + if len(args) < 1 { + return errors.New("librariangen: expected a command") + } + + // The --version flag is a special case and not a command. + if args[0] == "--version" { + fmt.Println(version) + return nil + } + + cmd := args[0] + flags := args[1:] + + if strings.HasPrefix(cmd, "-") { + return fmt.Errorf("librariangen: command cannot be a flag: %s", cmd) + } + + switch cmd { + case "generate": + slog.Warn("librariangen: generate command is not yet implemented") + return nil + case "release-init": + slog.Warn("librariangen: release-init command is not yet implemented") + return nil + case "configure": + slog.Warn("librariangen: configure command is not yet implemented") + return nil + case "build": + slog.Warn("librariangen: build command is not yet implemented") + return nil + default: + return fmt.Errorf("librariangen: unknown command: %s (with flags %s)", cmd, flags) + } + +} diff --git a/internal/librariangen/main_test.go b/internal/librariangen/main_test.go new file mode 100644 index 0000000000..a83e72bcb9 --- /dev/null +++ b/internal/librariangen/main_test.go @@ -0,0 +1,94 @@ +// 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 main + +import ( + "context" + "testing" +) + +func TestRun(t *testing.T) { + ctx := context.Background() + tests := []struct { + name string + args []string + wantErr bool + }{ + { + name: "no args", + args: []string{}, + wantErr: true, + }, + { + name: "version flag", + args: []string{"--version"}, + wantErr: false, + }, + { + name: "flag as command", + args: []string{"--foo"}, + wantErr: true, + }, + { + name: "unknown command", + args: []string{"foo"}, + wantErr: true, + }, + { + name: "build command no flags", + args: []string{"build"}, + wantErr: false, + }, + { + name: "build command with flags", + args: []string{"build", "--repo=.", "--librarian=./.librarian"}, + wantErr: false, + }, + { + name: "configure command", + args: []string{"configure"}, + wantErr: false, + }, + { + name: "generate command no flags", + args: []string{"generate"}, + wantErr: false, + }, + { + name: "generate command with flags", + args: []string{"generate", "--source=.", "--output=./build_out"}, + wantErr: false, + }, + { + name: "release-init command no flags", + args: []string{"release-init"}, + wantErr: false, + }, + { + name: "release-init command with flags", + args: []string{"release-init", "--repo=.", "--output=./build_out"}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Since we are only testing the command dispatching, we can pass a nil + // context. The generate function is not actually called. + if err := run(ctx, tt.args); (err != nil) != tt.wantErr { + t.Errorf("run() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} \ No newline at end of file From fa3fe474bc276d8fd386eae50bf203e7f02de290 Mon Sep 17 00:00:00 2001 From: Mike Eltsufin Date: Tue, 7 Oct 2025 20:07:23 +0000 Subject: [PATCH 2/3] chore: fix go.mod location --- .github/workflows/librariangen-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/librariangen-ci.yml b/.github/workflows/librariangen-ci.yml index 190477ad37..2ef93d7d10 100644 --- a/.github/workflows/librariangen-ci.yml +++ b/.github/workflows/librariangen-ci.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version-file: "go.mod" + go-version-file: "internal/librariangen/go.mod" - name: Display Go version run: go version From a407d733df5650041d4bfe01b4dd49f78fe175cf Mon Sep 17 00:00:00 2001 From: Mike Eltsufin Date: Tue, 7 Oct 2025 21:06:40 +0000 Subject: [PATCH 3/3] refactor(librariangen): move docker build to local script The Docker build and test steps were moved from the GitHub Actions workflow to a local script. This is because the base Docker image is not accessible from GitHub Actions. The CI workflow now only runs the Go tests. The new `build-docker-and-test.sh` script can be used to build the Docker image and run the version check locally. --- .github/workflows/librariangen-ci.yml | 17 ---------- .../librariangen/build-docker-and-test.sh | 32 +++++++++++++++++++ 2 files changed, 32 insertions(+), 17 deletions(-) create mode 100755 internal/librariangen/build-docker-and-test.sh diff --git a/.github/workflows/librariangen-ci.yml b/.github/workflows/librariangen-ci.yml index 2ef93d7d10..b520774aec 100644 --- a/.github/workflows/librariangen-ci.yml +++ b/.github/workflows/librariangen-ci.yml @@ -30,20 +30,3 @@ jobs: run: go test ./... working-directory: internal/librariangen - - name: Build Docker image - run: docker build -t librariangen-test ./internal/librariangen - - - name: Run version command - id: version - run: | - output=$(docker run --rm -e GOOGLE_SDK_JAVA_LOGGING_LEVEL=quiet librariangen-test --version) - echo "version_output=$output" >> $GITHUB_OUTPUT - - - name: Verify version - run: | - version_output="${{ steps.version.outputs.version_output }}" - if [[ ! "$version_output" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Version format is incorrect. Got: $version_output" - exit 1 - fi - echo "Version check passed. Version is $version_output" diff --git a/internal/librariangen/build-docker-and-test.sh b/internal/librariangen/build-docker-and-test.sh new file mode 100755 index 0000000000..1c6fda1428 --- /dev/null +++ b/internal/librariangen/build-docker-and-test.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# 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. + + +set -e +SCRIPT_DIR=$(dirname "$0") +IMAGE_NAME="librariangen-test" + +echo "Building Docker image..." +docker build -t "${IMAGE_NAME}" "${SCRIPT_DIR}" + +echo "Running version check..." +output=$(docker run --rm -e GOOGLE_SDK_JAVA_LOGGING_LEVEL=quiet "${IMAGE_NAME}" --version) + +if [[ ! "$output" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Version format is incorrect. Got: $output" + exit 1 +fi + +echo "Version check passed. Version is $output" \ No newline at end of file