diff --git a/core/__snapshots__/TestGenerateBuildPlanForExamples_ruby-single-version_1.snap.json b/core/__snapshots__/TestGenerateBuildPlanForExamples_ruby-single-version_1.snap.json new file mode 100755 index 000000000..a2e469514 --- /dev/null +++ b/core/__snapshots__/TestGenerateBuildPlanForExamples_ruby-single-version_1.snap.json @@ -0,0 +1,171 @@ +{ + "caches": { + "apt": { + "directory": "/var/cache/apt", + "type": "locked" + }, + "apt-lists": { + "directory": "/var/lib/apt/lists", + "type": "locked" + } + }, + "deploy": { + "base": { + "step": "packages:apt:runtime" + }, + "inputs": [ + { + "include": [ + "/mise/shims", + "/mise/installs", + "/usr/local/bin/mise", + "/etc/mise/config.toml", + "/root/.local/state/mise" + ], + "step": "packages:mise" + }, + { + "include": [ + "/usr/local/bundle" + ], + "step": "install" + }, + { + "include": [ + "/app" + ], + "step": "build" + } + ], + "startCommand": "bundle exec rackup config.ru -p ${PORT:-3000}", + "variables": { + "BUNDLE_GEMFILE": "/app/Gemfile", + "GEM_HOME": "/usr/local/bundle", + "GEM_PATH": "/usr/local/bundle", + "MALLOC_ARENA_MAX": "2" + } + }, + "steps": [ + { + "caches": [ + "apt", + "apt-lists" + ], + "commands": [ + { + "cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y cargo libyaml-dev rustc'", + "customName": "install apt packages: cargo libyaml-dev rustc" + } + ], + "inputs": [ + { + "image": "ghcr.io/railwayapp/railpack-builder:latest" + } + ], + "name": "packages:apt:build" + }, + { + "assets": { + "mise.toml": "[mise.toml]" + }, + "commands": [ + { + "path": "/mise/shims" + }, + { + "customName": "create mise config", + "name": "mise.toml", + "path": "/etc/mise/config.toml" + }, + { + "cmd": "sh -c 'mise trust -a \u0026\u0026 mise install'", + "customName": "install mise packages: ruby" + } + ], + "inputs": [ + { + "step": "packages:apt:build" + } + ], + "name": "packages:mise", + "variables": { + "MISE_CACHE_DIR": "/mise/cache", + "MISE_CONFIG_DIR": "/mise", + "MISE_DATA_DIR": "/mise", + "MISE_INSTALLS_DIR": "/mise/installs", + "MISE_SHIMS_DIR": "/mise/shims" + } + }, + { + "commands": [ + { + "cmd": "gem install -N bundler:2.4.22" + }, + { + "dest": "Gemfile", + "src": "Gemfile" + }, + { + "dest": "Gemfile.lock", + "src": "Gemfile.lock" + }, + { + "cmd": "bundle install" + }, + { + "path": "/usr/local/bundle" + } + ], + "inputs": [ + { + "step": "packages:mise" + } + ], + "name": "install", + "variables": { + "BUNDLE_GEMFILE": "/app/Gemfile", + "GEM_HOME": "/usr/local/bundle", + "GEM_PATH": "/usr/local/bundle", + "MALLOC_ARENA_MAX": "2" + } + }, + { + "commands": [ + { + "dest": ".", + "src": "." + } + ], + "inputs": [ + { + "step": "install" + } + ], + "name": "build", + "variables": { + "BUNDLE_GEMFILE": "/app/Gemfile", + "GEM_HOME": "/usr/local/bundle", + "GEM_PATH": "/usr/local/bundle", + "MALLOC_ARENA_MAX": "2" + } + }, + { + "caches": [ + "apt", + "apt-lists" + ], + "commands": [ + { + "cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y libyaml-dev'", + "customName": "install apt packages: libyaml-dev" + } + ], + "inputs": [ + { + "image": "ghcr.io/railwayapp/railpack-runtime:latest" + } + ], + "name": "packages:apt:runtime" + } + ] +} \ No newline at end of file diff --git a/core/providers/ruby/ruby.go b/core/providers/ruby/ruby.go index 27d27d004..8dd251e0a 100644 --- a/core/providers/ruby/ruby.go +++ b/core/providers/ruby/ruby.go @@ -245,8 +245,9 @@ func (p *RubyProvider) InstallMisePackages(ctx *generate.GenerateContext, miseSt miseStep.Version(ruby, utils.ExtractSemverVersion(string(versionFile)), ".ruby-version") } - if gemfileVersion := parseVersionFromGemfile(ctx); gemfileVersion != "" { - miseStep.Version(ruby, gemfileVersion, "Gemfile") + if constraint := extractRubyConstraintFromGemfile(ctx); constraint != "" { + miseStep.Resolver.SetVersionAvailable(ruby, isStableRubyVersion) + miseStep.Version(ruby, constraint, "Gemfile") } miseStep.AddSupportingAptPackage("libyaml-dev") @@ -261,6 +262,11 @@ func (p *RubyProvider) InstallMisePackages(ctx *generate.GenerateContext, miseSt } } +func isStableRubyVersion(version string) bool { + return !(strings.Contains(version, "-dev") || strings.Contains(version, "-preview") || + strings.Contains(version, "-rc") || strings.Contains(version, "dev")) +} + func (p *RubyProvider) getRubyVersion(ctx *generate.GenerateContext) string { miseStepBuilder := ctx.GetMiseStepBuilder() pkg := miseStepBuilder.Resolver.Get("ruby") @@ -320,35 +326,9 @@ func (p *RubyProvider) addMetadata(ctx *generate.GenerateContext) { } var ( - gemfileVersionRegex = regexp.MustCompile(`ruby (?:'|")(.*)(?:'|")[^>]"`) - gemfileLockVersionRegex = regexp.MustCompile(`ruby ((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))[^>]`) + gemfileVersionRegex = regexp.MustCompile(`ruby\s+(?:'|")([^'"]+)(?:'|")`) ) -func parseVersionFromGemfile(ctx *generate.GenerateContext) string { - gemfile, err := ctx.App.ReadFile("Gemfile") - if err != nil { - return "" - } - - matches := gemfileVersionRegex.FindStringSubmatch(string(gemfile)) - - if len(matches) > 2 { - return matches[2] - } - - gemfileLock, err := ctx.App.ReadFile("Gemfile.lock") - if err != nil { - return "" - } - - matches = gemfileLockVersionRegex.FindStringSubmatch(string(gemfileLock)) - if len(matches) > 1 { - return matches[1] - } - - return "" -} - func parseBundlerVersionFromGemfile(ctx *generate.GenerateContext) string { gemfileLock, err := ctx.App.ReadFile("Gemfile.lock") if err != nil { @@ -393,3 +373,15 @@ func parseLocalPathsFromGemfile(ctx *generate.GenerateContext) []string { return paths } + +func extractRubyConstraintFromGemfile(ctx *generate.GenerateContext) string { + gemfile, err := ctx.App.ReadFile("Gemfile") + if err != nil { + return "" + } + matches := gemfileVersionRegex.FindStringSubmatch(string(gemfile)) + if len(matches) > 1 { + return matches[1] + } + return "" +} diff --git a/core/providers/ruby/ruby_test.go b/core/providers/ruby/ruby_test.go index 046c1b2c2..78afb19d1 100644 --- a/core/providers/ruby/ruby_test.go +++ b/core/providers/ruby/ruby_test.go @@ -3,6 +3,7 @@ package ruby import ( "testing" + "github.com/Masterminds/semver/v3" "github.com/stretchr/testify/require" testingUtils "github.com/railwayapp/railpack/core/testing" @@ -36,3 +37,83 @@ func TestDetect(t *testing.T) { }) } } + +func TestRubyVersionDetection_Examples(t *testing.T) { + cases := []struct { + desc string + path string + expect string + }{ + { + desc: ".ruby-version present (ruby-3)", + path: "../../../examples/ruby-3", + expect: "3.2.1", + }, + { + desc: "Gemfile.lock with version (ruby-vanilla)", + path: "../../../examples/ruby-vanilla", + expect: "3.4.2", + }, + { + desc: "Gemfile with version constraint < 3.5.0 (ruby-no-version)", + path: "../../../examples/ruby-no-version", + expect: "3.4.4", + }, + { + desc: "Gemfile with single version constraint (ruby-single-version)", + path: "../../../examples/ruby-single-version", + expect: "3.4.2", + }, + } + + for _, c := range cases { + t.Run(c.desc, func(t *testing.T) { + ctx := testingUtils.CreateGenerateContext(t, c.path) + miseStep := ctx.GetMiseStepBuilder() + provider := RubyProvider{} + provider.InstallMisePackages(ctx, miseStep) + + resolvedPackages, err := miseStep.Resolver.ResolvePackages() + require.NoError(t, err) + + resolvedPkg, exists := resolvedPackages["ruby"] + if !exists { + t.Fatalf("ruby package not found in resolved packages") + } + + if resolvedPkg.ResolvedVersion == nil { + t.Fatalf("ruby package version not resolved") + } + + require.Equal(t, c.expect, *resolvedPkg.ResolvedVersion) + }) + } +} + +func TestRubyVersionConstraintResolution(t *testing.T) { + t.Run("constraint_resolution_works", func(t *testing.T) { + ctx := testingUtils.CreateGenerateContext(t, "../../../examples/ruby-no-version") + + miseStep := ctx.GetMiseStepBuilder() + provider := RubyProvider{} + provider.InstallMisePackages(ctx, miseStep) + + resolvedPackages, err := miseStep.Resolver.ResolvePackages() + require.NoError(t, err) + + resolvedPkg, exists := resolvedPackages["ruby"] + require.True(t, exists, "ruby package should be found in resolved packages") + require.NotNil(t, resolvedPkg.ResolvedVersion, "ruby package version should be resolved") + + actualVersion := *resolvedPkg.ResolvedVersion + + v, err := semver.NewVersion(actualVersion) + require.NoError(t, err) + require.True(t, v.Major() == 3 && v.Minor() >= 2 && v.Minor() < 5, + "Resolved version %s should be >= 3.2.0 and < 3.5.0", actualVersion) + + require.True(t, v.Minor() >= 4, "Should resolve to a recent version, got %s", actualVersion) + + require.Equal(t, "3.4.4", actualVersion, "Should resolve to the highest stable version under 3.5.0") + }) +} diff --git a/core/resolver/resolver.go b/core/resolver/resolver.go index bded4398b..1eed10b19 100644 --- a/core/resolver/resolver.go +++ b/core/resolver/resolver.go @@ -156,3 +156,7 @@ func (r *Resolver) SetVersionAvailable(ref PackageRef, isVersionAvailable func(v func (r *Resolver) SetSkipMiseInstall(ref PackageRef, skipMiseInstall bool) { r.packages[ref.Name].SkipMiseInstall = skipMiseInstall } + +func (r *Resolver) GetMise() *mise.Mise { + return r.mise +} diff --git a/examples/ruby-no-version/Gemfile b/examples/ruby-no-version/Gemfile index de9564366..1316f1c5a 100644 --- a/examples/ruby-no-version/Gemfile +++ b/examples/ruby-no-version/Gemfile @@ -1,5 +1,5 @@ - source 'https://rubygems.org' +ruby '>= 3.2.0', '< 3.5.0' +gem 'puma', '~> 5.6' gem 'sinatra' -gem "puma", "~> 5.6" diff --git a/examples/ruby-single-version/Gemfile b/examples/ruby-single-version/Gemfile new file mode 100644 index 000000000..72bebf26d --- /dev/null +++ b/examples/ruby-single-version/Gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' +ruby '3.4.2' + +gem 'puma', '~> 5.6' +gem 'sinatra' diff --git a/examples/ruby-single-version/Gemfile.lock b/examples/ruby-single-version/Gemfile.lock new file mode 100644 index 000000000..b255d9cc6 --- /dev/null +++ b/examples/ruby-single-version/Gemfile.lock @@ -0,0 +1,20 @@ +GEM + remote: https://rubygems.org/ + specs: + puma (5.6.7) + nio4r (~> 2.0) + sinatra (3.0.5) + mustermann (~> 3.0) + rack (~> 3.0) + rack-protection (= 3.0.5) + tilt (~> 2.0) + +PLATFORMS + x86_64-linux + +DEPENDENCIES + puma (~> 5.6) + sinatra + +BUNDLED WITH + 2.4.22 diff --git a/examples/ruby-single-version/app.rb b/examples/ruby-single-version/app.rb new file mode 100644 index 000000000..1f8c6e75b --- /dev/null +++ b/examples/ruby-single-version/app.rb @@ -0,0 +1,5 @@ +require 'sinatra' + +get '/' do + "Hello from Ruby #{RUBY_VERSION}!" +end diff --git a/examples/ruby-single-version/config.ru b/examples/ruby-single-version/config.ru new file mode 100644 index 000000000..76a6edff8 --- /dev/null +++ b/examples/ruby-single-version/config.ru @@ -0,0 +1,2 @@ +require './app' +run Sinatra::Application diff --git a/go.mod b/go.mod index 60dd9aa87..cbadf04ae 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( require ( github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect