Skip to content

Commit 8ff5b4b

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 1102a2d commit 8ff5b4b

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
@@ -324,11 +324,11 @@ func (s *Supplier) InstallBundler() error {
324324
matches = []string{"", "2"}
325325
}
326326

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

331-
return s.installBundler("1.x.x")
331+
return s.installBundler("2.x.x")
332332
}
333333

334334
func (s *Supplier) InstallNode() error {
@@ -478,7 +478,7 @@ func (s *Supplier) VendorBundlePath() (string, error) {
478478
return "", err
479479
}
480480

481-
if strings.HasPrefix(bundlerVersion, "2.") {
481+
if !strings.HasPrefix(bundlerVersion, "1.") {
482482
return "vendor_bundle", nil
483483
}
484484

@@ -620,6 +620,15 @@ func (s *Supplier) UpdateRubygems() error {
620620
return fmt.Errorf("Could not install rubygems: %v", err)
621621
}
622622

623+
// Rubygems 4.x ships bundler 4.x as a default gem. Running setup.rb
624+
// overwrites the buildpack-installed bundler (2.x) with bundler 4.x,
625+
// which has incompatible output format changes and untested behavior.
626+
// Re-install the buildpack's bundler to restore the manifest version.
627+
s.Log.Debug("Re-installing bundler after rubygems update")
628+
if err := s.InstallBundler(); err != nil {
629+
return fmt.Errorf("Could not re-install bundler after rubygems update: %v", err)
630+
}
631+
623632
return nil
624633
}
625634

@@ -749,7 +758,7 @@ func (s *Supplier) InstallGems() error {
749758
return fmt.Errorf("could not read Bundled With version from gemfile.lock: %s", err)
750759
}
751760

752-
if bundledWithVersion != bundlerVersion && strings.HasPrefix(bundledWithVersion, "2") {
761+
if bundledWithVersion != bundlerVersion && !strings.HasPrefix(bundledWithVersion, "1") {
753762
if err := s.removeIncompatibleBundledWithVersion(bundledWithVersion); err != nil {
754763
return fmt.Errorf("could not remove Bundled With from end of "+
755764
"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

@@ -130,6 +130,28 @@ var _ = Describe("Supply", func() {
130130
})
131131
})
132132

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

@@ -379,6 +401,23 @@ var _ = Describe("Supply", func() {
379401
Expect(actualEnv).To(Equal(expectedEnv))
380402
})
381403
})
404+
405+
Describe("With Bundler version 4.x.x (future-proofing)", func() {
406+
BeforeEach(func() {
407+
mockVersions.EXPECT().GetBundlerVersion().Return("4.0.9", nil).AnyTimes()
408+
409+
mockStager.EXPECT().DepDir().Return("some/test-dir").AnyTimes()
410+
mockStager.EXPECT().WriteEnvFile(gomock.Any(), gomock.Any()).Return(nil)
411+
})
412+
413+
It("should use vendor_bundle path like bundler 2.x", func() {
414+
Expect(tempSupplier.AddPostRubyGemsInstallDefaultEnv()).To(Succeed())
415+
416+
expectedEnv := "some/test-dir/vendor_bundle"
417+
actualEnv := os.Getenv("BUNDLE_PATH")
418+
Expect(actualEnv).To(Equal(expectedEnv))
419+
})
420+
})
382421
})
383422

384423
Describe("CopyDirToTemp", func() {
@@ -1146,6 +1185,16 @@ var _ = Describe("Supply", func() {
11461185
})
11471186
mockCommand.EXPECT().Output(gomock.Any(), "ruby", "setup.rb")
11481187

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

src/ruby/versions/ruby.go

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

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

6466
if len(match) != 2 {
@@ -191,9 +193,11 @@ func (v *Versions) GemMajorVersion(gem string) (int, error) {
191193
}
192194
}
193195

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

0 commit comments

Comments
 (0)