From 6065d82a1aa1069b36d61822b3e9a613004bb607 Mon Sep 17 00:00:00 2001 From: Hongda Jiang Date: Mon, 29 Jun 2026 19:27:02 +0000 Subject: [PATCH 1/4] feat(internal/librarian): add debug command with env subcommand A new debug command with an env subcommand is added to the CLI to print the librarian environment. This includes resolved paths for LIBRARIAN_CACHE, LIBRARIAN_BIN, and language-specific tool installation directories. To support accessing the tool installation directories from the new command, the internal getInstallDir functions in the golang and java packages are exported as InstallDir. --- cmd/librarian/doc.go | 16 ++++++ internal/librarian/debug.go | 69 +++++++++++++++++++++++ internal/librarian/debug_test.go | 58 +++++++++++++++++++ internal/librarian/golang/command.go | 2 +- internal/librarian/golang/install.go | 20 +++---- internal/librarian/golang/install_test.go | 2 +- internal/librarian/java/install.go | 22 ++++---- internal/librarian/librarian.go | 1 + 8 files changed, 167 insertions(+), 23 deletions(-) create mode 100644 internal/librarian/debug.go create mode 100644 internal/librarian/debug_test.go diff --git a/cmd/librarian/doc.go b/cmd/librarian/doc.go index face1f6cdef..d594cb8546d 100644 --- a/cmd/librarian/doc.go +++ b/cmd/librarian/doc.go @@ -187,5 +187,21 @@ Usage: version prints the librarian binary version and exits. The version is embedded at build time and follows the conventions described at https://go.dev/ref/mod#versions. + +# Various debugging commands + +Usage: + + librarian debug [env] + +# Print environment variables for the librarian command line interface. + +Usage: + + librarian debug env + +env prints the librarian interpretation of the environment it is run in. +This includes the resolved LIBRARIAN_CACHE and LIBRARIAN_BIN paths, +as well as the language-specific tool installation directories. */ package main diff --git a/internal/librarian/debug.go b/internal/librarian/debug.go new file mode 100644 index 00000000000..862c9ad3718 --- /dev/null +++ b/internal/librarian/debug.go @@ -0,0 +1,69 @@ +// Copyright 2026 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 +// +// https://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 librarian + +import ( + "context" + "fmt" + "io" + + "github.com/googleapis/librarian/internal/cache" + "github.com/googleapis/librarian/internal/librarian/golang" + "github.com/googleapis/librarian/internal/librarian/java" + "github.com/urfave/cli/v3" +) + +// debugCommand returns the CLI command for librarian debugging tools. +func debugCommand() *cli.Command { + return &cli.Command{ + Name: "debug", + Usage: "various debugging commands", + UsageText: "librarian debug [command]", + Commands: []*cli.Command{ + envCommand(), + }, + } +} + +// envCommand returns the CLI command for printing the librarian environment. +func envCommand() *cli.Command { + return &cli.Command{ + Name: "env", + Usage: "print environment variables for the librarian command line interface.", + UsageText: "librarian debug env", + Description: `env prints the librarian interpretation of the environment it is run in. +This includes the resolved LIBRARIAN_CACHE and LIBRARIAN_BIN paths, +as well as the language-specific tool installation directories.`, + Action: func(ctx context.Context, cmd *cli.Command) error { + return runEnv(cmd.Root().Writer) + }, + } +} + +func runEnv(w io.Writer) error { + cacheDir, _ := cache.Directory() + buildDir, _ := cache.BinDirectory() + goToolsDir, _ := golang.InstallDir() + javaToolsDir, _ := java.InstallDir() + + fmt.Fprintf(w, "LIBRARIAN_CACHE=%s\n", cacheDir) + fmt.Fprintf(w, "LIBRARIAN_BIN=%s\n", buildDir) + + fmt.Fprintf(w, "\nLanguage-specific tool installation directories:\n") + fmt.Fprintf(w, " golang: %s\n", goToolsDir) + fmt.Fprintf(w, " java: %s\n", javaToolsDir) + + return nil +} diff --git a/internal/librarian/debug_test.go b/internal/librarian/debug_test.go new file mode 100644 index 00000000000..c693b6ffef0 --- /dev/null +++ b/internal/librarian/debug_test.go @@ -0,0 +1,58 @@ +// Copyright 2026 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 +// +// https://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 librarian + +import ( + "bytes" + "fmt" + "path/filepath" + "strings" + "testing" +) + +func TestRunEnv(t *testing.T) { + for _, test := range []struct { + name string + }{ + {"success"}, + } { + t.Run(test.name, func(t *testing.T) { + cacheDir := t.TempDir() + binDir := t.TempDir() + + t.Setenv("LIBRARIAN_CACHE", cacheDir) + t.Setenv("LIBRARIAN_BIN", binDir) + + var buf bytes.Buffer + if err := runEnv(&buf); err != nil { + t.Fatal(err) + } + + got := buf.String() + wants := []string{ + fmt.Sprintf("LIBRARIAN_CACHE=%s", cacheDir), + fmt.Sprintf("LIBRARIAN_BIN=%s", binDir), + fmt.Sprintf("golang: %s", filepath.Join(binDir, "go_tools")), + fmt.Sprintf("java: %s", filepath.Join(binDir, "java_tools")), + } + + for _, want := range wants { + if !strings.Contains(got, want) { + t.Errorf("runEnv() output missing %q\ngot:\n%s", want, got) + } + } + }) + } +} diff --git a/internal/librarian/golang/command.go b/internal/librarian/golang/command.go index 26cdf36327d..c1f10742b36 100644 --- a/internal/librarian/golang/command.go +++ b/internal/librarian/golang/command.go @@ -41,7 +41,7 @@ func runInDirWithEnv(ctx context.Context, dir string, env map[string]string, cmd // mergeEnv merges the given environment with the installation directory. func mergeEnv(env map[string]string) (map[string]string, error) { - toolsBinDir, err := getInstallDir() + toolsBinDir, err := InstallDir() if err != nil { return nil, err } diff --git a/internal/librarian/golang/install.go b/internal/librarian/golang/install.go index b5b79ffca95..7f25b4cdfac 100644 --- a/internal/librarian/golang/install.go +++ b/internal/librarian/golang/install.go @@ -45,8 +45,17 @@ func Install(ctx context.Context, tools *config.Tools) error { return installGoTools(ctx, tools.Go) } +// InstallDir gets the directory where tools should be installed. +func InstallDir() (string, error) { + dir, err := cache.BinDirectory() + if err != nil { + return "", err + } + return filepath.Abs(filepath.Join(dir, toolsDir)) +} + func installGoTools(ctx context.Context, goTools []*config.GoTool) error { - installDir, err := getInstallDir() + installDir, err := InstallDir() if err != nil { return err } @@ -62,12 +71,3 @@ func installGoTools(ctx context.Context, goTools []*config.GoTool) error { } return nil } - -// getInstallDir gets the directory where tools should be installed. -func getInstallDir() (string, error) { - dir, err := cache.BinDirectory() - if err != nil { - return "", err - } - return filepath.Abs(filepath.Join(dir, toolsDir)) -} diff --git a/internal/librarian/golang/install_test.go b/internal/librarian/golang/install_test.go index aa32a91a6c9..90f2f573549 100644 --- a/internal/librarian/golang/install_test.go +++ b/internal/librarian/golang/install_test.go @@ -96,7 +96,7 @@ func TestGetInstallDir(t *testing.T) { for k, v := range test.env { t.Setenv(k, v) } - got, err := getInstallDir() + got, err := InstallDir() if err != nil { t.Fatal(err) } diff --git a/internal/librarian/java/install.go b/internal/librarian/java/install.go index 151ffe44de6..73ae1fcc2e1 100644 --- a/internal/librarian/java/install.go +++ b/internal/librarian/java/install.go @@ -84,6 +84,15 @@ func Install(ctx context.Context, tools *config.Tools) error { return nil } +// InstallDir returns the absolute path of the installation directory for Java tools. +func InstallDir() (string, error) { + dir, err := cache.BinDirectory() + if err != nil { + return "", err + } + return filepath.Abs(filepath.Join(dir, toolsDir)) +} + // installExternalMavenTool downloads a Maven-based external tool, copies its compiled artifact // (.jar or .exe) to the sibling lib folder, and creates an executable wrapper script // in the bin folder pointing directly to that library file. @@ -233,18 +242,9 @@ func buildLocalMavenProject(ctx context.Context, localPath string) error { return nil } -// getInstallDir returns the absolute path of the installation directory for Java tools. -func getInstallDir() (string, error) { - dir, err := cache.BinDirectory() - if err != nil { - return "", err - } - return filepath.Abs(filepath.Join(dir, toolsDir)) -} - // getBinDir returns the absolute path of the directory where Java tool wrapper scripts are stored. func getBinDir() (string, error) { - installDir, err := getInstallDir() + installDir, err := InstallDir() if err != nil { return "", err } @@ -254,7 +254,7 @@ func getBinDir() (string, error) { // getLibDir returns the absolute path of the directory where Java tool library files (such as .jar // or .exe files) are stored. func getLibDir() (string, error) { - installDir, err := getInstallDir() + installDir, err := InstallDir() if err != nil { return "", err } diff --git a/internal/librarian/librarian.go b/internal/librarian/librarian.go index ffc1e8e20b9..4f9e3920753 100644 --- a/internal/librarian/librarian.go +++ b/internal/librarian/librarian.go @@ -66,6 +66,7 @@ func Run(ctx context.Context, args ...string) error { publishCommand(), tagCommand(), versionCommand(), + debugCommand(), }, } return cmd.Run(ctx, args) From 0fac39130660bf1e23aeea0025b932aca4265012 Mon Sep 17 00:00:00 2001 From: Hongda Jiang Date: Mon, 29 Jun 2026 21:05:42 +0000 Subject: [PATCH 2/4] gracefully handle error from Directory etc --- cmd/librarian/doc.go | 2 +- internal/librarian/debug.go | 17 +++++-- internal/librarian/debug_test.go | 81 ++++++++++++++++++++------------ 3 files changed, 64 insertions(+), 36 deletions(-) diff --git a/cmd/librarian/doc.go b/cmd/librarian/doc.go index d594cb8546d..1a62c4d5ea3 100644 --- a/cmd/librarian/doc.go +++ b/cmd/librarian/doc.go @@ -192,7 +192,7 @@ https://go.dev/ref/mod#versions. Usage: - librarian debug [env] + librarian debug [command] # Print environment variables for the librarian command line interface. diff --git a/internal/librarian/debug.go b/internal/librarian/debug.go index 862c9ad3718..5ada38a7bc9 100644 --- a/internal/librarian/debug.go +++ b/internal/librarian/debug.go @@ -53,10 +53,10 @@ as well as the language-specific tool installation directories.`, } func runEnv(w io.Writer) error { - cacheDir, _ := cache.Directory() - buildDir, _ := cache.BinDirectory() - goToolsDir, _ := golang.InstallDir() - javaToolsDir, _ := java.InstallDir() + cacheDir := dirOrErr(cache.Directory()) + buildDir := dirOrErr(cache.BinDirectory()) + goToolsDir := dirOrErr(golang.InstallDir()) + javaToolsDir := dirOrErr(java.InstallDir()) fmt.Fprintf(w, "LIBRARIAN_CACHE=%s\n", cacheDir) fmt.Fprintf(w, "LIBRARIAN_BIN=%s\n", buildDir) @@ -67,3 +67,12 @@ func runEnv(w io.Writer) error { return nil } + +// dirOrErr converts a directory path and potential error into a string. If an error +// occurred, it returns a formatted error string; otherwise, it returns the directory path. +func dirOrErr(dir string, err error) string { + if err != nil { + return fmt.Sprintf("", err) + } + return dir +} diff --git a/internal/librarian/debug_test.go b/internal/librarian/debug_test.go index c693b6ffef0..8a9a98433a2 100644 --- a/internal/librarian/debug_test.go +++ b/internal/librarian/debug_test.go @@ -23,36 +23,55 @@ import ( ) func TestRunEnv(t *testing.T) { - for _, test := range []struct { - name string - }{ - {"success"}, - } { - t.Run(test.name, func(t *testing.T) { - cacheDir := t.TempDir() - binDir := t.TempDir() - - t.Setenv("LIBRARIAN_CACHE", cacheDir) - t.Setenv("LIBRARIAN_BIN", binDir) - - var buf bytes.Buffer - if err := runEnv(&buf); err != nil { - t.Fatal(err) - } - - got := buf.String() - wants := []string{ - fmt.Sprintf("LIBRARIAN_CACHE=%s", cacheDir), - fmt.Sprintf("LIBRARIAN_BIN=%s", binDir), - fmt.Sprintf("golang: %s", filepath.Join(binDir, "go_tools")), - fmt.Sprintf("java: %s", filepath.Join(binDir, "java_tools")), - } - - for _, want := range wants { - if !strings.Contains(got, want) { - t.Errorf("runEnv() output missing %q\ngot:\n%s", want, got) - } - } - }) + cacheDir := t.TempDir() + binDir := t.TempDir() + + t.Setenv("LIBRARIAN_CACHE", cacheDir) + t.Setenv("LIBRARIAN_BIN", binDir) + + var buf bytes.Buffer + if err := runEnv(&buf); err != nil { + t.Fatal(err) + } + + got := buf.String() + wants := []string{ + fmt.Sprintf("LIBRARIAN_CACHE=%s", cacheDir), + fmt.Sprintf("LIBRARIAN_BIN=%s", binDir), + fmt.Sprintf("golang: %s", filepath.Join(binDir, "go_tools")), + fmt.Sprintf("java: %s", filepath.Join(binDir, "java_tools")), + } + + for _, want := range wants { + if !strings.Contains(got, want) { + t.Errorf("runEnv() output missing %q\ngot:\n%s", want, got) + } + } +} + +func TestRunEnv_Error(t *testing.T) { + // Unset environment variables to force path resolution errors. + t.Setenv("LIBRARIAN_CACHE", "") + t.Setenv("LIBRARIAN_BIN", "") + t.Setenv("HOME", "") + t.Setenv("XDG_CACHE_HOME", "") + + var buf bytes.Buffer + if err := runEnv(&buf); err != nil { + t.Fatal(err) + } + + got := buf.String() + wants := []string{ + "LIBRARIAN_CACHE= Date: Tue, 30 Jun 2026 14:56:05 +0000 Subject: [PATCH 3/4] remove empty lines --- internal/librarian/debug.go | 3 --- internal/librarian/debug_test.go | 7 ------- 2 files changed, 10 deletions(-) diff --git a/internal/librarian/debug.go b/internal/librarian/debug.go index 5ada38a7bc9..a18575b7504 100644 --- a/internal/librarian/debug.go +++ b/internal/librarian/debug.go @@ -57,14 +57,11 @@ func runEnv(w io.Writer) error { buildDir := dirOrErr(cache.BinDirectory()) goToolsDir := dirOrErr(golang.InstallDir()) javaToolsDir := dirOrErr(java.InstallDir()) - fmt.Fprintf(w, "LIBRARIAN_CACHE=%s\n", cacheDir) fmt.Fprintf(w, "LIBRARIAN_BIN=%s\n", buildDir) - fmt.Fprintf(w, "\nLanguage-specific tool installation directories:\n") fmt.Fprintf(w, " golang: %s\n", goToolsDir) fmt.Fprintf(w, " java: %s\n", javaToolsDir) - return nil } diff --git a/internal/librarian/debug_test.go b/internal/librarian/debug_test.go index 8a9a98433a2..7ce9c768c96 100644 --- a/internal/librarian/debug_test.go +++ b/internal/librarian/debug_test.go @@ -25,15 +25,12 @@ import ( func TestRunEnv(t *testing.T) { cacheDir := t.TempDir() binDir := t.TempDir() - t.Setenv("LIBRARIAN_CACHE", cacheDir) t.Setenv("LIBRARIAN_BIN", binDir) - var buf bytes.Buffer if err := runEnv(&buf); err != nil { t.Fatal(err) } - got := buf.String() wants := []string{ fmt.Sprintf("LIBRARIAN_CACHE=%s", cacheDir), @@ -41,7 +38,6 @@ func TestRunEnv(t *testing.T) { fmt.Sprintf("golang: %s", filepath.Join(binDir, "go_tools")), fmt.Sprintf("java: %s", filepath.Join(binDir, "java_tools")), } - for _, want := range wants { if !strings.Contains(got, want) { t.Errorf("runEnv() output missing %q\ngot:\n%s", want, got) @@ -55,12 +51,10 @@ func TestRunEnv_Error(t *testing.T) { t.Setenv("LIBRARIAN_BIN", "") t.Setenv("HOME", "") t.Setenv("XDG_CACHE_HOME", "") - var buf bytes.Buffer if err := runEnv(&buf); err != nil { t.Fatal(err) } - got := buf.String() wants := []string{ "LIBRARIAN_CACHE= Date: Tue, 30 Jun 2026 15:35:12 +0000 Subject: [PATCH 4/4] propogate writer error --- internal/librarian/debug.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/internal/librarian/debug.go b/internal/librarian/debug.go index a18575b7504..2c2d9fbbdd3 100644 --- a/internal/librarian/debug.go +++ b/internal/librarian/debug.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "io" + "strings" "github.com/googleapis/librarian/internal/cache" "github.com/googleapis/librarian/internal/librarian/golang" @@ -57,12 +58,15 @@ func runEnv(w io.Writer) error { buildDir := dirOrErr(cache.BinDirectory()) goToolsDir := dirOrErr(golang.InstallDir()) javaToolsDir := dirOrErr(java.InstallDir()) - fmt.Fprintf(w, "LIBRARIAN_CACHE=%s\n", cacheDir) - fmt.Fprintf(w, "LIBRARIAN_BIN=%s\n", buildDir) - fmt.Fprintf(w, "\nLanguage-specific tool installation directories:\n") - fmt.Fprintf(w, " golang: %s\n", goToolsDir) - fmt.Fprintf(w, " java: %s\n", javaToolsDir) - return nil + var b strings.Builder + fmt.Fprintf(&b, "LIBRARIAN_CACHE=%s\n", cacheDir) + fmt.Fprintf(&b, "LIBRARIAN_BIN=%s\n", buildDir) + fmt.Fprintln(&b) + fmt.Fprintln(&b, "Language-specific tool installation directories:") + fmt.Fprintf(&b, " golang: %s\n", goToolsDir) + fmt.Fprintf(&b, " java: %s\n", javaToolsDir) + _, err := io.WriteString(w, b.String()) + return err } // dirOrErr converts a directory path and potential error into a string. If an error