Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
15 changes: 14 additions & 1 deletion internal/librariangen/build/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"log/slog"
"path/filepath"

"cloud.google.com/go/internal/postprocessor/librarian/librariangen/config"
"cloud.google.com/go/internal/postprocessor/librarian/librariangen/execv"
"cloud.google.com/go/internal/postprocessor/librarian/librariangen/request"
)
Expand Down Expand Up @@ -57,12 +58,24 @@ func Build(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")
repoConfig, err := config.LoadRepoConfig(cfg.LibrarianDir)
if err != nil {
return fmt.Errorf("librariangen: failed to load repo config: %w", err)
}
if !repoConfig.CanBuild() {
return errors.New("build is not supported in this repo")
}

slog.Debug("librariangen: build command started")

buildReq, err := readBuildReq(cfg.LibrarianDir)
if err != nil {
return fmt.Errorf("librariangen: failed to read request: %w", err)
}
/*
if repoConfig.Type == genproto.RepoType {
return genproto.Build(ctx, cfg, repoConfig, buildReq)
}*/
moduleDir := filepath.Join(cfg.RepoDir, buildReq.ID)
if err := goBuild(ctx, moduleDir, buildReq.ID); err != nil {
return fmt.Errorf("librariangen: failed to run 'go build': %w", err)
Expand Down
31 changes: 31 additions & 0 deletions internal/librariangen/config/repoconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,14 @@ const RepoConfigFile string = "repo-config.yaml"

// RepoConfig is the configuration for all modules in the repository.
type RepoConfig struct {
// Type is the type of the repo, where custom behavior is needed.
// For most repos, this is not specified; where it *is* specified,
// it's typically the name of the repo (e.g. "go-genproto").
Type string `yaml:"type"`
// Modules is the list of all the modules in the repository which need overrides.
Modules []*ModuleConfig `yaml:"modules"`
// GenProtoPackages is the list of packages generated in the go-genproto repo.
GenProtoPackages []string `yaml:"genproto_packages"`
}

// ModuleConfig is the configuration for a single module.
Expand Down Expand Up @@ -96,6 +102,31 @@ func LoadRepoConfig(librarianDir string) (*RepoConfig, error) {
return &config, nil
}

// CanConfigure returns whether or not the configure command is supported for this
// repository, based on its Type.
func (rc *RepoConfig) CanConfigure() bool {
// Only the default repo type can be configured.
return rc.Type == ""
}

// CanGenerate returns whether or not the generate command is supported for this
// repository, based on its Type.
func (rc *RepoConfig) CanGenerate() bool {
return true
}

// CanBuild returns whether or not the build command is supported for this
// repository, based on its Type.
func (rc *RepoConfig) CanBuild() bool {
return true
}

// CanReleaseInit returns whether or not the release init command is supported for this
// repository, based on its Type.
func (rc *RepoConfig) CanReleaseInit() bool {
return rc.Type == ""
}

// GetModuleConfig returns the configuration for the named module
// (top-level directory). If no module-specific configuration is found,
// an empty configuration (with the right name) is returned.
Expand Down
22 changes: 10 additions & 12 deletions internal/librariangen/configure/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,15 @@ func Configure(ctx context.Context, cfg *Config) error {
if err := cfg.Validate(); err != nil {
return fmt.Errorf("librariangen: invalid configuration: %w", err)
}
repoConfig, err := config.LoadRepoConfig(cfg.LibrarianDir)
if err != nil {
return err
}
if !repoConfig.CanConfigure() {
return errors.New("configure is not supported in this repo")
}
slog.Debug("librariangen: configure command started")

configureReq, err := readConfigureReq(cfg.LibrarianDir)
if err != nil {
return fmt.Errorf("librariangen: failed to read request: %w", err)
Expand All @@ -106,7 +114,7 @@ func Configure(ctx context.Context, cfg *Config) error {
return err
}

response, err := configureLibrary(ctx, cfg, library, api)
response, err := configureLibrary(ctx, cfg, repoConfig, library, api)
if err != nil {
return err
}
Expand Down Expand Up @@ -182,17 +190,7 @@ func findLibraryAndAPIToConfigure(req *Request) (*request.Library, *request.API,
// returning the configure-response... it just happens to be "the library being configured"
// at the moment. If the format of configure-response ever changes, we'll need fewer
// changes if we don't make too many assumptions now.
func configureLibrary(ctx context.Context, cfg *Config, library *request.Library, api *request.API) (*request.Library, error) {
// It's just *possible* the new path has a manually configured
// client directory - but even if not, RepoConfig has the logic
// for figuring out the client directory. Even if the new path
// doesn't have a custom configuration, we can use this to
// work out the module path, e.g. if there's a major version other
// than v1.
repoConfig, err := config.LoadRepoConfig(cfg.LibrarianDir)
if err != nil {
return nil, err
}
func configureLibrary(ctx context.Context, cfg *Config, repoConfig *config.RepoConfig, library *request.Library, api *request.API) (*request.Library, error) {
var moduleConfig = repoConfig.GetModuleConfig(library.ID)

moduleRoot := filepath.Join(cfg.OutputDir, library.ID)
Expand Down
13 changes: 10 additions & 3 deletions internal/librariangen/generate/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"cloud.google.com/go/internal/postprocessor/librarian/librariangen/bazel"
"cloud.google.com/go/internal/postprocessor/librarian/librariangen/config"
"cloud.google.com/go/internal/postprocessor/librarian/librariangen/execv"
"cloud.google.com/go/internal/postprocessor/librarian/librariangen/genproto"
"cloud.google.com/go/internal/postprocessor/librarian/librariangen/postprocessor"
"cloud.google.com/go/internal/postprocessor/librarian/librariangen/protoc"
"cloud.google.com/go/internal/postprocessor/librarian/librariangen/request"
Expand Down Expand Up @@ -96,15 +97,21 @@ func Generate(ctx context.Context, cfg *Config) error {
if err := cfg.Validate(); err != nil {
return fmt.Errorf("librariangen: invalid configuration: %w", err)
}
repoConfig, err := config.LoadRepoConfig(cfg.LibrarianDir)
if err != nil {
return fmt.Errorf("librariangen: failed to load repo config: %w", err)
}
if !repoConfig.CanGenerate() {
return errors.New("generate is not supported in this repo")
}
slog.Debug("librariangen: generate command started")

generateReq, err := readGenerateReq(cfg.LibrarianDir)
if err != nil {
return fmt.Errorf("librariangen: failed to read request: %w", err)
}
repoConfig, err := config.LoadRepoConfig(cfg.LibrarianDir)
if err != nil {
return fmt.Errorf("librariangen: failed to load repo config: %w", err)
if repoConfig.Type == genproto.RepoType {
return genproto.Generate(ctx, cfg.SourceDir, cfg.OutputDir, generateReq)
}
moduleConfig := repoConfig.GetModuleConfig(generateReq.ID)

Expand Down
166 changes: 166 additions & 0 deletions internal/librariangen/genproto/genproto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// 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 genproto

import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"

"cloud.google.com/go/internal/postprocessor/librarian/librariangen/execv"
"cloud.google.com/go/internal/postprocessor/librarian/librariangen/protoc"
"cloud.google.com/go/internal/postprocessor/librarian/librariangen/request"
)

var (
execvRun = execv.Run
)

var goPkgOptRe = regexp.MustCompile(`(?m)^option go_package = (.*);`)

// RepoType is the value that should appear in repo-config.yaml to use the functions in this package.
const RepoType string = "go-genproto"

// Generate generates all relevant protos from sourceDir into outputDir,
// determining which golang packages are in scope based on the configured exclusions.
func Generate(ctx context.Context, sourceDir string, outputDir string, generateReq *request.Library) error {
packages, err := derivePackages(generateReq)
if err != nil {
return err
}
// Figure out what we're generating: a map from a full package name
// such as "google.golang.org/genproto/googleapis/type/date_range" to the proto files
// to generate for that package.
protos, err := protosForPackages(sourceDir, generateReq, packages)
if err != nil {
return err
}
// Now call protoc on the protos for each package.
slog.Info("generating protos")
args := protoc.BuildGenProto(sourceDir, outputDir, protos)
if err := execvRun(ctx, args, outputDir); err != nil {
return fmt.Errorf("librariangen: protoc failed: %w", err)
}

// Move the output to the right place.
generatedPath := filepath.Join(outputDir, "google.golang.org", "genproto", "googleapis")
wantedPath := filepath.Join(outputDir, "googleapis")
if err := os.Rename(generatedPath, wantedPath); err != nil {
return err
}

// Run goimports -w on the output root.
if err := goimports(ctx, outputDir); err != nil {
return fmt.Errorf("librariangen: failed to run 'goimports': %w", err)
}
return nil
}

// goimports runs the goimports tool on a directory to format Go files and
// manage imports.
func goimports(ctx context.Context, dir string) error {
slog.Info("librariangen: running goimports", "directory", dir)
// The `.` argument will make goimports process all go files in the directory
// and its subdirectories. The -w flag writes results back to source files.
args := []string{"goimports", "-w", "."}
return execvRun(ctx, args, dir)
}

func derivePackages(generateReq *request.Library) ([]string, error) {
packages := []string{}
const exclusionPrefix = "^googleapis"
const exclusionSuffix = "/[^/]*\\.go$"
for _, exclusion := range generateReq.RemoveRegex {
exclusion, hasPrefix := strings.CutPrefix(exclusion, exclusionPrefix)
if !hasPrefix {
return nil, fmt.Errorf("librariangen: exclusion does not have expected prefix: %s", exclusion)
}
exclusion, hasSuffix := strings.CutSuffix(exclusion, exclusionSuffix)
if !hasSuffix {
return nil, fmt.Errorf("librariangen: exclusion does not have expected suffix: %s", exclusion)
}
packages = append(packages, "google.golang.org/genproto/googleapis"+exclusion)
}
return packages, nil
}

/*
func Build(ctx context.Context, cfg *build.Config, repoConfig *config.RepoConfig, buildReq *request.Library) error {
// TODO: implement...
return nil
}
*/

// parseGoPkg parses the import path declared in the given file's `go_package`
// option. If the option is missing, parseGoPkg returns empty string.
func parseGoPkg(content []byte) (string, error) {
var pkgName string
if match := goPkgOptRe.FindSubmatch(content); len(match) > 0 {
pn, err := strconv.Unquote(string(match[1]))
if err != nil {
return "", err
}
pkgName = pn
}
if p := strings.IndexRune(pkgName, ';'); p > 0 {
pkgName = pkgName[:p]
}
return pkgName, nil
}

// goPkg reports the import path declared in the given file's `go_package`
// option. If the option is missing, goPkg returns empty string.
func goPkg(fileName string) (string, error) {
content, err := os.ReadFile(fileName)
if err != nil {
return "", err
}
return parseGoPkg(content)
}

func protosForPackages(sourceDir string, generateReq *request.Library, packages []string) ([]string, error) {
protos := []string{}
for _, api := range generateReq.APIs {
protoPath := filepath.Join(sourceDir, api.Path)
entries, err := os.ReadDir(protoPath)
if err != nil {
return nil, fmt.Errorf("librariangen: failed to read API source directory %s: %w", protoPath, err)
}
for _, entry := range entries {
if !strings.HasSuffix(entry.Name(), ".proto") {
continue
}
if strings.HasSuffix(entry.Name(), "compute_small.proto") {
continue
}
path := filepath.Join(protoPath, entry.Name())
pkg, err := goPkg(path)
if err != nil {
return nil, err
}
if !slices.Contains(packages, pkg) {
continue
}
protos = append(protos, path)
}
}
return protos, nil
}
14 changes: 14 additions & 0 deletions internal/librariangen/protoc/protoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,17 @@ func Build(lib *request.Library, api *request.API, config ConfigProvider, source

return args, nil
}

// BuildGenProto builds the command line arguments (including "protoc" itself) required
// for protoc for the go-genproto repo, for a single package.
func BuildGenProto(sourceDir, outputDir string, protos []string) []string {
args := []string{
"protoc",
"--experimental_allow_proto3_optional",
"--go_v1_out=" + outputDir,
"--go_v1_opt=plugins=grpc",
"-I=" + sourceDir,
}
args = append(args, protos...)
return args
}
14 changes: 9 additions & 5 deletions internal/librariangen/release/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"os"
Expand All @@ -44,7 +45,15 @@ type Config struct {

// Init is the entrypoint for the release-init command.
func Init(ctx context.Context, cfg *Config) error {
repoConfig, err := config.LoadRepoConfig(cfg.LibrarianDir)
if err != nil {
return fmt.Errorf("librariangen: failed to load repo config: %w", err)
}
if !repoConfig.CanReleaseInit() {
return errors.New("release init is not supported in this repo")
}
slog.Debug("librariangen: release.Init: starting", "config", cfg)

reqPath := filepath.Join(cfg.LibrarianDir, "release-init-request.json")
b, err := os.ReadFile(reqPath)
if err != nil {
Expand All @@ -56,11 +65,6 @@ func Init(ctx context.Context, cfg *Config) error {
return writeErrorResponse(cfg.LibrarianDir, fmt.Errorf("librariangen: failed to unmarshal request: %w", err))
}

repoConfig, err := config.LoadRepoConfig(cfg.LibrarianDir)
if err != nil {
return fmt.Errorf("librariangen: failed to load repo config: %w", err)
}

for _, lib := range req.Libraries {
if !lib.ReleaseTriggered {
continue
Expand Down