diff --git a/src/ruby/supply/supply.go b/src/ruby/supply/supply.go index 10e590c82..9af06174a 100644 --- a/src/ruby/supply/supply.go +++ b/src/ruby/supply/supply.go @@ -9,6 +9,7 @@ import ( "os/exec" "path/filepath" "regexp" + "strconv" "strings" "github.com/cloudfoundry/libbuildpack" @@ -324,11 +325,11 @@ func (s *Supplier) InstallBundler() error { matches = []string{"", "2"} } - if strings.HasPrefix(matches[1], "2") { - return s.installBundler("2.x.x") + if strings.HasPrefix(matches[1], "1") { + return s.installBundler("1.x.x") } - return s.installBundler("1.x.x") + return s.installBundler("2.x.x") } func (s *Supplier) InstallNode() error { @@ -478,7 +479,7 @@ func (s *Supplier) VendorBundlePath() (string, error) { return "", err } - if strings.HasPrefix(bundlerVersion, "2.") { + if !strings.HasPrefix(bundlerVersion, "1.") { return "vendor_bundle", nil } @@ -620,6 +621,17 @@ func (s *Supplier) UpdateRubygems() error { return fmt.Errorf("Could not install rubygems: %v", err) } + // Rubygems 4.x ships bundler 4.x as a default gem. Running setup.rb + // overwrites the buildpack-installed bundler (2.x) with bundler 4.x, + // which has incompatible output format changes and untested behavior. + // Re-install the buildpack's bundler to restore the manifest version. + if majorVersion, err := strconv.Atoi(strings.SplitN(dep.Version, ".", 2)[0]); err == nil && majorVersion >= 4 { + s.Log.Debug("Re-installing bundler after rubygems %s update", dep.Version) + if err := s.InstallBundler(); err != nil { + return fmt.Errorf("Could not re-install bundler after rubygems update: %v", err) + } + } + return nil } @@ -749,7 +761,7 @@ func (s *Supplier) InstallGems() error { return fmt.Errorf("could not read Bundled With version from gemfile.lock: %s", err) } - if bundledWithVersion != bundlerVersion && strings.HasPrefix(bundledWithVersion, "2") { + if bundledWithVersion != bundlerVersion && !strings.HasPrefix(bundledWithVersion, "1") { if err := s.removeIncompatibleBundledWithVersion(bundledWithVersion); err != nil { return fmt.Errorf("could not remove Bundled With from end of "+ "gemfile.lock: %s", err) diff --git a/src/ruby/supply/supply_test.go b/src/ruby/supply/supply_test.go index 189bd65fe..cd885f5fb 100644 --- a/src/ruby/supply/supply_test.go +++ b/src/ruby/supply/supply_test.go @@ -68,7 +68,7 @@ var _ = Describe("Supply", func() { mockCtrl = gomock.NewController(GinkgoT()) mockManifest = NewMockManifest(mockCtrl) - mockManifest.EXPECT().AllDependencyVersions("bundler").Return([]string{"1.17.2"}).AnyTimes() + mockManifest.EXPECT().AllDependencyVersions("bundler").Return([]string{"1.17.2", "2.7.2"}).AnyTimes() mockInstaller = NewMockInstaller(mockCtrl) @@ -130,6 +130,28 @@ var _ = Describe("Supply", func() { }) }) + Describe("InstallBundler with bundler 2.x BUNDLED WITH", func() { + + var tempSupplier supply.Supplier + + BeforeEach(func() { + tempSupplier = *supplier + mockStager := NewMockStager(mockCtrl) + tempSupplier.Stager = mockStager + + mockInstaller.EXPECT().InstallDependency(libbuildpack.Dependency{Name: "bundler", Version: "2.7.2"}, gomock.Any()) + mockStager.EXPECT().LinkDirectoryInDepDir(gomock.Any(), gomock.Any()) + mockStager.EXPECT().DepDir().AnyTimes() + + err := os.WriteFile(filepath.Join(buildDir, "Gemfile.lock"), []byte("BUNDLED WITH\n 2.4.0"), 0644) + Expect(err).NotTo(HaveOccurred()) + }) + + It("installs bundler 2.x matching constraint given", func() { + Expect(tempSupplier.InstallBundler()).To(Succeed()) + }) + }) + Describe("InstallNode", func() { var tempSupplier supply.Supplier @@ -379,6 +401,23 @@ var _ = Describe("Supply", func() { Expect(actualEnv).To(Equal(expectedEnv)) }) }) + + Describe("With Bundler version 4.x.x (future-proofing)", func() { + BeforeEach(func() { + mockVersions.EXPECT().GetBundlerVersion().Return("4.0.9", nil).AnyTimes() + + mockStager.EXPECT().DepDir().Return("some/test-dir").AnyTimes() + mockStager.EXPECT().WriteEnvFile(gomock.Any(), gomock.Any()).Return(nil) + }) + + It("should use vendor_bundle path like bundler 2.x", func() { + Expect(tempSupplier.AddPostRubyGemsInstallDefaultEnv()).To(Succeed()) + + expectedEnv := "some/test-dir/vendor_bundle" + actualEnv := os.Getenv("BUNDLE_PATH") + Expect(actualEnv).To(Equal(expectedEnv)) + }) + }) }) Describe("CopyDirToTemp", func() { @@ -1178,6 +1217,39 @@ var _ = Describe("Supply", func() { }) }) + Describe("UpdateRubygems with rubygems >= 4 re-installs bundler", func() { + BeforeEach(func() { + mockManifest.EXPECT().AllDependencyVersions("rubygems").AnyTimes().Return([]string{"4.0.9"}) + }) + Context("gem version is less than 4.0.9", func() { + BeforeEach(func() { + mockCommand.EXPECT().Output(gomock.Any(), "gem", "--version").AnyTimes().Return("3.4.19\n", nil) + mockVersions.EXPECT().VersionConstraint("3.4.19", ">= 4.0.9").AnyTimes().Return(false, nil) + }) + + It("updates rubygems and re-installs bundler", func() { + mockVersions.EXPECT().Engine().Return("ruby", nil) + mockInstaller.EXPECT().InstallDependency(gomock.Any(), gomock.Any()).Do(func(dep libbuildpack.Dependency, _ string) { + Expect(dep.Name).To(Equal("rubygems")) + Expect(dep.Version).To(Equal("4.0.9")) + }) + mockCommand.EXPECT().Output(gomock.Any(), "ruby", "setup.rb") + + // Rubygems >= 4 triggers bundler re-install. + // InstallBundler reads Gemfile.lock (not present here, so defaults + // to 2.x.x constraint) and installs bundler from the manifest. + mockInstaller.EXPECT().InstallDependency(gomock.Any(), gomock.Any()).Do(func(dep libbuildpack.Dependency, installDir string) { + Expect(dep.Name).To(Equal("bundler")) + Expect(dep.Version).To(Equal("2.7.2")) + // Create bin dir so LinkDirectoryInDepDir succeeds + Expect(os.MkdirAll(filepath.Join(installDir, "bin"), 0755)).To(Succeed()) + }) + + Expect(supplier.UpdateRubygems()).To(Succeed()) + }) + }) + }) + Describe("RewriteShebangs", func() { var depDir string BeforeEach(func() { diff --git a/src/ruby/versions/ruby.go b/src/ruby/versions/ruby.go index 12397ccb3..c078a7919 100644 --- a/src/ruby/versions/ruby.go +++ b/src/ruby/versions/ruby.go @@ -58,7 +58,9 @@ func (v *Versions) GetBundlerVersion() (string, error) { return "", err } - re := regexp.MustCompile(`Bundler version (\d+\.\d+\.\d+) .*`) + // Bundler 2.x outputs "Bundler version X.Y.Z (...)" but bundler 4.x + // omits the "Bundler version" prefix and outputs just "X.Y.Z (...)". + re := regexp.MustCompile(`(?:Bundler version )?(\d+\.\d+\.\d+)`) match := re.FindStringSubmatch(stdout.String()) if len(match) != 2 { @@ -191,9 +193,11 @@ func (v *Versions) GemMajorVersion(gem string) (int, error) { } } -//Should return true if either: +// Should return true if either: // (1) the only platform in the Gemfile.lock is windows (mingw/mswin) -// -or- +// +// -or- +// // (2) the Gemfile.lock line endings are /r/n, rather than just /n func (v *Versions) HasWindowsGemfileLock() (bool, error) { gemfileLockPath := v.Gemfile() + ".lock"