Skip to content

Commit 03942a1

Browse files
committed
Fix bundler 4.x incompatibility caused by rubygems 4.x update
Rubygems 4.0.9 ships bundler 4.x as a default gem. When UpdateRubygems() runs 'ruby setup.rb', it installs bundler 4.0.9 which overwrites the buildpack's bundler 2.x. Bundler 4.x changed 'bundle version' output format (omits 'Bundler version' prefix), breaking GetBundlerVersion(). Changes: - GetBundlerVersion() regex: handle both bundler 2.x and 4.x output formats - UpdateRubygems(): re-install manifest bundler after 'ruby setup.rb' - InstallBundler(): invert version selection to default to 2.x.x - VendorBundlePath(): only bundler 1.x uses nested path (future-proof) - InstallGems(): only bundler 1.x skips BUNDLED WITH removal (future-proof) - Tests: add bundler 2.x/4.x test cases, update UpdateRubygems test
1 parent 16fc596 commit 03942a1

3 files changed

Lines changed: 71 additions & 9 deletions

File tree

src/ruby/supply/supply.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -323,11 +323,11 @@ func (s *Supplier) InstallBundler() error {
323323
matches = []string{"", "2"}
324324
}
325325

326-
if strings.HasPrefix(matches[1], "2") {
327-
return s.installBundler("2.x.x")
326+
if strings.HasPrefix(matches[1], "1") {
327+
return s.installBundler("1.x.x")
328328
}
329329

330-
return s.installBundler("1.x.x")
330+
return s.installBundler("2.x.x")
331331
}
332332

