diff --git a/internal/librariangen/generate/generator.go b/internal/librariangen/generate/generator.go index f4100a205b..4509c98604 100644 --- a/internal/librariangen/generate/generator.go +++ b/internal/librariangen/generate/generator.go @@ -17,7 +17,6 @@ package generate import ( "archive/zip" "context" - "errors" "fmt" "io" "log/slog" @@ -27,61 +26,26 @@ import ( "cloud.google.com/java/internal/librariangen/bazel" "cloud.google.com/java/internal/librariangen/execv" + "cloud.google.com/java/internal/librariangen/languagecontainer/generate" "cloud.google.com/java/internal/librariangen/message" "cloud.google.com/java/internal/librariangen/protoc" ) // Test substitution vars. var ( - bazelParse = bazel.Parse - execvRun = execv.Run - requestParse = message.ParseLibrary - protocBuild = protoc.Build + bazelParse = bazel.Parse + execvRun = execv.Run + 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) - } +func Generate(ctx context.Context, cfg *generate.Config) error { slog.Debug("librariangen: generate command started") outputConfig := &protoc.OutputConfig{ - GAPICDir: filepath.Join(cfg.OutputDir, "gapic"), - GRPCDir: filepath.Join(cfg.OutputDir, "grpc"), - ProtoDir: filepath.Join(cfg.OutputDir, "proto"), + GAPICDir: filepath.Join(cfg.Context.OutputDir, "gapic"), + GRPCDir: filepath.Join(cfg.Context.OutputDir, "grpc"), + ProtoDir: filepath.Join(cfg.Context.OutputDir, "proto"), } defer func() { if err := cleanupIntermediateFiles(outputConfig); err != nil { @@ -89,15 +53,11 @@ func Generate(ctx context.Context, cfg *Config) error { } }() - generateReq, err := readGenerateReq(cfg.LibrarianDir) - if err != nil { - return fmt.Errorf("librariangen: failed to read request: %w", err) - } + generateReq := cfg.Request - if err := invokeProtoc(ctx, cfg, generateReq, outputConfig); err != nil { + if err := invokeProtoc(ctx, cfg.Context, generateReq, outputConfig); err != nil { return fmt.Errorf("librariangen: gapic generation failed: %w", err) } - // Unzip the temp-codegen.srcjar. srcjarPath := filepath.Join(outputConfig.GAPICDir, "temp-codegen.srcjar") srcjarDest := outputConfig.GAPICDir @@ -105,7 +65,7 @@ func Generate(ctx context.Context, cfg *Config) error { return fmt.Errorf("librariangen: failed to unzip %s: %w", srcjarPath, err) } - if err := restructureOutput(cfg.OutputDir, generateReq.ID); err != nil { + if err := restructureOutput(cfg.Context.OutputDir, generateReq.ID); err != nil { return fmt.Errorf("librariangen: failed to restructure output: %w", err) } @@ -116,15 +76,15 @@ func Generate(ctx context.Context, cfg *Config) error { // 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 *message.Library, outputConfig *protoc.OutputConfig) error { +func invokeProtoc(ctx context.Context, genCtx *generate.Context, generateReq *message.Library, outputConfig *protoc.OutputConfig) error { for _, api := range generateReq.APIs { - apiServiceDir := filepath.Join(cfg.SourceDir, api.Path) + apiServiceDir := filepath.Join(genCtx.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, outputConfig) + args, err := protocBuild(apiServiceDir, bazelConfig, genCtx.SourceDir, outputConfig) if err != nil { return fmt.Errorf("librariangen: failed to build protoc command for api %q in library %q: %w", api.Path, generateReq.ID, err) } @@ -136,28 +96,13 @@ func invokeProtoc(ctx context.Context, cfg *Config, generateReq *message.Library } } - if err := execvRun(ctx, args, cfg.OutputDir); err != nil { + if err := execvRun(ctx, args, genCtx.OutputDir); err != nil { return fmt.Errorf("librariangen: protoc failed for api %q in library %q: %w, execvRun error: %v", api.Path, generateReq.ID, err, 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) (*message.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) diff --git a/internal/librariangen/generate/generator_test.go b/internal/librariangen/generate/generator_test.go index 16c82f752c..385983d7c7 100644 --- a/internal/librariangen/generate/generator_test.go +++ b/internal/librariangen/generate/generator_test.go @@ -22,6 +22,7 @@ import ( "path/filepath" "testing" + "cloud.google.com/java/internal/librariangen/languagecontainer/generate" "cloud.google.com/java/internal/librariangen/protoc" ) @@ -252,12 +253,20 @@ java_gapic_library( protocRunCount++ return tt.protocErr } - cfg := &Config{ + genCtx := &generate.Context{ LibrarianDir: e.librarianDir, InputDir: "fake-input", OutputDir: e.outputDir, SourceDir: e.sourceDir, } + cfg, err := generate.NewConfig(genCtx) + if err != nil && tt.wantErr { + // If we expect an error, and NewConfig fails, that's ok. + return + } + if err != nil { + t.Fatalf("failed to create generate config: %v", err) + } if err := Generate(context.Background(), cfg); (err != nil) != tt.wantErr { t.Errorf("Generate() error = %v, wantErr %v", err, tt.wantErr) } @@ -268,68 +277,6 @@ java_gapic_library( } } -func TestConfig_Validate(t *testing.T) { - tests := []struct { - name string - cfg *Config - wantErr bool - }{ - { - name: "valid", - cfg: &Config{ - LibrarianDir: "a", - InputDir: "b", - OutputDir: "c", - SourceDir: "d", - }, - wantErr: false, - }, - { - name: "missing librarian dir", - cfg: &Config{ - InputDir: "b", - OutputDir: "c", - SourceDir: "d", - }, - wantErr: true, - }, - { - name: "missing input dir", - cfg: &Config{ - LibrarianDir: "a", - OutputDir: "c", - SourceDir: "d", - }, - wantErr: true, - }, - { - name: "missing output dir", - cfg: &Config{ - LibrarianDir: "a", - InputDir: "b", - SourceDir: "d", - }, - wantErr: true, - }, - { - name: "missing source dir", - cfg: &Config{ - LibrarianDir: "a", - InputDir: "b", - OutputDir: "c", - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.cfg.Validate(); (err != nil) != tt.wantErr { - t.Errorf("Config.Validate() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - func TestRestructureOutput(t *testing.T) { e := newTestEnv(t) defer e.cleanup(t) diff --git a/internal/librariangen/languagecontainer/generate/generate.go b/internal/librariangen/languagecontainer/generate/generate.go new file mode 100644 index 0000000000..18077949c7 --- /dev/null +++ b/internal/librariangen/languagecontainer/generate/generate.go @@ -0,0 +1,87 @@ +// 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 contains types for language container's generate command. +package generate + +import ( + "errors" + "fmt" + "log/slog" + "path/filepath" + + "cloud.google.com/java/internal/librariangen/message" +) + +// Context holds the directory paths for the generate command. +// https://github.com/googleapis/librarian/blob/main/doc/language-onboarding.md#generate +type Context 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 context is valid. +func (c *Context) Validate() error { + if c.LibrarianDir == "" { + return errors.New("languagecontainer: librarian directory must be set") + } + if c.InputDir == "" { + return errors.New("languagecontainer: input directory must be set") + } + if c.OutputDir == "" { + return errors.New("languagecontainer: output directory must be set") + } + if c.SourceDir == "" { + return errors.New("languagecontainer: source directory must be set") + } + return nil +} + +// Config for the generate command. This holds the context (the directory paths) +// and the request parsed from the generate-request.json file. +type Config struct { + Context *Context + // This request is parsed from the generate-request.json file in + // the LibrarianDir of the context. + Request *message.Library +} + +// NewConfig creates a new Config, parsing the generate-request.json file +// from the LibrarianDir in the given Context. +func NewConfig(ctx *Context) (*Config, error) { + if err := ctx.Validate(); err != nil { + return nil, fmt.Errorf("invalid context: %w", err) + } + reqPath := filepath.Join(ctx.LibrarianDir, "generate-request.json") + slog.Debug("languagecontainer: reading generate request", "path", reqPath) + + generateReq, err := message.ParseLibrary(reqPath) + if err != nil { + return nil, err + } + slog.Debug("languagecontainer: successfully unmarshalled request", "library_id", generateReq.ID) + return &Config{ + Context: ctx, + Request: generateReq, + }, nil +} diff --git a/internal/librariangen/languagecontainer/generate/generate_test.go b/internal/librariangen/languagecontainer/generate/generate_test.go new file mode 100644 index 0000000000..a532a9c0ab --- /dev/null +++ b/internal/librariangen/languagecontainer/generate/generate_test.go @@ -0,0 +1,113 @@ +// 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 ( + "path/filepath" + "testing" + + "cloud.google.com/java/internal/librariangen/message" + "github.com/google/go-cmp/cmp" +) + +func TestNewConfig(t *testing.T) { + // This reads generate-request.json from testdata. + librarianDir := filepath.Join("..", "testdata") + want := &Config{ + Context: &Context{ + LibrarianDir: librarianDir, + InputDir: "in", + OutputDir: "out", + SourceDir: "source", + }, + Request: &message.Library{ + ID: "chronicle", + Version: "0.1.1", + APIs: []message.API{ + { + Path: "google/cloud/chronicle/v1", + ServiceConfig: "chronicle_v1.yaml", + }, + }, + SourcePaths: []string{ + "chronicle", + "internal/generated/snippets/chronicle", + }, + PreserveRegex: []string{ + "chronicle/aliasshim/aliasshim.go", + "chronicle/CHANGES.md", + }, + RemoveRegex: []string{ + "chronicle", + "internal/generated/snippets/chronicle", + }, + }, + } + + ctx := &Context{ + LibrarianDir: librarianDir, + InputDir: "in", + OutputDir: "out", + SourceDir: "source", + } + got, err := NewConfig(ctx) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("NewConfig() mismatch (-want +got):\n%s", diff) + } +} + +func TestNewConfig_validate(t *testing.T) { + tests := []struct { + name string + context *Context + }{ + { + name: "empty librarian dir", + context: &Context{}, + }, + { + name: "empty input dir", + context: &Context{ + LibrarianDir: "librarian", + }, + }, + { + name: "empty output dir", + context: &Context{ + LibrarianDir: "librarian", + InputDir: "in", + }, + }, + { + name: "empty source dir", + context: &Context{ + LibrarianDir: "librarian", + InputDir: "in", + OutputDir: "out", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := NewConfig(tt.context); err == nil { + t.Error("NewConfig() error = nil, want not nil") + } + }) + } +} diff --git a/internal/librariangen/languagecontainer/languagecontainer.go b/internal/librariangen/languagecontainer/languagecontainer.go index 20115bee15..bce7a94a77 100644 --- a/internal/librariangen/languagecontainer/languagecontainer.go +++ b/internal/librariangen/languagecontainer/languagecontainer.go @@ -29,17 +29,21 @@ import ( "os" "path/filepath" + "cloud.google.com/java/internal/librariangen/languagecontainer/generate" "cloud.google.com/java/internal/librariangen/languagecontainer/release" "cloud.google.com/java/internal/librariangen/message" ) // LanguageContainer defines the functions for language-specific container operations. type LanguageContainer struct { + Generate func(context.Context, *generate.Config) error ReleaseInit func(context.Context, *release.Config) (*message.ReleaseInitResponse, error) // Other container functions like Generate and Build will also be part of the struct. } // Run accepts an implementation of the LanguageContainer. +// The args parameter contains the command-line arguments passed to the container, +// without including the program name. Usually it's os.Args[1:]. func Run(args []string, container *LanguageContainer) int { // Logic to parse args and call the appropriate method on the container. // For example, if args[1] is "generate": @@ -53,20 +57,50 @@ func Run(args []string, container *LanguageContainer) int { flags := args[1:] switch cmd { case "generate": - slog.Warn("librariangen: generate command is not yet implemented") - return 1 + if container.Generate == nil { + slog.Error("languagecontainer: generate command is not implemented") + return 1 + } + return handleGenerate(flags, container) case "configure": - slog.Warn("librariangen: configure command is not yet implemented") + slog.Warn("languagecontainer: configure command is missing") return 1 case "release-init": + if container.ReleaseInit == nil { + slog.Error("languagecontainer: generate command is missing") + return 1 + } return handleReleaseInit(flags, container) case "build": - slog.Warn("librariangen: build command is not yet implemented") + slog.Warn("languagecontainer: build command is not yet implemented") return 1 default: - slog.Error(fmt.Sprintf("librariangen: unknown command: %s (with flags %v)", cmd, flags)) + slog.Error(fmt.Sprintf("languagecontainer: unknown command: %s (with flags %v)", cmd, flags)) + return 1 + } +} + +func handleGenerate(flags []string, container *LanguageContainer) int { + genCtx := &generate.Context{} + generateFlags := flag.NewFlagSet("generate", flag.ContinueOnError) + generateFlags.StringVar(&genCtx.LibrarianDir, "librarian", "/librarian", "Path to the librarian-tool input directory. Contains generate-request.json.") + generateFlags.StringVar(&genCtx.InputDir, "input", "/input", "Path to the .librarian/generator-input directory from the language repository.") + generateFlags.StringVar(&genCtx.OutputDir, "output", "/output", "Path to the empty directory where librariangen writes its output.") + generateFlags.StringVar(&genCtx.SourceDir, "source", "/source", "Path to a complete checkout of the googleapis repository.") + if err := generateFlags.Parse(flags); err != nil { + slog.Error("failed to parse flags", "error", err) + return 1 + } + cfg, err := generate.NewConfig(genCtx) + if err != nil { + slog.Error("failed to create generate config", "error", err) + return 1 + } + if err := container.Generate(context.Background(), cfg); err != nil { + slog.Error("generate failed", "error", err) return 1 } + slog.Info("languagecontainer: generate command executed successfully") return 0 } @@ -110,6 +144,6 @@ func handleReleaseInit(flags []string, container *LanguageContainer) int { slog.Error("failed to write response file", "path", responsePath, "error", err) return 1 } - slog.Info("librariangen: release-init command executed successfully") + slog.Info("languagecontainer: release-init command executed successfully") return 0 } diff --git a/internal/librariangen/languagecontainer/languagecontainer_test.go b/internal/librariangen/languagecontainer/languagecontainer_test.go index be30c9cb6b..846596b2ff 100644 --- a/internal/librariangen/languagecontainer/languagecontainer_test.go +++ b/internal/librariangen/languagecontainer/languagecontainer_test.go @@ -21,6 +21,7 @@ import ( "path/filepath" "testing" + "cloud.google.com/java/internal/librariangen/languagecontainer/generate" "cloud.google.com/java/internal/librariangen/languagecontainer/release" "cloud.google.com/java/internal/librariangen/message" "github.com/google/go-cmp/cmp" @@ -31,6 +32,9 @@ func TestRun(t *testing.T) { if err := os.WriteFile(filepath.Join(tmpDir, "release-init-request.json"), []byte("{}"), 0644); err != nil { t.Fatal(err) } + if err := os.WriteFile(filepath.Join(tmpDir, "generate-request.json"), []byte("{}"), 0644); err != nil { + t.Fatal(err) + } tests := []struct { name string args []string @@ -53,9 +57,20 @@ func TestRun(t *testing.T) { wantCode: 1, // Not implemented yet }, { - name: "generate command", + name: "generate command with default flags", args: []string{"generate"}, - wantCode: 1, // Not implemented yet + wantCode: 1, // Fails because default /librarian does not exist. + }, + { + name: "generate command success", + args: []string{"generate", "-librarian", tmpDir}, + wantCode: 0, + }, + { + name: "generate command failure", + args: []string{"generate", "-librarian", tmpDir}, + wantCode: 1, + wantErr: true, }, { name: "release-init command success", @@ -72,6 +87,12 @@ func TestRun(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { container := LanguageContainer{ + Generate: func(ctx context.Context, c *generate.Config) error { + if tt.wantErr { + return os.ErrNotExist + } + return nil + }, ReleaseInit: func(ctx context.Context, c *release.Config) (*message.ReleaseInitResponse, error) { if tt.wantErr { return nil, os.ErrNotExist @@ -164,3 +185,84 @@ func TestRun_ReleaseInitReadsContextArgs(t *testing.T) { t.Errorf("gotConfig.Context.OutputDir = %q, want %q", got, want) } } + +func TestRun_GenerateReadsContextArgs(t *testing.T) { + tmpDir := t.TempDir() + librarianDir := filepath.Join(tmpDir, "librarian") + if err := os.Mkdir(librarianDir, 0755); err != nil { + t.Fatal(err) + } + // generate.NewConfig reads generate-request.json. + if err := os.WriteFile(filepath.Join(librarianDir, "generate-request.json"), []byte("{}"), 0644); err != nil { + t.Fatal(err) + } + inputDir := filepath.Join(tmpDir, "input") + if err := os.Mkdir(inputDir, 0755); err != nil { + t.Fatal(err) + } + outputDir := filepath.Join(tmpDir, "output") + if err := os.Mkdir(outputDir, 0755); err != nil { + t.Fatal(err) + } + sourceDir := filepath.Join(tmpDir, "source") + if err := os.Mkdir(sourceDir, 0755); err != nil { + t.Fatal(err) + } + args := []string{"generate", "-librarian", librarianDir, "-input", inputDir, "-output", outputDir, "-source", sourceDir} + var gotConfig *generate.Config + container := LanguageContainer{ + Generate: func(ctx context.Context, c *generate.Config) error { + gotConfig = c + return nil + }, + } + if code := Run(args, &container); code != 0 { + t.Errorf("Run() = %v, want 0", code) + } + if got, want := gotConfig.Context.LibrarianDir, librarianDir; got != want { + t.Errorf("gotConfig.Context.LibrarianDir = %q, want %q", got, want) + } + if got, want := gotConfig.Context.InputDir, inputDir; got != want { + t.Errorf("gotConfig.Context.InputDir = %q, want %q", got, want) + } + if got, want := gotConfig.Context.OutputDir, outputDir; got != want { + t.Errorf("gotConfig.Context.OutputDir = %q, want %q", got, want) + } + if got, want := gotConfig.Context.SourceDir, sourceDir; got != want { + t.Errorf("gotConfig.Context.SourceDir = %q, want %q", got, want) + } +} + +func TestRun_unimplementedCommands(t *testing.T) { + tests := []struct { + name string + args []string + container *LanguageContainer + }{ + { + name: "generate is nil", + args: []string{"generate"}, + container: &LanguageContainer{ + ReleaseInit: func(context.Context, *release.Config) (*message.ReleaseInitResponse, error) { + return nil, nil + }, + }, + }, + { + name: "release-init is nil", + args: []string{"release-init"}, + container: &LanguageContainer{ + Generate: func(context.Context, *generate.Config) error { + return nil + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotCode := Run(tt.args, tt.container); gotCode != 1 { + t.Errorf("Run() = %v, want 1", gotCode) + } + }) + } +} diff --git a/internal/librariangen/languagecontainer/testdata/generate-request.json b/internal/librariangen/languagecontainer/testdata/generate-request.json new file mode 100644 index 0000000000..894b5639fc --- /dev/null +++ b/internal/librariangen/languagecontainer/testdata/generate-request.json @@ -0,0 +1,22 @@ +{ + "id": "chronicle", + "version": "0.1.1", + "apis": [ + { + "path": "google/cloud/chronicle/v1", + "service_config": "chronicle_v1.yaml" + } + ], + "source_roots": [ + "chronicle", + "internal/generated/snippets/chronicle" + ], + "preserve_regex": [ + "chronicle/aliasshim/aliasshim.go", + "chronicle/CHANGES.md" + ], + "remove_regex": [ + "chronicle", + "internal/generated/snippets/chronicle" + ] +} \ No newline at end of file diff --git a/internal/librariangen/main.go b/internal/librariangen/main.go index cab98ca252..085c0b29a7 100644 --- a/internal/librariangen/main.go +++ b/internal/librariangen/main.go @@ -15,13 +15,9 @@ package main import ( - "context" - "errors" - "flag" "fmt" "log/slog" "os" - "strings" "cloud.google.com/java/internal/librariangen/generate" "cloud.google.com/java/internal/librariangen/languagecontainer" @@ -35,22 +31,28 @@ func main() { os.Exit(runCLI(os.Args)) } -var ( - generateFunc = generate.Generate -) - func runCLI(args []string) int { logLevel := parseLogLevel(os.Getenv("GOOGLE_SDK_JAVA_LOGGING_LEVEL")) slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: logLevel, }))) slog.Info("librariangen: invoked", "args", args) - if err := run(context.Background(), args[1:]); err != nil { - slog.Error("librariangen: failed", "error", err) + if len(args) < 2 { + slog.Error("librariangen: expected a command") return 1 } - slog.Info("librariangen: finished successfully") - return 0 + + // The --version flag is a special case and not a command. + if args[1] == "--version" { + fmt.Println(version) + return 0 + } + + container := languagecontainer.LanguageContainer{ + Generate: generate.Generate, + ReleaseInit: release.Init, + } + return languagecontainer.Run(args[1:], &container) } func parseLogLevel(logLevelEnv string) slog.Level { @@ -63,53 +65,3 @@ func parseLogLevel(logLevelEnv string) slog.Level { return slog.LevelInfo } } - -// 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": - // TODO(suztomo): Move this as part of container.Generate and - // remove the need of this run function. - return handleGenerate(ctx, flags) - default: - container := languagecontainer.LanguageContainer{ - ReleaseInit: release.Init, - } - if exitCode := languagecontainer.Run(args, &container); exitCode != 0 { - return fmt.Errorf("command failed with exit code %d", exitCode) - } - return nil - } -} - -// handleGenerate parses flags for the generate command and calls the generator. -func handleGenerate(ctx context.Context, args []string) error { - cfg := &generate.Config{} - generateFlags := flag.NewFlagSet("generate", flag.ContinueOnError) - generateFlags.StringVar(&cfg.LibrarianDir, "librarian", "/librarian", "Path to the librarian-tool input directory. Contains generate-request.json.") - generateFlags.StringVar(&cfg.InputDir, "input", "/input", "Path to the .librarian/generator-input directory from the language repository.") - generateFlags.StringVar(&cfg.OutputDir, "output", "/output", "Path to the empty directory where librariangen writes its output.") - generateFlags.StringVar(&cfg.SourceDir, "source", "/source", "Path to a complete checkout of the googleapis repository.") - if err := generateFlags.Parse(args); err != nil { - return fmt.Errorf("librariangen: failed to parse flags: %w", err) - } - return generateFunc(ctx, cfg) -}