diff --git a/go.mod b/go.mod index 73425a562..0a4170dbc 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 49d97ae0c..8d96750a6 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/src/ruby/supply/supply.go b/src/ruby/supply/supply.go index efb35b2aa..d7b9df6bc 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" @@ -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 { @@ -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 } @@ -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 } @@ -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) diff --git a/src/ruby/supply/supply_test.go b/src/ruby/supply/supply_test.go index ca9e58471..ab489e993 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) @@ -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 @@ -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() { @@ -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() { diff --git a/src/ruby/versions/ruby.go b/src/ruby/versions/ruby.go index aa653d728..ffb6f020e 100644 --- a/src/ruby/versions/ruby.go +++ b/src/ruby/versions/ruby.go @@ -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 { @@ -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" diff --git a/vendor/github.com/cloudfoundry/switchblade/internal/cloudfoundry/initialize.go b/vendor/github.com/cloudfoundry/switchblade/internal/cloudfoundry/initialize.go index dc7f18d00..2db7b291a 100644 --- a/vendor/github.com/cloudfoundry/switchblade/internal/cloudfoundry/initialize.go +++ b/vendor/github.com/cloudfoundry/switchblade/internal/cloudfoundry/initialize.go @@ -43,7 +43,8 @@ func (i Initialize) Run(buildpacks []Buildpack) error { if err == nil { var payload struct { Resources []struct { - Position int `json:"position"` + Position int `json:"position"` + Stack string `json:"stack"` } `json:"resources"` } err = json.NewDecoder(buffer).Decode(&payload) @@ -55,13 +56,20 @@ func (i Initialize) Run(buildpacks []Buildpack) error { position = strconv.Itoa(payload.Resources[0].Position) } - err = i.cli.Execute(pexec.Execution{ - Args: []string{"delete-buildpack", "-f", buildpack.Name}, - Stdout: logs, - Stderr: logs, - }) - if err != nil { - return fmt.Errorf("failed to delete buildpack: %s\n\nOutput:\n%s", err, logs) + for _, resource := range payload.Resources { + args := []string{"delete-buildpack", "-f", buildpack.Name} + if resource.Stack != "" { + args = append(args, "-s", resource.Stack) + } + + err = i.cli.Execute(pexec.Execution{ + Args: args, + Stdout: logs, + Stderr: logs, + }) + if err != nil { + return fmt.Errorf("failed to delete buildpack: %s\n\nOutput:\n%s", err, logs) + } } } diff --git a/vendor/modules.txt b/vendor/modules.txt index 4921da456..84522e8c3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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