Skip to content

Commit 6e85547

Browse files
authored
cleanup(librarian): avoid traversing directories twice (#4544)
It just bothers me to see code that does the same job twice. Or three times, as we were checking the existence of the top-level directory to clean up at least 3 times.
1 parent c4d97d7 commit 6e85547

File tree

5 files changed

+58
-44
lines changed

5 files changed

+58
-44
lines changed

internal/librarian/clean.go

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -26,49 +26,11 @@ import (
2626
// should contain paths relative to dir. It returns an error if any file
2727
// in keep does not exist.
2828
func checkAndClean(dir string, keep []string) error {
29-
keepSet, err := check(dir, keep)
30-
if err != nil {
31-
return err
32-
}
33-
return clean(dir, keepSet)
34-
}
35-
36-
// check validates the given directory and returns a set of files to keep.
37-
// It ensures that the provided directory exists and is a directory.
38-
// It also verifies that all files specified in 'keep' exist within 'dir'.
39-
func check(dir string, keep []string) (map[string]bool, error) {
40-
info, err := os.Stat(dir)
41-
if err != nil {
42-
if errors.Is(err, fs.ErrNotExist) {
43-
return nil, nil
44-
}
45-
return nil, fmt.Errorf("cannot access output directory %q: %w", dir, err)
46-
}
47-
if !info.IsDir() {
48-
return nil, fmt.Errorf("%q is not a directory", dir)
49-
}
5029
keepSet := make(map[string]bool)
5130
for _, k := range keep {
52-
path := filepath.Join(dir, k)
53-
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
54-
return nil, fmt.Errorf("keep file %q does not exist", k)
55-
}
56-
// Effectively get a canonical relative path. While in most cases
57-
// this will be equal to k, it might not be - in particular,
58-
// on Windows the directory separator in paths returned by Rel
59-
// will be a backslash.
60-
rel, err := filepath.Rel(dir, path)
61-
if err != nil {
62-
return nil, err
63-
}
64-
keepSet[rel] = true
31+
keepSet[filepath.Clean(k)] = true
6532
}
66-
return keepSet, nil
67-
}
68-
69-
// clean removes files from dir that are not in keepSet.
70-
func clean(dir string, keepSet map[string]bool) error {
71-
return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
33+
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
7234
if err != nil {
7335
return err
7436
}
@@ -80,8 +42,28 @@ func clean(dir string, keepSet map[string]bool) error {
8042
return err
8143
}
8244
if keepSet[rel] {
45+
keepSet[rel] = false
8346
return nil
8447
}
8548
return os.Remove(path)
8649
})
50+
if err != nil {
51+
if errors.Is(err, fs.ErrNotExist) {
52+
// The top-level directory was not found. This happens when
53+
// calling `librarian generate` on new libraries and it is not
54+
// an error.
55+
return nil
56+
}
57+
return err
58+
}
59+
var missing []string
60+
for relative, v := range keepSet {
61+
if v {
62+
missing = append(missing, relative)
63+
}
64+
}
65+
if len(missing) != 0 {
66+
return fmt.Errorf("some keep files %q do not exist", keep)
67+
}
68+
return nil
8769
}

internal/librarian/clean_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,29 @@ func TestCleanOutput(t *testing.T) {
117117
})
118118
}
119119
}
120+
121+
// checkAndClean() needs to work when adding a library. In that case the
122+
// destination does not exist.
123+
func TestCheckAndCleanMissingDirectory(t *testing.T) {
124+
for _, test := range []struct {
125+
name string
126+
keep []string
127+
}{
128+
{
129+
name: "no keep files",
130+
keep: []string{},
131+
},
132+
{
133+
name: "with keep files",
134+
keep: []string{"README.md", "src/something-else-to-keep.md"},
135+
},
136+
} {
137+
t.Run(test.name, func(t *testing.T) {
138+
dir := t.TempDir()
139+
path := filepath.Join(dir, "does-not-exist")
140+
if err := checkAndClean(path, test.keep); err != nil {
141+
t.Fatal(err)
142+
}
143+
})
144+
}
145+
}

internal/librarian/fake.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ func fakeDefaultLibraryName(api string) string {
9797
}
9898

9999
func fakeClean(library *config.Library) error {
100+
// When calling `librarian generate` on new libraries the top-level directory
101+
// does not exist. Clean should not treat that as an error. Otherwise we
102+
// need to check the directory existence before calling clean, only to check
103+
// it again because `clean()` typically walks the directory.
104+
if _, err := os.Stat(library.Output); os.IsNotExist(err) {
105+
return nil
106+
}
100107
// We always generate a README.md file, so it's fine to delete that.
101108
// This function shouldn't be called if the output directory doesn't exist.
102109
return os.Remove(filepath.Join(library.Output, "README.md"))

internal/librarian/fake_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ func TestFakeClean_Error(t *testing.T) {
9494
}
9595
tmpDir := t.TempDir()
9696
t.Chdir(tmpDir)
97+
if err := os.MkdirAll(library.Output, 0755); err != nil {
98+
t.Fatal(err)
99+
}
97100
err := fakeClean(library)
98101
wantErr := os.ErrNotExist
99102
if !errors.Is(err, wantErr) {

internal/librarian/generate.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"context"
1919
"errors"
2020
"fmt"
21-
"os"
2221
"strings"
2322

2423
"github.com/googleapis/librarian/internal/config"
@@ -118,9 +117,6 @@ func runGenerate(ctx context.Context, cfg *config.Config, all bool, libraryName
118117
// delegating to language-specific code to clean each library.
119118
func cleanLibraries(language string, libraries []*config.Library) error {
120119
for _, library := range libraries {
121-
if _, err := os.Stat(library.Output); os.IsNotExist(err) {
122-
continue
123-
}
124120
switch language {
125121
case config.LanguageDart:
126122
if err := checkAndClean(library.Output, library.Keep); err != nil {

0 commit comments

Comments
 (0)