Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ toolchain go1.24.0
require (
github.com/blang/semver v3.5.1+incompatible
github.com/cloudfoundry/libbuildpack v0.0.0-20251202224209-b07cc3dab65e
github.com/cloudfoundry/switchblade v0.9.4
github.com/cloudfoundry/switchblade v0.9.5
github.com/golang/mock v1.6.0
github.com/kr/text v0.2.0
github.com/onsi/ginkgo v1.16.5
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -744,8 +744,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudfoundry/libbuildpack v0.0.0-20251202224209-b07cc3dab65e h1:L9bl+eey+J8CQ5Dv24nJ5giUx00gdigZv4ElqzR0uRA=
github.com/cloudfoundry/libbuildpack v0.0.0-20251202224209-b07cc3dab65e/go.mod h1:kn4FHMwI8bTd9gT92wPGjXHzUvGcj8CkPxG8q3AGBAQ=
github.com/cloudfoundry/switchblade v0.9.4 h1:93O90a/DRRcZ4h50htDh4z7+FMliqy/lQH6IFgVa+mQ=
github.com/cloudfoundry/switchblade v0.9.4/go.mod h1:hIEQdGAsuNnzlyQfsD5OIORt38weSBar6Wq5/JX6Omo=
github.com/cloudfoundry/switchblade v0.9.5 h1:GTga1Uu6kGOL+n1TRTHyZm170N5/B/ou6wU90MiKKys=
github.com/cloudfoundry/switchblade v0.9.5/go.mod h1:hIEQdGAsuNnzlyQfsD5OIORt38weSBar6Wq5/JX6Omo=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
Expand Down
22 changes: 17 additions & 5 deletions src/ruby/supply/supply.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"

"github.com/cloudfoundry/libbuildpack"
Expand Down Expand Up @@ -323,11 +324,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 {
Expand Down Expand Up @@ -468,7 +469,7 @@ func (s *Supplier) VendorBundlePath() (string, error) {
return "", err
}

if strings.HasPrefix(bundlerVersion, "2.") {
if !strings.HasPrefix(bundlerVersion, "1.") {
return "vendor_bundle", nil
}

Expand Down Expand Up @@ -610,6 +611,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
}

Expand Down Expand Up @@ -739,7 +751,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)
Expand Down
74 changes: 73 additions & 1 deletion src/ruby/supply/supply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -133,6 +133,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

Expand Down Expand Up @@ -389,6 +411,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() {
Expand Down Expand Up @@ -1177,6 +1216,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() {
Expand Down
10 changes: 7 additions & 3 deletions src/ruby/versions/ruby.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,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 {
Expand Down Expand Up @@ -192,9 +194,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"
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vendor/modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ github.com/cloudfoundry/libbuildpack/cutlass
github.com/cloudfoundry/libbuildpack/cutlass/docker
github.com/cloudfoundry/libbuildpack/cutlass/glow
github.com/cloudfoundry/libbuildpack/packager
# github.com/cloudfoundry/switchblade v0.9.4
# github.com/cloudfoundry/switchblade v0.9.5
## explicit; go 1.23.0
github.com/cloudfoundry/switchblade
github.com/cloudfoundry/switchblade/internal/cloudfoundry
Expand Down