Skip to content

Commit 29d188d

Browse files
authored
feat(librariangen): scaffold Java language container for Librarian (#3926)
This change introduces the initial scaffolding for `librariangen`, a containerized Go application that will serve as the Java-specific code generator within the Librarian pipeline. The primary goal of this change is to establish the foundational components for the Java language container, including: * A new `librariangen` CLI application: Written in Go, this will be the entry point for Java code generation tasks invoked by Librarian. It currently includes basic command dispatching and support for `--version`. * A multi-stage `Dockerfile`: This creates a minimal container image for librariangen. * Unit tests and CI: A new GitHub Actions workflow has been added to build and test the librariangen executable. This scaffolding is the first step in implementing the full code generation logic within the Librarian system, eventually replacing the Java hermetic code generation system. The code is largely adapted from the Go language container at https://github.com/googleapis/google-cloud-go/tree/main/internal/librariangen. Fixes: googleapis/librarian#2500
1 parent a8e081c commit 29d188d

File tree

7 files changed

+314
-0
lines changed

7 files changed

+314
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: librariangen CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
paths:
7+
- 'internal/librariangen/**'
8+
pull_request:
9+
paths:
10+
- 'internal/librariangen/**'
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
test-docker-image:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Set up Go
22+
uses: actions/setup-go@v5
23+
with:
24+
go-version-file: "internal/librariangen/go.mod"
25+
26+
- name: Display Go version
27+
run: go version
28+
29+
- name: Run Go tests
30+
run: go test ./...
31+
working-directory: internal/librariangen
32+

internal/librariangen/Dockerfile

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
# This Dockerfile creates a MOSS-compliant image for the Go librariangen,
16+
# which is invoked by the Librarian tool. It uses a multi-stage build to
17+
# create a minimal final image.
18+
19+
# --- Builder Stage ---
20+
# This stage builds the librariangen binary using the MOSS-compliant base image.
21+
FROM marketplace.gcr.io/google/debian12:latest AS builder
22+
23+
# Set environment variables for tool versions for easy updates.
24+
ENV GO_VERSION=1.24.0
25+
26+
# Install build dependencies.
27+
RUN apt-get update && \
28+
apt-get install -y \
29+
build-essential \
30+
ca-certificates \
31+
curl \
32+
wget && \
33+
rm -rf /var/lib/apt/lists/*
34+
35+
# Install the specific Go version required for compatibility.
36+
RUN wget https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz -O go.tar.gz && \
37+
tar -C /usr/local -xzf go.tar.gz && \
38+
rm go.tar.gz
39+
ENV PATH=/usr/local/go/bin:$PATH
40+
RUN go version
41+
42+
WORKDIR /src
43+
44+
# Copy go.mod and go.sum to download dependencies before copying all the source.
45+
# This allows Docker to cache the dependencies layer.
46+
# COPY go.mod go.sum ./
47+
# RUN go mod download
48+
49+
# Copy all source code.
50+
COPY . .
51+
52+
# Build the librariangen binary.
53+
RUN CGO_ENABLED=0 GOOS=linux go build -v -o /librariangen .
54+
55+
# --- Final Stage ---
56+
# This stage creates the final, minimal image with the compiled binary and
57+
# all required runtime dependencies pinned to specific versions for compatibility.
58+
FROM marketplace.gcr.io/google/debian12:latest
59+
60+
# Copy the compiled librariangen binary from the builder stage.
61+
COPY --from=builder /librariangen /usr/local/bin/librariangen
62+
63+
# Set the entrypoint for the container to run the compiled librariangen.
64+
# The Librarian will provide commands like 'generate' as arguments.
65+
ENTRYPOINT [ "/usr/local/bin/librariangen" ]

internal/librariangen/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Java GAPIC Generator for Librarian (librariangen)
2+
3+
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.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/bash
2+
# Copyright 2025 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
17+
set -e
18+
SCRIPT_DIR=$(dirname "$0")
19+
IMAGE_NAME="librariangen-test"
20+
21+
echo "Building Docker image..."
22+
docker build -t "${IMAGE_NAME}" "${SCRIPT_DIR}"
23+
24+
echo "Running version check..."
25+
output=$(docker run --rm -e GOOGLE_SDK_JAVA_LOGGING_LEVEL=quiet "${IMAGE_NAME}" --version)
26+
27+
if [[ ! "$output" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
28+
echo "Version format is incorrect. Got: $output"
29+
exit 1
30+
fi
31+
32+
echo "Version check passed. Version is $output"

internal/librariangen/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module cloud.google.com/java/internal/librariangen
2+
3+
go 1.24.4

internal/librariangen/main.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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 main
16+
17+
import (
18+
"context"
19+
"errors"
20+
"fmt"
21+
"log/slog"
22+
"os"
23+
"strings"
24+
)
25+
26+
const version = "0.1.0"
27+
28+
// main is the entrypoint for the librariangen CLI.
29+
func main() {
30+
logLevel := slog.LevelInfo
31+
switch os.Getenv("GOOGLE_SDK_JAVA_LOGGING_LEVEL") {
32+
case "debug":
33+
logLevel = slog.LevelDebug
34+
case "quiet":
35+
logLevel = slog.LevelError + 1
36+
}
37+
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
38+
Level: logLevel,
39+
})))
40+
slog.Info("librariangen: invoked", "args", os.Args)
41+
if err := run(context.Background(), os.Args[1:]); err != nil {
42+
slog.Error("librariangen: failed", "error", err)
43+
os.Exit(1)
44+
}
45+
slog.Info("librariangen: finished successfully")
46+
}
47+
48+
// run executes the appropriate command based on the CLI's invocation arguments.
49+
// The idiomatic structure is `librariangen [command] [flags]`.
50+
func run(ctx context.Context, args []string) error {
51+
if len(args) < 1 {
52+
return errors.New("librariangen: expected a command")
53+
}
54+
55+
// The --version flag is a special case and not a command.
56+
if args[0] == "--version" {
57+
fmt.Println(version)
58+
return nil
59+
}
60+
61+
cmd := args[0]
62+
flags := args[1:]
63+
64+
if strings.HasPrefix(cmd, "-") {
65+
return fmt.Errorf("librariangen: command cannot be a flag: %s", cmd)
66+
}
67+
68+
switch cmd {
69+
case "generate":
70+
slog.Warn("librariangen: generate command is not yet implemented")
71+
return nil
72+
case "release-init":
73+
slog.Warn("librariangen: release-init command is not yet implemented")
74+
return nil
75+
case "configure":
76+
slog.Warn("librariangen: configure command is not yet implemented")
77+
return nil
78+
case "build":
79+
slog.Warn("librariangen: build command is not yet implemented")
80+
return nil
81+
default:
82+
return fmt.Errorf("librariangen: unknown command: %s (with flags %s)", cmd, flags)
83+
}
84+
85+
}

internal/librariangen/main_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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 main
16+
17+
import (
18+
"context"
19+
"testing"
20+
)
21+
22+
func TestRun(t *testing.T) {
23+
ctx := context.Background()
24+
tests := []struct {
25+
name string
26+
args []string
27+
wantErr bool
28+
}{
29+
{
30+
name: "no args",
31+
args: []string{},
32+
wantErr: true,
33+
},
34+
{
35+
name: "version flag",
36+
args: []string{"--version"},
37+
wantErr: false,
38+
},
39+
{
40+
name: "flag as command",
41+
args: []string{"--foo"},
42+
wantErr: true,
43+
},
44+
{
45+
name: "unknown command",
46+
args: []string{"foo"},
47+
wantErr: true,
48+
},
49+
{
50+
name: "build command no flags",
51+
args: []string{"build"},
52+
wantErr: false,
53+
},
54+
{
55+
name: "build command with flags",
56+
args: []string{"build", "--repo=.", "--librarian=./.librarian"},
57+
wantErr: false,
58+
},
59+
{
60+
name: "configure command",
61+
args: []string{"configure"},
62+
wantErr: false,
63+
},
64+
{
65+
name: "generate command no flags",
66+
args: []string{"generate"},
67+
wantErr: false,
68+
},
69+
{
70+
name: "generate command with flags",
71+
args: []string{"generate", "--source=.", "--output=./build_out"},
72+
wantErr: false,
73+
},
74+
{
75+
name: "release-init command no flags",
76+
args: []string{"release-init"},
77+
wantErr: false,
78+
},
79+
{
80+
name: "release-init command with flags",
81+
args: []string{"release-init", "--repo=.", "--output=./build_out"},
82+
wantErr: false,
83+
},
84+
}
85+
for _, tt := range tests {
86+
t.Run(tt.name, func(t *testing.T) {
87+
// Since we are only testing the command dispatching, we can pass a nil
88+
// context. The generate function is not actually called.
89+
if err := run(ctx, tt.args); (err != nil) != tt.wantErr {
90+
t.Errorf("run() error = %v, wantErr %v", err, tt.wantErr)
91+
}
92+
})
93+
}
94+
}

0 commit comments

Comments
 (0)