Skip to content

Commit b477cb2

Browse files
authored
fix(internal/librarian/rust): update top-level Cargo.toml after version bump (#3995)
In `librarian bump`, if a library version bumps, also update the top level Cargo.toml for that entry. lock file upgrade is already done at the end of bump [here](https://github.com/googleapis/librarian/blob/43b900a12d48f699ffee781990826c93087b8f6f/internal/librarian/bump.go#L201). Included small refactor to use semver package validated versions. Manual tested with google-cloud-rust, with a manual fake change in auth. Fix #3720
1 parent 3483e39 commit b477cb2

3 files changed

Lines changed: 139 additions & 6 deletions

File tree

internal/librarian/rust/bump.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"path/filepath"
2424

2525
"github.com/googleapis/librarian/internal/config"
26+
"github.com/googleapis/librarian/internal/semver"
2627
)
2728

2829
var (
@@ -46,9 +47,14 @@ func Bump(ctx context.Context, library *config.Library, output, version, gitExe,
4647
return writeVersion(library, output, version)
4748
}
4849

49-
func writeVersion(library *config.Library, output, version string) error {
50+
func writeVersion(library *config.Library, output, versionString string) error {
51+
// validate version before writing to Cargo.toml
52+
version, err := semver.Parse(versionString)
53+
if err != nil {
54+
return err
55+
}
5056
cargoFile := filepath.Join(output, "Cargo.toml")
51-
_, err := os.Stat(cargoFile)
57+
_, err = os.Stat(cargoFile)
5258
switch {
5359
case err != nil && !os.IsNotExist(err):
5460
return err
@@ -57,7 +63,7 @@ func writeVersion(library *config.Library, output, version string) error {
5763
name = "%s"
5864
version = "%s"
5965
edition = "2021"
60-
`, library.Name, version)
66+
`, library.Name, version.String())
6167
if err := os.WriteFile(cargoFile, []byte(cargo), 0644); err != nil {
6268
return err
6369
}
@@ -66,6 +72,11 @@ edition = "2021"
6672
return err
6773
}
6874
}
69-
library.Version = version
75+
76+
// Update the workspace manifest if it exists.
77+
if err := updateWorkspaceVersion("Cargo.toml", library.Name, version); err != nil && !os.IsNotExist(err) {
78+
return err
79+
}
80+
library.Version = version.String()
7081
return nil
7182
}

internal/librarian/rust/bump_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,94 @@ func TestBumpLibraryNoVersion(t *testing.T) {
175175
})
176176
}
177177
}
178+
179+
func TestBumpUpdatesWorkspaceDependency(t *testing.T) {
180+
testhelper.RequireCommand(t, "cargo")
181+
testhelper.RequireCommand(t, "taplo")
182+
for _, test := range []struct {
183+
name string
184+
rootCargo string
185+
libName string
186+
oldVersion string
187+
newVersion string
188+
want string
189+
}{
190+
{
191+
name: "single line table",
192+
rootCargo: `[workspace.dependencies]
193+
google-cloud-storage = { version = "0.1.0", path = "storage" }
194+
google-cloud-auth = { default-features = false, version = "1.5", path = "src/auth" }
195+
`,
196+
libName: "google-cloud-storage",
197+
oldVersion: "0.1.0",
198+
newVersion: "0.2.0",
199+
want: `google-cloud-storage = { version = "0.2.0", path = "storage" }`,
200+
},
201+
{
202+
name: "multiple spaces",
203+
rootCargo: `[workspace.dependencies]
204+
google-cloud-storage = { version = "0.1.0", path = "storage" }
205+
`,
206+
libName: "google-cloud-storage",
207+
oldVersion: "0.1.0",
208+
newVersion: "0.2.0",
209+
want: `google-cloud-storage = { version = "0.2.0", path = "storage" }`,
210+
},
211+
{
212+
name: "no spaces around equals",
213+
rootCargo: `[workspace.dependencies]
214+
google-cloud-storage={version="0.1.0",path="storage"}
215+
`,
216+
libName: "google-cloud-storage",
217+
oldVersion: "0.1.0",
218+
newVersion: "0.2.0",
219+
want: `google-cloud-storage={version = "0.2.0",path="storage"}`,
220+
},
221+
{
222+
name: "multiple occurrences",
223+
rootCargo: `[workspace.dependencies]
224+
google-cloud-storage = { version = "0.1.0", path = "storage" }
225+
[dependencies]
226+
google-cloud-storage = { version = "0.1.0", path = "storage" }
227+
`,
228+
libName: "google-cloud-storage",
229+
oldVersion: "0.1.0",
230+
newVersion: "0.2.0",
231+
want: `version = "0.2.0"`, // Both lines should contain this
232+
},
233+
} {
234+
t.Run(test.name, func(t *testing.T) {
235+
tmpDir := t.TempDir()
236+
t.Chdir(tmpDir)
237+
238+
libDir := "storage"
239+
if err := os.WriteFile("Cargo.toml", []byte(test.rootCargo), 0644); err != nil {
240+
t.Fatal(err)
241+
}
242+
createCrate(t, libDir, test.libName, test.oldVersion)
243+
lib := &config.Library{
244+
Name: test.libName,
245+
Version: test.oldVersion,
246+
Output: libDir,
247+
}
248+
249+
if err := writeVersion(lib, libDir, test.newVersion); err != nil {
250+
t.Fatal(err)
251+
}
252+
253+
checkCargoVersion(t, filepath.Join(libDir, "Cargo.toml"), test.newVersion)
254+
rootContents, err := os.ReadFile("Cargo.toml")
255+
if err != nil {
256+
t.Fatal(err)
257+
}
258+
got := string(rootContents)
259+
if test.name == "multiple occurrences" {
260+
if strings.Count(got, test.want) != 2 {
261+
t.Errorf("expected 2 occurrences of %q, got %d:\n%s", test.want, strings.Count(got, test.want), got)
262+
}
263+
} else if !strings.Contains(got, test.want) {
264+
t.Errorf("root Cargo.toml was not updated:\nwant: %q\ngot:\n%s", test.want, got)
265+
}
266+
})
267+
}
268+
}

