Skip to content

Commit 124dad3

Browse files
authored
feat(internal/config): add tools section (#4981)
Adds config.Tools section to librarian.yaml to define language-specific tool dependencies. `librarian install` support is added for Rust For #4848
1 parent ac7685c commit 124dad3

File tree

9 files changed

+229
-0
lines changed

9 files changed

+229
-0
lines changed

doc/config-schema.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This document describes the schema for the librarian.yaml.
1010
| `version` | string | Is the librarian tool version to use. |
1111
| `repo` | string | Is the repository name, such as "googleapis/google-cloud-python". It is used for:<br>- Providing to the Java GAPIC generator for observability features.<br>- Generating the .repo-metadata.json. |
1212
| `sources` | [Sources](#sources-configuration) (optional) | References external source repositories. |
13+
| `tools` | [Tools](#tools-configuration) (optional) | Defines required tools. |
1314
| `release` | [Release](#release-configuration) (optional) | Holds the configuration parameter for publishing and release subcommands. |
1415
| `default` | [Default](#default-configuration) (optional) | Contains default settings for all libraries. They apply to all libraries unless overridden. |
1516
| `libraries` | list of [Library](#library-configuration) (optional) | Contains configuration overrides for libraries that need special handling, and differ from default settings. |
@@ -49,6 +50,19 @@ This document describes the schema for the librarian.yaml.
4950
| `sha256` | string | Is the expected hash of the tarball for this commit. |
5051
| `subpath` | string | Is a directory inside the fetched archive that should be treated as the root for operations. |
5152

53+
## Tools Configuration
54+
55+
| Field | Type | Description |
56+
| :--- | :--- | :--- |
57+
| `cargo` | list of [CargoTool](#cargotool-configuration) (optional) | Defines tools to install via cargo. |
58+
59+
## CargoTool Configuration
60+
61+
| Field | Type | Description |
62+
| :--- | :--- | :--- |
63+
| `name` | string | Is the cargo package name. |
64+
| `version` | string | Is the version to install. |
65+
5266
## Default Configuration
5367

5468
| Field | Type | Description |

internal/config/config.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ type Config struct {
4646
// Sources references external source repositories.
4747
Sources *Sources `yaml:"sources,omitempty"`
4848

49+
// Tools defines required tools.
50+
Tools *Tools `yaml:"tools,omitempty"`
51+
4952
// Release holds the configuration parameter for publishing and release subcommands.
5053
Release *Release `yaml:"release,omitempty"`
5154

@@ -121,6 +124,21 @@ type Source struct {
121124
Subpath string `yaml:"subpath,omitempty"`
122125
}
123126

127+
// Tools defines required tools.
128+
type Tools struct {
129+
// Cargo defines tools to install via cargo.
130+
Cargo []*CargoTool `yaml:"cargo,omitempty"`
131+
}
132+
133+
// CargoTool defines a tool to install via cargo.
134+
type CargoTool struct {
135+
// Name is the cargo package name.
136+
Name string `yaml:"name"`
137+
138+
// Version is the version to install.
139+
Version string `yaml:"version"`
140+
}
141+
124142
// Default contains default settings for all libraries.
125143
type Default struct {
126144
// Keep lists files and directories to preserve during regeneration.

internal/config/config_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ func TestRustRead(t *testing.T) {
5252
SHA256: "f572d396fae9206628714fb2ce00f72e94f2258f",
5353
},
5454
},
55+
Tools: &Tools{
56+
Cargo: []*CargoTool{
57+
{
58+
Name: "cargo-semver-checks",
59+
Version: "0.46.0",
60+
},
61+
{
62+
Name: "taplo-cli",
63+
Version: "0.10.0",
64+
},
65+
},
66+
},
5567
Default: &Default{
5668
Output: "src/generated/",
5769
TagFormat: "{name}/v{version}",

internal/config/testdata/rust/librarian.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ sources:
2929
conformance:
3030
commit: 1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b
3131
sha256: f572d396fae9206628714fb2ce00f72e94f2258f
32+
tools:
33+
cargo:
34+
- name: cargo-semver-checks
35+
version: "0.46.0"
36+
- name: taplo-cli
37+
version: "0.10.0"
3238
default:
3339
output: src/generated/
3440
release_level: stable

internal/librarian/librarian.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/googleapis/librarian/internal/command"
2525
"github.com/googleapis/librarian/internal/config"
2626
"github.com/googleapis/librarian/internal/librarian/golang"
27+
"github.com/googleapis/librarian/internal/librarian/rust"
2728
"github.com/googleapis/librarian/internal/yaml"
2829
"github.com/urfave/cli/v3"
2930
)
@@ -76,6 +77,8 @@ func installCommand() *cli.Command {
7677
switch cfg.Language {
7778
case config.LanguageGo:
7879
return golang.Install(ctx)
80+
case config.LanguageRust:
81+
return rust.Install(ctx, cfg.Tools)
7982
default:
8083
return fmt.Errorf("language %q does not support install", cfg.Language)
8184
}

internal/librarian/rust/install.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2026 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+
// https://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 rust
16+
17+
import (
18+
"context"
19+
"errors"
20+
"fmt"
21+
22+
"github.com/googleapis/librarian/internal/command"
23+
"github.com/googleapis/librarian/internal/config"
24+
)
25+
26+
// ErrMissingToolVersion indicates a cargo tool entry is missing its version.
27+
var ErrMissingToolVersion = errors.New("cargo tool missing version")
28+
29+
// Install installs cargo tool dependencies defined in the tools configuration.
30+
func Install(ctx context.Context, tools *config.Tools) error {
31+
if len(tools.Cargo) == 0 {
32+
return nil
33+
}
34+
for _, tool := range tools.Cargo {
35+
if tool.Version == "" {
36+
return fmt.Errorf("%w: %s", ErrMissingToolVersion, tool.Name)
37+
}
38+
t := fmt.Sprintf("%s@%s", tool.Name, tool.Version)
39+
if err := command.Run(ctx, "cargo", "install", "--locked", t); err != nil {
40+
return err
41+
}
42+
}
43+
return nil
44+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2026 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+
// https://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 rust
16+
17+
import (
18+
"errors"
19+
"os"
20+
"path/filepath"
21+
"testing"
22+
23+
"github.com/googleapis/librarian/internal/config"
24+
)
25+
26+
func TestInstall(t *testing.T) {
27+
bin := t.TempDir()
28+
if err := os.WriteFile(filepath.Join(bin, "cargo"), []byte("#!/bin/sh\n"), 0o755); err != nil {
29+
t.Fatal(err)
30+
}
31+
t.Setenv("PATH", bin+":"+os.Getenv("PATH"))
32+
33+
tools := &config.Tools{
34+
Cargo: []*config.CargoTool{
35+
{Name: "cargo-semver-checks", Version: "0.46.0"},
36+
{Name: "taplo-cli", Version: "0.10.0"},
37+
},
38+
}
39+
if err := Install(t.Context(), tools); err != nil {
40+
t.Fatal(err)
41+
}
42+
}
43+
44+
func TestInstall_MissingVersion(t *testing.T) {
45+
tools := &config.Tools{
46+
Cargo: []*config.CargoTool{
47+
{Name: "some-tool"},
48+
},
49+
}
50+
err := Install(t.Context(), tools)
51+
if !errors.Is(err, ErrMissingToolVersion) {
52+
t.Fatalf("got %v, want %v", err, ErrMissingToolVersion)
53+
}
54+
}
55+
56+
func TestInstall_NoCargoTools(t *testing.T) {
57+
if err := Install(t.Context(), &config.Tools{}); err != nil {
58+
t.Fatal(err)
59+
}
60+
}

internal/librarian/tidy.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/googleapis/librarian/internal/config"
2626
"github.com/googleapis/librarian/internal/librarian/golang"
2727
"github.com/googleapis/librarian/internal/librarian/java"
28+
"github.com/googleapis/librarian/internal/librarian/rust"
2829
"github.com/googleapis/librarian/internal/serviceconfig"
2930
"github.com/googleapis/librarian/internal/yaml"
3031
"github.com/urfave/cli/v3"
@@ -54,6 +55,9 @@ func tidyCommand() *cli.Command {
5455
// RunTidyOnConfig formats and validates the provided librarian configuration
5556
// and writes it to disk, relative to the specified repository root directory.
5657
func RunTidyOnConfig(ctx context.Context, repoDir string, cfg *config.Config) error {
58+
if err := validateTools(cfg); err != nil {
59+
return err
60+
}
5761
if err := validateLibraries(cfg); err != nil {
5862
return err
5963
}
@@ -104,6 +108,18 @@ func isDerivableOutput(cfg *config.Config, lib *config.Library) bool {
104108
return lib.Output == derivedOutput
105109
}
106110

111+
func validateTools(cfg *config.Config) error {
112+
if cfg.Tools == nil {
113+
return nil
114+
}
115+
for _, tool := range cfg.Tools.Cargo {
116+
if tool.Version == "" {
117+
return fmt.Errorf("%w: %s", rust.ErrMissingToolVersion, tool.Name)
118+
}
119+
}
120+
return nil
121+
}
122+
107123
func validateLibraries(cfg *config.Config) error {
108124
var (
109125
errs []error
@@ -175,6 +191,11 @@ func tidyRustConfig(lib *config.Library) *config.Library {
175191
}
176192

177193
func formatConfig(cfg *config.Config) *config.Config {
194+
if cfg.Tools != nil {
195+
slices.SortFunc(cfg.Tools.Cargo, func(a, b *config.CargoTool) int {
196+
return strings.Compare(a.Name, b.Name)
197+
})
198+
}
178199
if cfg.Default != nil && cfg.Default.Rust != nil {
179200
slices.SortFunc(cfg.Default.Rust.PackageDependencies, func(a, b *config.RustPackageDependency) int {
180201
return strings.Compare(a.Name, b.Name)

internal/librarian/tidy_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"github.com/google/go-cmp/cmp"
2525
"github.com/googleapis/librarian/internal/config"
26+
"github.com/googleapis/librarian/internal/librarian/rust"
2627
"github.com/googleapis/librarian/internal/sample"
2728
"github.com/googleapis/librarian/internal/yaml"
2829
)
@@ -68,8 +69,47 @@ func TestValidateLibraries(t *testing.T) {
6869
}
6970
}
7071

72+
func TestValidateTools_NoTools(t *testing.T) {
73+
if err := validateTools(&config.Config{}); err != nil {
74+
t.Fatal(err)
75+
}
76+
}
77+
78+
func TestValidateTools(t *testing.T) {
79+
cfg := &config.Config{
80+
Tools: &config.Tools{
81+
Cargo: []*config.CargoTool{
82+
{Name: "taplo-cli", Version: "0.10.0"},
83+
},
84+
},
85+
}
86+
if err := validateTools(cfg); err != nil {
87+
t.Fatal(err)
88+
}
89+
}
90+
91+
func TestValidateTools_MissingVersion(t *testing.T) {
92+
cfg := &config.Config{
93+
Tools: &config.Tools{
94+
Cargo: []*config.CargoTool{
95+
{Name: "taplo-cli"},
96+
},
97+
},
98+
}
99+
err := validateTools(cfg)
100+
if !errors.Is(err, rust.ErrMissingToolVersion) {
101+
t.Fatalf("got %v, want %v", err, rust.ErrMissingToolVersion)
102+
}
103+
}
104+
71105
func TestFormatConfig(t *testing.T) {
72106
cfg := formatConfig(&config.Config{
107+
Tools: &config.Tools{
108+
Cargo: []*config.CargoTool{
109+
{Name: "taplo-cli", Version: "0.10.0"},
110+
{Name: "cargo-semver-checks", Version: "0.46.0"},
111+
},
112+
},
73113
Default: &config.Default{
74114
Rust: &config.RustDefault{
75115
PackageDependencies: []*config.RustPackageDependency{
@@ -157,6 +197,17 @@ func TestFormatConfig(t *testing.T) {
157197
t.Errorf("mismatch (-want +got):\n%s", diff)
158198
}
159199
})
200+
201+
t.Run("sorts cargo tools by name", func(t *testing.T) {
202+
want := []string{"cargo-semver-checks", "taplo-cli"}
203+
var got []string
204+
for _, tool := range cfg.Tools.Cargo {
205+
got = append(got, tool.Name)
206+
}
207+
if diff := cmp.Diff(want, got); diff != "" {
208+
t.Errorf("mismatch (-want +got):\n%s", diff)
209+
}
210+
})
160211
}
161212

162213
func TestTidyCommand(t *testing.T) {

0 commit comments

Comments
 (0)