Skip to content

Commit 7324098

Browse files
authored
fix(devtools/cmd/sidekick): add multi module rust libraries (#3227)
Add modules to a rust library if the library has multiple `.sidekick.toml` files. These are the veneer libraries in google-cloud-rust repository. I use a separate code path to parse these libraries, rather than consolidate the code path with parsing GAPIC library because the GAPICs and venners live in separate directories, `src/generated` and `src/` respectively. Use separate code path to parse two types of library seems intuitive. Rust GAPIC library lives in `src/generated` directory of google-cloud-rust. Each library has one `Cargo.toml` and `.sidekick.toml` in the library directory, e.g., `src/generated/cloud/alloydb/v1`. Rust Venner library lives in `src` directory (except for `src/generated`) of google-cloud-rust. Each library has one `Cargo.toml` and multiple `.sidekick.toml`, one per module, e.g., `src/storage/src/generated/gapic`. Tested with google-cloud-storage: ``` migrate-sidekick . librarian generate google-cloud-storage ``` Only `.sidekick.toml` within src/storage are removed. Fixes #3193
1 parent d2de998 commit 7324098

File tree

14 files changed

+398
-29
lines changed

14 files changed

+398
-29
lines changed

devtools/cmd/migrate-sidekick/main.go

Lines changed: 154 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"flag"
2121
"fmt"
2222
"log/slog"
23+
"maps"
2324
"os"
2425
"path/filepath"
2526
"reflect"
@@ -34,6 +35,7 @@ import (
3435

3536
const (
3637
sidekickFile = ".sidekick.toml"
38+
cargoFile = "Cargo.toml"
3739
discoveryArchivePrefix = "https://github.com/googleapis/discovery-artifact-manager/archive/"
3840
googleapisArchivePrefix = "https://github.com/googleapis/googleapis/archive/"
3941
tarballSuffix = ".tar.gz"
@@ -42,7 +44,6 @@ const (
4244
var (
4345
errRepoNotFound = errors.New("-repo flag is required")
4446
errSidekickNotFound = errors.New(".sidekick.toml not found")
45-
errSrcNotFound = errors.New("src/generated directory not found")
4647
errTidyFailed = errors.New("librarian tidy failed")
4748
errUnableToCalculateOutputPath = errors.New("unable to calculate output path")
4849
)
@@ -95,31 +96,43 @@ func run(args []string) error {
9596
}
9697
repoPath := flagSet.Arg(0)
9798

98-
slog.Info("Reading sidekick.toml...", "path", repoPath)
99-
99+
// Read root .sidekick.toml for defaults
100100
defaults, err := readRootSidekick(repoPath)
101101
if err != nil {
102102
return fmt.Errorf("failed to read root .sidekick.toml: %w", err)
103103
}
104104

105-
// Find all .sidekick.toml files
106-
sidekickFiles, err := findSidekickFiles(repoPath)
105+
// Find all .sidekick.toml files for GAPIC libraries.
106+
sidekickFiles, err := findSidekickFiles(filepath.Join(repoPath, "src", "generated"))
107107
if err != nil {
108108
return fmt.Errorf("failed to find sidekick.toml files: %w", err)
109109
}
110110

111-
// Read all sidekick.toml files
112-
libraries, err := readSidekickFiles(sidekickFiles, repoPath)
111+
libraries, err := buildGAPIC(sidekickFiles, repoPath)
113112
if err != nil {
114113
return fmt.Errorf("failed to read sidekick.toml files: %w", err)
115114
}
116115

117-
cfg := buildConfig(libraries, defaults)
116+
cargoFiles, err := findCargos(filepath.Join(repoPath, "src"))
117+
if err != nil {
118+
return fmt.Errorf("failed to find Cargo.toml files: %w", err)
119+
}
120+
121+
veneers, err := buildVeneer(cargoFiles)
122+
if err != nil {
123+
return fmt.Errorf("failed to build veneers: %w", err)
124+
}
125+
126+
allLibraries := make(map[string]*config.Library, len(libraries)+len(veneers))
127+
maps.Copy(allLibraries, libraries)
128+
maps.Copy(allLibraries, veneers)
129+
130+
cfg := buildConfig(allLibraries, defaults)
118131

119132
if err := yaml.Write(*outputPath, cfg); err != nil {
120133
return fmt.Errorf("failed to write config: %w", err)
121134
}
122-
slog.Info("Wrote config to output file", "path", outputPath)
135+
slog.Info("Wrote config to output file", "path", *outputPath)
123136

124137
if err := librarian.RunTidy(); err != nil {
125138
slog.Error(errTidyFailed.Error(), "error", err)
@@ -215,16 +228,14 @@ func parsePackageDependency(name, spec string) *config.RustPackageDependency {
215228
return dep
216229
}
217230

218-
// findSidekickFiles finds all .sidekick.toml files in the repository.
219-
func findSidekickFiles(repoPath string) ([]string, error) {
231+
// findSidekickFiles finds all .sidekick.toml files within the given path.
232+
func findSidekickFiles(path string) ([]string, error) {
220233
var files []string
221-
222-
generatedPath := filepath.Join(repoPath, "src", "generated")
223-
err := filepath.Walk(generatedPath, func(path string, info os.FileInfo, err error) error {
234+
err := filepath.WalkDir(path, func(path string, d os.DirEntry, err error) error {
224235
if err != nil {
225-
return errSrcNotFound
236+
return err
226237
}
227-
if !info.IsDir() && info.Name() == sidekickFile {
238+
if !d.IsDir() && d.Name() == sidekickFile {
228239
files = append(files, path)
229240
}
230241
return nil
@@ -241,8 +252,7 @@ func findSidekickFiles(repoPath string) ([]string, error) {
241252
return files, nil
242253
}
243254

244-
// readSidekickFiles reads all sidekick.toml files and extracts library information.
245-
func readSidekickFiles(files []string, repoPath string) (map[string]*config.Library, error) {
255+
func buildGAPIC(files []string, repoPath string) (map[string]*config.Library, error) {
246256
libraries := make(map[string]*config.Library)
247257

248258
for _, file := range files {
@@ -271,15 +281,9 @@ func readSidekickFiles(files []string, repoPath string) (map[string]*config.Libr
271281

272282
// Read Cargo.toml in the same directory to get the actual library name
273283
dir := filepath.Dir(file)
274-
cargoPath := filepath.Join(dir, "Cargo.toml")
275-
cargoData, err := os.ReadFile(cargoPath)
284+
cargo, err := readTOML[CargoConfig](filepath.Join(dir, cargoFile))
276285
if err != nil {
277-
return nil, fmt.Errorf("failed to read %s: %w", cargoPath, err)
278-
}
279-
280-
var cargo CargoConfig
281-
if err := toml.Unmarshal(cargoData, &cargo); err != nil {
282-
return nil, fmt.Errorf("failed to unmarshal %s: %w", cargoPath, err)
286+
return nil, fmt.Errorf("failed to read cargo: %w", err)
283287
}
284288

285289
libraryName := cargo.Package.Name
@@ -424,6 +428,116 @@ func deriveLibraryName(apiPath string) string {
424428
return "google-cloud-" + strings.ReplaceAll(trimmedPath, "/", "-")
425429
}
426430

431+
// findCargos returns all Cargo.toml files within the given path.
432+
//
433+
// A file is filtered if the file lives in a path that contains src/generated.
434+
func findCargos(path string) ([]string, error) {
435+
var files []string
436+
err := filepath.WalkDir(path, func(path string, d os.DirEntry, err error) error {
437+
if err != nil {
438+
return err
439+
}
440+
441+
if d.IsDir() && strings.Contains(path, "src/generated") {
442+
return filepath.SkipDir
443+
}
444+
445+
if d.IsDir() || d.Name() != cargoFile {
446+
return nil
447+
}
448+
449+
files = append(files, path)
450+
451+
return nil
452+
})
453+
return files, err
454+
}
455+
456+
func buildVeneer(files []string) (map[string]*config.Library, error) {
457+
veneers := make(map[string]*config.Library)
458+
for _, file := range files {
459+
cargo, err := readTOML[CargoConfig](file)
460+
if err != nil {
461+
return nil, err
462+
}
463+
dir := filepath.Dir(file)
464+
rustModules, err := buildModules(dir)
465+
if err != nil {
466+
return nil, err
467+
}
468+
name := cargo.Package.Name
469+
veneers[name] = &config.Library{
470+
Name: name,
471+
Veneer: true,
472+
Output: dir,
473+
Version: cargo.Package.Version,
474+
CopyrightYear: "2025",
475+
}
476+
if rustModules != nil {
477+
veneers[name].Rust = &config.RustCrate{
478+
Modules: rustModules,
479+
}
480+
}
481+
}
482+
483+
return veneers, nil
484+
}
485+
486+
func buildModules(path string) ([]*config.RustModule, error) {
487+
var modules []*config.RustModule
488+
err := filepath.WalkDir(path, func(path string, d os.DirEntry, err error) error {
489+
if err != nil {
490+
return err
491+
}
492+
493+
if d.IsDir() || d.Name() != sidekickFile {
494+
return nil
495+
}
496+
497+
sidekick, err := readTOML[SidekickConfig](path)
498+
if err != nil {
499+
return err
500+
}
501+
502+
includedIds, _ := sidekick.Source["included-ids"].(string)
503+
includeList, _ := sidekick.Source["include-list"].(string)
504+
skippedIds, _ := sidekick.Source["skipped-ids"].(string)
505+
506+
hasVeneer, _ := sidekick.Codec["has-veneer"].(string)
507+
includeGrpcOnlyMethods, _ := sidekick.Codec["include-grpc-only-methods"].(string)
508+
routingRequired, _ := sidekick.Codec["routing-required"].(string)
509+
modulePath, _ := sidekick.Codec["module-path"].(string)
510+
nameOverrides, _ := sidekick.Codec["name-overrides"].(string)
511+
postProcessProtos, _ := sidekick.Codec["post-process-protos"].(string)
512+
templateOverride, _ := sidekick.Codec["template-override"].(string)
513+
generateSetterSamples, ok := sidekick.Codec["generate-setter-samples"].(string)
514+
if !ok {
515+
generateSetterSamples = "true"
516+
}
517+
518+
modules = append(modules, &config.RustModule{
519+
GenerateSetterSamples: strToBool(generateSetterSamples),
520+
HasVeneer: strToBool(hasVeneer),
521+
IncludedIds: strToSlice(includedIds),
522+
IncludeGrpcOnlyMethods: strToBool(includeGrpcOnlyMethods),
523+
IncludeList: includeList,
524+
ModulePath: modulePath,
525+
NameOverrides: nameOverrides,
526+
Output: filepath.Dir(path),
527+
PostProcessProtos: postProcessProtos,
528+
RoutingRequired: strToBool(routingRequired),
529+
ServiceConfig: sidekick.General.ServiceConfig,
530+
SkippedIds: strToSlice(skippedIds),
531+
Source: sidekick.General.SpecificationSource,
532+
Template: strings.TrimPrefix(templateOverride, "templates/"),
533+
})
534+
535+
return nil
536+
})
537+
538+
return modules, err
539+
}
540+
427541
// buildConfig builds the complete config from libraries.
428542
func buildConfig(libraries map[string]*config.Library, defaults *config.Config) *config.Config {
429543
cfg := defaults
@@ -506,3 +620,17 @@ func strToSlice(s string) []string {
506620
func isEmptyRustCrate(r *config.RustCrate) bool {
507621
return reflect.DeepEqual(r, &config.RustCrate{})
508622
}
623+
624+
func readTOML[T any](file string) (*T, error) {
625+
data, err := os.ReadFile(file)
626+
if err != nil {
627+
return nil, fmt.Errorf("failed to read %s: %w", file, err)
628+
}
629+
630+
var tomlData T
631+
if err := toml.Unmarshal(data, &tomlData); err != nil {
632+
return nil, fmt.Errorf("failed to unmarshal %s: %w", file, err)
633+
}
634+
635+
return &tomlData, nil
636+
}

0 commit comments

Comments
 (0)