internal/librarian/rust/update_manifest.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ import (
1818
"context"
1919
"fmt"
2020
"os"
21+
"regexp"
2122
"slices"
2223
"strings"
2324

2425
"github.com/googleapis/librarian/internal/command"
26+
"github.com/googleapis/librarian/internal/semver"
2527
)
2628

2729
// CrateInfo contains the package information.
@@ -40,7 +42,7 @@ type Cargo struct {
4042
// line-based approach to preserve comments and formatting, which is important
4143
// because some Cargo.toml files are hand-crafted and contain comments that
4244
// must be preserved.
43-
func updateCargoVersion(path, newVersion string) error {
45+
func updateCargoVersion(path string, newVersion semver.Version) error {
4446
contents, err := os.ReadFile(path)
4547
if err != nil {
4648
return err
@@ -53,7 +55,36 @@ func updateCargoVersion(path, newVersion string) error {
5355
}
5456
// The number of spaces may seem weird. They match the number of spaces in
5557
// the mustache template.
56-
lines[idx] = fmt.Sprintf(`version = "%s"`, newVersion)
58+
lines[idx] = fmt.Sprintf(`version = "%s"`, newVersion.String())
59+
return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644)
60+
}
61+
62+
var versionRegex = regexp.MustCompile(`version\s*=\s*"[^"]*"`)
63+
64+
// updateWorkspaceVersion updates the version of a crate in a workspace Cargo.toml.
65+
// It searches for a line that starts with the crate name followed by "=" and
66+
// contains a "version =" field.
67+
func updateWorkspaceVersion(path, crateName string, newVersion semver.Version) error {
68+
contents, err := os.ReadFile(path)
69+
if err != nil {
70+
return err
71+
}
72+
lines := strings.Split(string(contents), "\n")
73+
updated := false
74+
for i, line := range lines {
75+
trimmed := strings.TrimSpace(line)
76+
if !strings.HasPrefix(trimmed, crateName) {
77+
continue
78+
}
79+
after := strings.TrimSpace(trimmed[len(crateName):])
80+
if strings.HasPrefix(after, "=") && versionRegex.MatchString(line) {
81+
lines[i] = versionRegex.ReplaceAllString(line, fmt.Sprintf(`version = "%s"`, newVersion.String()))
82+
updated = true
83+
}
84+
}
85+
if !updated {
86+
return nil
87+
}
5788
return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644)
5889
}
5990

0 commit comments

Comments
 (0)