333333
func (s *Supplier) InstallNode() error {
@@ -468,7 +468,7 @@ func (s *Supplier) VendorBundlePath() (string, error) {
468468
return "", err
469469
}
470470

471-
if strings.HasPrefix(bundlerVersion, "2.") {
471+
if !strings.HasPrefix(bundlerVersion, "1.") {
472472
return "vendor_bundle", nil
473473
}
474474

@@ -610,6 +610,15 @@ func (s *Supplier) UpdateRubygems() error {
610610
return fmt.Errorf("Could not install rubygems: %v", err)
611611
}
612612

613+
// Rubygems 4.x ships bundler 4.x as a default gem. Running setup.rb
614+
// overwrites the buildpack-installed bundler (2.x) with bundler 4.x,
615+
// which has incompatible output format changes and untested behavior.
616+
// Re-install the buildpack's bundler to restore the manifest version.
617+
s.Log.Debug("Re-installing bundler after rubygems update")
618+
if err := s.InstallBundler(); err != nil {
619+
return fmt.Errorf("Could not re-install bundler after rubygems update: %v", err)
620+
}
621+
613622
return nil
614623
}
615624

@@ -739,7 +748,7 @@ func (s *Supplier) InstallGems() error {
739748
return fmt.Errorf("could not read Bundled With version from gemfile.lock: %s", err)
740749
}
741750

742-
if bundledWithVersion != bundlerVersion && strings.HasPrefix(bundledWithVersion, "2") {
751+
if bundledWithVersion != bundlerVersion && !strings.HasPrefix(bundledWithVersion, "1") {
743752
if err := s.removeIncompatibleBundledWithVersion(bundledWithVersion); err != nil {
744753
return fmt.Errorf("could not remove Bundled With from end of "+
745754
"gemfile.lock: %s", err)

src/ruby/supply/supply_test.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ var _ = Describe("Supply", func() {
6868
mockCtrl = gomock.NewController(GinkgoT())
6969

7070
mockManifest = NewMockManifest(mockCtrl)
71-
mockManifest.EXPECT().AllDependencyVersions("bundler").Return([]string{"1.17.2"}).AnyTimes()
71+
mockManifest.EXPECT().AllDependencyVersions("bundler").Return([]string{"1.17.2", "2.7.2"}).AnyTimes()
7272

7373
mockInstaller = NewMockInstaller(mockCtrl)
7474

@@ -133,6 +133,28 @@ var _ = Describe("Supply", func() {
133133
})
134134
})
135135

136+
Describe("InstallBundler with bundler 2.x BUNDLED WITH", func() {
137+
138+
var tempSupplier supply.Supplier
139+
140+
BeforeEach(func() {
141+
tempSupplier = *supplier
142+
mockStager := NewMockStager(mockCtrl)
143+
tempSupplier.Stager = mockStager
144+
145+
mockInstaller.EXPECT().InstallDependency(libbuildpack.Dependency{Name: "bundler", Version: "2.7.2"}, gomock.Any())
146+
mockStager.EXPECT().LinkDirectoryInDepDir(gomock.Any(), gomock.Any())
147+
mockStager.EXPECT().DepDir().AnyTimes()
148+
149+
err := os.WriteFile(filepath.Join(buildDir, "Gemfile.lock"), []byte("BUNDLED WITH\n 2.4.0"), 0644)
150+
Expect(err).NotTo(HaveOccurred())
151+
})
152+
153+
It("installs bundler 2.x matching constraint given", func() {
154+
Expect(tempSupplier.InstallBundler()).To(Succeed())
155+
})
156+
})
157+
136158
Describe("InstallNode", func() {
137159
var tempSupplier supply.Supplier
138160

@@ -389,6 +411,23 @@ var _ = Describe("Supply", func() {
389411
Expect(actualEnv).To(Equal(expectedEnv))
390412
})
391413
})
414+
415+
Describe("With Bundler version 4.x.x (future-proofing)", func() {
416+
BeforeEach(func() {
417+
mockVersions.EXPECT().GetBundlerVersion().Return("4.0.9", nil).AnyTimes()
418+
419+
mockStager.EXPECT().DepDir().Return("some/test-dir").AnyTimes()
420+
mockStager.EXPECT().WriteEnvFile(gomock.Any(), gomock.Any()).Return(nil)
421+
})
422+
423+
It("should use vendor_bundle path like bundler 2.x", func() {
424+
Expect(tempSupplier.AddPostRubyGemsInstallDefaultEnv()).To(Succeed())
425+
426+
expectedEnv := "some/test-dir/vendor_bundle"
427+
actualEnv := os.Getenv("BUNDLE_PATH")
428+
Expect(actualEnv).To(Equal(expectedEnv))
429+
})
430+
})
392431
})
393432

394433
Describe("CopyDirToTemp", func() {
@@ -1145,6 +1184,16 @@ var _ = Describe("Supply", func() {
11451184
})
11461185
mockCommand.EXPECT().Output(gomock.Any(), "ruby", "setup.rb")
11471186

1187+
// After ruby setup.rb, UpdateRubygems re-installs bundler.
1188+
// InstallBundler reads Gemfile.lock (not present here, so defaults
1189+
// to 2.x.x constraint) and installs bundler from the manifest.
1190+
mockInstaller.EXPECT().InstallDependency(gomock.Any(), gomock.Any()).Do(func(dep libbuildpack.Dependency, installDir string) {
1191+
Expect(dep.Name).To(Equal("bundler"))
1192+
Expect(dep.Version).To(Equal("2.7.2"))
1193+
// Create bin dir so LinkDirectoryInDepDir succeeds
1194+
Expect(os.MkdirAll(filepath.Join(installDir, "bin"), 0755)).To(Succeed())
1195+
})
1196+
11481197
Expect(supplier.UpdateRubygems()).To(Succeed())
11491198
})
11501199

src/ruby/versions/ruby.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ func (v *Versions) GetBundlerVersion() (string, error) {
5959
return "", err
6060
}
6161

62-
re := regexp.MustCompile(`Bundler version (\d+\.\d+\.\d+) .*`)
62+
// Bundler 2.x outputs "Bundler version X.Y.Z (...)" but bundler 4.x
63+
// omits the "Bundler version" prefix and outputs just "X.Y.Z (...)".
64+
re := regexp.MustCompile(`(?:Bundler version )?(\d+\.\d+\.\d+)`)
6365
match := re.FindStringSubmatch(stdout.String())
6466

6567
if len(match) != 2 {
@@ -192,9 +194,11 @@ func (v *Versions) GemMajorVersion(gem string) (int, error) {
192194
}
193195
}
194196

195-
//Should return true if either:
197+
// Should return true if either:
196198
// (1) the only platform in the Gemfile.lock is windows (mingw/mswin)
197-
// -or-
199+
//
200+
// -or-
201+
//
198202
// (2) the Gemfile.lock line endings are /r/n, rather than just /n
199203
func (v *Versions) HasWindowsGemfileLock() (bool, error) {
200204
gemfileLockPath := v.Gemfile() + ".lock"

0 commit comments

Comments
 (0)