diff --git a/.github/workflows/librariangen-ci.yml b/.github/workflows/librariangen-ci.yml new file mode 100644 index 0000000000..b520774aec --- /dev/null +++ b/.github/workflows/librariangen-ci.yml @@ -0,0 +1,32 @@ +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: "internal/librariangen/go.mod" + + - name: Display Go version + run: go version + + - name: Run Go tests + run: go test ./... + working-directory: internal/librariangen + 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/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 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