Skip to content

Commit 4df7577

Browse files
authored
feat(internal/librarian): allow language-specific parallelism (#4005)
Refactors generation such that each phase of clean/generate/format is treated as a whole across all libraries, moving towards language-specific determination of when to perform operations in parallel within each phase. This commit only delegates that choice for generation, but the same pattern can be followed for cleaning and formatting when there's a desire to do so. Fixes #3933
1 parent fa4618f commit 4df7577

17 files changed

Lines changed: 2869 additions & 120 deletions

internal/librarian/dart/generate.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,23 @@ import (
2626
sidekickdart "github.com/googleapis/librarian/internal/sidekick/dart"
2727
"github.com/googleapis/librarian/internal/sidekick/parser"
2828
"github.com/googleapis/librarian/internal/sidekick/source"
29+
"golang.org/x/sync/errgroup"
2930
)
3031

31-
// Generate generates a Dart client library.
32-
func Generate(ctx context.Context, library *config.Library, sources *source.Sources) error {
32+
// GenerateLibraries generates all the given libraries in parallel.
33+
func GenerateLibraries(ctx context.Context, libraries []*config.Library, sources *source.Sources) error {
34+
// Generate all libraries in parallel.
35+
g, gctx := errgroup.WithContext(ctx)
36+
for _, lib := range libraries {
37+
g.Go(func() error {
38+
return generate(gctx, lib, sources)
39+
})
40+
}
41+
return g.Wait()
42+
}
43+
44+
// generate generates a Dart client library.
45+
func generate(ctx context.Context, library *config.Library, sources *source.Sources) error {
3346
modelConfig, err := toModelConfig(library, library.APIs[0], sources)
3447
if err != nil {
3548
return err

internal/librarian/dart/generate_test.go

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package dart
1616

1717
import (
18+
"errors"
1819
"os"
1920
"path/filepath"
2021
"strings"
@@ -26,6 +27,89 @@ import (
2627
"github.com/googleapis/librarian/internal/testhelper"
2728
)
2829

30+
// TestGenerateLibraries performs simple testing that multiple libraries can
31+
// be generated. Only the presence of a single expected file per library is
32+
// performed; TestGenerate is responsible for more detailed testing of
33+
// per-library generation.
34+
func TestGenerateLibraries(t *testing.T) {
35+
testhelper.RequireCommand(t, "protoc")
36+
testhelper.RequireCommand(t, "dart")
37+
38+
googleapisDir, err := filepath.Abs("../../testdata/googleapis")
39+
if err != nil {
40+
t.Fatal(err)
41+
}
42+
outDir := t.TempDir()
43+
libraries := []*config.Library{
44+
{
45+
Name: "google_cloud_rpc",
46+
APIs: []*config.API{{Path: "google/rpc"}},
47+
CopyrightYear: "2025",
48+
Dart: &config.DartPackage{
49+
IssueTrackerURL: "https://placeholder",
50+
Packages: map[string]string{
51+
"package:google_cloud_protobuf": "^0.5.0",
52+
},
53+
},
54+
},
55+
{
56+
Name: "google_type",
57+
APIs: []*config.API{{Path: "google/type"}},
58+
CopyrightYear: "2025",
59+
Dart: &config.DartPackage{
60+
IssueTrackerURL: "https://placeholder",
61+
Packages: map[string]string{
62+
"package:google_cloud_protobuf": "^0.5.0",
63+
},
64+
},
65+
},
66+
}
67+
for _, library := range libraries {
68+
library.Output = filepath.Join(outDir, "generated", library.Name)
69+
}
70+
sources := &source.Sources{
71+
Googleapis: googleapisDir,
72+
}
73+
if err := GenerateLibraries(t.Context(), libraries, sources); err != nil {
74+
t.Fatal(err)
75+
}
76+
// Just check that a pubspec.yaml has been created for each library.
77+
for _, library := range libraries {
78+
expectedPubspec := filepath.Join(library.Output, "pubspec.yaml")
79+
_, err := os.Stat(expectedPubspec)
80+
if err != nil {
81+
t.Errorf("Stat(%s) returned error: %v", expectedPubspec, err)
82+
}
83+
}
84+
}
85+
86+
func TestGenerateLibraries_Error(t *testing.T) {
87+
testhelper.RequireCommand(t, "protoc")
88+
testhelper.RequireCommand(t, "dart")
89+
90+
googleapisDir, err := filepath.Abs("../../testdata/googleapis")
91+
if err != nil {
92+
t.Fatal(err)
93+
}
94+
outDir := t.TempDir()
95+
libraries := []*config.Library{
96+
{
97+
Name: "broken",
98+
SpecificationFormat: "invalid",
99+
Output: filepath.Join(outDir, "broken"),
100+
APIs: []*config.API{{Path: "broken"}},
101+
},
102+
}
103+
sources := &source.Sources{
104+
Googleapis: googleapisDir,
105+
}
106+
gotErr := GenerateLibraries(t.Context(), libraries, sources)
107+
wantErr := errInvalidSpecificationFormat
108+
if !errors.Is(gotErr, wantErr) {
109+
t.Errorf("GenerateLibraries error = %v, wantErr %v", gotErr, wantErr)
110+
}
111+
}
112+
29113
func TestGenerate(t *testing.T) {
30114
testhelper.RequireCommand(t, "protoc")
31115
testhelper.RequireCommand(t, "dart")
@@ -66,7 +150,7 @@ func TestGenerate(t *testing.T) {
66150
sources := &source.Sources{
67151
Googleapis: googleapisDir,
68152
}
69-
if err := Generate(t.Context(), library, sources); err != nil {
153+
if err := generate(t.Context(), library, sources); err != nil {
70154
t.Fatal(err)
71155
}
72156
if err := Format(t.Context(), library); err != nil {

internal/librarian/fake.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ func fakeBumpLibrary(lib *config.Library, nextVersion string) error {
3030
return nil
3131
}
3232

33+
func fakeGenerateLibraries(libraries []*config.Library) error {
34+
for _, library := range libraries {
35+
if err := fakeGenerate(library); err != nil {
36+
return err
37+
}
38+
}
39+
return nil
40+
}
41+
3342
func fakeGenerate(library *config.Library) error {
3443
if _, err := os.Stat(library.Output); err != nil {
3544
if !os.IsNotExist(err) {

internal/librarian/fake_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"github.com/googleapis/librarian/internal/config"
2424
)
2525

26-
func TestGenerate(t *testing.T) {
26+
func TestGenerateLibraries(t *testing.T) {
2727
const (
2828
libraryName = "test-library"
2929
outputDir = "output"
@@ -35,7 +35,7 @@ func TestGenerate(t *testing.T) {
3535

3636
tmpDir := t.TempDir()
3737
t.Chdir(tmpDir)
38-
if err := generate(t.Context(), "fake", library, "", nil); err != nil {
38+
if err := generateLibraries(t.Context(), "fake", []*config.Library{library}, "", nil); err != nil {
3939
t.Fatal(err)
4040
}
4141

internal/librarian/generate.go

Lines changed: 90 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import (
2929
"github.com/googleapis/librarian/internal/sidekick/source"
3030
"github.com/googleapis/librarian/internal/yaml"
3131
"github.com/urfave/cli/v3"
32-
"golang.org/x/sync/errgroup"
3332
)
3433

3534
const (
@@ -76,23 +75,20 @@ func runGenerate(ctx context.Context, cfg *config.Config, all bool, libraryName
7675
if cfg.Sources == nil {
7776
return errEmptySources
7877
}
79-
return generateLibraries(ctx, all, cfg, libraryName)
80-
}
8178

82-
func generateLibraries(ctx context.Context, all bool, cfg *config.Config, libraryName string) error {
8379
googleapisDir, rustDartSources, err := LoadSources(ctx, cfg)
8480
if err != nil {
8581
return err
8682
}
8783

88-
// Prepare and clean libraries sequentially.
89-
// This avoids race conditions when output directories are nested.
84+
// Prepare the libraries to generate by skipping as specified and applying
85+
// defaults.
9086
var libraries []*config.Library
9187
for _, lib := range cfg.Libraries {
9288
if !shouldGenerate(lib, all, libraryName) {
9389
continue
9490
}
95-
prepared, err := prepareLibrary(cfg.Language, lib, cfg.Default)
91+
prepared, err := applyDefaults(cfg.Language, lib, cfg.Default)
9692
if err != nil {
9793
return err
9894
}
@@ -110,22 +106,17 @@ func generateLibraries(ctx context.Context, all bool, cfg *config.Config, librar
110106
return fmt.Errorf("%w: %q", ErrLibraryNotFound, libraryName)
111107
}
112108

113-
// Generate all libraries in parallel.
114-
g, gctx := errgroup.WithContext(ctx)
115-
for _, lib := range libraries {
116-
g.Go(func() error {
117-
return generate(gctx, cfg.Language, lib, googleapisDir, rustDartSources)
118-
})
109+
// Clean, generate and format libraries. Each of these steps is completed
110+
// before the next one starts, but each language can choose whether to
111+
// implement the step in parallel across all libraries or in sequence.
112+
if err := cleanLibraries(cfg.Language, libraries); err != nil {
113+
return err
119114
}
120-
if err := g.Wait(); err != nil {
115+
if err := generateLibraries(ctx, cfg.Language, libraries, googleapisDir, rustDartSources); err != nil {
121116
return err
122117
}
123-
124-
// Format all libraries sequentially.
125-
for _, lib := range libraries {
126-
if err := formatLibrary(ctx, cfg.Language, lib); err != nil {
127-
return err
128-
}
118+
if err := formatLibraries(ctx, cfg.Language, libraries); err != nil {
119+
return err
129120
}
130121
return postGenerate(ctx, cfg.Language)
131122
}
@@ -158,6 +149,85 @@ func LoadSources(ctx context.Context, cfg *config.Config) (string, *source.Sourc
158149
return googleapisDir, rustDartSources, nil
159150
}
160151

152+
// cleanLibraries iterates over all the given libraries sequentially,
153+
// delegating to language-specific code to clean each library.
154+
func cleanLibraries(language string, libraries []*config.Library) error {
155+
for _, library := range libraries {
156+
switch language {
157+
case languageFake:
158+
// No cleaning needed.
159+
case languageDart, languagePython:
160+
if err := checkAndClean(library.Output, library.Keep); err != nil {
161+
return err
162+
}
163+
case languageGo:
164+
if err := golang.Clean(library); err != nil {
165+
return err
166+
}
167+
case languageRust:
168+
keep, err := rust.Keep(library)
169+
if err != nil {
170+
return fmt.Errorf("library %q: %w", library.Name, err)
171+
}
172+
if err := checkAndClean(library.Output, keep); err != nil {
173+
return err
174+
}
175+
}
176+
}
177+
return nil
178+
}
179+
180+
// generateLibraries delegates to language-specific code to generate all the
181+
// given libraries.
182+
func generateLibraries(ctx context.Context, language string, libraries []*config.Library, googleapisDir string, src *source.Sources) error {
183+
switch language {
184+
case languageFake:
185+
return fakeGenerateLibraries(libraries)
186+
case languageDart:
187+
return dart.GenerateLibraries(ctx, libraries, src)
188+
case languagePython:
189+
return python.GenerateLibraries(ctx, libraries, googleapisDir)
190+
case languageGo:
191+
return golang.GenerateLibraries(ctx, libraries, googleapisDir)
192+
case languageRust:
193+
return rust.GenerateLibraries(ctx, libraries, src)
194+
default:
195+
return fmt.Errorf("language %q does not support generation", language)
196+
}
197+
}
198+
199+
// formatLibraries iterates over all the given libraries sequentially,
200+
// delegating to language-specific code to format each library.
201+
func formatLibraries(ctx context.Context, language string, libraries []*config.Library) error {
202+
for _, library := range libraries {
203+
switch language {
204+
case languageFake:
205+
if err := fakeFormat(library); err != nil {
206+
return err
207+
}
208+
case languageDart:
209+
if err := dart.Format(ctx, library); err != nil {
210+
return err
211+
}
212+
case languageGo:
213+
if err := golang.Format(ctx, library); err != nil {
214+
return err
215+
}
216+
case languageRust:
217+
if err := rust.Format(ctx, library); err != nil {
218+
return err
219+
}
220+
case languagePython:
221+
// TODO(https://github.com/googleapis/librarian/issues/3730): separate
222+
// generation and formatting for Python.
223+
return nil
224+
default:
225+
return fmt.Errorf("language %q does not support formatting", language)
226+
}
227+
}
228+
return nil
229+
}
230+
161231
// postGenerate performs repository-level actions after all individual
162232
// libraries have been generated.
163233
func postGenerate(ctx context.Context, language string) error {
@@ -201,76 +271,3 @@ func shouldGenerate(lib *config.Library, all bool, libraryName string) bool {
201271
}
202272
return all || lib.Name == libraryName
203273
}
204-
205-
// prepareLibrary applies defaults and cleans the output directory.
206-
func prepareLibrary(language string, lib *config.Library, defaults *config.Default) (*config.Library, error) {
207-
library, err := applyDefaults(language, lib, defaults)
208-
if err != nil {
209-
return nil, err
210-
}
211-
switch language {
212-
case languageFake:
213-
// No cleaning needed.
214-
case languageDart, languagePython:
215-
if err := checkAndClean(library.Output, library.Keep); err != nil {
216-
return nil, err
217-
}
218-
case languageGo:
219-
return golang.Clean(library)
220-
case languageRust:
221-
keep, err := rust.Keep(library)
222-
if err != nil {
223-
return nil, fmt.Errorf("library %q: %w", library.Name, err)
224-
}
225-
if err := checkAndClean(library.Output, keep); err != nil {
226-
return nil, err
227-
}
228-
}
229-
return library, nil
230-
}
231-
232-
func generate(ctx context.Context, language string, library *config.Library, googleapisDir string, src *source.Sources) error {
233-
switch language {
234-
case languageFake:
235-
if err := fakeGenerate(library); err != nil {
236-
return err
237-
}
238-
case languageDart:
239-
if err := dart.Generate(ctx, library, src); err != nil {
240-
return err
241-
}
242-
case languagePython:
243-
if err := python.Generate(ctx, library, googleapisDir); err != nil {
244-
return err
245-
}
246-
case languageGo:
247-
if err := golang.Generate(ctx, library, googleapisDir); err != nil {
248-
return err
249-
}
250-
case languageRust:
251-
if err := rust.Generate(ctx, library, src); err != nil {
252-
return err
253-
}
254-
default:
255-
return fmt.Errorf("language %q does not support generation", language)
256-
}
257-
return nil
258-
}
259-
260-
func formatLibrary(ctx context.Context, language string, library *config.Library) error {
261-
switch language {
262-
case languageFake:
263-
return fakeFormat(library)
264-
case languageDart:
265-
return dart.Format(ctx, library)
266-
case languageGo:
267-
return golang.Format(ctx, library)
268-
case languageRust:
269-
return rust.Format(ctx, library)
270-
case languagePython:
271-
// TODO(https://github.com/googleapis/librarian/issues/3730): separate
272-
// generation and formatting for Python.
273-
return nil
274-
}
275-
return fmt.Errorf("language %q does not support formatting", language)
276-
}

0 commit comments

Comments
 (0)