├── .rspec ├── tmp └── .gitkeep ├── changelogs ├── unreleased │ └── .gitkeep ├── v204 │ ├── default_ruby.md │ └── clean_binstubs.md ├── v272 │ └── default_ruby.md ├── v294 │ ├── default_ruby.md │ └── bundler_default.md ├── v289 │ └── ruby-3-3-7.md ├── v292 │ └── ruby-3.2.7.md ├── v293 │ └── ruby-3.4.2.md ├── v265 │ └── ruby-323.md ├── v276 │ └── ruby_3.3.4.md ├── v278 │ └── ruby_3.2.5.md ├── v279 │ └── ruby-3.3.5.md ├── v281 │ └── ruby-336.md ├── v287 │ └── ruby_3.4.0.md ├── v275 │ └── jruby-9.4.8.0.md ├── v290 │ └── jruby-9.4.10.0.md ├── v291 │ └── jruby-9.4.11.0.md ├── v284 │ └── node-yarn.md ├── v274 │ └── jruby_93150.md ├── v270 │ └── ruby_versions_307_315_324_331.md ├── v286 │ └── 3.4.0-rc1.md ├── v280 │ └── ruby-340-preview2.md ├── v288 │ ├── bundler_2.5.23.md │ └── bundler_2.6.x_support.md ├── v309 │ └── default_ruby.md ├── v316 │ └── default_v339.md ├── v267 │ ├── bundler_major_minor.md │ └── ruby_gemfile_lock.md └── v273 │ └── windows_gemfile.md ├── spec ├── fixtures │ ├── windows_lockfile │ │ ├── Gemfile │ │ └── Gemfile.lock │ └── invalid_encoding.log ├── hatchet │ ├── rails8_spec.rb │ ├── rails23_spec.rb │ ├── bundler_spec.rb │ ├── buildpack_spec.rb │ ├── rails7_spec.rb │ ├── bugs_spec.rb │ ├── rails3_spec.rb │ ├── rails6_spec.rb │ ├── getting_started_spec.rb │ ├── node_spec.rb │ ├── rubies_spec.rb │ ├── rails4_spec.rb │ └── ci_spec.rb ├── helpers │ ├── node_installer_spec.rb │ ├── yarn_installer_spec.rb │ ├── fetcher_spec.rb │ ├── config_spec.rb │ ├── heroku_build_report_spec.rb │ ├── stale_file_cleaner_spec.rb │ ├── bundle_list_spec.rb │ ├── rake_runner_spec.rb │ ├── heroku_ruby_installer_spec.rb │ ├── shell_spec.rb │ ├── binstub_check_spec.rb │ ├── download_presence_spec.rb │ ├── bundler_wrapper_spec.rb │ ├── outdated_ruby_version_spec.rb │ ├── rails_runner_spec.rb │ └── gemfile_lock_spec.rb ├── spec_helper.rb ├── unit │ └── bash_functions_spec.rb └── rake │ └── deploy_check_spec.rb ├── lib ├── language_pack │ ├── version.rb │ ├── test.rb │ ├── test │ │ ├── rails7.rb │ │ ├── ruby.rb │ │ └── rails2.rb │ ├── rails7.rb │ ├── rails8.rb │ ├── rails6.rb │ ├── helpers │ │ ├── nodebin.rb │ │ ├── yarn_installer.rb │ │ ├── stale_file_cleaner.rb │ │ ├── node_installer.rb │ │ ├── plugin_installer.rb │ │ ├── bundler_cache.rb │ │ ├── binstub_wrapper.rb │ │ ├── binstub_check.rb │ │ ├── bundle_list.rb │ │ ├── download_presence.rb │ │ ├── gemfile_lock.rb │ │ ├── rake_runner.rb │ │ ├── rails_runner.rb │ │ └── outdated_ruby_version.rb │ ├── rails42.rb │ ├── rails5.rb │ ├── rack.rb │ ├── rails41.rb │ ├── metadata.rb │ ├── fetcher.rb │ ├── installers │ │ └── heroku_ruby_installer.rb │ ├── rails2.rb │ ├── rails4.rb │ ├── cache.rb │ ├── ruby_version.rb │ ├── base.rb │ └── rails3.rb ├── heroku_build_report.rb ├── language_pack.rb └── rake │ └── deploy_check.rb ├── .gitignore ├── bin ├── detect ├── report ├── release ├── test ├── test-compile ├── support │ ├── ruby_compile │ ├── ruby_test-compile │ ├── download_ruby │ ├── ruby_test │ └── bash_functions.sh └── compile ├── Gemfile ├── .github ├── CODEOWNERS ├── workflows │ ├── check_changelog.yml │ ├── hatchet_app_cleaner.yml │ ├── prepare-release.yml │ ├── ci.yml │ └── document_ruby_version.yml └── dependabot.yml ├── LICENSE ├── hatchet.json ├── Gemfile.lock ├── Rakefile ├── hatchet.lock └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changelogs/unreleased/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fixtures/windows_lockfile/Gemfile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fixtures/windows_lockfile/Gemfile.lock: -------------------------------------------------------------------------------- 1 | BUNDLED WITH 2 | 2.0.2 3 | -------------------------------------------------------------------------------- /spec/hatchet/rails8_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe "Rails 8" do 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/invalid_encoding.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genius/heroku-buildpack-ruby/master/spec/fixtures/invalid_encoding.log -------------------------------------------------------------------------------- /lib/language_pack/version.rb: -------------------------------------------------------------------------------- 1 | require "language_pack/base" 2 | 3 | module LanguagePack 4 | class LanguagePack::Base 5 | BUILDPACK_VERSION = "v318" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | repos/* 2 | .DS_Store 3 | vendor/bundler/* 4 | vendor/bundle/* 5 | .env 6 | .ruby-version 7 | buildpacks/* 8 | .anvil/ 9 | tmp/*.log 10 | log/*.log 11 | spec/examples.txt 12 | -------------------------------------------------------------------------------- /bin/detect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | APP_DIR=$1 6 | 7 | if [ -f "$APP_DIR/Gemfile" ]; then 8 | echo "Ruby" 9 | exit 0 10 | else 11 | exit 1 12 | fi 13 | -------------------------------------------------------------------------------- /changelogs/v204/default_ruby.md: -------------------------------------------------------------------------------- 1 | ## Default Ruby version for new apps is now 2.5.6 2 | 3 | The [default Ruby version for new Ruby applications is 2.5.6](https://devcenter.heroku.com/articles/ruby-support#default-ruby-version-for-new-apps). You’ll only get the default if the application does not specify a ruby version. -------------------------------------------------------------------------------- /changelogs/v272/default_ruby.md: -------------------------------------------------------------------------------- 1 | ## Default Ruby version for new apps is now 3.1.6 2 | 3 | The [default Ruby version for new Ruby applications is 3.1.6](https://devcenter.heroku.com/articles/ruby-support#default-ruby-version-for-new-apps). You’ll only get the default if the application does not specify a ruby version. 4 | -------------------------------------------------------------------------------- /changelogs/v294/default_ruby.md: -------------------------------------------------------------------------------- 1 | ## Default Ruby version for new apps is now 3.3.7 2 | 3 | The [default Ruby version for new Ruby applications is 3.3.7](https://devcenter.heroku.com/articles/ruby-support#default-ruby-version-for-new-apps). You’ll only get the default if the application does not specify a ruby version. 4 | -------------------------------------------------------------------------------- /lib/language_pack/test.rb: -------------------------------------------------------------------------------- 1 | require "language_pack" 2 | 3 | module LanguagePack::Test 4 | end 5 | 6 | # Behavior changes for the test pack work by opening existing language_pack 7 | # classes and over-writing their behavior to extend test functionality 8 | require "language_pack/test/ruby" 9 | require "language_pack/test/rails2" 10 | require "language_pack/test/rails7" 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | ruby "3.3.9" 5 | 6 | group :development, :test do 7 | gem "toml-rb" 8 | gem "heroku_hatchet" 9 | gem "rspec-core" 10 | gem "rspec-expectations" 11 | gem "excon" 12 | gem "rake" 13 | gem "parallel_split_test" 14 | gem 'rspec-retry' 15 | gem 'json' 16 | gem 'redis' 17 | end 18 | -------------------------------------------------------------------------------- /changelogs/v289/ruby-3-3-7.md: -------------------------------------------------------------------------------- 1 | ## Ruby version 3.3.7 is now available 2 | 3 | [Ruby v3.3.7](/articles/ruby-support#ruby-versions) is now available on Heroku. To run your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 4 | 5 | ```ruby 6 | ruby "3.3.7" 7 | ``` 8 | 9 | For more information on [Ruby 3.3.7, you can view the release announcement](https://www.ruby-lang.org/en/news/). 10 | -------------------------------------------------------------------------------- /changelogs/v292/ruby-3.2.7.md: -------------------------------------------------------------------------------- 1 | ## Ruby version 3.2.7 is now available 2 | 3 | [Ruby v3.2.7](/articles/ruby-support#ruby-versions) is now available on Heroku. To run your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 4 | 5 | ```ruby 6 | ruby "3.2.7" 7 | ``` 8 | 9 | For more information on [Ruby 3.2.7, you can view the release announcement](https://www.ruby-lang.org/en/news/). 10 | -------------------------------------------------------------------------------- /changelogs/v293/ruby-3.4.2.md: -------------------------------------------------------------------------------- 1 | ## Ruby version 3.4.2 is now available 2 | 3 | [Ruby v3.4.2](/articles/ruby-support#ruby-versions) is now available on Heroku. To run your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 4 | 5 | ```ruby 6 | ruby "3.4.2" 7 | ``` 8 | 9 | For more information on [Ruby 3.4.2, you can view the release announcement](https://www.ruby-lang.org/en/news/). 10 | -------------------------------------------------------------------------------- /changelogs/v265/ruby-323.md: -------------------------------------------------------------------------------- 1 | ## Ruby version 3.2.3 is now available 2 | 3 | [Ruby v3.2.3](/articles/ruby-support#ruby-versions) is now available on Heroku. To run 4 | your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 5 | 6 | ```ruby 7 | ruby "3.2.3" 8 | ``` 9 | 10 | For more information on [Ruby 3.2.3, you can view the release announcement](https://www.ruby-lang.org/en/news/). 11 | -------------------------------------------------------------------------------- /changelogs/v276/ruby_3.3.4.md: -------------------------------------------------------------------------------- 1 | ## Ruby version 3.3.4 is now available 2 | 3 | [Ruby v3.3.4](/articles/ruby-support#ruby-versions) is now available on Heroku. To run 4 | your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 5 | 6 | ```ruby 7 | ruby "3.3.4" 8 | ``` 9 | 10 | For more information on [Ruby 3.3.4, you can view the release announcement](https://www.ruby-lang.org/en/news/). 11 | -------------------------------------------------------------------------------- /changelogs/v278/ruby_3.2.5.md: -------------------------------------------------------------------------------- 1 | ## Ruby version 3.2.5 is now available 2 | 3 | [Ruby v3.2.5](/articles/ruby-support#ruby-versions) is now available on Heroku. To run 4 | your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 5 | 6 | ```ruby 7 | ruby "3.2.5" 8 | ``` 9 | 10 | For more information on [Ruby 3.2.5, you can view the release announcement](https://www.ruby-lang.org/en/news/). 11 | -------------------------------------------------------------------------------- /changelogs/v279/ruby-3.3.5.md: -------------------------------------------------------------------------------- 1 | ## Ruby version 3.3.5 is now available 2 | 3 | [Ruby v3.3.5](/articles/ruby-support#ruby-versions) is now available on Heroku. To run 4 | your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 5 | 6 | ```ruby 7 | ruby "3.3.5" 8 | ``` 9 | 10 | For more information on [Ruby 3.3.5, you can view the release announcement](https://www.ruby-lang.org/en/news/). 11 | -------------------------------------------------------------------------------- /changelogs/v281/ruby-336.md: -------------------------------------------------------------------------------- 1 | ## Ruby version 3.3.6 is now available 2 | 3 | [Ruby v3.3.6](/articles/ruby-support#ruby-versions) is now available on Heroku. To run 4 | your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 5 | 6 | ```ruby 7 | ruby "3.3.6" 8 | ``` 9 | 10 | For more information on [Ruby 3.3.6, you can view the release announcement](https://www.ruby-lang.org/en/news/). 11 | -------------------------------------------------------------------------------- /changelogs/v287/ruby_3.4.0.md: -------------------------------------------------------------------------------- 1 | ## Ruby version 3.4.0 is now available 2 | 3 | [Ruby v3.4.0](/articles/ruby-support-reference#ruby-versions) is now available on Heroku. To run your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 4 | 5 | ```ruby 6 | ruby "3.4.0" 7 | ``` 8 | 9 | For more information on [Ruby 3.4.0, you can view the release announcement](https://www.ruby-lang.org/en/news/). 10 | -------------------------------------------------------------------------------- /spec/hatchet/rails23_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe "Rails 2.3.x" do 4 | it "should deploy" do 5 | skip("Need RAILS_LTS_CREDS env var set") unless ENV["RAILS_LTS_CREDS"] 6 | 7 | Hatchet::Runner.new('rails_lts_23_default_ruby', config: rails_lts_config, stack: rails_lts_stack).tap do |app| 8 | app.deploy do 9 | # assert deploy is successful 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /changelogs/v275/jruby-9.4.8.0.md: -------------------------------------------------------------------------------- 1 | ## JRuby version 9.4.8.0 is now available 2 | 3 | [JRuby v9.4.8.0](/articles/ruby-support#ruby-versions) is now available on Heroku. To run 4 | your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 5 | 6 | ```ruby 7 | ruby "3.1.4", engine: "jruby", engine_version: "9.4.8.0" 8 | ``` 9 | 10 | The JRuby release notes can be found on the [JRuby website](https://www.jruby.org/news). 11 | -------------------------------------------------------------------------------- /bin/report: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | CACHE_DIR="${2}" 6 | REPORT_FILE="${CACHE_DIR}/.heroku/ruby/build_report.yml" 7 | 8 | # Whilst the release file is always written by the buildpack, some apps use 9 | # third-party slug cleaner buildpacks to remove this and other files, so we 10 | # cannot assume it still exists by the time the release step runs. 11 | if [[ -f "${REPORT_FILE}" ]]; then 12 | cat "${REPORT_FILE}" 13 | fi 14 | -------------------------------------------------------------------------------- /changelogs/v290/jruby-9.4.10.0.md: -------------------------------------------------------------------------------- 1 | ## JRuby version 9.4.10.0 is now available 2 | 3 | [JRuby v9.4.10.0](/articles/ruby-support-reference#ruby-versions) is now available on Heroku. To run 4 | your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 5 | 6 | ```ruby 7 | ruby "3.1.4", engine: "jruby", engine_version: "9.4.10.0" 8 | ``` 9 | 10 | The JRuby release notes can be found on the [JRuby website](https://www.jruby.org/news). 11 | -------------------------------------------------------------------------------- /changelogs/v291/jruby-9.4.11.0.md: -------------------------------------------------------------------------------- 1 | ## JRuby version 9.4.11.0 is now available 2 | 3 | [JRuby v9.4.11.0](/articles/ruby-support-reference#ruby-versions) is now available on Heroku. To run 4 | your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 5 | 6 | ```ruby 7 | ruby "3.1.4", engine: "jruby", engine_version: "9.4.11.0" 8 | ``` 9 | 10 | The JRuby release notes can be found on the [JRuby website](https://www.jruby.org/news). 11 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | BUILD_DIR="${1:-}" 6 | RELEASE_FILE="${BUILD_DIR}/tmp/heroku-buildpack-release-step.yml" 7 | 8 | # Whilst the release file is always written by the buildpack, some apps use 9 | # third-party slug cleaner buildpacks to remove this and other files, so we 10 | # cannot assume it still exists by the time the release step runs. 11 | if [[ -f "${RELEASE_FILE}" ]]; then 12 | cat "${RELEASE_FILE}" 13 | fi 14 | -------------------------------------------------------------------------------- /spec/hatchet/bundler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Bundler" do 4 | it "can be configured with BUNDLE_WITHOUT env var with spaces in it" do 5 | Hatchet::Runner.new("default_ruby", config: {"BUNDLE_WITHOUT" => "foo bar baz"}).tap do |app| 6 | app.deploy do 7 | expect(app.output).to match("BUNDLE_WITHOUT='foo:bar:baz'") 8 | expect(app.output).to match("Your BUNDLE_WITHOUT contains a space") 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/language_pack/test/rails7.rb: -------------------------------------------------------------------------------- 1 | # Opens up the class of the Rails7 language pack and 2 | # overwrites methods defined in `language_pack/test/ruby.rb` or `language_pack/test/rails2.rb` 3 | class LanguagePack::Rails7 4 | # Rails removed the db:schema:load_if_ruby and `db:structure:load_if_sql` tasks 5 | # they've been replaced by `db:schema:load` instead 6 | def db_prepare_test_rake_tasks 7 | ["db:schema:load", "db:migrate"].map { |name| rake.task(name) } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/language_pack/rails7.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require "language_pack" 3 | require "language_pack/rails6" 4 | 5 | class LanguagePack::Rails7 < LanguagePack::Rails6 6 | # @return [Boolean] true if it's a Rails 7.x app 7 | def self.use? 8 | rails_version = bundler.gem_version('railties') 9 | return false unless rails_version 10 | is_rails = rails_version >= Gem::Version.new('7.a') && 11 | rails_version < Gem::Version.new('8.a') 12 | return is_rails 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/language_pack/rails8.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require "language_pack" 3 | require "language_pack/rails7" 4 | 5 | class LanguagePack::Rails8 < LanguagePack::Rails7 6 | # @return [Boolean] true if it's a Rails 8.x app 7 | def self.use? 8 | rails_version = bundler.gem_version('railties') 9 | return false unless rails_version 10 | is_rails = rails_version >= Gem::Version.new('8.a') && 11 | rails_version < Gem::Version.new('9.a') 12 | return is_rails 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default to requesting pull request reviews from the Heroku Languages team. 2 | #ECCN:Open Source 3 | #GUSINFO:Languages,Heroku Ruby Platform 4 | * @heroku/languages 5 | 6 | # However, request review from the language owner instead for files that are updated 7 | # by Dependabot or release automation, to reduce team review request noise. 8 | CHANGELOG.md @schneems 9 | Gemfile.lock @schneems 10 | /.github/workflows/ @schneems 11 | /changelogs/ @schneems 12 | /lib/language_pack/version.rb @schneems 13 | -------------------------------------------------------------------------------- /spec/hatchet/buildpack_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe "Buildpack internals" do 4 | it "handles PATH with a newline in it correctly" do 5 | buildpacks = [ 6 | "https://github.com/sharpstone/export_path_with_newlines_buildpack", 7 | :default, 8 | "https://github.com/heroku/null-buildpack" 9 | ] 10 | Hatchet::Runner.new("default_ruby", buildpacks: buildpacks).deploy do |app| 11 | expect(app.output).to_not match("No such file or directory") 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /changelogs/v284/node-yarn.md: -------------------------------------------------------------------------------- 1 | ## Ruby apps now default to Node version 22.11.0 and Yarn version 1.22.22 2 | 3 | Applications using the `heroku/ruby` buildpack that do not have a version of Node installed by another buildpack (such as the `heroku/nodejs` buildpack) will now receive: 4 | 5 | - Node version 22.11.0 6 | - Yarn version 1.22.22 7 | 8 | These versions and instructions on how to specify a specific version of these binaries can be found on the [installed binaries section of the Heroku Ruby Support page](https://devcenter.heroku.com/articles/ruby-support#installed-binaries). 9 | -------------------------------------------------------------------------------- /changelogs/v274/jruby_93150.md: -------------------------------------------------------------------------------- 1 | ## JRuby version 9.3.15.0 is now available 2 | 3 | [JRuby v9.3.15.0](/articles/ruby-support#ruby-versions) is now available on Heroku. To run 4 | your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 5 | 6 | ```ruby 7 | ruby "2.6.8", engine: "jruby", engine_version: "9.3.15.0" 8 | ``` 9 | 10 | The JRuby release notes can be found on the [JRuby website](https://www.jruby.org/news). 11 | 12 | 19 | -------------------------------------------------------------------------------- /lib/language_pack/rails6.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require "language_pack" 3 | require "language_pack/rails5" 4 | 5 | class LanguagePack::Rails6 < LanguagePack::Rails5 6 | # @return [Boolean] true if it's a Rails 6.x app 7 | def self.use? 8 | rails_version = bundler.gem_version('railties') 9 | return false unless rails_version 10 | is_rails = rails_version >= Gem::Version.new('6.x') && 11 | rails_version < Gem::Version.new('7.a') 12 | return is_rails 13 | end 14 | 15 | def compile 16 | FileUtils.mkdir_p("tmp/pids") 17 | super 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/helpers/node_installer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe LanguagePack::Helpers::NodeInstaller do 4 | describe "#install" do 5 | LanguagePack::Base::KNOWN_ARCHITECTURES.each do |arch| 6 | it "should extract a node binary on #{arch}" do 7 | Dir.mktmpdir do |dir| 8 | Dir.chdir(dir) do 9 | installer = LanguagePack::Helpers::NodeInstaller.new(arch: "arm64") 10 | installer.install 11 | 12 | expect(File.exist?("node")).to be(true) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /changelogs/v270/ruby_versions_307_315_324_331.md: -------------------------------------------------------------------------------- 1 | # Ruby versions 3.0.7, 3.1.5, 3.2.4, 3.3.1 are now available 2 | 3 | The following Ruby versions are now available on the Heroku platform: 4 | 5 | - Ruby 3.0.7 6 | - Ruby 3.1.5 7 | - Ruby 3.2.4 8 | - Ruby 3.3.1 9 | 10 | The latest versions are on the [Ruby support page](https://devcenter.heroku.com/articles/ruby-support). 11 | 12 | > note 13 | > [Ruby 3.0.x is now EOL](https://www.ruby-lang.org/en/news/2024/04/23/ruby-3-0-7-released/) and therefore is no longer within Heroku's support policy. Heroku strongly recommends upgrading to Ruby 3.1.x or later. 14 | -------------------------------------------------------------------------------- /changelogs/v204/clean_binstubs.md: -------------------------------------------------------------------------------- 1 | ## Ruby buildpack now cleans unused binstubs before install 2 | 3 | After a Ruby application's dependencies have been installed via `bundle install` the old dependencies are cleaned up by running `bundle clean`. Recently it was discovered that this does not clean up unused binstubs. To correct this problem the Ruby buildpack now manually cleans up generated binstubs in `vendor/bundler/bin`. Then when `bundle install` is executed, only currently used binstubs will be generated. 4 | 5 | [Buildpack PR](https://github.com/heroku/heroku-buildpack-ruby/pull/914) and [Heroku Ruby support](https://devcenter.heroku.com/articles/ruby-support). -------------------------------------------------------------------------------- /spec/helpers/yarn_installer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe LanguagePack::Helpers::YarnInstaller do 4 | describe "#install" do 5 | 6 | it "should extract the yarn package" do 7 | Dir.mktmpdir do |dir| 8 | Dir.chdir(dir) do 9 | installer = LanguagePack::Helpers::YarnInstaller.new 10 | installer.install 11 | 12 | # webpacker gem checks for yarnpkg 13 | # https://github.com/rails/webpacker/blob/master/lib/install/bin/yarn.tt#L5 14 | expect(File.exist?("yarn-v#{installer.version}/bin/yarnpkg")).to be(true) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /.github/workflows/check_changelog.yml: -------------------------------------------------------------------------------- 1 | name: Check Changelog 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, labeled, unlabeled, synchronize] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | check-changelog: 12 | runs-on: ubuntu-latest 13 | if: (!contains(github.event.pull_request.labels.*.name, 'skip changelog')) 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Check that CHANGELOG is touched 18 | run: | 19 | git fetch origin ${{ github.base_ref }} --depth 1 && \ 20 | git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "bundler" 4 | directory: "/" 5 | open-pull-requests-limit: 1 # Limit concurrent CI runs from executing, do not remove 6 | schedule: 7 | interval: "monthly" 8 | labels: 9 | - "dependencies" 10 | - "ruby" 11 | - "skip changelog" 12 | groups: 13 | ruby-dependencies: 14 | update-types: 15 | - "minor" 16 | - "patch" 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "monthly" 21 | labels: 22 | - "dependencies" 23 | - "github actions" 24 | - "skip changelog" 25 | -------------------------------------------------------------------------------- /changelogs/v286/3.4.0-rc1.md: -------------------------------------------------------------------------------- 1 | ## Ruby version 3.4.0-rc1 is now available 2 | 3 | [Ruby v3.4.0-rc1](/articles/ruby-support-reference#ruby-versions) is now available on Heroku. To run your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 4 | 5 | ```ruby 6 | ruby "3.4.0.rc1" 7 | ``` 8 | 9 | For more information on [Ruby 3.4.0-rc1, you can view the release announcement](https://www.ruby-lang.org/en/news/). 10 | 11 | > Note 12 | > This version of Ruby is not suitable for production applications. 13 | > However, it can be used to test that your application is ready for 14 | > the official release of Ruby 3.4.0 and 15 | > to provide feedback to the Ruby core team. 16 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # The actual `bin/test-compile` code lives in `bin/ruby_test-compile`. This file instead 3 | # bootstraps the ruby needed and then executes `bin/ruby_test-compile` 4 | 5 | set -euo pipefail 6 | 7 | BIN_DIR=$(cd "$(dirname "$0")" || exit; pwd) # absolute path 8 | 9 | # shellcheck source=bin/support/bash_functions.sh 10 | source "$BIN_DIR/support/bash_functions.sh" 11 | 12 | bootstrap_ruby_dir=$(mktemp -d) 13 | "$BIN_DIR"/support/download_ruby "$BIN_DIR" "$bootstrap_ruby_dir" 14 | trap 'rm -rf "$bootstrap_ruby_dir"' EXIT 15 | 16 | export PATH="$bootstrap_ruby_dir/bin/:$PATH" 17 | unset GEM_PATH 18 | 19 | "$bootstrap_ruby_dir"/bin/ruby "$BIN_DIR/support/ruby_test" "$@" 20 | -------------------------------------------------------------------------------- /changelogs/v280/ruby-340-preview2.md: -------------------------------------------------------------------------------- 1 | ## Ruby version 3.4.0-preview2 is now available 2 | 3 | [Ruby v3.4.0-preview2](/articles/ruby-support#ruby-versions) is now available on Heroku. To run 4 | your app using this version of Ruby, add the following `ruby` directive to your Gemfile: 5 | 6 | ```ruby 7 | ruby "3.4.0.preview2" 8 | ``` 9 | 10 | For more information on [Ruby 3.4.0-preview2, you can view the release announcement](https://www.ruby-lang.org/en/news/). 11 | 12 | > Note 13 | > This version of Ruby is not suitable for production applications. 14 | > However, it can be used to test that your application is ready for 15 | > the official release of Ruby 3.4.0 and 16 | > to provide feedback to the Ruby core team. 17 | -------------------------------------------------------------------------------- /spec/helpers/fetcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Fetches" do 4 | LanguagePack::Helpers::BundlerWrapper::BLESSED_BUNDLER_VERSIONS.each do |_, version| 5 | it "bundler #{version}" do 6 | Dir.mktmpdir do |dir| 7 | Dir.chdir(dir) do 8 | lockfile = Pathname("Gemfile.lock") 9 | FileUtils.touch(lockfile) 10 | lockfile.write("BUNDLED WITH\n #{version}") 11 | 12 | fetcher = LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL) 13 | fetcher.fetch_untar("bundler/#{LanguagePack::Helpers::BundlerWrapper.new.dir_name}.tgz") 14 | 15 | expect(run!("ls bin")).to match("bundle") 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/nodebin.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | class LanguagePack::Helpers::Nodebin 4 | NODE_VERSION = "22.11.0" 5 | YARN_VERSION = "1.22.22" 6 | 7 | def self.hardcoded_node_lts(arch: ) 8 | arch = "x64" if arch == "amd64" 9 | { 10 | "number" => NODE_VERSION, 11 | "url" => "https://nodejs.org/download/release/v#{NODE_VERSION}/node-v#{NODE_VERSION}-linux-#{arch}.tar.gz", 12 | } 13 | end 14 | 15 | def self.hardcoded_yarn 16 | { 17 | "number" => YARN_VERSION, 18 | "url" => "https://heroku-nodebin.s3.us-east-1.amazonaws.com/yarn/release/yarn-v#{YARN_VERSION}.tar.gz" 19 | } 20 | end 21 | 22 | def self.node_lts(arch: ) 23 | hardcoded_node_lts(arch: arch) 24 | end 25 | 26 | def self.yarn 27 | hardcoded_yarn 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /changelogs/v294/bundler_default.md: -------------------------------------------------------------------------------- 1 | ## Ruby applications with no specified bundler versions now receive Bundler 2.x 2 | 3 | Previously applications with no `BUNDLED WITH` in their `Gemfile.lock` would receive bundler `1.x`. They will now receive the new [default bundler version](https://devcenter.heroku.com/articles/ruby-support-reference#default-bundler-version) `2.3.x`. 4 | 5 | It is strongly recommended that you have both a `RUBY VERSION` and `BUNDLED WITH` version listed in your `Gemfile.lock`. If you do not have those values, you can generate them and commit them to git: 6 | 7 | ``` 8 | $ bundle update --ruby 9 | $ git add Gemfile.lock 10 | $ git commit -m "Update Gemfile.lock" 11 | ``` 12 | 13 | Applications without these values specified in the `Gemfile.lock` may break unexpectedly when the defaults change. 14 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/yarn_installer.rb: -------------------------------------------------------------------------------- 1 | class LanguagePack::Helpers::YarnInstaller 2 | attr_reader :version 3 | 4 | def initialize 5 | # Grab latest yarn, until release practice stabilizes 6 | # https://github.com/yarnpkg/yarn/issues/376#issuecomment-253366910 7 | nodebin = LanguagePack::Helpers::Nodebin.yarn 8 | @version = nodebin["number"] 9 | @url = nodebin["url"] 10 | @fetcher = LanguagePack::Fetcher.new("") 11 | end 12 | 13 | def name 14 | "yarn-v#{@version}" 15 | end 16 | 17 | def binary_path 18 | "#{name}/bin/" 19 | end 20 | 21 | def install 22 | Dir.mktmpdir do |dir| 23 | Dir.chdir(dir) do 24 | @fetcher.fetch_untar(@url, strip_components: 1) 25 | end 26 | 27 | FileUtils.cp_r(dir, name) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /changelogs/v288/bundler_2.5.23.md: -------------------------------------------------------------------------------- 1 | ## Bundler version 2.5.23 is now available for Ruby Applications 2 | 3 | The [Ruby Buildpack](https://devcenter.heroku.com/articles/ruby-support#libraries) installs a version of bundler based on the major and minor version listed in the `Gemfile.lock` under the `BUNDLED WITH` key: 4 | 5 | - `BUNDLED WITH` 2.5.x will receive bundler `2.5.23` 6 | 7 | It is strongly recommended that you have both a `RUBY VERSION` and `BUNDLED WITH` version listed in your `Gemfile.lock`. If you do not have those values, you can generate them and commit them to git: 8 | 9 | ``` 10 | $ bundle update --ruby 11 | $ git add Gemfile.lock 12 | $ git commit -m "Update Gemfile.lock" 13 | ``` 14 | 15 | Applications without these values specified in the `Gemfile.lock` may break unexpectedly when the defaults change. 16 | -------------------------------------------------------------------------------- /changelogs/v288/bundler_2.6.x_support.md: -------------------------------------------------------------------------------- 1 | ## Bundler version 2.6.2 is now available for Ruby Applications 2 | 3 | The [Ruby Buildpack](https://devcenter.heroku.com/articles/ruby-support#libraries) installs a version of bundler based on the major and minor version listed in the `Gemfile.lock` under the `BUNDLED WITH` key: 4 | 5 | - `BUNDLED WITH` 2.6.x and above will receive bundler `2.6.2` 6 | 7 | It is strongly recommended that you have both a `RUBY VERSION` and `BUNDLED WITH` version listed in your `Gemfile.lock`. If you do not have those values, you can generate them and commit them to git: 8 | 9 | ``` 10 | $ bundle update --ruby 11 | $ git add Gemfile.lock 12 | $ git commit -m "Update Gemfile.lock" 13 | ``` 14 | 15 | Applications without these values specified in the `Gemfile.lock` may break unexpectedly when the defaults change. 16 | -------------------------------------------------------------------------------- /lib/language_pack/rails42.rb: -------------------------------------------------------------------------------- 1 | require "language_pack" 2 | require "language_pack/rails41" 3 | 4 | class LanguagePack::Rails42 < LanguagePack::Rails41 5 | # detects if this is a Rails 4.2 app 6 | # @return [Boolean] true if it's a Rails 4.2 app 7 | def self.use? 8 | rails_version = bundler.gem_version('railties') 9 | return false unless rails_version 10 | is_rails42 = rails_version >= Gem::Version.new('4.2.0') && 11 | rails_version < Gem::Version.new('5.0.0') 12 | return is_rails42 13 | end 14 | 15 | def setup_profiled(**args) 16 | super(**args) 17 | set_env_default "RAILS_SERVE_STATIC_FILES", "enabled" 18 | end 19 | 20 | def default_config_vars 21 | super.merge({ 22 | "RAILS_SERVE_STATIC_FILES" => env("RAILS_SERVE_STATIC_FILES") || "enabled" 23 | }) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /changelogs/v309/default_ruby.md: -------------------------------------------------------------------------------- 1 | ## Default Ruby version for new apps is now 3.3.8 2 | 3 | The [default Ruby version for new Ruby applications is 3.3.8](https://devcenter.heroku.com/articles/ruby-support#default-ruby-version-for-new-apps). You’ll only get the default if the application does not specify a Ruby version. 4 | 5 | Heroku highly recommends specifying your desired Ruby version. You can specify a Ruby version in your `Gemfile`: 6 | 7 | ```ruby 8 | ruby "3.3.9" 9 | ``` 10 | 11 | Once you have a Ruby version specified in your `Gemfile`, update the `Gemfile.lock` by running the following command: 12 | 13 | ```term 14 | $ bundle update --ruby 15 | ``` 16 | 17 | Make sure you commit the results to git before attempting to deploy again: 18 | 19 | ```term 20 | $ git add Gemfile Gemfile.lock 21 | $ git commit -m "update ruby version" 22 | ``` 23 | -------------------------------------------------------------------------------- /changelogs/v316/default_v339.md: -------------------------------------------------------------------------------- 1 | ## Default Ruby version for new apps is now 3.3.9 2 | 3 | The [default Ruby version for new Ruby applications is 3.3.9](https://devcenter.heroku.com/articles/ruby-support#default-ruby-version-for-new-apps). You’ll only get the default if the application does not specify a Ruby version. 4 | 5 | Heroku highly recommends specifying your desired Ruby version. You can specify a Ruby version in your `Gemfile`: 6 | 7 | ```ruby 8 | ruby "3.3.9" 9 | ``` 10 | 11 | Once you have a Ruby version specified in your `Gemfile`, update the `Gemfile.lock` by running the following command: 12 | 13 | ```term 14 | $ bundle update --ruby 15 | ``` 16 | 17 | Make sure you commit the results to git before attempting to deploy again: 18 | 19 | ```term 20 | $ git add Gemfile Gemfile.lock 21 | $ git commit -m "update ruby version" 22 | ``` 23 | -------------------------------------------------------------------------------- /spec/helpers/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Boot Strap Config" do 4 | it "matches toml config" do 5 | require 'toml-rb' 6 | config = TomlRB.load_file("buildpack.toml") 7 | bootstrap_version = config["buildpack"]["ruby_version"] 8 | expect(bootstrap_version).to eq(LanguagePack::RubyVersion::BOOTSTRAP_VERSION_NUMBER) 9 | 10 | expect(`ruby -v`).to match(Regexp.escape(LanguagePack::RubyVersion::BOOTSTRAP_VERSION_NUMBER)) 11 | 12 | ci_task = Pathname(".github").join("workflows").join("hatchet_app_cleaner.yml").read 13 | ci_task_yml = YAML.load(ci_task) 14 | task = ci_task_yml["jobs"]["hatchet-app-cleaner"]["steps"].detect {|step| step["uses"].match?(/ruby\/setup-ruby/)} or raise "Not found" 15 | expect(task["with"]["ruby-version"]).to match(LanguagePack::RubyVersion::BOOTSTRAP_VERSION_NUMBER) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/language_pack/rails5.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require "language_pack" 3 | require "language_pack/rails42" 4 | 5 | class LanguagePack::Rails5 < LanguagePack::Rails42 6 | # @return [Boolean] true if it's a Rails 5.x app 7 | def self.use? 8 | rails_version = bundler.gem_version('railties') 9 | return false unless rails_version 10 | is_rails = rails_version >= Gem::Version.new('5.x') && 11 | rails_version < Gem::Version.new('6.0.0') 12 | return is_rails 13 | end 14 | 15 | def setup_profiled(**args) 16 | super(**args) 17 | set_env_default "RAILS_LOG_TO_STDOUT", "enabled" 18 | end 19 | 20 | def default_config_vars 21 | super.merge({ 22 | "RAILS_LOG_TO_STDOUT" => "enabled" 23 | }) 24 | end 25 | 26 | def install_plugins 27 | # do not install plugins, do not call super, do not warn 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/hatchet/rails7_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe "Rails 7" do 4 | it "should detect successfully" do 5 | Hatchet::App.new('rails-jsbundling').in_directory_fork do 6 | expect(LanguagePack::Rails6.use?).to eq(false) 7 | expect(LanguagePack::Rails7.use?).to eq(true) 8 | end 9 | end 10 | 11 | it "works with jsbundling" do 12 | Hatchet::Runner.new("rails-jsbundling").tap do |app| 13 | app.deploy do 14 | expect(app.output).to include("yarn install") 15 | expect(app.output).to include("Asset precompilation completed") 16 | end 17 | end 18 | end 19 | 20 | it "Works on Heroku CI" do 21 | Hatchet::Runner.new("rails-jsbundling").run_ci do |test_run| 22 | expect(test_run.output).to match("db:schema:load") 23 | expect(test_run.output).to match("db:migrate") 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/hatchet/bugs_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe "Bugs" do 4 | context "database connections" do 5 | it "fails with better error message" do 6 | Hatchet::Runner.new("ruby-getting-started", allow_failure: true).tap do |app| 7 | app.before_deploy do 8 | Pathname("Rakefile").write(<<~EOM) 9 | require 'bundler' 10 | Bundler.require(:default) 11 | 12 | require 'active_record' 13 | 14 | task "assets:precompile" do 15 | # Try to connect to a database that doesn't exist yet 16 | ActiveRecord::Base.establish_connection 17 | ActiveRecord::Base.connection.execute("") 18 | end 19 | EOM 20 | end 21 | 22 | app.deploy do 23 | expect(app.output).to match("https://devcenter.heroku.com/articles/pre-provision-database") 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/stale_file_cleaner.rb: -------------------------------------------------------------------------------- 1 | class LanguagePack::Helpers::StaleFileCleaner 2 | FILE_STAT_CACHE = Hash.new {|h, k| h[k] = File.stat(k) } 3 | 4 | def initialize(dir) 5 | @dir = dir 6 | raise "need or dir" if @dir.nil? 7 | end 8 | 9 | def clean_over(limit) # limit in bytes 10 | old_files_over(limit).each {|asset| FileUtils.rm(asset) } 11 | end 12 | 13 | def glob 14 | "#{@dir}/**/*" 15 | end 16 | 17 | def files 18 | @files ||= Dir[glob].reject {|file| File.directory?(file) } 19 | end 20 | 21 | def sorted_files 22 | @sorted ||= files.sort_by {|a| FILE_STAT_CACHE[a].mtime } 23 | end 24 | 25 | def total_size 26 | @size ||= sorted_files.inject(0) {|sum, asset| sum += FILE_STAT_CACHE[asset].size } 27 | end 28 | 29 | 30 | def old_files_over(limit) 31 | diff = total_size - limit 32 | sorted_files.take_while {|asset| diff -= FILE_STAT_CACHE[asset].size if diff > 0 } || [] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /.github/workflows/hatchet_app_cleaner.yml: -------------------------------------------------------------------------------- 1 | name: Hatchet app cleaner 2 | 3 | on: 4 | schedule: 5 | # Daily at 6am UTC. 6 | - cron: "0 6 * * *" 7 | # Allow the workflow to be manually triggered too. 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | hatchet-app-cleaner: 15 | runs-on: ubuntu-latest 16 | env: 17 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} 18 | HEROKU_API_USER: ${{ secrets.HEROKU_API_USER }} 19 | HEROKU_DISABLE_AUTOUPDATE: 1 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - name: Install Ruby and dependencies 24 | uses: ruby/setup-ruby@v1 25 | with: 26 | bundler-cache: true 27 | ruby-version: "3.3.9" 28 | - name: Run Hatchet destroy 29 | # Only apps older than 10 minutes are destroyed, to ensure that any 30 | # in progress CI runs are not interrupted. 31 | run: bundle exec hatchet destroy --older-than 10 32 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/node_installer.rb: -------------------------------------------------------------------------------- 1 | class LanguagePack::Helpers::NodeInstaller 2 | attr_reader :version 3 | 4 | def initialize(arch: ) 5 | nodebin = LanguagePack::Helpers::Nodebin.node_lts(arch: arch) 6 | @version = nodebin["number"] 7 | @url = nodebin["url"] 8 | @fetcher = LanguagePack::Fetcher.new("") 9 | end 10 | 11 | def binary_path 12 | File.basename(@url).delete_suffix(".tar.gz") 13 | end 14 | 15 | def install 16 | # Untar of this file produces artifacts that the app does not need to run. 17 | # If we ran this command in the app directory, we would have to manually 18 | # clean up un-used files. Instead we untar in a temp directory which 19 | # helps us avoid accidentally deleting code out of the user's slug by mistake. 20 | Dir.mktmpdir do |dir| 21 | node_bin = "#{binary_path}/bin/node" 22 | 23 | Dir.chdir(dir) do 24 | @fetcher.fetch_untar(@url, node_bin) 25 | end 26 | 27 | FileUtils.mv("#{dir}/#{node_bin}", ".") 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /.github/workflows/prepare-release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | # Disable all GITHUB_TOKEN permissions, since the GitHub App token is used instead. 7 | permissions: {} 8 | 9 | jobs: 10 | prepare-release: 11 | uses: heroku/languages-github-actions/.github/workflows/_classic-buildpack-prepare-release.yml@latest 12 | secrets: inherit 13 | with: 14 | custom_update_command: | 15 | set -euo pipefail 16 | 17 | sed --in-place --regexp-extended \ 18 | --expression "s/v${EXISTING_VERSION}/v${NEW_VERSION}/" \ 19 | lib/language_pack/version.rb 20 | 21 | if compgen -G 'changelogs/unreleased/*.md' > /dev/null; then 22 | # The unreleased changelogs directory contains a `.gitkeep` file, so we have to 23 | # copy the markdown files individually instead of renaming the directory. 24 | NEW_CHANGELOG_DIR="changelogs/v${NEW_VERSION}/" 25 | mkdir -p "${NEW_CHANGELOG_DIR}" 26 | mv changelogs/unreleased/*.md "${NEW_CHANGELOG_DIR}" 27 | fi 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License: 2 | 3 | Copyright (C) 2012 Heroku, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /changelogs/v267/bundler_major_minor.md: -------------------------------------------------------------------------------- 1 | ## Bundler versions 2.4.22 and 2.5.6 are now available for Ruby Applications 2 | 3 | The [Ruby Buildpack](https://devcenter.heroku.com/articles/ruby-support#libraries) now installs a version of bundler based on the major and minor version listed in the `Gemfile.lock` under the `BUNDLED WITH` key. Previously, it only used the major version. Now, this logic will be used: 4 | 5 | - `BUNDLED WITH` 1.x will receive bundler `1.17.3` 6 | - `BUNDLED WITH` 2.0.x to 2.3.x will receive bundler `2.3.25` 7 | - `BUNDLED WITH` 2.4.x will receive bundler `2.4.22` 8 | - `BUNDLED WITH` 2.5.x and above will receive bundler `2.5.6` 9 | 10 | It is strongly recommended that you have both a `RUBY VERSION` and `BUNDLED WITH` version listed in your `Gemfile.lock`. If you do not have those values, you can generate them and commit them to git: 11 | 12 | ``` 13 | $ bundle update --ruby 14 | $ git add Gemfile.lock 15 | $ git commit -m "Update Gemfile.lock" 16 | ``` 17 | 18 | Applications without these values specified in the `Gemfile.lock` may break unexpectedly when the defaults change. 19 | -------------------------------------------------------------------------------- /lib/language_pack/rack.rb: -------------------------------------------------------------------------------- 1 | require "language_pack" 2 | require "language_pack/ruby" 3 | 4 | # Rack Language Pack. This is for any non-Rails Rack apps like Sinatra. 5 | class LanguagePack::Rack < LanguagePack::Ruby 6 | 7 | # detects if this is a valid Rack app by seeing if "config.ru" exists 8 | # @return [Boolean] true if it's a Rack app 9 | def self.use? 10 | bundler.gem_version('rack') 11 | end 12 | 13 | def name 14 | "Ruby/Rack" 15 | end 16 | 17 | def default_config_vars 18 | super.merge({ 19 | "RACK_ENV" => env("RACK_ENV") || "production" 20 | }) 21 | end 22 | 23 | def default_process_types 24 | # let's special case thin here if we detect it 25 | web_process = bundler.has_gem?("thin") ? 26 | "bundle exec thin start -R config.ru -e $RACK_ENV -p ${PORT:-5000}" : 27 | "bundle exec rackup config.ru -p ${PORT:-5000}" 28 | 29 | super.merge({ 30 | "web" => web_process 31 | }) 32 | end 33 | 34 | private 35 | 36 | # sets up the profile.d script for this buildpack 37 | def setup_profiled(**args) 38 | super(**args) 39 | set_env_default "RACK_ENV", "production" 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/hatchet/rails3_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe "Rails 3.x" do 4 | it "should deploy and inject plugins" do 5 | skip("Need RAILS_LTS_CREDS env var set") unless ENV["RAILS_LTS_CREDS"] 6 | 7 | Hatchet::Runner.new("rails3_default_ruby", config: rails_lts_config, stack: rails_lts_stack).tap do |app| 8 | app.before_deploy do 9 | set_lts_ruby_version 10 | set_bundler_version(version: :default) 11 | end 12 | 13 | app.deploy do 14 | # Rails 3 doesn't work with Postgres 8+ out of the box and Rails 15 | # LTS hasn't patched this yet. We're skipping asset compilation for now 16 | # by deleting the Rakefile 17 | # 18 | # expect(app.output).to include("Asset precompilation completed") 19 | 20 | expect(app.output).to match("WARNING") 21 | expect(app.output).to match("Add 'rails_12factor' gem to your Gemfile to skip plugin injection") 22 | 23 | ls = app.run("ls vendor/plugins") 24 | expect(ls).to match("rails3_serve_static_assets") 25 | expect(ls).to match("rails_log_stdout") 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/language_pack/rails41.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require "language_pack" 3 | require "language_pack/rails4" 4 | 5 | class LanguagePack::Rails41 < LanguagePack::Rails4 6 | # detects if this is a Rails 4.x app 7 | # @return [Boolean] true if it's a Rails 4.x app 8 | def self.use? 9 | rails_version = bundler.gem_version('railties') 10 | return false unless rails_version 11 | is_rails4 = rails_version >= Gem::Version.new('4.1.0.beta1') && 12 | rails_version < Gem::Version.new('5.0.0') 13 | return is_rails4 14 | end 15 | 16 | def setup_profiled(**args) 17 | super(**args) 18 | set_env_default "SECRET_KEY_BASE", app_secret 19 | end 20 | 21 | def default_config_vars 22 | super.merge({ 23 | "SECRET_KEY_BASE" => env("SECRET_KEY_BASE") || app_secret 24 | }) 25 | end 26 | 27 | private 28 | def app_secret 29 | key = "secret_key_base" 30 | 31 | @app_secret ||= begin 32 | if @metadata.exists?(key) 33 | @metadata.read(key).strip 34 | else 35 | secret = SecureRandom.hex(64) 36 | @metadata.write(key, secret) 37 | 38 | secret 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /hatchet.json: -------------------------------------------------------------------------------- 1 | { 2 | "rake": [ 3 | "sharpstone/asset_precompile_fail", 4 | "sharpstone/asset_precompile_pass", 5 | "sharpstone/asset_precompile_not_found", 6 | "sharpstone/no_rakefile", 7 | "sharpstone/bad_rakefile" 8 | ], 9 | "bundler": [ 10 | "sharpstone/git_gemspec", 11 | "sharpstone/no_lockfile", 12 | "sharpstone/sqlite3_gemfile" 13 | ], 14 | "ruby": [ 15 | "sharpstone/ruby_version_does_not_exist", 16 | "sharpstone/jruby-minimal", 17 | "sharpstone/empty-procfile", 18 | "sharpstone/bad_ruby_version" 19 | ], 20 | "rack": [ 21 | "sharpstone/default_ruby" 22 | ], 23 | "rails_versions": [ 24 | "sharpstone/rails_lts_23_default_ruby", 25 | "sharpstone/rails3_default_ruby", 26 | "sharpstone/rails42_default_ruby", 27 | "sharpstone/rails61", 28 | "sharpstone/rails-jsbundling", 29 | "sharpstone/rails_8_ruby_schema", 30 | "sharpstone/rails_8_sql_schema" 31 | ], 32 | "heroku": [ 33 | "heroku/ruby-getting-started" 34 | ], 35 | "node": [ 36 | "sharpstone/minimal_webpacker" 37 | ], 38 | "ci": [ 39 | "sharpstone/heroku-ci-json-example", 40 | "sharpstone/ruby_no_rails_test" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /spec/hatchet/rails6_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe "Rails 6" do 4 | it "should detect successfully" do 5 | Hatchet::App.new('rails61').in_directory_fork do 6 | expect(LanguagePack::Rails5.use?).to eq(false) 7 | expect(LanguagePack::Rails6.use?).to eq(true) 8 | end 9 | end 10 | 11 | it "deploys and serves web requests via puma" do 12 | before_deploy = Proc.new do 13 | run! "echo 'web: bundle exec puma -t 5:5 -p ${PORT:-3000} -e ${RACK_ENV:-development}' > Procfile" 14 | 15 | # Test Clean task does not get called if it does not exist 16 | # This file will only have the `assets:precompile` task in it, but not `assets:clean` 17 | run! %Q{echo 'task "assets:precompile" do ; end' > Rakefile} 18 | end 19 | 20 | Hatchet::Runner.new('rails61', before_deploy: before_deploy, config: rails_lts_config, stack: rails_lts_stack).deploy do |app| 21 | expect(app.output).to match("Fetching railties 6") 22 | 23 | expect(app.output).to match("rake assets:precompile") 24 | expect(app.output).to_not match("rake assets:clean") 25 | 26 | expect(web_boot_status(app)).to_not eq("crashed") 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/language_pack/metadata.rb: -------------------------------------------------------------------------------- 1 | require "language_pack" 2 | require "language_pack/base" 3 | 4 | class LanguagePack::Metadata 5 | FOLDER = "vendor/heroku" 6 | 7 | def initialize(cache) 8 | if cache 9 | @cache = cache 10 | @cache.load FOLDER 11 | end 12 | end 13 | 14 | def [](key) 15 | read(key) 16 | end 17 | 18 | def []=(key, value) 19 | write(key, value) 20 | end 21 | 22 | def read(key) 23 | full_key = "#{FOLDER}/#{key}" 24 | File.read(full_key).strip if exists?(key) 25 | end 26 | 27 | def exists?(key) 28 | full_key = "#{FOLDER}/#{key}" 29 | File.exist?(full_key) && !Dir.exist?(full_key) 30 | end 31 | alias_method :include?, :exists? 32 | 33 | def write(key, value, isave = true) 34 | FileUtils.mkdir_p(FOLDER) 35 | 36 | full_key = "#{FOLDER}/#{key}" 37 | File.open(full_key, 'w') {|f| f.puts value } 38 | save if isave 39 | 40 | return true 41 | end 42 | 43 | def touch(key) 44 | write(key, "true") 45 | end 46 | 47 | def fetch(key) 48 | return read(key) if exists?(key) 49 | 50 | value = yield 51 | 52 | write(key, value.to_s) 53 | return value 54 | end 55 | 56 | def save(file = FOLDER) 57 | @cache ? @cache.add(file) : false 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /bin/test-compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # The actual `bin/test-compile` code lives in `bin/ruby_test-compile`. This file instead 3 | # bootstraps the ruby needed and then executes `bin/ruby_test-compile` 4 | 5 | BUILD_DIR=$1 6 | CACHE_DIR=$2 7 | ENV_DIR=$3 8 | BIN_DIR=$(cd "$(dirname "$0")" || exit; pwd) # absolute path 9 | 10 | # shellcheck source=bin/support/bash_functions.sh 11 | source "$BIN_DIR/support/bash_functions.sh" 12 | 13 | bootstrap_ruby_dir=$(mktemp -d) 14 | "$BIN_DIR"/support/download_ruby "$BIN_DIR" "$bootstrap_ruby_dir" 15 | trap 'rm -rf "$bootstrap_ruby_dir"' EXIT 16 | 17 | export PATH="$bootstrap_ruby_dir/bin/:$PATH" 18 | unset GEM_PATH 19 | 20 | if detect_needs_java "$BUILD_DIR"; then 21 | cat < Installing Java 31 | 32 | EOM 33 | 34 | compile_buildpack_v2 "$BUILD_DIR" "$CACHE_DIR" "$ENV_DIR" "https://buildpack-registry.s3.us-east-1.amazonaws.com/buildpacks/heroku/jvm.tgz" "heroku/jvm" 35 | fi 36 | 37 | "$bootstrap_ruby_dir"/bin/ruby "$BIN_DIR/support/ruby_test-compile" "$@" 38 | -------------------------------------------------------------------------------- /bin/support/ruby_compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This script compiles an application so it can run on Heroku. 4 | # It will install the application's specified version of Ruby, it's dependencies 5 | # and certain framework specific requirements (such as calling `rake assets:precompile` 6 | # for rails apps). You can see all features described in the devcenter 7 | # https://devcenter.heroku.com/articles/ruby-support 8 | $stdout.sync = true 9 | 10 | $:.unshift File.expand_path("../../../lib", __FILE__) 11 | require "language_pack" 12 | require "language_pack/shell_helpers" 13 | HerokuBuildReport.set_global( 14 | # Coupled with `bin/report` 15 | path: Pathname(ARGV[1]) 16 | .join(".heroku") 17 | .join("ruby") 18 | .join("build_report.yml") 19 | ).tap(&:clear!) 20 | 21 | begin 22 | app_path = Pathname(ARGV[0]) 23 | cache_path = Pathname(ARGV[1]) 24 | gemfile_lock = LanguagePack.gemfile_lock(app_path: app_path) 25 | Dir.chdir(app_path) 26 | 27 | LanguagePack::ShellHelpers.initialize_env(ARGV[2]) 28 | if pack = LanguagePack.detect( 29 | app_path: app_path, 30 | cache_path: cache_path, 31 | gemfile_lock: gemfile_lock 32 | ) 33 | pack.topic("Compiling #{pack.name}") 34 | pack.compile 35 | end 36 | rescue Exception => e 37 | LanguagePack::ShellHelpers.display_error_and_exit(e) 38 | end 39 | -------------------------------------------------------------------------------- /spec/hatchet/getting_started_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe "Heroku ruby getting started" do 4 | it "works on Heroku-24" do 5 | Hatchet::Runner.new("ruby-getting-started", stack: "heroku-24").deploy do |app| 6 | expect(app.output).to_not include("Purging Cache") 7 | # Assert sprockets build cache not present on runtime 8 | expect(app.run("ls tmp/cache/assets")).to_not match("sprockets") 9 | 10 | # Re-deploy with cache 11 | run!("git commit --allow-empty -m empty") 12 | app.push! 13 | 14 | # Assert no warnings from `cp` 15 | # https://github.com/heroku/heroku-buildpack-ruby/pull/1586/files#r2064284286 16 | expect(app.output).to_not include("cp --help") 17 | expect(app.run("which ruby").strip).to eq("/app/bin/ruby") 18 | end 19 | end 20 | 21 | it "works on Heroku-22" do 22 | Hatchet::Runner.new("ruby-getting-started", stack: "heroku-22").deploy do |app| 23 | # Re-deploy with cache 24 | run!("git commit --allow-empty -m empty") 25 | app.push! 26 | 27 | # Assert no warnings from `cp` 28 | # https://github.com/heroku/heroku-buildpack-ruby/pull/1586/files#r2064284286 29 | expect(app.output).to_not include("cp --help") 30 | expect(app.run("which ruby").strip).to eq("/app/bin/ruby") 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/helpers/heroku_build_report_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Build report" do 4 | it "handles complex object serialization by converting them to strings" do 5 | Dir.mktmpdir do |dir| 6 | path = Pathname(dir).join(".report.yml") 7 | report = HerokuBuildReport::YamlReport.new( 8 | path: path 9 | ) 10 | value = Gem::Version.new("3.4.2") 11 | expect(report.complex_object?(value)).to eq(true) 12 | expect(value.to_yaml).to_not eq(value.to_s.to_yaml) 13 | report.capture("key" => value) 14 | 15 | expect(report.data).to eq({"key" => "3.4.2"}) 16 | expect(path.read).to eq(<<~EOF) 17 | --- 18 | key: 3.4.2 19 | EOF 20 | end 21 | end 22 | 23 | it "writes valid yaml" do 24 | Dir.mktmpdir do |dir| 25 | path = Pathname(dir).join(".report.yml") 26 | report = HerokuBuildReport::YamlReport.new( 27 | path: path 28 | ) 29 | report.capture( 30 | "string" => "'with single quotes'", 31 | "string_plain" => "plain", 32 | "number" => 22, 33 | "boolean" => true, 34 | ) 35 | 36 | expect(path.read).to eq(<<~EOF) 37 | --- 38 | string: "'with single quotes'" 39 | string_plain: plain 40 | number: 22 41 | boolean: true 42 | EOF 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/helpers/stale_file_cleaner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Cleans Stale Files" do 4 | 5 | it "removes files if they go over the limit" do 6 | file_size = 1000 7 | Dir.mktmpdir do |dir| 8 | old_file = create_file_with_size_in(file_size, dir) 9 | sleep 1 # need mtime of files to be different 10 | new_file = create_file_with_size_in(file_size, dir) 11 | 12 | expect(old_file.exist?).to be_truthy 13 | expect(new_file.exist?).to be_truthy 14 | 15 | ::LanguagePack::Helpers::StaleFileCleaner.new(dir).clean_over(2*file_size - 50) 16 | 17 | expect(old_file.exist?).to be_falsey 18 | expect(new_file.exist?).to be_truthy 19 | end 20 | end 21 | 22 | it "leaves files if they are under the limit" do 23 | file_size = 1000 24 | Dir.mktmpdir do |dir| 25 | old_file = create_file_with_size_in(file_size, dir) 26 | sleep 1 # need mtime of files to be different 27 | new_file = create_file_with_size_in(file_size, dir) 28 | 29 | expect(old_file.exist?).to be_truthy 30 | expect(new_file.exist?).to be_truthy 31 | dir_size = File.stat(dir) 32 | 33 | ::LanguagePack::Helpers::StaleFileCleaner.new(dir).clean_over(2*file_size + 50) 34 | 35 | expect(old_file.exist?).to be_truthy 36 | expect(new_file.exist?).to be_truthy 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /bin/compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # The actual compilation code lives in `bin/support/ruby_compile`. This file instead 3 | # bootstraps the ruby needed and then executes `bin/support/ruby_compile` 4 | 5 | set -euo pipefail 6 | 7 | BUILD_DIR=$1 8 | CACHE_DIR=$2 9 | ENV_DIR=$3 10 | BIN_DIR=$(cd "$(dirname "$0")" || exit; pwd) # absolute path 11 | 12 | # shellcheck source=bin/support/bash_functions.sh 13 | source "$BIN_DIR/support/bash_functions.sh" 14 | 15 | checks::ensure_supported_stack "${STACK:?Required env var STACK is not set}" 16 | 17 | bootstrap_ruby_dir=$(mktemp -d) 18 | "$BIN_DIR"/support/download_ruby "$BIN_DIR" "$bootstrap_ruby_dir" 19 | trap 'rm -rf "$bootstrap_ruby_dir"' EXIT 20 | 21 | export PATH="$bootstrap_ruby_dir/bin/:$PATH" 22 | unset GEM_PATH 23 | 24 | if detect_needs_java "$BUILD_DIR"; then 25 | cat < Installing Java 35 | 36 | EOM 37 | 38 | compile_buildpack_v2 "$BUILD_DIR" "$CACHE_DIR" "$ENV_DIR" "https://buildpack-registry.s3.us-east-1.amazonaws.com/buildpacks/heroku/jvm.tgz" "heroku/jvm" 39 | fi 40 | 41 | "$bootstrap_ruby_dir"/bin/ruby "$BIN_DIR/support/ruby_compile" "$@" 42 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/plugin_installer.rb: -------------------------------------------------------------------------------- 1 | require "language_pack/shell_helpers" 2 | 3 | module LanguagePack 4 | module Helpers 5 | # Takes an array of plugin names and vendor_url 6 | # fetches plugins from url, installs them 7 | class PluginsInstaller 8 | attr_accessor :plugins, :vendor_url 9 | include LanguagePack::ShellHelpers 10 | 11 | def initialize(plugins, vendor_url = LanguagePack::Base::VENDOR_URL) 12 | @plugins = plugins || [] 13 | @vendor_url = vendor_url 14 | end 15 | 16 | # vendors all the plugins into the slug 17 | def install 18 | return true unless plugins.any? 19 | plugins.each { |plugin| vendor(plugin) } 20 | end 21 | 22 | def plugin_dir(name = "") 23 | Pathname.new("vendor/plugins").join(name) 24 | end 25 | 26 | # vendors an individual plugin 27 | # @param [String] name of the plugin 28 | def vendor(name) 29 | directory = plugin_dir(name) 30 | return true if directory.exist? 31 | directory.mkpath 32 | Dir.chdir(directory) do |dir| 33 | run("curl #{vendor_url}/#{name}.tgz -s --fail --retry 3 --retry-connrefused --connect-timeout #{curl_connect_timeout_in_seconds} -o - | tar xzf -") 34 | end 35 | end 36 | 37 | def curl_connect_timeout_in_seconds 38 | env('CURL_CONNECT_TIMEOUT') || 3 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /bin/support/ruby_test-compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This script installs an app's Ruby version and dependencies. It's 4 | # essentially the same thing as `ruby_compile` (which is called by bin/compile) 5 | # except that some behavior is cusomtized by the inclusion of the 6 | # `language_pack/test` file. One difference is dependencies 7 | # the `bin/compile` installs dependencies via bundler and excludes `development:test` 8 | # gems, however we need `test` gems to run tests, so instead only `development` 9 | # is excluded. 10 | # 11 | # It also sets up the database (if one is present) and populates the database 12 | # with the appropriate schema. 13 | $stdout.sync = true 14 | 15 | $:.unshift File.expand_path("../../../lib", __FILE__) 16 | require "language_pack" 17 | require "language_pack/shell_helpers" 18 | require "language_pack/test" 19 | include LanguagePack::ShellHelpers 20 | 21 | begin 22 | app_path = Pathname(ARGV[0]) 23 | cache_path = Pathname(ARGV[1]) 24 | gemfile_lock = LanguagePack.gemfile_lock(app_path: app_path) 25 | Dir.chdir(app_path) 26 | 27 | if pack = LanguagePack.detect( 28 | app_path: app_path, 29 | cache_path: cache_path, 30 | gemfile_lock: gemfile_lock 31 | ) 32 | LanguagePack::ShellHelpers.initialize_env(ARGV[2]) 33 | pack.topic("Setting up Test for #{pack.name}") 34 | pack.compile 35 | end 36 | rescue Exception => e 37 | LanguagePack::ShellHelpers.display_error_and_exit(e) 38 | end 39 | -------------------------------------------------------------------------------- /changelogs/v267/ruby_gemfile_lock.md: -------------------------------------------------------------------------------- 1 | ## Ruby applications without a `RUBY VERSION` in the Gemfile.lock may receive a default Ruby version 2 | 3 | Previously, it was possible to specify a full version of Ruby in the `Gemfile` even if it was not present in the `Gemfile.lock`. The Ruby directive in the `Gemfile` was parsed by bundler and emitted via the command `bundle --platform ruby`. This behavior has changed with bundler `2.4+`, so only ruby versions listed in the `RUBY VERSION` key of the `Gemfile.lock` will be returned. If your application uses bundler 2.4+ and does not have a `RUBY VERSION` specified in the `Gemfile.lock`, it will receive a default version of Ruby. 4 | 5 | It is strongly recommended that you have both a `RUBY VERSION` and `BUNDLED WITH` version listed in your `Gemfile.lock`. If you do not have those values, you can generate them and commit them to git: 6 | 7 | ``` 8 | $ bundle update --ruby 9 | $ git add Gemfile.lock 10 | $ git commit -m "Update Gemfile.lock" 11 | ``` 12 | 13 | Applications without these values specified in the `Gemfile.lock` may break unexpectedly when the defaults change. 14 | 15 | If your app relies on specifying the ruby version in the `Gemfile` but not the `Gemfile.lock` and it is not yet using Bundler 2.4+, you may preserve this behavior by not upgrading the bundler version in your `Gemfile.lock`, however, this behavior is deprecated. It will be removed at a future date. It is recommended you lock your Ruby version now to avoid an unexpected breakage in the future. 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | # Avoid duplicate builds on PRs. 6 | branches: 7 | - main 8 | pull_request: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Run ShellCheck bin top level 20 | run: | 21 | shellcheck bin/support/bash_functions.sh bin/support/download_ruby -x && 22 | shellcheck bin/compile bin/detect bin/release bin/test bin/test-compile -x 23 | 24 | integration-test: 25 | runs-on: ubuntu-24.04 26 | env: 27 | HATCHET_APP_LIMIT: 300 28 | HATCHET_EXPENSIVE_MODE: 1 29 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} 30 | HEROKU_API_USER: ${{ secrets.HEROKU_API_USER }} 31 | RAILS_LTS_CREDS: ${{ secrets.RAILS_LTS_CREDS }} 32 | HEROKU_DISABLE_AUTOUPDATE: 1 33 | PARALLEL_SPLIT_TEST_PROCESSES: 85 34 | RSPEC_RETRY_RETRY_COUNT: 1 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | - name: Install Ruby and dependencies 39 | uses: ruby/setup-ruby@v1 40 | with: 41 | bundler-cache: true 42 | ruby-version: "3.3.9" 43 | - name: Hatchet setup 44 | run: bundle exec hatchet ci:setup 45 | - name: Run Hatchet integration tests 46 | # parallel_split_test runs rspec in parallel, with concurrency equal to PARALLEL_SPLIT_TEST_PROCESSES. 47 | run: bundle exec parallel_split_test spec/ 48 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/bundler_cache.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | require "fileutils" 3 | require "language_pack/cache" 4 | 5 | # manipulating the `vendor/bundle` Bundler cache directory. 6 | # supports storing the cache in a "stack" directory 7 | class LanguagePack::BundlerCache 8 | attr_reader :bundler_dir 9 | 10 | # @param [LanguagePack::Cache] cache object 11 | # @param [String] stack buildpack is running on 12 | def initialize(cache, stack = nil) 13 | @cache = cache 14 | @stack = stack 15 | @bundler_dir = Pathname.new("vendor/bundle") 16 | @stack_dir = @stack ? Pathname.new(@stack) + @bundler_dir : @bundler_dir 17 | end 18 | 19 | # removes the bundler cache dir BOTH in the cache and local directory 20 | def clear(stack = nil) 21 | stack ||= @stack 22 | @cache.clear(stack) 23 | @bundler_dir.rmtree 24 | end 25 | 26 | # converts to cache directory to support stacks. only copy contents if the stack hasn't changed 27 | # @param [Boolean] denote if there's a stack change or not 28 | def convert_stack(stack_change) 29 | @cache.cache_copy(@bundler_dir, @stack_dir) unless stack_change 30 | @cache.clear(@bundler_dir) 31 | end 32 | 33 | # detects if using the non stack directory layout 34 | def old? 35 | @cache.exists?(@bundler_dir) 36 | end 37 | 38 | def exists? 39 | @cache.exists?(@stack_dir) 40 | end 41 | 42 | # writes cache contents to cache store 43 | def store 44 | @cache.store(@bundler_dir, @stack_dir) 45 | end 46 | 47 | # loads cache contents from the cache store 48 | def load 49 | @cache.load(@stack_dir, @bundler_dir) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/language_pack/test/ruby.rb: -------------------------------------------------------------------------------- 1 | # Opens up the class of the Ruby language pack and 2 | # overwrites methods defined in `language_pack/ruby.rb` 3 | # 4 | # Other "test packs" futher extend this behavior by hooking into 5 | # methods or over writing methods defined here. 6 | class LanguagePack::Ruby 7 | def compile 8 | new_app? 9 | remove_vendor_bundle 10 | warn_bad_binstubs 11 | install_ruby(install_path: slug_vendor_ruby) 12 | setup_language_pack_environment( 13 | ruby_layer_path: File.expand_path("."), 14 | gem_layer_path: File.expand_path("."), 15 | bundle_path: "vendor/bundle", 16 | bundle_default_without: "development" 17 | ) 18 | setup_export 19 | allow_git do 20 | install_bundler_in_app(slug_vendor_base) 21 | load_bundler_cache 22 | build_bundler 23 | post_bundler 24 | create_database_yml 25 | install_binaries 26 | prepare_tests 27 | end 28 | setup_profiled(ruby_layer_path: "$HOME", gem_layer_path: "$HOME") # $HOME is set to /app at run time 29 | super 30 | end 31 | 32 | def db_prepare_test_rake_tasks 33 | ["db:schema:load", "db:migrate"].map {|name| rake.task(name) } 34 | end 35 | 36 | def prepare_tests 37 | rake_tasks = db_prepare_test_rake_tasks.select(&:is_defined?) 38 | return true if rake_tasks.empty? 39 | 40 | topic "Preparing test database" 41 | rake_tasks.each do |rake_task| 42 | rake_task.invoke(env: rake_env) 43 | if rake_task.success? 44 | puts "#{rake_task.task} completed (#{"%.2f" % rake_task.time}s)" 45 | else 46 | error "Could not prepare database for test" 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/helpers/bundle_list_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | 4 | describe "Bundle list" do 5 | it "parses bundle list output" do 6 | output = <<~EOF 7 | Gems included by the bundle: 8 | * actioncable (6.1.4.1) 9 | * actionmailbox (6.1.4.1) 10 | * actionmailer (6.1.4.1) 11 | * actionpack (6.1.4.1) 12 | * actiontext (6.1.4.1) 13 | * actionview (6.1.4.1) 14 | * activejob (6.1.4.1) 15 | * activemodel (6.1.4.1) 16 | * activerecord (6.1.4.1) 17 | * activestorage (6.1.4.1) 18 | * activesupport (6.1.4.1) 19 | * addressable (2.8.0) 20 | * ast (2.4.2) 21 | * railties (6.1.4.1) 22 | Use `bundle info` to print more detailed information about a gem 23 | EOF 24 | 25 | bundle_list = LanguagePack::Helpers::BundleList.new( 26 | output: output 27 | ) 28 | expect(bundle_list.has_gem?("railties")).to be_truthy 29 | expect(bundle_list.gem_version("railties")).to eq(Gem::Version.new("6.1.4.1")) 30 | expect(bundle_list.has_gem?("nope")).to be_falsey 31 | 32 | expect(bundle_list.length).to eq(14) 33 | end 34 | 35 | it "handles git SHA gems" do 36 | output = <<~EOF 37 | Gems included by the bundle: 38 | * railties (6.1.4.1 asdf1) 39 | Use `bundle info` to print more detailed information about a gem 40 | EOF 41 | 42 | bundle_list = LanguagePack::Helpers::BundleList.new( 43 | output: output 44 | ) 45 | expect(bundle_list.has_gem?("railties")).to be_truthy 46 | expect(bundle_list.gem_version("railties")).to eq(Gem::Version.new("6.1.4.1")) 47 | expect(bundle_list.has_gem?("nope")).to be_falsey 48 | 49 | expect(bundle_list.length).to eq(1) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /.github/workflows/document_ruby_version.yml: -------------------------------------------------------------------------------- 1 | name: Add Ruby version to changelog && prepare release 2 | run-name: "Add ${{ inputs.is_jruby && 'J' || ''}}Ruby ${{ inputs.ruby_version }} to the CHANGELOG.md and prepare a release" 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | ruby_version: 8 | description: "The Ruby version to announce" 9 | type: string 10 | required: true 11 | is_jruby: 12 | description: "JRuby release? (as opposed to MRI)" 13 | type: boolean 14 | default: false 15 | required: false 16 | 17 | # Disable all GITHUB_TOKEN permissions, since the GitHub App token is used instead. 18 | permissions: {} 19 | 20 | jobs: 21 | prepare-release: 22 | uses: heroku/languages-github-actions/.github/workflows/_classic-buildpack-prepare-release.yml@latest 23 | secrets: inherit 24 | with: 25 | custom_update_command: | 26 | set -euo pipefail 27 | DATE_TODAY="$(date --utc --iso-8601)" 28 | 29 | sed --in-place "/## \[v${NEW_VERSION}\] - ${DATE_TODAY}/a\\ 30 | \\ 31 | - ${{ inputs.is_jruby && 'J' || ''}}Ruby ${{inputs.ruby_version}} is now available" CHANGELOG.md 32 | 33 | sed --in-place --regexp-extended \ 34 | --expression "s/v${EXISTING_VERSION}/v${NEW_VERSION}/" \ 35 | lib/language_pack/version.rb 36 | 37 | if compgen -G 'changelogs/unreleased/*.md' > /dev/null; then 38 | # The unreleased changelogs directory contains a `.gitkeep` file, so we have to 39 | # copy the markdown files individually instead of renaming the directory. 40 | NEW_CHANGELOG_DIR="changelogs/v${NEW_VERSION}/" 41 | mkdir -p "${NEW_CHANGELOG_DIR}" 42 | mv changelogs/unreleased/*.md "${NEW_CHANGELOG_DIR}" 43 | fi 44 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | base64 (0.3.0) 5 | citrus (3.0.2) 6 | connection_pool (2.5.3) 7 | diff-lcs (1.6.2) 8 | erubis (2.7.0) 9 | excon (1.2.8) 10 | logger 11 | heroics (0.1.3) 12 | base64 13 | erubis (~> 2.0) 14 | excon 15 | moneta 16 | multi_json (>= 1.9.2) 17 | webrick 18 | heroku_hatchet (8.0.6) 19 | excon (< 2) 20 | platform-api (~> 3) 21 | rrrretry (~> 1) 22 | thor (~> 1) 23 | threaded (~> 0) 24 | json (2.13.2) 25 | logger (1.7.0) 26 | moneta (1.0.0) 27 | multi_json (1.17.0) 28 | parallel (1.27.0) 29 | parallel_split_test (0.10.0) 30 | parallel (>= 0.5.13) 31 | rspec-core (>= 3.9.0) 32 | platform-api (3.8.0) 33 | heroics (~> 0.1.1) 34 | moneta (~> 1.0.0) 35 | rate_throttle_client (~> 0.1.0) 36 | racc (1.8.1) 37 | rake (13.3.0) 38 | rate_throttle_client (0.1.2) 39 | redis (5.4.1) 40 | redis-client (>= 0.22.0) 41 | redis-client (0.25.1) 42 | connection_pool 43 | rrrretry (1.0.0) 44 | rspec-core (3.13.5) 45 | rspec-support (~> 3.13.0) 46 | rspec-expectations (3.13.5) 47 | diff-lcs (>= 1.2.0, < 2.0) 48 | rspec-support (~> 3.13.0) 49 | rspec-retry (0.6.2) 50 | rspec-core (> 3.3) 51 | rspec-support (3.13.4) 52 | thor (1.4.0) 53 | threaded (0.0.4) 54 | toml-rb (4.0.0) 55 | citrus (~> 3.0, > 3.0) 56 | racc (~> 1.7) 57 | webrick (1.9.1) 58 | 59 | PLATFORMS 60 | ruby 61 | 62 | DEPENDENCIES 63 | excon 64 | heroku_hatchet 65 | json 66 | parallel_split_test 67 | rake 68 | redis 69 | rspec-core 70 | rspec-expectations 71 | rspec-retry 72 | toml-rb 73 | 74 | RUBY VERSION 75 | ruby 3.3.9p170 76 | 77 | BUNDLED WITH 78 | 2.5.22 79 | -------------------------------------------------------------------------------- /spec/helpers/rake_runner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Rake Runner" do 4 | it "runs rake tasks that exist" do 5 | Hatchet::App.new('asset_precompile_pass').in_directory_fork do 6 | rake = LanguagePack::Helpers::RakeRunner.new.load_rake_tasks! 7 | task = rake.task("assets:precompile") 8 | task.invoke 9 | 10 | expect(task.output).to match("success!") 11 | expect(task.status).to eq(:pass) 12 | expect(task.time).not_to be_nil 13 | end 14 | end 15 | 16 | it "detects when rake tasks fail" do 17 | Hatchet::App.new('asset_precompile_fail').in_directory_fork do 18 | rake = LanguagePack::Helpers::RakeRunner.new.load_rake_tasks! 19 | task = rake.task("assets:precompile") 20 | task.invoke 21 | 22 | expect(task.output).to match("assets:precompile fails") 23 | expect(task.status).to eq(:fail) 24 | expect(task.time).not_to be_nil 25 | end 26 | end 27 | 28 | it "can show errors from bad Rakefiles" do 29 | Hatchet::App.new('bad_rakefile').in_directory_fork do 30 | rake = LanguagePack::Helpers::RakeRunner.new.load_rake_tasks! 31 | task = rake.task("assets:precompile") 32 | expect(rake.rakefile_can_load?).to be_falsey 33 | expect(task.task_defined?).to be_falsey 34 | end 35 | end 36 | 37 | it "detects if task is missing" do 38 | Hatchet::App.new('asset_precompile_not_found').in_directory_fork do 39 | task = LanguagePack::Helpers::RakeRunner.new.task("assets:precompile") 40 | expect(task.task_defined?).to be_falsey 41 | end 42 | end 43 | 44 | it "detects when no rakefile is present" do 45 | Hatchet::App.new('no_rakefile').in_directory_fork do 46 | runner = LanguagePack::Helpers::RakeRunner.new 47 | expect(runner.rakefile_can_load?).to be_falsey 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # CAREFUL! Changes made to this file aren't tested 2 | # 3 | # If you need new functionality, consider putting it in lib/rake 4 | # and also adding tests, then calling that code from here 5 | # 6 | require "fileutils" 7 | require "tmpdir" 8 | require 'hatchet/tasks' 9 | require_relative 'lib/rake/deploy_check' 10 | 11 | namespace :buildpack do 12 | desc "prepares the next version of the buildpack for release" 13 | task :prepare do 14 | puts("Use https://github.com/heroku/heroku-buildpack-ruby/actions/workflows/prepare-release.yml") 15 | end 16 | 17 | desc "releases the next version of the buildpack" 18 | task :release do 19 | puts "Checking login state" 20 | sh("heroku whoami") do |out, status| 21 | if status.success? 22 | puts "Success" 23 | else 24 | raise "Ensure login works: `heroku login`" 25 | end 26 | end 27 | 28 | deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby") 29 | puts "Attempting to deploy #{deploy.next_version}, overwrite with RELEASE_VERSION env var" 30 | deploy.check! 31 | if deploy.push_tag? 32 | sh("git tag -f #{deploy.next_version}") do |out, status| 33 | raise "Could not `git tag -f #{deploy.next_version}`: #{out}" unless status.success? 34 | end 35 | sh("git push --tags") do |out, status| 36 | raise "Could not `git push --tags`: #{out}" unless status.success? 37 | end 38 | end 39 | 40 | command = "heroku buildpacks:publish heroku/ruby #{deploy.next_version}" 41 | puts "Releasing to heroku: `#{command}`" 42 | exec(command) 43 | end 44 | end 45 | 46 | begin 47 | require 'rspec/core/rake_task' 48 | 49 | desc "Run specs" 50 | RSpec::Core::RakeTask.new(:spec) do |t| 51 | t.rspec_opts = %w(-fd --color) 52 | #t.ruby_opts = %w(-w) 53 | end 54 | task :default => :spec 55 | rescue LoadError => e 56 | end 57 | -------------------------------------------------------------------------------- /lib/language_pack/fetcher.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | require "language_pack/shell_helpers" 3 | 4 | module LanguagePack 5 | class Fetcher 6 | class FetchError < StandardError; end 7 | 8 | include ShellHelpers 9 | 10 | def initialize(host_url, stack: nil, arch: nil) 11 | @host_url = Pathname.new(host_url) 12 | # File.basename prevents accidental directory traversal 13 | @host_url += File.basename(stack) if stack 14 | @host_url += File.basename(arch) if arch 15 | end 16 | 17 | def exists?(path, max_attempts = 1) 18 | curl = curl_command("-I #{@host_url.join(path)}") 19 | run!(curl, error_class: FetchError, max_attempts: max_attempts, silent: true) 20 | rescue FetchError 21 | false 22 | end 23 | 24 | def fetch(path) 25 | curl = curl_command("-O #{@host_url.join(path)}") 26 | run!(curl, error_class: FetchError) 27 | end 28 | 29 | def fetch_untar(path, files_to_extract = nil, strip_components: 0) 30 | curl = curl_command("#{@host_url.join(path)} -s -o") 31 | tar_cmd = ["tar", "--strip-components=#{strip_components}", "-xzf", "- #{files_to_extract}"] 32 | run! "#{curl} - | #{tar_cmd.join(" ")}", 33 | error_class: FetchError, 34 | max_attempts: 3 35 | end 36 | 37 | def fetch_bunzip2(path, files_to_extract = nil) 38 | curl = curl_command("#{@host_url.join(path)} -s -o") 39 | run!("#{curl} - | tar jxf - #{files_to_extract}", error_class: FetchError) 40 | end 41 | 42 | private 43 | def curl_command(command) 44 | "set -o pipefail; curl -L --fail --retry 5 --retry-delay 1 --connect-timeout #{curl_connect_timeout_in_seconds} --max-time #{curl_timeout_in_seconds} #{command}" 45 | end 46 | 47 | def curl_timeout_in_seconds 48 | env('CURL_TIMEOUT') || 30 49 | end 50 | 51 | def curl_connect_timeout_in_seconds 52 | env('CURL_CONNECT_TIMEOUT') || 3 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/heroku_build_report.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'pathname' 3 | 4 | # Observability reporting for builds 5 | # 6 | # Example usage: 7 | # 8 | # HerokuBuildReport::GLOBAL.capture( 9 | # "ruby_version" => "3.4.2" 10 | # ) 11 | module HerokuBuildReport 12 | # Accumulates data in memory and writes it to the specified path in YAML format 13 | # 14 | # Writes data to disk on every capture. Later `bin/report` emits the disk contents 15 | class YamlReport 16 | attr_reader :data 17 | 18 | def initialize(path: ) 19 | @path = Pathname(path).expand_path 20 | @path.dirname.mkpath 21 | FileUtils.touch(@path) 22 | @data = {} 23 | end 24 | 25 | def clear! 26 | @data.clear 27 | @path.write("") 28 | end 29 | 30 | def complex_object?(value) 31 | value.to_yaml.match?(/!ruby\/object:/) 32 | end 33 | 34 | def capture(metrics = {}) 35 | metrics.each do |(key, value)| 36 | return if key.nil? || key.to_s.strip.empty? 37 | 38 | key = key&.strip 39 | raise "Key cannot be empty (#{key.inspect} => #{value})" if key.nil? || key.empty? 40 | 41 | # Don't serialize complex values by accident 42 | if complex_object?(value) 43 | value = value.to_s 44 | end 45 | 46 | @data["#{key}"] = value 47 | end 48 | 49 | @path.write(@data.to_yaml) 50 | end 51 | end 52 | 53 | # Current load order of the various "language packs" 54 | def self.set_global(path: ) 55 | YamlReport.new(path: path).tap { |report| 56 | # Silence warning about setting a constant 57 | begin 58 | old_verbose = $VERBOSE 59 | $VERBOSE = nil 60 | const_set(:GLOBAL, report) 61 | ensure 62 | $VERBOSE = old_verbose 63 | end 64 | } 65 | end 66 | 67 | # Stores data in memory only, does not persist to disk 68 | def self.dev_null 69 | YamlReport.new(path: "/dev/null") 70 | end 71 | 72 | GLOBAL = self.dev_null # Changed via `set_global` 73 | end 74 | -------------------------------------------------------------------------------- /lib/language_pack.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | require 'benchmark' 3 | 4 | require 'language_pack/shell_helpers' 5 | require "language_pack/helpers/gemfile_lock" 6 | 7 | # General Language Pack module 8 | module LanguagePack 9 | module Helpers 10 | end 11 | 12 | def self.gemfile_lock(app_path: ) 13 | path = app_path.join("Gemfile.lock") 14 | if path.exist? 15 | LanguagePack::Helpers::GemfileLock.new( 16 | contents: path.read 17 | ) 18 | else 19 | raise BuildpackError.new("Gemfile.lock required. Please check it in.") 20 | end 21 | end 22 | 23 | # detects which language pack to use 24 | def self.detect(app_path:, cache_path:, gemfile_lock: ) 25 | pack_klass = [ Rails8, Rails7, Rails6, Rails5, Rails42, Rails41, Rails4, Rails3, Rails2, Rack, Ruby ].detect do |klass| 26 | klass.use? 27 | end 28 | 29 | if pack_klass 30 | pack_klass.new( 31 | app_path: app_path, 32 | cache_path: cache_path, 33 | gemfile_lock: gemfile_lock 34 | ) 35 | else 36 | nil 37 | end 38 | end 39 | end 40 | 41 | $:.unshift File.expand_path("../../vendor", __FILE__) 42 | $:.unshift File.expand_path("..", __FILE__) 43 | 44 | require 'heroku_build_report' 45 | 46 | require "language_pack/helpers/plugin_installer" 47 | require "language_pack/helpers/stale_file_cleaner" 48 | require "language_pack/helpers/bundle_list" 49 | require "language_pack/helpers/rake_runner" 50 | require "language_pack/helpers/rails_runner" 51 | require "language_pack/helpers/bundler_wrapper" 52 | require "language_pack/helpers/outdated_ruby_version" 53 | require "language_pack/helpers/download_presence" 54 | require "language_pack/installers/heroku_ruby_installer" 55 | 56 | require "language_pack/ruby" 57 | require "language_pack/rack" 58 | require "language_pack/rails2" 59 | require "language_pack/rails3" 60 | require "language_pack/rails4" 61 | require "language_pack/rails41" 62 | require "language_pack/rails42" 63 | require "language_pack/rails5" 64 | require "language_pack/rails6" 65 | require "language_pack/rails7" 66 | require "language_pack/rails8" 67 | -------------------------------------------------------------------------------- /spec/hatchet/node_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Node and Yarn" do 4 | it "works without the node buildpack" do 5 | buildpacks = [ 6 | :default, 7 | "https://github.com/sharpstone/force_absolute_paths_buildpack" 8 | ] 9 | config = {FORCE_ABSOLUTE_PATHS_BUILDPACK_IGNORE_PATHS: "BUNDLE_PATH"} 10 | 11 | Hatchet::Runner.new("minimal_webpacker", buildpacks: buildpacks, config: config).deploy do |app, heroku| 12 | # https://rubular.com/r/4bkL8fYFTQwt0Q 13 | expect(app.output).to match(/vendor\/yarn-v\d+\.\d+\.\d+\/bin\/yarn is the yarn directory/) 14 | expect(app.output).to_not include(".heroku/yarn/bin/yarn is the yarn directory") 15 | 16 | expect(app.output).to include("bin/node is the node directory") 17 | expect(app.output).to_not include(".heroku/node/bin/node is the node directory") 18 | 19 | expect(app.output).to include("Installing a default version (#{LanguagePack::Helpers::Nodebin::YARN_VERSION}) of Yarn") 20 | expect(app.output).to include("Installing a default version (#{LanguagePack::Helpers::Nodebin::NODE_VERSION}) of Node.js") 21 | 22 | expect(app.run("which node")).to match("/app/bin/node") # We put node in bin/node 23 | expect(app.run("which yarn")).to match("/app/vendor/yarn-") # We put yarn in /app/vendor/yarn- 24 | end 25 | end 26 | 27 | it "works with the node buildpack" do 28 | buildpacks = [ 29 | "heroku/nodejs", 30 | :default, 31 | "https://github.com/sharpstone/force_absolute_paths_buildpack" 32 | ] 33 | config = {FORCE_ABSOLUTE_PATHS_BUILDPACK_IGNORE_PATHS: "BUNDLE_PATH"} 34 | 35 | Hatchet::Runner.new("minimal_webpacker", buildpacks: buildpacks, config: config).deploy do |app, heroku| 36 | expect(app.output).to include("yarn install") 37 | expect(app.output).to include(".heroku/node/bin/yarn is the yarn directory ") 38 | expect(app.output).to include(".heroku/node/bin/node is the node directory") 39 | 40 | expect(app.run("which node")).to match("/app/.heroku/node/bin") 41 | expect(app.run("which yarn")).to match("/app/.heroku/node/bin") 42 | end 43 | end 44 | end 45 | 46 | -------------------------------------------------------------------------------- /hatchet.lock: -------------------------------------------------------------------------------- 1 | --- 2 | - - "./repos/bundler/git_gemspec" 3 | - 7755e19caf122e1373bce73ffe9b333e9411d732 4 | - - "./repos/bundler/no_lockfile" 5 | - 1947ce9a9c276d5df1c323b2ad78d1d85c7ab4c0 6 | - - "./repos/bundler/sqlite3_gemfile" 7 | - 116db685f54dae18f703b3beb90e64fdbddb048d 8 | - - "./repos/ci/heroku-ci-json-example" 9 | - 728cc99c8e80290cc07441d61a8bcd4596e696fb 10 | - - "./repos/ci/ruby_no_rails_test" 11 | - c5925ab061f65433ec5dcbc890975f580e74c5ce 12 | - - "./repos/heroku/ruby-getting-started" 13 | - main 14 | - - "./repos/node/minimal_webpacker" 15 | - d659577a612b12ddb44ce34e28124c025ecb22f3 16 | - - "./repos/rack/default_ruby" 17 | - master 18 | - - "./repos/rails_versions/rails-jsbundling" 19 | - 50e9fdf7c3a37623c676989ddebac4ee5349734b 20 | - - "./repos/rails_versions/rails3_default_ruby" 21 | - 984b6d02353519251c2d1e885bf8914a2584f26a 22 | - - "./repos/rails_versions/rails42_default_ruby" 23 | - fbdaf031823f09d1514ec1d55dcb04ca2f4264ca 24 | - - "./repos/rails_versions/rails61" 25 | - 47828a3e8c79b7869ca0d9a3afa4be065fa46e96 26 | - - "./repos/rails_versions/rails_8_ruby_schema" 27 | - a993a3f00c8e09f733bc3b783f7e37148d0af1d4 28 | - - "./repos/rails_versions/rails_8_sql_schema" 29 | - d5089812196931e858a97119de1640a86825a272 30 | - - "./repos/rails_versions/rails_lts_23_default_ruby" 31 | - f3597fd6f0887763eb65ec2f83a5e9a5155d5625 32 | - - "./repos/rake/asset_precompile_fail" 33 | - 16f7834331d6bb3fc5c284130b14eb1ff74d99d5 34 | - - "./repos/rake/asset_precompile_not_found" 35 | - 57fc7f239bf9319e2f77fe580ba68445f6635191 36 | - - "./repos/rake/asset_precompile_pass" 37 | - ce976c727d9f477957c499f39f4cf9603c378103 38 | - - "./repos/rake/bad_rakefile" 39 | - 3cb9c7bf6494c59bd25fa74c2aa4531119e12a46 40 | - - "./repos/rake/no_rakefile" 41 | - d2ec2084b825218418dac54bd6276ac896b31cdd 42 | - - "./repos/ruby/bad_ruby_version" 43 | - 7bf3470265a87ea6361640aa4bfce6ce3b743520 44 | - - "./repos/ruby/empty-procfile" 45 | - 7cae0aae424c2028b81b5d37ee24d42db8e545b9 46 | - - "./repos/ruby/jruby-minimal" 47 | - f79860bc2866449fe065484f1542aaadd3f7cfd2 48 | - - "./repos/ruby/ruby_version_does_not_exist" 49 | - 9b6ad8e3b4fa7850393a5f232bb40ff3cd414d8b 50 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/binstub_wrapper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'delegate' 4 | 5 | # This is a helper class, it wraps a pathname object 6 | # and adds helper methods used to pull out the first line of the file ("the shebang") 7 | # as well as determining if the file is binary or not. 8 | # 9 | # binstub = BinstubWrapper.new(Pathname.new(Dir.pwd).join("bin/rails")) 10 | # binstub.file? # => true 11 | # binstub.binary? #=> false 12 | # binstub.bad_shebang? #=> false 13 | class LanguagePack::Helpers::BinstubWrapper < SimpleDelegator 14 | def initialize(string_or_pathname) 15 | @binstub = Pathname.new(string_or_pathname) 16 | super @binstub 17 | end 18 | 19 | # Returns false if the shebang line has a ruby binary 20 | # that is not simply "ruby" or "ruby.exe" 21 | # 22 | # Example: 23 | # 24 | # bin_dir = Pathname.new(Dir.pwd).join("bin") 25 | # binstub = BinstubWrapper.new(bin_dir.join("rails_good")) 26 | # binstub.shebang # => "#!/usr/bin/env ruby\n" 27 | # binstub.bad_shebang? # => false 28 | # 29 | # binstub = BinstubWrapper.new(bin_dir.join("rails_bad")) 30 | # binstub.shebang # => "#!/usr/bin/env ruby2.5\n" 31 | # binstub.bad_shebang? # => true 32 | def bad_shebang? 33 | return false if binary? 34 | 35 | shebang.match?(/^#!\s*\/usr\/bin\/env\s*ruby(\d.*)$/) # https://rubular.com/r/ozbNEPVInc3sSN 36 | end 37 | 38 | # The first line of a binstub contains the "shebang" line 39 | # that tells the operating system how to execute the file 40 | # for example: 41 | # 42 | # binstub = BinstubWrapper.new(Pathname.new(Dir.pwd).join("bin/rails")) 43 | # binstub.shebang # => "#!/usr/bin/env ruby\n" 44 | def shebang 45 | @shebang ||= begin 46 | @binstub.open(&:readline) 47 | rescue EOFError 48 | String.new("") 49 | end 50 | end 51 | 52 | # Binary files (may) not have valid UTF-8 encoding. In order to 53 | # compare a shebang line, we must first check if the shebang 54 | # line is binary or not. To do that, we can see if it is not valid 55 | # UTF-8 56 | def binary? 57 | !valid_utf8? 58 | end 59 | 60 | def valid_utf8? 61 | shebang.force_encoding("UTF-8").valid_encoding? 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/hatchet/rubies_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe "Ruby versions" do 4 | it "should deploy jdk on heroku-24" do 5 | Hatchet::Runner.new("default_ruby", stack: "heroku-24").tap do |app| 6 | app.before_deploy do |app| 7 | Pathname("Gemfile.lock").write(<<~EOM) 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | rack (3.1.8) 12 | rake (13.2.1) 13 | webrick (1.9.1) 14 | 15 | PLATFORMS 16 | java 17 | 18 | DEPENDENCIES 19 | rack 20 | rake 21 | webrick 22 | 23 | RUBY VERSION 24 | ruby 3.1.4p0 (jruby 9.4.8.0) 25 | 26 | BUNDLED WITH 27 | 2.5.23 28 | EOM 29 | 30 | Pathname("Rakefile").write(<<~'EOM') 31 | task "assets:precompile" do 32 | puts "JRUBY_OPTS is: #{ENV['JRUBY_OPTS']}" 33 | end 34 | EOM 35 | end 36 | 37 | app.deploy do 38 | expect(app.output).to match("JRUBY_OPTS is: -Xcompile.invokedynamic=false") 39 | 40 | app.set_config("JRUBY_BUILD_OPTS" => "--dev") 41 | app.commit! 42 | app.push! 43 | expect(app.output).to match("JRUBY_OPTS is: --dev") 44 | 45 | expect(app.run("ruby -v")).to match("jruby") 46 | end 47 | end 48 | end 49 | end 50 | 51 | describe "Upgrading ruby apps" do 52 | it "works when changing versions" do 53 | version = "3.3.1" 54 | expect(version).to_not eq(LanguagePack::RubyVersion::DEFAULT_VERSION_NUMBER) 55 | app = Hatchet::Runner.new("default_ruby", stack: DEFAULT_STACK) 56 | app.deploy do |app| 57 | # default version 58 | expect(app.run("env | grep MALLOC_ARENA_MAX")).to match("MALLOC_ARENA_MAX=2") 59 | expect(app.run("env | grep DISABLE_SPRING")).to match("DISABLE_SPRING=1") 60 | 61 | # Deploy again 62 | run!(%Q{echo "ruby '#{version}'" >> Gemfile}) 63 | run!("git add -A; git commit -m update-ruby") 64 | app.push! 65 | expect(app.output).to match(version) 66 | expect(app.run("ruby -v")).to match(version) 67 | expect(app.output).to match("Ruby version change detected") 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/binstub_check.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'language_pack/helpers/binstub_wrapper' 4 | # This class is designed to check for binstubs for validity 5 | # 6 | # Example: 7 | # 8 | # check = LanguagePack::Helpers::BinstubCheck.new(app_root_dir: Dir.pwd, warn_object: self) 9 | # check.call 10 | class LanguagePack::Helpers::BinstubCheck 11 | attr_reader :bad_binstubs 12 | 13 | def initialize(app_root_dir:, warn_object: ) 14 | @bin_dir = Pathname.new(app_root_dir).join("bin") 15 | @warn_object = warn_object 16 | @bad_binstubs = [] 17 | end 18 | 19 | # Checks all binstubs in the directory for a 20 | # bad shebang line. If any are present 21 | # a warning is created on the passed in `warn_object` 22 | def call 23 | return unless @bin_dir.directory? 24 | 25 | each_binstub do |binstub| 26 | @bad_binstubs << binstub if binstub.bad_shebang? 27 | end 28 | 29 | warn unless @bad_binstubs.empty? 30 | end 31 | 32 | # Iterates and yields each binstub in a directory 33 | # as a BinstubWrapper 34 | private def each_binstub 35 | @bin_dir.entries.each do |basename| 36 | binstub = LanguagePack::Helpers::BinstubWrapper.new(@bin_dir.join(basename)) 37 | 38 | next unless binstub.file? # Needed since "." and ".." are returned by Pathname#entries 39 | yield binstub 40 | end 41 | end 42 | 43 | private def warn 44 | message = <<~EOM 45 | Improperly formatted binstubs detected in your project 46 | 47 | The following file(s) have appear to contain a problematic "shebang" line 48 | 49 | #{@bad_binstubs.map {|binstub| " - bin/#{binstub.basename}" }.join("\n")} 50 | 51 | For example bin/#{@bad_binstubs.first.basename} has the shebang line: 52 | 53 | ``` 54 | #{@bad_binstubs.first.open(&:readline).strip} 55 | ``` 56 | 57 | It should be: 58 | 59 | ``` 60 | #!/usr/bin/env ruby 61 | ``` 62 | 63 | A malformed shebang line may cause your program to crash. 64 | 65 | For more information about binstubs and "shebang" lines see: 66 | https://devcenter.heroku.com/articles/bad-ruby-binstub-shebang 67 | EOM 68 | 69 | @warn_object.warn(message, inline: true) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /bin/support/download_ruby: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Downloads a bootstrap copy of Ruby for execution of the buildpack 4 | # this is needed so we can totally control the Ruby version and are 5 | # not dependant on the Ruby version of the stack image 6 | 7 | # fail hard 8 | set -o pipefail 9 | # fail harder 10 | set -eu 11 | 12 | BIN_DIR=$1 13 | RUBY_BOOTSTRAP_DIR=$2 14 | 15 | # Stack is set by codon, listed here so shellcheck knows about it 16 | STACK=${STACK:-} 17 | 18 | curl_retry_on_18() { 19 | local ec=18; 20 | local attempts=0; 21 | while (( ec == 18 && attempts++ < 3 )); do 22 | curl "$@" # -C - would return code 33 if unsupported by server 23 | ec=$? 24 | done 25 | return $ec 26 | } 27 | 28 | ruby_url() { 29 | local stack=$1 30 | local version=$2 31 | 32 | if [ "$stack" == "heroku-24" ]; then 33 | local arch 34 | arch=$(dpkg --print-architecture) 35 | echo "${BUILDPACK_VENDOR_URL:-https://heroku-buildpack-ruby.s3.us-east-1.amazonaws.com}/$stack/$arch/ruby-$version.tgz" 36 | else 37 | echo "${BUILDPACK_VENDOR_URL:-https://heroku-buildpack-ruby.s3.us-east-1.amazonaws.com}/$stack/ruby-$version.tgz" 38 | fi 39 | } 40 | 41 | # Pull ruby version out of buildpack.toml to be used with bootstrapping 42 | regex=".*ruby_version = [\'\"]([0-9]+\.[0-9]+\.[0-9]+)[\'\"].*" 43 | if [[ $(cat "$BIN_DIR/../buildpack.toml") =~ $regex ]] 44 | then 45 | heroku_buildpack_ruby_url=$(ruby_url "$STACK" "${BASH_REMATCH[1]}") 46 | else 47 | heroku_buildpack_ruby_url="" 48 | echo "Could not detect ruby version to bootstrap" 49 | exit 1 50 | fi 51 | 52 | mkdir -p "$RUBY_BOOTSTRAP_DIR" 53 | 54 | curl_retry_on_18 --fail --show-error --retry 3 --retry-connrefused --connect-timeout "${CURL_CONNECT_TIMEOUT:-3}" --silent --location -o "$RUBY_BOOTSTRAP_DIR/ruby.tgz" "$heroku_buildpack_ruby_url" || { 55 | cat<\S+) \((?[a-zA-Z0-9\.]+)(? [a-zA-Z0-9]+)?\)/) do 65 | captures = Regexp.last_match.named_captures 66 | @gems[captures["name"]] = captures["version"] 67 | end 68 | end 69 | 70 | def has_gem?(name) 71 | @gems[name] 72 | end 73 | 74 | def length 75 | @gems.length 76 | end 77 | 78 | def gem_version(name) 79 | if version = @gems[name] 80 | Gem::Version.new(version) 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heroku Buildpack for Ruby 2 | ![ruby](https://cloud.githubusercontent.com/assets/51578/13712725/3c6b3368-e793-11e5-83c1-728440111358.png) 3 | 4 | This is a [Heroku Buildpack](http://devcenter.heroku.com/articles/buildpacks) for Ruby, Rack, and Rails apps. It uses [Bundler](https://bundler.io) for dependency management. 5 | 6 | This buildpack requires 64-bit Linux. 7 | 8 | ## Usage 9 | 10 | ### Ruby 11 | 12 | Example Usage: 13 | 14 | $ ls 15 | Gemfile Gemfile.lock 16 | 17 | $ heroku create --buildpack heroku/ruby 18 | 19 | $ git push heroku main 20 | ... 21 | -----> Heroku receiving push 22 | -----> Fetching custom buildpack 23 | -----> Ruby app detected 24 | -----> Installing dependencies using Bundler version 1.1.rc 25 | Running: bundle install --without development:test --path vendor/bundle --deployment 26 | Fetching gem metadata from http://rubygems.org/.. 27 | Installing rack (1.3.5) 28 | Using bundler (1.1.rc) 29 | Your bundle is complete! It was installed into ./vendor/bundle 30 | Cleaning up the bundler cache. 31 | -----> Discovering process types 32 | Procfile declares types -> (none) 33 | Default types for Ruby -> console, rake 34 | 35 | The buildpack will detect your app as Ruby if it has a `Gemfile` and `Gemfile.lock` files in the root directory. It will then proceed to run `bundle install` after setting up the appropriate environment for [ruby](http://ruby-lang.org) and [Bundler](https://bundler.io). 36 | 37 | ## Documentation 38 | 39 | For more information about using Ruby and buildpacks on Heroku, see these Dev Center articles: 40 | 41 | - [Heroku Ruby Support](https://devcenter.heroku.com/articles/ruby-support) 42 | - [Getting Started with Ruby on Heroku](https://devcenter.heroku.com/articles/getting-started-with-ruby) 43 | - [Getting Started with Rails 7 on Heroku](https://devcenter.heroku.com/articles/getting-started-with-rails7) 44 | - [Buildpacks](https://devcenter.heroku.com/articles/buildpacks) 45 | - [Buildpack API](https://devcenter.heroku.com/articles/buildpack-api) 46 | 47 | ## Hacking 48 | 49 | To use this buildpack, fork it on Github. Push up changes to your fork, then create a test app with `--buildpack ` and push to it. 50 | 51 | ### Testing 52 | 53 | ```sh 54 | $ bundle exec hatchet install 55 | ``` 56 | 57 | ```sh 58 | $ bundle exec rake spec 59 | ``` 60 | -------------------------------------------------------------------------------- /spec/hatchet/ci_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "CI" do 4 | it "Does not cause the double ruby rainbow bug" do 5 | Hatchet::Runner.new("heroku-ci-json-example").run_ci do |test_run| 6 | expect(test_run.status).to eq(:succeeded) 7 | 8 | install_bundler_count = test_run.output.scan("Installing bundler").count 9 | expect(install_bundler_count).to eq(1), "Expected output to only install bundler once but was found #{install_bundler_count} times. output:\n#{test_run.output}" 10 | end 11 | end 12 | 13 | it "Works with Rails: ruby schema apps" do 14 | Hatchet::Runner.new("rails_8_ruby_schema", stack: "heroku-24").tap do |app| 15 | app.before_deploy do 16 | Pathname("app.json").write(<<~EOF) 17 | { 18 | "environments": { 19 | "test": { 20 | "addons":[ 21 | "heroku-postgresql:in-dyno" 22 | ] 23 | } 24 | } 25 | } 26 | EOF 27 | end 28 | 29 | app.run_ci do |test_run| 30 | expect(test_run.output).to match("db:schema:load completed") 31 | end 32 | end 33 | end 34 | 35 | it "Works with Rails: SQL schema apps" do 36 | Hatchet::Runner.new("rails_8_sql_schema", stack: "heroku-24").tap do |app| 37 | app.before_deploy do 38 | Pathname("app.json").write(<<~EOF) 39 | { 40 | "environments": { 41 | "test": { 42 | "addons":[ 43 | "heroku-postgresql:in-dyno" 44 | ] 45 | } 46 | } 47 | } 48 | EOF 49 | end 50 | 51 | app.run_ci do |test_run| 52 | expect(test_run.output).to match("db:schema:load completed") 53 | end 54 | end 55 | end 56 | 57 | it "Works with a vanilla ruby app" do 58 | Hatchet::Runner.new("ruby_no_rails_test").run_ci do |test_run| 59 | # Test no whitespace in front of output 60 | expect(test_run.output).to_not match(/^ +Finished in/) 61 | expect(test_run.output).to match(/^Finished in/) 62 | end 63 | end 64 | 65 | it "Uses the cache" do 66 | runner = Hatchet::Runner.new("ruby_no_rails_test") 67 | runner.run_ci do |test_run| 68 | fetching_rake = "Fetching rake" 69 | expect(test_run.output).to match(fetching_rake) 70 | 71 | test_run.run_again 72 | 73 | expect(test_run.output).to_not match(fetching_rake) 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/download_presence.rb: -------------------------------------------------------------------------------- 1 | # This class is used to check whether a binary exists on one or more stacks. 2 | # The main motivation for adding this logic is to help people who are upgrading 3 | # to a new stack if it does not have a given Ruby version. For example if someone 4 | # is using Ruby 1.9.3 on the cedar-14 stack then they should be informed that it 5 | # does not exist if they try to use it on the Heroku-18 stack. 6 | # 7 | # Example 8 | # 9 | # download = LanguagePack::Helpers::DownloadPresence.new( 10 | # 'ruby-3.1.7.tgz', 11 | # stacks: ['heroku-22', 'heroku-24'] 12 | # ) 13 | # 14 | # download.call 15 | # 16 | # puts download.exists? #=> true 17 | # puts download.valid_stack_list #=> ['heroku-22', 'heroku-24'] 18 | class LanguagePack::Helpers::DownloadPresence 19 | # heroku-22 and heroku-24 have identical ruby versions supported 20 | STACKS = ['heroku-22', 'heroku-24'] 21 | 22 | def initialize(file_name:, arch: , multi_arch_stacks:, stacks: STACKS ) 23 | @file_name = file_name 24 | @stacks = stacks 25 | @fetchers = [] 26 | @threads = [] 27 | @stacks.each do |stack| 28 | if multi_arch_stacks.include?(stack) 29 | @fetchers << LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL, stack: stack, arch: arch) 30 | else 31 | @fetchers << LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL, stack: stack) 32 | end 33 | end 34 | end 35 | 36 | def supported_stack?(current_stack: ) 37 | @stacks.include?(current_stack) 38 | end 39 | 40 | def next_stack(current_stack: ) 41 | return unless supported_stack?(current_stack: current_stack) 42 | 43 | next_index = @stacks.index(current_stack) + 1 44 | @stacks[next_index] 45 | end 46 | 47 | def exists_on_next_stack?(current_stack: ) 48 | return false unless supported_stack?(current_stack: current_stack) 49 | 50 | next_index = @stacks.index(current_stack) + 1 51 | @threads[next_index].value 52 | end 53 | 54 | def valid_stack_list 55 | raise "not invoked yet, use the `call` method first" if @threads.empty? 56 | 57 | @threads.map.with_index do |thread, i| 58 | @stacks[i] if thread.value 59 | end.compact 60 | end 61 | 62 | def exists? 63 | raise "not invoked yet, use the `call` method first" if @threads.empty? 64 | 65 | @threads.any? {|t| t.value } 66 | end 67 | 68 | def does_not_exist? 69 | !exists? 70 | end 71 | 72 | def call 73 | @fetchers.map do |fetcher| 74 | @threads << Thread.new do 75 | fetcher.exists?(@file_name, 3) 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/language_pack/installers/heroku_ruby_installer.rb: -------------------------------------------------------------------------------- 1 | require 'language_pack/base' 2 | require 'language_pack/shell_helpers' 3 | 4 | module LanguagePack::Installers; end 5 | 6 | class LanguagePack::Installers::HerokuRubyInstaller 7 | BASE_URL = LanguagePack::Base::VENDOR_URL 8 | BIN_DIR = Pathname("bin") 9 | 10 | include LanguagePack::ShellHelpers 11 | attr_reader :fetcher 12 | 13 | def initialize(stack: , multi_arch_stacks: , arch: , report: HerokuBuildReport::GLOBAL) 14 | @report = report 15 | if multi_arch_stacks.include?(stack) 16 | @fetcher = LanguagePack::Fetcher.new(BASE_URL, stack: stack, arch: arch) 17 | else 18 | @fetcher = LanguagePack::Fetcher.new(BASE_URL, stack: stack) 19 | end 20 | end 21 | 22 | def install(ruby_version, install_dir) 23 | @report.capture( 24 | "ruby.version" => ruby_version.ruby_version, 25 | "ruby.engine" => ruby_version.engine, 26 | "ruby.engine.version" => ruby_version.engine_version, 27 | "ruby.major" => ruby_version.major, 28 | "ruby.minor" => ruby_version.minor, 29 | "ruby.patch" => ruby_version.patch, 30 | "ruby.default" => ruby_version.default?, 31 | ) 32 | fetch_unpack(ruby_version, install_dir) 33 | setup_binstubs(install_dir) 34 | end 35 | 36 | def fetch_unpack(ruby_version, install_dir) 37 | FileUtils.mkdir_p(install_dir) 38 | Dir.chdir(install_dir) do 39 | @fetcher.fetch_untar("#{ruby_version.version_for_download}.tgz") 40 | end 41 | end 42 | 43 | private def setup_binstubs(install_dir) 44 | BIN_DIR.mkpath 45 | run("ln -s ruby #{install_dir}/bin/ruby.exe") 46 | 47 | install_pathname = Pathname.new(install_dir) 48 | Dir["#{install_dir}/bin/*"].each do |vendor_bin| 49 | # for Ruby 2.6.0+ don't symlink the Bundler bin so our shim works 50 | next if vendor_bin.include?("bundle") 51 | 52 | # The bin/rake binstub generated when compiling ruby does not load bundler 53 | # which can cause unexpected failures. Deleting this binstub allows two things: 54 | # 55 | # - If the app includes a custom binstub allows it to be used 56 | # - If the app does not include a custom binstub, then it will fall back to vendor/bundle/bin/rake 57 | # which is generated by bundler 58 | # 59 | # Discussion: https://github.com/heroku/heroku-buildpack-ruby/issues/1025#issuecomment-653102430 60 | next if vendor_bin.include?("rake") 61 | 62 | if install_pathname.absolute? 63 | run("ln -s #{vendor_bin} #{BIN_DIR}") 64 | else 65 | run("ln -s ../#{vendor_bin} #{BIN_DIR}") 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/language_pack/rails2.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | require "language_pack" 3 | require "language_pack/rack" 4 | 5 | # Rails 2 Language Pack. This is for any Rails 2.x apps. 6 | class LanguagePack::Rails2 < LanguagePack::Ruby 7 | # detects if this is a valid Rails 2 app 8 | # @return [Boolean] true if it's a Rails 2 app 9 | def self.use? 10 | rails_version = bundler.gem_version('rails') 11 | return false unless rails_version 12 | is_rails2 = rails_version >= Gem::Version.new('2.0.0') && 13 | rails_version < Gem::Version.new('3.0.0') 14 | return is_rails2 15 | end 16 | 17 | def initialize(app_path: , cache_path: , gemfile_lock:) 18 | super(app_path: app_path, cache_path: cache_path, gemfile_lock: gemfile_lock) 19 | @rails_runner = LanguagePack::Helpers::RailsRunner.new 20 | end 21 | 22 | def name 23 | "Ruby/Rails" 24 | end 25 | 26 | def default_env_vars 27 | { 28 | "RAILS_ENV" => "production", 29 | "RACK_ENV" => "production" 30 | } 31 | end 32 | 33 | def default_config_vars 34 | config_vars = super 35 | default_env_vars.map do |key, value| 36 | config_vars[key] = env(key) || value 37 | end 38 | config_vars 39 | end 40 | 41 | def default_process_types 42 | web_process = bundler.has_gem?("thin") ? 43 | "bundle exec thin start -e $RAILS_ENV -p ${PORT:-5000}" : 44 | "bundle exec ruby script/server -p ${PORT:-5000}" 45 | 46 | process_types = super 47 | process_types["web"] = web_process 48 | process_types["worker"] = "bundle exec rake jobs:work" if has_jobs_work_task? 49 | process_types["console"] = "bundle exec script/console" 50 | process_types 51 | end 52 | 53 | def compile 54 | install_plugins 55 | super 56 | end 57 | 58 | def best_practice_warnings 59 | if env("RAILS_ENV") != "production" 60 | warn(<<~WARNING) 61 | You are deploying to a non-production environment: #{ env("RAILS_ENV").inspect }. 62 | This is not recommended. 63 | See https://devcenter.heroku.com/articles/deploying-to-a-custom-rails-environment for more information. 64 | WARNING 65 | end 66 | super 67 | end 68 | 69 | private 70 | def has_jobs_work_task? 71 | rake.task("jobs:work").is_defined? 72 | end 73 | 74 | def install_plugins 75 | plugins = ["rails_log_stdout"].reject { |plugin| bundler.has_gem?(plugin) } 76 | topic "Rails plugin injection" 77 | LanguagePack::Helpers::PluginsInstaller.new(plugins).install 78 | end 79 | 80 | # sets up the profile.d script for this buildpack 81 | def setup_profiled(**args) 82 | super(**args) 83 | default_env_vars.each do |key, value| 84 | set_env_default key, value 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/helpers/heroku_ruby_installer_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe LanguagePack::Installers::HerokuRubyInstaller do 4 | def installer(report: HerokuBuildReport::GLOBAL) 5 | LanguagePack::Installers::HerokuRubyInstaller.new( 6 | multi_arch_stacks: ["heroku-24"], 7 | stack: "heroku-24", 8 | arch: "amd64", 9 | report: report 10 | ) 11 | end 12 | 13 | def ruby_version 14 | LanguagePack::RubyVersion.bundle_platform_ruby(bundler_output: "ruby-3.1.7") 15 | end 16 | 17 | describe "#fetch_unpack" do 18 | it "should fetch and unpack mri" do 19 | Dir.mktmpdir do |dir| 20 | Dir.chdir(dir) do 21 | installer.fetch_unpack(ruby_version, dir) 22 | 23 | expect(File).to exist("bin/ruby") 24 | end 25 | end 26 | end 27 | end 28 | 29 | describe "#install" do 30 | it "should install ruby and setup binstubs" do 31 | Dir.mktmpdir do |dir| 32 | Dir.chdir(dir) do 33 | report = HerokuBuildReport.dev_null 34 | installer(report: report).install(ruby_version, "#{dir}/vendor/ruby") 35 | 36 | expect(File.symlink?("#{dir}/bin/ruby")).to be true 37 | expect(File.symlink?("#{dir}/bin/ruby.exe")).to be true 38 | expect(File).to exist("#{dir}/vendor/ruby/bin/ruby") 39 | 40 | expect(report.data["ruby.version"]).to eq("3.1.7") 41 | expect(report.data["ruby.engine"]).to eq(:ruby) 42 | expect(report.data["ruby.engine.version"]).to eq(report.data["ruby.version"]) 43 | expect(report.data["ruby.major"]).to eq(3) 44 | expect(report.data["ruby.minor"]).to eq(1) 45 | expect(report.data["ruby.patch"]).to eq(7) 46 | end 47 | end 48 | end 49 | 50 | it "should report jruby correctly" do 51 | Dir.mktmpdir do |dir| 52 | Dir.chdir(dir) do 53 | report = HerokuBuildReport.dev_null 54 | 55 | LanguagePack::Installers::HerokuRubyInstaller.new( 56 | multi_arch_stacks: ["heroku-24"], 57 | stack: "heroku-24", 58 | arch: "arm64", 59 | report: report 60 | ).install( 61 | LanguagePack::RubyVersion.bundle_platform_ruby(bundler_output: "ruby-3.1.4-p0-jruby-9.4.9.0"), 62 | "#{dir}/vendor/ruby" 63 | ) 64 | 65 | expect(report.data["ruby.version"]).to eq("3.1.4") 66 | expect(report.data["ruby.engine"]).to eq(:jruby) 67 | expect(report.data["ruby.engine.version"]).to eq("9.4.9.0") 68 | expect(report.data["ruby.major"]).to eq(3) 69 | expect(report.data["ruby.minor"]).to eq(1) 70 | expect(report.data["ruby.patch"]).to eq(4) 71 | end 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/helpers/shell_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "ShellHelpers" do 4 | module RecordPuts 5 | attr_reader :puts_calls, :print_calls 6 | def puts(*args) 7 | @puts_calls ||= [] 8 | @puts_calls << args 9 | end 10 | 11 | def print(*args) 12 | @print_calls ||= [] 13 | @print_calls << args 14 | end 15 | end 16 | 17 | class FakeShell 18 | include RecordPuts 19 | include LanguagePack::ShellHelpers 20 | end 21 | 22 | describe "pipe" do 23 | it "does not double append newlines" do 24 | sh = FakeShell.new 25 | sh.pipe('bundle install') 26 | first_line = sh.print_calls.first.first 27 | expect(first_line.end_with?("\n\n")).to be(false) 28 | end 29 | end 30 | 31 | describe "#command_options_to_string" do 32 | it "formats ugly keys correctly" do 33 | env = {%Q{ un"matched } => "bad key"} 34 | result = FakeShell.new.command_options_to_string("bundle install", env: env) 35 | expected = %r{env \\ un\\\"matched\\ =bad\\ key bash -c bundle\\ install 2>&1} 36 | expect(result.strip).to match(expected) 37 | end 38 | 39 | it "formats ugly values correctly" do 40 | env = {"BAD VALUE" => %Q{ )(*&^%$#'$'\n''@!~\'\ }} 41 | result = FakeShell.new.command_options_to_string("bundle install", env: env) 42 | expected = %r{env BAD\\ VALUE=\\ \\\)\\\(\\\*\\&\\\^\\%\\\$\\#\\'\\\$\\''\n'\\'\\'@\\!\\~\\'\\ bash -c bundle\\ install 2>&1} 43 | expect(result.strip).to match(expected) 44 | end 45 | end 46 | 47 | describe "#run!" do 48 | it "retries failed commands when passed max_attempts: > 1" do 49 | sh = FakeShell.new 50 | expect { sh.run!("false", max_attempts: 3) }.to raise_error(StandardError) 51 | 52 | expect(sh.print_calls).to eq([ 53 | [" Command: 'false' failed on attempt 1 of 3.\n"], 54 | [" Command: 'false' failed on attempt 2 of 3.\n"], 55 | ]) 56 | end 57 | end 58 | 59 | describe "#puts" do 60 | context 'when the message has an invalid utf-8 character' do 61 | it 'no error is raised by puts directly' do 62 | sh = FakeShell.new 63 | 64 | bad_lines = File.read("spec/fixtures/invalid_encoding.log") 65 | sh.puts(bad_lines) 66 | end 67 | 68 | it 'from an internal call, it catches and annotates it' do 69 | sh = FakeShell.new 70 | 71 | def sh.print(string) 72 | # Strip emits a UTF-8 error 73 | string.strip 74 | end 75 | 76 | bad_lines = File.read("spec/fixtures/invalid_encoding.log") 77 | expect { sh.puts(bad_lines) }.to raise_error do |error| 78 | expect(error.message).to include("Invalid string:") 79 | end 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/language_pack/test/rails2.rb: -------------------------------------------------------------------------------- 1 | # Opens up the class of the Rails2 language pack and 2 | # overwrites methods defined in `language_pack/test/ruby.rb` 3 | class LanguagePack::Rails2 4 | # sets up the profile.d script for this buildpack 5 | def setup_profiled(ruby_layer_path: , gem_layer_path: ) 6 | super 7 | set_env_default "RACK_ENV", "test" 8 | set_env_default "RAILS_ENV", "test" 9 | end 10 | 11 | def default_env_vars 12 | { 13 | "RAILS_ENV" => "test", 14 | "RACK_ENV" => "test" 15 | } 16 | end 17 | 18 | def rake_env 19 | super.merge(default_env_vars) 20 | end 21 | 22 | def prepare_tests 23 | # need to clear db:create before db:schema:load_if_ruby gets called by super 24 | topic "Clearing #{db_test_tasks_to_clear.join(" ")} rake tasks" 25 | clear_db_test_tasks 26 | super 27 | end 28 | 29 | def db_test_tasks_to_clear 30 | # db:test:purge is called by everything in the db:test namespace 31 | # db:create is called by :db:schema:load_if_ruby 32 | # db:structure:dump is not needed for tests, but breaks Rails 3.2 db:structure:load on Heroku 33 | ["db:test:purge", "db:create", "db:structure:dump"] 34 | end 35 | 36 | # rails test runner + rspec depend on db:test:purge which drops/creates a db which doesn't work on Heroku's DB plans 37 | def clear_db_test_tasks 38 | FileUtils::mkdir_p 'lib/tasks' 39 | File.open("lib/tasks/heroku_clear_tasks.rake", "w") do |file| 40 | file.puts "# rubocop:disable all" 41 | content = db_test_tasks_to_clear.map do |task_name| 42 | <<~FILE 43 | if Rake::Task.task_defined?('#{task_name}') 44 | Rake::Task['#{task_name}'].clear 45 | task '#{task_name}' do 46 | end 47 | end 48 | FILE 49 | end.join("\n") 50 | file.print content 51 | file.puts "# rubocop:enable all" 52 | end 53 | end 54 | 55 | def db_prepare_test_rake_tasks 56 | schema_load = rake.task("db:schema:load_if_ruby") 57 | structure_load = rake.task("db:structure:load_if_sql") 58 | db_migrate = rake.task("db:migrate") 59 | 60 | return [] if db_migrate.not_defined? 61 | 62 | if schema_load.not_defined? && structure_load.not_defined? 63 | result = detect_schema_format 64 | case result.lines.last.strip 65 | when "ruby" 66 | schema_load = rake.task("db:schema:load") 67 | when "sql" # currently not a possible edge case, we think 68 | structure_load = rake.task("db:structure:load") 69 | else 70 | puts "Could not determine schema/structure from `ActiveRecord::Base.schema_format`:\n#{result}" 71 | end 72 | end 73 | 74 | [schema_load, structure_load, db_migrate] 75 | end 76 | 77 | 78 | def detect_schema_format 79 | run("rails runner 'puts ActiveRecord::Base.schema_format'", user_env: true) 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /bin/support/ruby_test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This script will detect which test script or framework an app is likely to be 4 | # using and run the appropriate command. It is expected that `ruby_test-compile` 5 | # will be run before this file, this installs a version of Ruby and any dependencies 6 | # a customer's app needs. 7 | $stdout.sync = true 8 | 9 | $:.unshift File.expand_path("../../../lib", __FILE__) 10 | require "language_pack" 11 | require "language_pack/shell_helpers" 12 | require "language_pack/test" 13 | require "language_pack/ruby" 14 | 15 | include LanguagePack::ShellHelpers 16 | 17 | def execute_test(command) 18 | topic("Running test: #{command}") 19 | execute_command(command) 20 | exit $?.exitstatus 21 | end 22 | 23 | def execute_command(command) 24 | # Normally the `pipe` command will indent output so that it 25 | # matches the build output, however in a test TAP depends on 26 | # having no whitespace before output. To avoid adding whitespace 27 | # for the original Kernel.puts to be used by passing in the 28 | # Kernel object. 29 | pipe(command, :user_env => true, :output_object => Kernel) 30 | end 31 | 32 | def user_env_hash 33 | LanguagePack::ShellHelpers.user_env_hash 34 | end 35 | 36 | # $ bin/test BUILD_DIR ENV_DIR ARTIFACT_DIR 37 | build_dir, env_dir, _ = ARGV 38 | LanguagePack::ShellHelpers.initialize_env(env_dir) 39 | Dir.chdir(build_dir) 40 | 41 | # The `ruby_test-compile` program installs a version of Ruby for the 42 | # user's application. It needs the propper `PATH`, where ever Ruby is installed 43 | # otherwise we end up using the buildpack's version of Ruby 44 | # 45 | # This is needed here because LanguagePack::Ruby.slug_vendor_base shells out to the user's ruby binary 46 | user_env_hash["PATH"] = "#{build_dir}/bin:#{ENV["PATH"]}" 47 | 48 | bundler = LanguagePack::Helpers::BundlerWrapper.new( 49 | gemfile_path: "#{build_dir}/Gemfile", 50 | bundler_path: LanguagePack::Ruby.slug_vendor_base # This was previously installed by bin/support/ruby_test-compile 51 | ) 52 | 53 | # - Add bundler's bin directory to the PATH 54 | # - Always make sure `$HOME/bin` is first on the path 55 | user_env_hash["PATH"] = "#{build_dir}/bin:#{bundler.bundler_path}/bin:#{user_env_hash["PATH"]}" 56 | user_env_hash["GEM_PATH"] = LanguagePack::Ruby.slug_vendor_base 57 | 58 | # - Sets BUNDLE_GEMFILE 59 | # - Loads bundler's internal Gemfile.lock parser so we can use `bundler.has_gem?` 60 | bundler.install 61 | 62 | execute_test( 63 | if bundler.has_gem?("rspec-core") 64 | if File.exist?("bin/rspec") 65 | "bin/rspec" 66 | else 67 | "bundle exec rspec" 68 | end 69 | elsif File.exist?("bin/rails") && bundler.has_gem?("railties") && bundler.gem_version("railties") >= Gem::Version.new("5.x") 70 | "bin/rails test" 71 | elsif File.exist?("bin/rake") 72 | "bin/rake test" 73 | else 74 | "rake test" 75 | end 76 | ) 77 | 78 | -------------------------------------------------------------------------------- /spec/helpers/binstub_check_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper.rb" 2 | 3 | describe LanguagePack::Helpers::BinstubCheck do 4 | def get_ruby_path! 5 | out = `which ruby`.strip 6 | raise "command `which ruby` failed with output: #{out}" unless $?.success? 7 | 8 | return Pathname.new(out) 9 | end 10 | 11 | def get_ruby_bin_dir! 12 | ruby_bin_dir = get_ruby_path!.join("..") 13 | raise "#{ruby_bin_dir} is not a directory" unless File.directory?(ruby_bin_dir) 14 | 15 | return ruby_bin_dir 16 | end 17 | 18 | it "handles empty binstubs" do 19 | Tempfile.create("foo.txt") do |f| 20 | expect { Pathname.new(f).open(&:readline) }.to raise_error(EOFError) 21 | 22 | binstub = LanguagePack::Helpers::BinstubWrapper.new(f.path) 23 | expect(binstub.bad_shebang?).to be_falsey 24 | expect(binstub.binary?).to be_falsey 25 | end 26 | end 27 | 28 | it "can determine if a file is binary or not" do 29 | binstub = LanguagePack::Helpers::BinstubWrapper.new(get_ruby_path!) 30 | 31 | expect(binstub.bad_shebang?).to be_falsey 32 | expect(binstub.binary?).to be_truthy 33 | 34 | Tempfile.create("foo.txt") do |f| 35 | f.write("foo") 36 | f.close 37 | binstub = LanguagePack::Helpers::BinstubWrapper.new(f.path) 38 | 39 | expect(binstub.bad_shebang?).to be_falsey 40 | expect(binstub.binary?).to be_falsey 41 | end 42 | end 43 | 44 | it "doesn't error on empty directories" do 45 | Dir.mktmpdir do |dir| 46 | warn_obj = Object.new 47 | check = LanguagePack::Helpers::BinstubCheck.new(app_root_dir: dir, warn_object: warn_obj) 48 | check.call 49 | end 50 | end 51 | 52 | it "does not raise an error when running against a directory with a binary file in it" do 53 | ruby_bin_dir = get_ruby_bin_dir! 54 | check = LanguagePack::Helpers::BinstubCheck.new(app_root_dir: ruby_bin_dir, warn_object: Object.new) 55 | check.call 56 | end 57 | 58 | it "checks binstubs and finds bad ones" do 59 | Dir.mktmpdir do |dir| 60 | bin_dir = Pathname.new(dir).join("bin") 61 | bin_dir.mkpath 62 | 63 | # Bad binstub 64 | bin_dir.join("bad_binstub_example").write(<<~EOM) 65 | #!/usr/bin/env ruby2.5 66 | 67 | nothing else matters 68 | EOM 69 | 70 | # Good binstub 71 | bin_dir.join("good_binstub_example").write(<<~EOM) 72 | #!/usr/bin/env bash 73 | 74 | nothing else matters 75 | EOM 76 | bin_dir.join("good_binstub_example_two").write("#!/usr/bin/env ruby") 77 | 78 | warn_obj = Object.new 79 | def warn_obj.warn(*args, **kwargs); @msg = args.first; end 80 | def warn_obj.msg; @msg; end 81 | 82 | check = LanguagePack::Helpers::BinstubCheck.new(app_root_dir: dir, warn_object: warn_obj) 83 | check.call 84 | 85 | expect(check.bad_binstubs.count).to eq(1) 86 | expect(warn_obj.msg).to include("bin/bad_binstub_example") 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/language_pack/rails4.rb: -------------------------------------------------------------------------------- 1 | require "language_pack" 2 | require "language_pack/rails3" 3 | 4 | # Rails 4 Language Pack. This is for all Rails 4.x apps. 5 | class LanguagePack::Rails4 < LanguagePack::Rails3 6 | ASSETS_CACHE_LIMIT = 52428800 # bytes 7 | 8 | # detects if this is a Rails 4.x app 9 | # @return [Boolean] true if it's a Rails 4.x app 10 | def self.use? 11 | rails_version = bundler.gem_version('railties') 12 | return false unless rails_version 13 | is_rails4 = rails_version >= Gem::Version.new('4.0.0.beta') && 14 | rails_version < Gem::Version.new('4.1.0.beta1') 15 | return is_rails4 16 | end 17 | 18 | def name 19 | "Ruby/Rails" 20 | end 21 | 22 | def default_process_types 23 | super.merge({ 24 | "web" => "bin/rails server -p ${PORT:-5000} -e $RAILS_ENV", 25 | "console" => "bin/rails console" 26 | }) 27 | end 28 | 29 | def compile 30 | super 31 | end 32 | 33 | private 34 | 35 | def install_plugins 36 | return false if bundler.has_gem?('rails_12factor') 37 | plugins = ["rails_serve_static_assets", "rails_stdout_logging"].reject { |plugin| bundler.has_gem?(plugin) } 38 | return false if plugins.empty? 39 | 40 | warn <<~WARNING 41 | Include 'rails_12factor' gem to enable all platform features 42 | See https://devcenter.heroku.com/articles/rails-integration-gems for more information. 43 | WARNING 44 | # do not install plugins, do not call super 45 | end 46 | 47 | def public_assets_folder 48 | "public/assets" 49 | end 50 | 51 | def default_assets_cache 52 | "tmp/cache/assets" 53 | end 54 | 55 | def cleanup 56 | super 57 | return if assets_compile_enabled? 58 | return unless Dir.exist?(default_assets_cache) 59 | FileUtils.remove_dir(default_assets_cache) 60 | end 61 | 62 | def run_assets_precompile_rake_task 63 | if Dir.glob("public/assets/{.sprockets-manifest-*.json,manifest-*.json}", File::FNM_DOTMATCH).any? 64 | puts "Detected manifest file, assuming assets were compiled locally" 65 | return true 66 | end 67 | 68 | precompile = rake.task("assets:precompile") 69 | return true if precompile.not_defined? 70 | 71 | topic("Preparing app for Rails asset pipeline") 72 | 73 | @cache.load_without_overwrite public_assets_folder 74 | @cache.load default_assets_cache 75 | 76 | precompile.invoke(env: rake_env) 77 | 78 | if precompile.success? 79 | puts "Asset precompilation completed (#{"%.2f" % precompile.time}s)" 80 | 81 | clean_task = rake.task("assets:clean") 82 | if clean_task.task_defined? 83 | puts "Cleaning assets" 84 | clean_task.invoke(env: rake_env) 85 | 86 | cleanup_assets_cache 87 | @cache.store public_assets_folder 88 | @cache.store default_assets_cache 89 | end 90 | else 91 | precompile_fail(precompile.output) 92 | end 93 | end 94 | 95 | def cleanup_assets_cache 96 | LanguagePack::Helpers::StaleFileCleaner.new(default_assets_cache).clean_over(ASSETS_CACHE_LIMIT) 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/core' 2 | require 'hatchet' 3 | require 'fileutils' 4 | require 'stringio' 5 | require 'hatchet' 6 | require 'rspec/retry' 7 | require 'language_pack' 8 | require 'language_pack/shell_helpers' 9 | 10 | ENV["HATCHET_BUILDPACK_BASE"] ||= "https://github.com/heroku/heroku-buildpack-ruby" 11 | 12 | ENV['RACK_ENV'] = 'test' 13 | 14 | DEFAULT_STACK = 'heroku-24' 15 | 16 | 17 | def hatchet_path(path = "") 18 | Pathname(__FILE__).join("../../repos").expand_path.join(path) 19 | end 20 | 21 | RSpec.configure do |config| 22 | config.alias_example_to :fit, focused: true 23 | config.full_backtrace = true 24 | config.verbose_retry = true # show retry status in spec process 25 | config.example_status_persistence_file_path = 'spec/examples.txt' 26 | 27 | config.expect_with :rspec do |c| 28 | c.max_formatted_output_length = Float::INFINITY 29 | c.syntax = :expect 30 | end 31 | config.mock_with :nothing 32 | config.include LanguagePack::ShellHelpers 33 | end 34 | 35 | def successful_body(app, options = {}) 36 | retry_limit = options[:retry_limit] || 50 37 | url = "http://#{app.name}.herokuapp.com" 38 | Excon.get(url, :idempotent => true, :expects => 200, :retry_limit => retry_limit).body 39 | end 40 | 41 | def create_file_with_size_in(size, dir) 42 | name = File.join(dir, SecureRandom.hex(16)) 43 | File.open(name, 'w') {|f| f.print([ 1 ].pack("C") * size) } 44 | Pathname.new name 45 | end 46 | 47 | def buildpack_path 48 | File.expand_path(File.join("../.."), __FILE__) 49 | end 50 | 51 | def fixture_path(path) 52 | Pathname.new(__FILE__).join("../fixtures").expand_path.join(path) 53 | end 54 | 55 | def set_lts_ruby_version 56 | Pathname("Gemfile").write("ruby '3.3.6'", mode: "a") 57 | end 58 | 59 | def set_bundler_version(version: ) 60 | gemfile_lock = Pathname("Gemfile.lock").read 61 | 62 | if version == :default 63 | version = "" 64 | else 65 | version = "BUNDLED WITH\n #{version}" 66 | end 67 | gemfile_lock.gsub!(/^BUNDLED WITH$(\r?\n) (?\d+)\.(?\d+)\.\d+/m, version) 68 | gemfile_lock << "\n#{version}" unless gemfile_lock.match?(/^BUNDLED WITH/) 69 | 70 | Pathname("Gemfile.lock").write(gemfile_lock) 71 | end 72 | 73 | def rails_lts_config 74 | { 'BUNDLE_GEMS__RAILSLTS__COM' => ENV["RAILS_LTS_CREDS"] } 75 | end 76 | 77 | def rails_lts_stack 78 | "heroku-22" 79 | end 80 | 81 | def hatchet_path(path = "") 82 | Pathname.new(__FILE__).join("../../repos").expand_path.join(path) 83 | end 84 | 85 | def dyno_status(app, ps_name = "web") 86 | app 87 | .api_rate_limit.call 88 | .dyno 89 | .list(app.name) 90 | .detect {|x| x["type"] == ps_name } 91 | end 92 | 93 | def wait_for_dyno_boot(app, ps_name = "web", sleep_val = 1) 94 | while ["starting", "restarting"].include?(dyno_status(app, ps_name)["state"]) 95 | sleep sleep_val 96 | end 97 | dyno_status(app, ps_name) 98 | end 99 | 100 | def web_boot_status(app) 101 | wait_for_dyno_boot(app)["state"] 102 | end 103 | 104 | def root_dir 105 | Pathname(__dir__).join("..") 106 | end 107 | -------------------------------------------------------------------------------- /lib/language_pack/cache.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | require "language_pack" 3 | 4 | # Manipulates/handles contents of the cache directory 5 | class LanguagePack::Cache 6 | # @param [String] path to the cache store 7 | def initialize(cache_path) 8 | if cache_path 9 | @cache_base = Pathname.new(cache_path) 10 | else 11 | @cache_base = nil 12 | end 13 | end 14 | 15 | # removes the the specified path from the cache 16 | # @param [String] relative path from the cache_base 17 | def clear(path) 18 | return unless @cache_base 19 | 20 | target = (@cache_base + path) 21 | target.exist? && target.rmtree 22 | end 23 | 24 | # Overwrite cache contents 25 | # When called the cache destination will be cleared and the new contents coppied over 26 | # This method is perferable as LanguagePack::Cache#add can cause accidental cache bloat. 27 | # 28 | # @param [String] path of contents to store. it will be stored using this a relative path from the cache_base. 29 | # @param [String] relative path to store the cache contents, if nil it will assume the from path 30 | def store(from, path = nil) 31 | return unless @cache_base 32 | 33 | path ||= from 34 | clear path 35 | copy from, (@cache_base + path) 36 | end 37 | 38 | # Adds file to cache without clearing the destination 39 | # Use LanguagePack::Cache#store to avoid accidental cache bloat 40 | def add(from, path = nil) 41 | return unless @cache_base 42 | 43 | path ||= from 44 | copy from, (@cache_base + path) 45 | end 46 | 47 | # load cache contents 48 | # @param [String] relative path of the cache contents 49 | # @param [String] path of where to store it locally, if nil, assume same relative path as the cache contents 50 | def load(path, dest = nil) 51 | return unless @cache_base 52 | 53 | dest ||= path 54 | copy (@cache_base + path), dest 55 | end 56 | 57 | def load_without_overwrite(path, dest=nil) 58 | return unless @cache_base 59 | 60 | dest ||= path 61 | 62 | case ENV["STACK"] 63 | when "heroku-22" 64 | copy (@cache_base + path), dest, "-a -n" 65 | else 66 | copy (@cache_base + path), dest, "-a --update=none" 67 | end 68 | end 69 | 70 | # copy cache contents 71 | # @param [String] source directory 72 | # @param [String] destination directory 73 | def copy(from, to, options='-a') 74 | return unless @cache_base 75 | 76 | return false unless File.exist?(from) 77 | FileUtils.mkdir_p File.dirname(to) 78 | command = "cp #{options} #{from}/. #{to}" 79 | system(command) 80 | raise "Command failed `#{command}`" unless $? 81 | end 82 | 83 | # copy contents between to places in the cache 84 | # @param [String] source cache directory 85 | # @param [String] destination directory 86 | def cache_copy(from,to) 87 | return unless @cache_base 88 | 89 | copy(@cache_base + from, @cache_base + to) 90 | end 91 | 92 | # check if the cache content exists 93 | # @param [String] relative path of the cache contents 94 | # @param [Boolean] true if the path exists in the cache and false if otherwise 95 | def exists?(path) 96 | return unless @cache_base 97 | 98 | File.exist?(@cache_base + path) 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/unit/bash_functions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Bash functions" do 4 | it "fails on old stacks" do 5 | out = exec_with_bash_functions(<<~EOM, raise_on_fail: false) 6 | checks::ensure_supported_stack "heroku-20" 7 | EOM 8 | 9 | expect($?.success?).to be_falsey, "Expected command failure but got unexpected success. Output:\n\n#{out}" 10 | expect(out).to include("This buildpack no longer supports the 'heroku-20' stack") 11 | end 12 | 13 | it "knows the latest stacks" do 14 | out = exec_with_bash_functions(<<~EOM) 15 | checks::ensure_supported_stack "heroku-24" 16 | EOM 17 | 18 | expect(out).to be_empty 19 | end 20 | 21 | it "Detects jruby in the Gemfile.lock" do 22 | Dir.mktmpdir do |dir| 23 | dir = Pathname(dir) 24 | dir.join("Gemfile.lock").write <<~EOM 25 | RUBY VERSION 26 | ruby 2.5.7p001 (jruby 9.2.13.0) 27 | EOM 28 | 29 | out = exec_with_bash_functions <<~EOM 30 | which_java() 31 | { 32 | return 1 33 | } 34 | 35 | if detect_needs_java "#{dir}"; then 36 | echo "jruby detected" 37 | else 38 | echo "nope" 39 | fi 40 | EOM 41 | 42 | expect(out).to eq("jruby detected") 43 | 44 | dir.join("Gemfile.lock").write <<~EOM 45 | EOM 46 | 47 | out = exec_with_bash_functions <<~EOM 48 | which_java() 49 | { 50 | return 1 51 | } 52 | 53 | if detect_needs_java "#{dir}"; then 54 | echo "jruby detected" 55 | else 56 | echo "nope" 57 | fi 58 | EOM 59 | 60 | expect(out).to eq("nope") 61 | end 62 | end 63 | 64 | it "Detects java for jruby detection" do 65 | Dir.mktmpdir do |dir| 66 | dir = Pathname(dir) 67 | dir.join("Gemfile.lock").write <<~EOM 68 | RUBY VERSION 69 | ruby 2.5.7p001 (jruby 9.2.13.0) 70 | EOM 71 | 72 | out = exec_with_bash_functions <<~EOM 73 | which_java() 74 | { 75 | return 0 76 | } 77 | 78 | if detect_needs_java "#{dir}"; then 79 | echo "jruby detected" 80 | else 81 | echo "already installed" 82 | fi 83 | EOM 84 | 85 | expect(out).to eq("already installed") 86 | end 87 | end 88 | 89 | 90 | def bash_functions_file 91 | root_dir.join("bin", "support", "bash_functions.sh") 92 | end 93 | 94 | def exec_with_bash_functions(code, stack: "heroku-24", raise_on_fail: true) 95 | contents = <<~EOM 96 | #! /usr/bin/env bash 97 | set -eu 98 | 99 | STACK="#{stack}" 100 | 101 | #{bash_functions_file.read} 102 | 103 | #{code} 104 | EOM 105 | 106 | file = Tempfile.new 107 | file.write(contents) 108 | file.close 109 | FileUtils.chmod("+x", file.path) 110 | 111 | out = nil 112 | success = false 113 | begin 114 | Timeout.timeout(60) do 115 | out = `#{file.path} 2>&1`.strip 116 | success = $?.success? 117 | end 118 | rescue Timeout::Error 119 | out = "Command timed out" 120 | success = false 121 | end 122 | 123 | if raise_on_fail && !success 124 | message = <<~EOM 125 | Contents: 126 | 127 | #{contents.lines.map.with_index { |line, number| " #{number.next} #{line.chomp}"}.join("\n") } 128 | 129 | Expected running script to succeed, but it did not. If this was expected, use `raise_on_fail: false` 130 | 131 | Output: 132 | 133 | #{out} 134 | EOM 135 | 136 | raise message 137 | else 138 | out 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /changelogs/v273/windows_gemfile.md: -------------------------------------------------------------------------------- 1 | ## Native Gemfile.lock support for Windows Ruby users with Bundler 2.2+ 2 | 3 | Ruby applications that use Windows and Bundler 2.2+ will no longer have their `Gemfile.lock` deleted and re-generated on deployment. Instead, they will rely on Bundler 2.2+'s support for multiple platforms to correctly resolve dependencies. We recommend any Windows Ruby user to upgrade their Bundler version in the `Gemfile.lock` to Bundler 2.2+ to take advantage of this behavior: 4 | 5 | > note 6 | > The leading `>` indicates a command prompt and should not be copied. 7 | 8 | ``` 9 | > gem install bundler 10 | > bundle update --bundler 11 | > bundle lock --add-platform ruby 12 | > bundle lock --add-platform x86_64-linux 13 | > bundle install 14 | > git add Gemfile.lock 15 | > git commit -m "Upgrade bundler" 16 | ``` 17 | 18 | This change is reflected in the [Deploying a Ruby Project Generated on Windows](https://devcenter.heroku.com/articles/bundler-windows-gemfile) Dev Center article. More information on the history of the behavior can be found [on this GitHub issue](https://github.com/heroku/heroku-buildpack-ruby/issues/1157). 19 | 20 | 74 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/gemfile_lock.rb: -------------------------------------------------------------------------------- 1 | module LanguagePack 2 | module Helpers 3 | # Centralize logic for extracting information from the `Gemfile.lock` format 4 | # 5 | # - Extracts Ruby version from `RUBY VERSION` 6 | # - Extracts Bundler version from `BUNDLED WITH` 7 | # 8 | # Example: 9 | # 10 | # gemfile_lock = GemfileLock.new(contents: <<~EOF) 11 | # RUBY VERSION 12 | # ruby 3.3.5p100 13 | # BUNDLED WITH 14 | # 2.3.4 15 | # EOF 16 | # 17 | # expect(gemfile_lock.bundler.version).to eq("2.3.4") 18 | # expect(gemfile_lock.ruby.ruby_version).to eq("3.3.5") 19 | class GemfileLock 20 | attr_reader :ruby, :bundler 21 | 22 | def initialize(contents: , report: HerokuBuildReport::GLOBAL) 23 | @ruby = RubyVersionParse.new(contents: contents, report: report) 24 | @bundler = BundlerVersionParse.new(contents: contents, report: report) 25 | end 26 | 27 | # Holds information about the RUBY VERSION of the parsed Gemfile.lock 28 | class RubyVersionParse 29 | # Ruby version from Gemfile.lock i.e. `3.3.8` 30 | # Either 3 numbers or nil 31 | attr_reader :ruby_version, 32 | # Contains pre-release info 33 | # - String: i.e. "rc2" is a prerelease 34 | # - nil: No pre-release (or no version at all) 35 | :pre, 36 | # Either :ruby or :jruby 37 | :engine, 38 | # `engine_version` is the JRuby version or for Ruby, it is the same as `ruby_version` 39 | # i.e. `..` 40 | :engine_version 41 | 42 | def initialize(contents: , report: HerokuBuildReport::GLOBAL) 43 | if match = contents.match(/^RUBY VERSION(\r?\n) ruby (?\d+\.\d+\.\d+)((\-|\.)(?
\S*))?/m)
 44 |             @pre = match[:pre]
 45 |             @empty = false
 46 |             @ruby_version = match[:version]
 47 |           else
 48 |             if contents.match?(/RUBY VERSION/)
 49 |               report.capture("gemfile_lock.ruby_version.failed_parse" => true)
 50 |               if match = contents.match(/(?RUBY VERSION(\r?\n).*)$/)
 51 |                 report.capture("gemfile_lock.ruby_version.failed_contents" => match[:contents])
 52 |               end
 53 |             end
 54 |             @pre = nil
 55 |             @empty = true
 56 |             @ruby_version = nil
 57 |           end
 58 | 
 59 |           if jruby = contents.to_s.match(/^RUBY VERSION(\r?\n)   ruby [^\(]*\(jruby (?(\d+|\.)+)\)/m)
 60 |             @engine = :jruby
 61 |             @engine_version = jruby[:version]
 62 |           else
 63 |             @engine = :ruby
 64 |             @engine_version = ruby_version
 65 |           end
 66 |         end
 67 | 
 68 |         def empty?
 69 |           @empty
 70 |         end
 71 |       end
 72 | 
 73 |       class BundlerVersionParse
 74 |         # Bundler value from `Gemfile.lock` (String or nil) i.e. `2.5.23`
 75 |         attr_reader :version
 76 | 
 77 |         def initialize(contents: , report: HerokuBuildReport::GLOBAL)
 78 |           if match = contents.match(/^BUNDLED WITH(\r?\n)   (?(?\d+)\.(?\d+)\.\d+)/m)
 79 |             @empty = false
 80 |             @version = match[:version]
 81 |           else
 82 |             if contents.match?(/BUNDLED WITH/)
 83 |               report.capture("gemfile_lock.bundler_version.failed_parse" => true)
 84 |               if match = contents.match(/(?BUNDLED WITH(\r?\n).*)$/)
 85 |                 report.capture("gemfile_lock.bundler_version.failed_contents" => match[:contents])
 86 |               end
 87 |             end
 88 |             @empty = true
 89 |             @version = nil
 90 |           end
 91 |         end
 92 | 
 93 |         def empty?
 94 |           @empty
 95 |         end
 96 |       end
 97 |     end
 98 |   end
 99 | end
100 | 


--------------------------------------------------------------------------------
/lib/language_pack/helpers/rake_runner.rb:
--------------------------------------------------------------------------------
  1 | class LanguagePack::Helpers::RakeRunner
  2 |   include LanguagePack::ShellHelpers
  3 | 
  4 |   class CannotLoadRakefileError < StandardError
  5 |   end
  6 | 
  7 |   class RakeTask
  8 |     ALLOWED = [:pass, :fail, :no_load, :not_found]
  9 |     include LanguagePack::ShellHelpers
 10 | 
 11 |     attr_accessor :output, :time, :task, :status, :task_defined, :rakefile_can_load
 12 | 
 13 |     alias :rakefile_can_load? :rakefile_can_load
 14 |     alias :task_defined?      :task_defined
 15 |     alias :is_defined?        :task_defined
 16 | 
 17 |     def initialize(task, options = {})
 18 |       @task            = task
 19 |       @default_options = {user_env: true}.merge(options)
 20 |       @status          = :nil
 21 |       @output          = ""
 22 |     end
 23 | 
 24 |     def not_defined?
 25 |       !is_defined?
 26 |     end
 27 | 
 28 |     def success?
 29 |       status == :pass
 30 |     end
 31 | 
 32 |     def status?
 33 |       @status && @status != :nil
 34 |     end
 35 | 
 36 |     # Is set by RakeTask#invoke to one of the ALLOWED verbs
 37 |     def status
 38 |       raise "Status not set for #{self.inspect}" if @status == :nil
 39 |       raise "Not allowed status: #{@status} for #{self.inspect}" unless ALLOWED.include?(@status)
 40 |       @status
 41 |     end
 42 | 
 43 |     def invoke(options = {})
 44 |       options      = @default_options.merge(options)
 45 |       quiet_option = options.delete(:quiet)
 46 | 
 47 |       puts "Running: rake #{task}" unless quiet_option
 48 |       time = Benchmark.realtime do
 49 |         cmd = "rake #{task}"
 50 | 
 51 |         if quiet_option
 52 |           self.output = run("rake #{task}", options)
 53 |         else
 54 |           self.output = pipe("rake #{task}", options)
 55 |         end
 56 |       end
 57 |       self.time = time
 58 | 
 59 |       if $?.success?
 60 |         self.status = :pass
 61 |       else
 62 |         self.status = :fail
 63 |       end
 64 |       return self
 65 |     end
 66 |   end
 67 | 
 68 |   def initialize
 69 |     if !has_rake_installed?
 70 |       @rake_tasks    = ""
 71 |       @rakefile_can_load = false
 72 |     end
 73 |   end
 74 | 
 75 |   def cannot_load_rakefile?
 76 |     !rakefile_can_load?
 77 |   end
 78 | 
 79 |   def rakefile_can_load?
 80 |     @rakefile_can_load
 81 |   end
 82 | 
 83 |   def load_rake_tasks(options = {})
 84 |     @rake_tasks        ||= RakeTask.new("-P --trace").invoke(options.merge(quiet: true)).output
 85 |     @rakefile_can_load ||= $?.success?
 86 |     @rake_tasks
 87 |   end
 88 | 
 89 |   def load_rake_tasks!(options = {}, raise_on_fail = false)
 90 |     return if !has_rake_installed?
 91 | 
 92 |     out = load_rake_tasks(options)
 93 | 
 94 |     if cannot_load_rakefile?
 95 |       msg =  "Could not detect rake tasks\n"
 96 |       msg << "ensure you can run `$ bundle exec rake -P` against your app\n"
 97 |       msg << "and using the production group of your Gemfile.\n"
 98 |       msg << out
 99 |       raise CannotLoadRakefileError, msg if raise_on_fail
100 |       puts msg
101 |     end
102 | 
103 |     return self
104 |   end
105 | 
106 |   def task_defined?(task)
107 |     return false if cannot_load_rakefile?
108 |     @task_available ||= Hash.new {|hash, key| hash[key] = @rake_tasks.match(/\s#{key}\s/) }
109 |     @task_available[task]
110 |   end
111 | 
112 |   def not_found?(task)
113 |     !task_defined?(task)
114 |   end
115 | 
116 |   def task(rake_task, options = {})
117 |     t = RakeTask.new(rake_task, options)
118 |     t.task_defined      = task_defined?(rake_task)
119 |     t.rakefile_can_load = rakefile_can_load?
120 |     t
121 |   end
122 | 
123 |   def invoke(task, options = {})
124 |     self.task(task, options).invoke
125 |   end
126 | 
127 |   def has_rake_installed?
128 |     @has_rake ||= has_rakefile?
129 |   end
130 | 
131 |   private def has_rakefile?
132 |     %W{ Rakefile rakefile  rakefile.rb Rakefile.rb}.detect {|file| File.exist?(file) }
133 |   end
134 | end
135 | 


--------------------------------------------------------------------------------
/spec/helpers/download_presence_spec.rb:
--------------------------------------------------------------------------------
  1 | require "spec_helper"
  2 | 
  3 | describe LanguagePack::Helpers::DownloadPresence do
  4 |   it "handles multi-arch transitions for files that exist" do
  5 |     download = LanguagePack::Helpers::DownloadPresence.new(
  6 |       multi_arch_stacks: ["heroku-24"],
  7 |       file_name: 'ruby-3.1.4.tgz',
  8 |       stacks: ["heroku-22", "heroku-24"],
  9 |       arch: "amd64"
 10 |     )
 11 | 
 12 |     download.call
 13 | 
 14 |     expect(download.next_stack(current_stack: "heroku-22")).to eq("heroku-24")
 15 |     expect(download.next_stack(current_stack: "heroku-24")).to be_falsey
 16 | 
 17 |     expect(download.exists_on_next_stack?(current_stack:"heroku-22")).to be_truthy
 18 |   end
 19 | 
 20 |   it "handles multi-arch transitions for files that do not exist" do
 21 |     download = LanguagePack::Helpers::DownloadPresence.new(
 22 |       multi_arch_stacks: ["heroku-24"],
 23 |       file_name: 'ruby-3.0.5.tgz',
 24 |       # Heroku 20 is not longer supported however heroku-22 and heroku-24
 25 |       # have identical Ruby versions supported, this test can be updated to
 26 |       # use heroku-24 and heroku-26 (when that stack is released)
 27 |       stacks: ["heroku-20", "heroku-24"],
 28 |       arch: "amd64"
 29 |     )
 30 | 
 31 |     download.call
 32 | 
 33 |     expect(download.next_stack(current_stack: "heroku-20")).to eq("heroku-24")
 34 |     expect(download.next_stack(current_stack: "heroku-24")).to be_falsey
 35 | 
 36 |     expect(download.exists_on_next_stack?(current_stack:"heroku-20")).to be_falsey
 37 |   end
 38 | 
 39 |   it "knows if exists on the next stack" do
 40 |     download = LanguagePack::Helpers::DownloadPresence.new(
 41 |       multi_arch_stacks: ["heroku-24"],
 42 |       file_name: "#{LanguagePack::RubyVersion::DEFAULT_VERSION}.tgz",
 43 |       stacks: ['heroku-22', 'heroku-24'],
 44 |       arch: "amd64"
 45 |     )
 46 | 
 47 |     download.call
 48 | 
 49 |     expect(download.next_stack(current_stack: "heroku-22")).to eq("heroku-24")
 50 |     expect(download.next_stack(current_stack: "heroku-24")).to be_falsey
 51 | 
 52 |     expect(download.exists_on_next_stack?(current_stack:"heroku-22")).to be_truthy
 53 |   end
 54 | 
 55 |   it "detects when a package is present on two stacks but not a third" do
 56 |     download = LanguagePack::Helpers::DownloadPresence.new(
 57 |       multi_arch_stacks: [],
 58 |       file_name: 'ruby-2.3.0.tgz',
 59 |       stacks: ['cedar-14', 'heroku-16', 'heroku-18'],
 60 |       arch: nil
 61 |     )
 62 | 
 63 |     download.call
 64 | 
 65 |     expect(download.exists?).to eq(true)
 66 |     expect(download.valid_stack_list).to eq(['cedar-14', 'heroku-16'])
 67 |   end
 68 | 
 69 |   it "detects when a package does not exist" do
 70 |     download = LanguagePack::Helpers::DownloadPresence.new(
 71 |       multi_arch_stacks: [],
 72 |       file_name: 'does-not-exist.tgz',
 73 |       stacks: ['heroku-22', 'heroku-24'],
 74 |       arch: nil
 75 |     )
 76 | 
 77 |     download.call
 78 | 
 79 |     expect(download.exists?).to eq(false)
 80 |     expect(download.valid_stack_list).to eq([])
 81 |   end
 82 | 
 83 |   it "detects default ruby version" do
 84 |     download = LanguagePack::Helpers::DownloadPresence.new(
 85 |       multi_arch_stacks: ['heroku-24'],
 86 |       file_name: "#{LanguagePack::RubyVersion::DEFAULT_VERSION}.tgz",
 87 |       arch: "amd64"
 88 |     )
 89 | 
 90 |     download.call
 91 | 
 92 |     expect(download.exists?).to eq(true)
 93 |     expect(download.valid_stack_list).to include(LanguagePack::Helpers::DownloadPresence::STACKS.last)
 94 |   end
 95 | 
 96 |   it "handles the current stack not being in the known stacks list" do
 97 |     download = LanguagePack::Helpers::DownloadPresence.new(
 98 |       multi_arch_stacks: [],
 99 |       file_name: "#{LanguagePack::RubyVersion::DEFAULT_VERSION}.tgz",
100 |       arch: nil
101 |     )
102 | 
103 |     download.call
104 | 
105 |     expect(download.supported_stack?(current_stack: "unknown-stack")).to be_falsey
106 |     expect(download.next_stack(current_stack: "unknown-stack")).to be_nil
107 |     expect(download.exists_on_next_stack?(current_stack:"unknown-stack")).to be_falsey
108 |   end
109 | end
110 | 


--------------------------------------------------------------------------------
/lib/rake/deploy_check.rb:
--------------------------------------------------------------------------------
  1 | # A class for checking if a local copy of a repo can be tagged for a deploy
  2 | #
  3 | # Example:
  4 | #
  5 | #   deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby")
  6 | #   deploy.check! # Checks that current main branch matches what's on github
  7 | #
  8 | # It also can pull tag versions and determine the next sequential version automatically:
  9 | #
 10 | #   deploy.next_version # => "v999" # Assuming the last version was v998
 11 | #
 12 | # It can determine status of tags
 13 | #
 14 | #   deploy.push_tags? #=> false
 15 | #
 16 | class DeployCheck
 17 |   attr_reader :github_url
 18 | 
 19 |   def initialize(github: , next_version: ENV["RELEASE_VERSION"])
 20 |     @github = github
 21 |     @github_url = "https://github.com/#{github}"
 22 |     @next_version = next_version
 23 |     @remote_tag_array = nil
 24 |   end
 25 | 
 26 |   def check!
 27 |     check_version!
 28 |     check_unstaged!
 29 |     check_branch!
 30 |     check_changelog!
 31 |     check_sync!
 32 |   end
 33 | 
 34 |   def push_tag?
 35 |     return true if !tag_exists_on_remote?
 36 |     return false if remote_tag_matches?
 37 | 
 38 |     # Tag exists, but is not the same commit, raise an error
 39 |     raise <<~EOM
 40 |       The tag you're pushing (#{next_version}) to #{@github} already exists and does not have the same SHA.
 41 |       You must resolve this manually.
 42 |     EOM
 43 |   end
 44 | 
 45 |   # Returns tuthy value if the remote contains the next version already
 46 |   def tag_exists_on_remote?
 47 |     remote_tag_array.include?(next_version)
 48 |   end
 49 | 
 50 |   # Returns a truthy value if the remote tag SHA matches the current local sha
 51 |   def remote_tag_matches?(remote_sha: remote_commit_sha(next_version), local_sha: local_commit_sha)
 52 |     remote_sha == local_sha
 53 |   end
 54 | 
 55 |   # Raises an error if there are unstaged modifications
 56 |   def check_unstaged!
 57 |     run!("git diff --quiet HEAD") do
 58 |       raise "Must have all changes committed. There are unstaged commits locally"
 59 |     end
 60 |   end
 61 | 
 62 |   # Raises an error if not on the designated branch
 63 |   def check_branch!(name = "main")
 64 |     out = run("git rev-parse --abbrev-ref HEAD")
 65 |     raise "Must be on main branch. Branch: #{out}" unless out == name
 66 |   end
 67 | 
 68 |   # Raises an error if the changelog does not have an entry with the designated version
 69 |   def check_changelog!
 70 |     if !File.read("CHANGELOG.md").include?("## [#{next_version}]")
 71 |       raise "Expected CHANGELOG.md to include [#{next_version}] but it did not"
 72 |     end
 73 |   end
 74 | 
 75 |   # Raises an error if the local sha does not match the remote sha
 76 |   def check_sync!(local_sha: local_commit_sha, remote_sha: remote_commit_sha)
 77 |     return if remote_sha == local_sha
 78 | 
 79 |     raise <<~EOM
 80 |       Must be in-sync with #{@github}. Local commit: #{local_sha.inspect} #{@github}: #{remote_sha.inspect}
 81 |       "Make sure that you've pulled: `git pull --rebase #{@github_url} main`
 82 |     EOM
 83 |   end
 84 | 
 85 |   def check_version!
 86 |     version = next_version
 87 | 
 88 |     raise "Must look like a version: #{version}. Must start with `v` and include only digits" unless version.match?(/^v\d+$/)
 89 |   end
 90 | 
 91 |   def local_commit_sha
 92 |     run!("git rev-parse HEAD")
 93 |   end
 94 | 
 95 |   def remote_commit_sha(branch_or_tag = "main")
 96 |     run!("git ls-remote #{@github_url} #{branch_or_tag}").split("\t").first
 97 |   end
 98 | 
 99 |   def run(cmd)
100 |     `#{cmd}`.strip
101 |   end
102 | 
103 |   def next_version
104 |     @next_version || "v#{next_version_number}"
105 |   end
106 | 
107 |   def remote_tag_array
108 |     @remote_tag_array ||= begin
109 |       cmd = String.new("")
110 |       cmd << "git ls-remote --tags #{@github_url}"
111 |       cmd << "| awk '{print $2}' | cut -d '/' -f 3 | cut -d '^' -f 1"
112 |       run!(cmd).each_line.map(&:strip).select {|line| line.strip.match?(/^v\d+$/) } # https://rubular.com/r/8eFB9r8nOVrM7H
113 |     end
114 |   end
115 | 
116 |   private def next_version_number
117 |    integer_tag_array = remote_tag_array.map {|line| line.sub(/^v/, '').to_i }.sort # Ascending order
118 |    integer_tag_array.last.next
119 |   end
120 | 
121 |   def run!(cmd)
122 |     out = run(cmd)
123 |     return out if $?.success?
124 | 
125 |     if block_given?
126 |       yield out, $?
127 |     else
128 |       raise "Command #{cmd} expected to return successfully did not: #{out.inspect}"
129 |     end
130 |   end
131 | end
132 | 


--------------------------------------------------------------------------------
/lib/language_pack/helpers/rails_runner.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | # This class is used for running `rails runner` against
  4 | # apps, primarially for the intention of detecting configuration.
  5 | #
  6 | # The main benefit of this class is that multiple config
  7 | # queries can be grouped together so the application only
  8 | # has to be booted once. Calling `did_match` on a
  9 | # RailsConfig object will trigger the `rails runner` command
 10 | # to be executed.
 11 | #
 12 | # Example usage:
 13 | #
 14 | #    rails_config   = RailsRunner.new
 15 | #    local_storage  = rails_config.detect("active_storage.service")
 16 | #    assets_compile = rails_config.detect("assets.compile")
 17 | #
 18 | #    local_storage.success?             # => true
 19 | #    local_storage.did_match?("local")  # => false
 20 | #
 21 | #    assets_compile.success?            # => true
 22 | #    assets_compile.did_match?("false") # => true
 23 | #
 24 | class LanguagePack::Helpers::RailsRunner
 25 |   # This class is used to help pull configuration values
 26 |   # from a rails application. It takes in a configuration key
 27 |   # and a reference to the parent RailsRunner object which
 28 |   # allows it obtain the `rails runner` output and success
 29 |   # status of the operation.
 30 |   #
 31 |   # For example:
 32 |   #
 33 |   #    config = RailsConfig.new("active_storage.service", rails_runner)
 34 |   #    config.to_command # => "puts %Q{heroku.detecting.config.for.active_storage.service=Rails.application.config.try(:active_storage).try(:service)}; "
 35 |   #
 36 |   class RailsConfig
 37 |     def initialize(config, rails_runner, options={})
 38 |       @config       = config
 39 |       @rails_runner = rails_runner
 40 |       @debug        = options[:debug]
 41 | 
 42 |       @success      = nil
 43 |       @did_time_out = false
 44 |       @heroku_key   = "heroku.detecting.config.for.#{config}"
 45 | 
 46 |       @rails_config = String.new('#{')
 47 |       @rails_config << 'Rails.application.config'
 48 |       config.split('.').each do |part|
 49 |         @rails_config << ".try(:#{part})"
 50 |       end
 51 |       @rails_config << '}'
 52 |     end
 53 | 
 54 |     def success?
 55 |       @rails_runner.success? && @rails_runner.output =~ %r(#{@heroku_key})
 56 |     end
 57 | 
 58 |     def did_match?(val)
 59 |       @rails_runner.output =~ %r(#{@heroku_key}=#{val})
 60 |     end
 61 | 
 62 |     def to_command
 63 |       cmd = String.new('begin; ')
 64 |       cmd << 'puts %Q{'
 65 |       cmd << "#{@heroku_key}=#{@rails_config}"
 66 |       cmd << '}; '
 67 |       cmd << 'rescue => e; '
 68 |       cmd << 'puts e; puts e.backtrace; ' if @debug
 69 |       cmd << 'end;'
 70 |       cmd
 71 |     end
 72 |   end
 73 | 
 74 |   include LanguagePack::ShellHelpers
 75 | 
 76 |   def initialize(debug = env('HEROKU_DEBUG_RAILS_RUNNER'), timeout = 65)
 77 |     @command_array = []
 78 |     @output        = nil
 79 |     @success       = false
 80 |     @debug         = debug
 81 |     @timeout_val   = timeout # seconds
 82 |   end
 83 | 
 84 |   def detect(config_string)
 85 |     config = RailsConfig.new(config_string, self, debug: @debug)
 86 |     @command_array << config.to_command
 87 |     config
 88 |   end
 89 | 
 90 |   def output
 91 |     @output ||= call
 92 |   end
 93 | 
 94 |   def success?
 95 |     output && @success
 96 |   end
 97 | 
 98 |   def command
 99 |     %Q{rails runner "#{@command_array.join(' ')}"}
100 |   end
101 | 
102 |   def timeout?
103 |     @did_time_out
104 |   end
105 | 
106 |   private
107 |     def call
108 |       topic("Detecting rails configuration")
109 |       puts "$ #{command}" if @debug
110 |       out = execute_command!
111 |       puts out if @debug
112 |       out
113 |     end
114 | 
115 |     def execute_command!
116 |       process = ProcessSpawn.new(command,
117 |         user_env: true,
118 |         timeout:  @timeout_val,
119 |         file:     "./.heroku/ruby/config_detect/rails.txt"
120 |       )
121 | 
122 |       @success      = process.success?
123 |       @did_time_out = process.timeout?
124 |       out           = process.output
125 | 
126 |       if timeout?
127 |         message = String.new("Detecting rails configuration timeout\n")
128 |         message << "set HEROKU_DEBUG_RAILS_RUNNER=1 to debug" unless @debug
129 |         warn(message)
130 |       elsif !@success
131 |         message = String.new("Detecting rails configuration failed\n")
132 |         message << "set HEROKU_DEBUG_RAILS_RUNNER=1 to debug" unless @debug
133 |         warn(message)
134 |       end
135 | 
136 |       return out
137 |     end
138 | end
139 | 
140 | 


--------------------------------------------------------------------------------
/bin/support/bash_functions.sh:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env bash
  2 | 
  3 | curl_retry_on_18() {
  4 |   local ec=18;
  5 |   local attempts=0;
  6 |   while (( ec == 18 && attempts++ < 3 )); do
  7 |     curl "$@" # -C - would return code 33 if unsupported by server
  8 |     ec=$?
  9 |   done
 10 |   return $ec
 11 | }
 12 | 
 13 | which_java()
 14 | {
 15 |   which java > /dev/null
 16 | }
 17 | 
 18 | # Detects if a given Gemfile.lock has jruby in it
 19 | # $ cat Gemfile.lock | grep jruby # => ruby 2.5.7p001 (jruby 9.2.13.0)
 20 | detect_needs_java()
 21 | {
 22 |   local app_dir=$1
 23 |   local gemfile_lock="$app_dir/Gemfile.lock"
 24 |   # local needs_jruby=0
 25 |   local skip_java_install=1
 26 | 
 27 |   if which_java; then
 28 |     return $skip_java_install
 29 |   fi
 30 | 
 31 |   grep "(jruby " "$gemfile_lock" --quiet &> /dev/null
 32 | }
 33 | 
 34 | # Runs another buildpack against the build dir
 35 | #
 36 | # Example:
 37 | #
 38 | #   compile_buildpack_v2 "$build_dir" "$cache_dir" "$env_dir" "https://buildpack-registry.s3.us-east-1.amazonaws.com/buildpacks/heroku/nodejs.tgz" "heroku/nodejs"
 39 | #
 40 | compile_buildpack_v2()
 41 | {
 42 |   local build_dir=$1
 43 |   local cache_dir=$2
 44 |   local env_dir=$3
 45 |   local buildpack=$4
 46 |   local name=$5
 47 | 
 48 |   local dir
 49 |   local url
 50 |   local branch
 51 | 
 52 |   dir=$(mktemp -t buildpackXXXXX)
 53 |   rm -rf "$dir"
 54 | 
 55 |   url=${buildpack%#*}
 56 |   branch=${buildpack#*#}
 57 | 
 58 |   if [ "$branch" == "$url" ]; then
 59 |     branch=""
 60 |   fi
 61 | 
 62 |   if [ "$url" != "" ]; then
 63 |     echo "-----> Downloading Buildpack: ${name}"
 64 | 
 65 |     if [[ "$url" =~ \.tgz$ ]] || [[ "$url" =~ \.tgz\? ]]; then
 66 |       mkdir -p "$dir"
 67 |       curl_retry_on_18 -s --fail --retry 3 --retry-connrefused --connect-timeout "${CURL_CONNECT_TIMEOUT:-3}" "$url" | tar xvz -C "$dir" >/dev/null 2>&1
 68 |     else
 69 |       git clone "$url" "$dir" >/dev/null 2>&1
 70 |     fi
 71 |     cd "$dir" || return
 72 | 
 73 |     if [ "$branch" != "" ]; then
 74 |       git checkout "$branch" >/dev/null 2>&1
 75 |     fi
 76 | 
 77 |     # we'll get errors later if these are needed and don't exist
 78 |     chmod -f +x "$dir/bin/{detect,compile,release}" || true
 79 | 
 80 |     framework=$("$dir"/bin/detect "$build_dir")
 81 | 
 82 |     # shellcheck disable=SC2181
 83 |     if [ $? == 0 ]; then
 84 |       echo "-----> Detected Framework: $framework"
 85 |       "$dir"/bin/compile "$build_dir" "$cache_dir" "$env_dir"
 86 | 
 87 |       # shellcheck disable=SC2181
 88 |       if [ $? != 0 ]; then
 89 |         exit 1
 90 |       fi
 91 | 
 92 |       # check if the buildpack left behind an environment for subsequent ones
 93 |       if [ -e "$dir/export" ]; then
 94 |         set +u # http://redsymbol.net/articles/unofficial-bash-strict-mode/#sourcing-nonconforming-document
 95 |         # shellcheck disable=SC1091
 96 |         source "$dir/export"
 97 |         set -u # http://redsymbol.net/articles/unofficial-bash-strict-mode/#sourcing-nonconforming-document
 98 |       fi
 99 | 
100 |       if [ -x "$dir/bin/release" ]; then
101 |         "$dir"/bin/release "$build_dir" > "$1"/last_pack_release.out
102 |       fi
103 |     else
104 |       echo "Couldn't detect any framework for this buildpack. Exiting."
105 |       exit 1
106 |     fi
107 |   fi
108 | }
109 | 
110 | # After a stack is EOL, updates to the buildpack may fail with unexpected or unhelpful errors.
111 | # This can happen when the buildpack is being used off of the platform such as with `dokku`
112 | # which is not officially supported.
113 | function checks::ensure_supported_stack() {
114 | 	local stack="${1}"
115 | 
116 | 	case "${stack}" in
117 | 		# When removing support from a stack, move it to the bottom of the list
118 | 		heroku-22 | heroku-24)
119 | 			return 0
120 | 			;;
121 | 		heroku-18 | heroku-20)
122 | 			# This error will only ever be seen on non-Heroku environments, since the
123 | 			# Heroku build system rejects builds using EOL stacks.
124 | 			cat <<-EOF
125 | 				Error: The '${stack}' stack is no longer supported.
126 | 
127 | 				This buildpack no longer supports the '${stack}' stack since it has
128 | 				reached its end-of-life:
129 | 				https://devcenter.heroku.com/articles/stack#stack-support-details
130 | 
131 | 				Upgrade to a newer stack to continue using this buildpack.
132 | 			EOF
133 | 			exit 1
134 | 			;;
135 | 		*)
136 | 			cat <<-EOF
137 | 				Error: The '${stack}' stack isn't recognised.
138 | 
139 | 				This buildpack doesn't recognise or support the '${stack}' stack.
140 | 
141 | 				If '${stack}' is a valid stack, make sure that you are using the latest
142 | 				version of this buildpack and haven't pinned to an older release:
143 | 				https://devcenter.heroku.com/articles/managing-buildpacks#view-your-buildpacks
144 | 				https://devcenter.heroku.com/articles/managing-buildpacks#classic-buildpacks-references
145 | 			EOF
146 | 			exit 1
147 | 			;;
148 | 	esac
149 | }
150 | 


--------------------------------------------------------------------------------
/lib/language_pack/ruby_version.rb:
--------------------------------------------------------------------------------
  1 | require "language_pack/shell_helpers" # Holds BuildpackError
  2 | 
  3 | module LanguagePack
  4 |   class RubyVersion
  5 |     class BadVersionError < BuildpackError
  6 |       def initialize(output = "")
  7 |         msg = ""
  8 |         msg << output
  9 |         msg << "Can not parse Ruby Version:\n"
 10 |         msg << "Valid versions listed on: https://devcenter.heroku.com/articles/ruby-support\n"
 11 |         super msg
 12 |       end
 13 |     end
 14 | 
 15 |     BOOTSTRAP_VERSION_NUMBER = "3.3.9".freeze
 16 |     DEFAULT_VERSION_NUMBER = "3.3.9".freeze
 17 |     DEFAULT_VERSION        = "ruby-#{DEFAULT_VERSION_NUMBER}".freeze
 18 |     RUBY_VERSION_REGEX     = %r{
 19 |         (?\d+\.\d+\.\d+){0}
 20 |         (?p-?\d+){0}
 21 |         (?\w+){0}
 22 |         (?.+){0}
 23 | 
 24 |         ruby-\g(-\g)?(\.(?
\S*))?(-\g-\g)?
 25 |       }x
 26 | 
 27 |     # String formatted `..` for Ruby and JRuby
 28 |     attr_reader :ruby_version,
 29 |       # `engine` is `:ruby` or `:jruby`
 30 |       :engine,
 31 |       # `engine_version` is the Jruby version or for MRI it is the same as `ruby_version`
 32 |       # i.e. `..`
 33 |       :engine_version
 34 | 
 35 |     def self.bundle_platform_ruby(bundler_output:, last_version: nil)
 36 |       default = bundler_output.empty?
 37 |       if default
 38 |         default(last_version: last_version)
 39 |       elsif md = RUBY_VERSION_REGEX.match(bundler_output)
 40 |         new(
 41 |           pre: md[:pre],
 42 |           engine: md[:engine]&.to_sym || :ruby,
 43 |           default: default,
 44 |           ruby_version: md[:ruby_version],
 45 |           engine_version: md[:engine_version] || md[:ruby_version],
 46 |         )
 47 |       else
 48 |         raise BadVersionError.new("'#{bundler_output}' is not valid") unless md
 49 |       end
 50 |     end
 51 | 
 52 |     def self.from_gemfile_lock(ruby: , last_version: nil)
 53 |       if ruby.empty?
 54 |         default(last_version: last_version)
 55 |       else
 56 |         new(
 57 |           pre: ruby.pre,
 58 |           engine: ruby.engine,
 59 |           default: false,
 60 |           ruby_version: ruby.ruby_version,
 61 |           engine_version: ruby.engine_version,
 62 |         )
 63 |       end
 64 |     end
 65 | 
 66 |     def self.default(last_version: )
 67 |       ruby_version = last_version&.split("-")&.last || DEFAULT_VERSION_NUMBER
 68 |       new(
 69 |         pre: nil,
 70 |         engine: :ruby,
 71 |         default: true,
 72 |         ruby_version: ruby_version,
 73 |         engine_version: ruby_version,
 74 |       )
 75 |     end
 76 | 
 77 |     def initialize(
 78 |         pre:,
 79 |         engine:,
 80 |         default:,
 81 |         ruby_version:,
 82 |         engine_version:
 83 |       )
 84 |         @pre = pre
 85 |         @engine = engine
 86 |         @default = default
 87 |         @ruby_version = ruby_version
 88 |         @engine_version = engine_version
 89 |     end
 90 | 
 91 |     # i.e. `ruby-3.4.2`
 92 |     def version_for_download
 93 |       if @engine == :jruby
 94 |         "ruby-#{ruby_version}-jruby-#{engine_version}"
 95 |       elsif @pre
 96 |         "ruby-#{ruby_version}.#{@pre}"
 97 |       else
 98 |         "ruby-#{ruby_version}"
 99 |       end
100 |     end
101 | 
102 |     def file_name
103 |       "#{version_for_download}.tgz"
104 |     end
105 | 
106 |     def default?
107 |       @default
108 |     end
109 | 
110 |     # determine if we're using jruby
111 |     # @return [Boolean] true if we are and false if we aren't
112 |     def jruby?
113 |       engine == :jruby
114 |     end
115 | 
116 |     # convert to a Gemfile ruby DSL incantation
117 |     # @return [String] the string representation of the Gemfile ruby DSL
118 |     def to_gemfile
119 |       if @engine == :ruby
120 |         "ruby '#{ruby_version}'"
121 |       else
122 |         "ruby '#{ruby_version}', :engine => '#{engine}', :engine_version => '#{engine_version}'"
123 |       end
124 |     end
125 | 
126 |     def major
127 |       @ruby_version.split(".")[0].to_i
128 |     end
129 | 
130 |     def minor
131 |       @ruby_version.split(".")[1].to_i
132 |     end
133 | 
134 |     def patch
135 |       @ruby_version.split(".")[2].to_i
136 |     end
137 | 
138 |     # Returns the next logical version in the minor series
139 |     # for example if the current ruby version is
140 |     # `ruby-2.3.1` then then `next_logical_version(1)`
141 |     # will produce `ruby-2.3.2`.
142 |     def next_logical_version(increment = 1)
143 |       "ruby-#{major}.#{minor}.#{patch + increment}"
144 |     end
145 | 
146 |     def next_minor_version(increment = 1)
147 |       "ruby-#{major}.#{minor + increment}.0"
148 |     end
149 | 
150 |     def next_major_version(increment = 1)
151 |       "ruby-#{major + increment}.0.0"
152 |     end
153 |   end
154 | end
155 | 


--------------------------------------------------------------------------------
/spec/helpers/bundler_wrapper_spec.rb:
--------------------------------------------------------------------------------
  1 | require 'spec_helper'
  2 | 
  3 | describe "Bundle platform conversion" do
  4 |   it "converts `bundle platform --ruby` for prerelease versions" do
  5 |     actual = LanguagePack::Helpers::BundlerWrapper.platform_to_version("ruby 3.3.0.preview2")
  6 |     expect(actual).to eq("ruby-3.3.0.preview2")
  7 |   end
  8 | 
  9 |   it "converts `bundle platform --ruby` for released versions" do
 10 |     actual = LanguagePack::Helpers::BundlerWrapper.platform_to_version("ruby 3.1.4")
 11 |     expect(actual).to eq("ruby-3.1.4")
 12 |   end
 13 | end
 14 | 
 15 | describe "Bundler version detection" do
 16 |   it "supports minor versions" do
 17 |     wrapper_klass = LanguagePack::Helpers::BundlerWrapper
 18 | 
 19 |     version = wrapper_klass.detect_bundler_version(contents: "BUNDLED WITH\n   2.2.7")
 20 |     expect(wrapper_klass::BLESSED_BUNDLER_VERSIONS.key?("2.3")).to be_truthy
 21 |     expect(version).to eq(wrapper_klass::BLESSED_BUNDLER_VERSIONS["2.3"])
 22 | 
 23 |     version = wrapper_klass.detect_bundler_version(contents: "BUNDLED WITH\n   2.3.7")
 24 |     expect(wrapper_klass::BLESSED_BUNDLER_VERSIONS.key?("2.3")).to be_truthy
 25 |     expect(version).to eq(wrapper_klass::BLESSED_BUNDLER_VERSIONS["2.3"])
 26 | 
 27 |     version = wrapper_klass.detect_bundler_version(contents: "BUNDLED WITH\n   2.4.7")
 28 |     expect(wrapper_klass::BLESSED_BUNDLER_VERSIONS.key?("2.4")).to be_truthy
 29 |     expect(version).to eq(wrapper_klass::BLESSED_BUNDLER_VERSIONS["2.4"])
 30 | 
 31 |     version = wrapper_klass.detect_bundler_version(contents: "BUNDLED WITH\n   2.5.7")
 32 |     expect(wrapper_klass::BLESSED_BUNDLER_VERSIONS.key?("2.5")).to be_truthy
 33 |     expect(version).to eq(wrapper_klass::BLESSED_BUNDLER_VERSIONS["2.5"])
 34 | 
 35 |     version = wrapper_klass.detect_bundler_version(contents: "BUNDLED WITH\n   2.6.7")
 36 |     expect(wrapper_klass::BLESSED_BUNDLER_VERSIONS.key?("2.6")).to be_truthy
 37 |     expect(version).to eq(wrapper_klass::BLESSED_BUNDLER_VERSIONS["2.6"])
 38 | 
 39 |     version = wrapper_klass.detect_bundler_version(contents: "BUNDLED WITH\n   2.999.7")
 40 |     expect(wrapper_klass::BLESSED_BUNDLER_VERSIONS.key?("2.6")).to be_truthy
 41 |     expect(version).to eq(wrapper_klass::BLESSED_BUNDLER_VERSIONS["2.6"])
 42 | 
 43 |     expect {
 44 |       wrapper_klass.detect_bundler_version(contents: "BUNDLED WITH\n   3.6.7")
 45 |     }.to raise_error(wrapper_klass::UnsupportedBundlerVersion)
 46 |   end
 47 | end
 48 | 
 49 | describe "Multiple platform detection" do
 50 |   it "reports true on bundler 2.2+" do
 51 |     Dir.mktmpdir do |dir|
 52 |       gemfile = Pathname(dir).join("Gemfile")
 53 |       lockfile = Pathname(dir).join("Gemfile.lock").tap {|p| p.write("BUNDLED WITH\n   2.5.7") }
 54 |       report = HerokuBuildReport.dev_null
 55 | 
 56 |       bundler = LanguagePack::Helpers::BundlerWrapper.new(
 57 |         gemfile_path: gemfile,
 58 |         report: report
 59 |       )
 60 |       expect(report.data).to eq(
 61 |         {
 62 |           "ruby.dot_ruby_version" => nil,
 63 |           "bundler.bundled_with" => "2.5.7",
 64 |           "bundler.major" => "2",
 65 |           "bundler.minor" => "5",
 66 |           "bundler.patch" => "23",
 67 |           "bundler.version_installed" => "2.5.23",
 68 |         }
 69 |       )
 70 |     end
 71 |   end
 72 | end
 73 | 
 74 | describe "BundlerWrapper mutates rubyopt" do
 75 |   before(:each) do
 76 |     if ENV['RUBYOPT']
 77 |       @original_rubyopt = ENV['RUBYOPT']
 78 |       ENV['RUBYOPT'] = ENV['RUBYOPT'].sub('-rbundler/setup', '')
 79 |     end
 80 | 
 81 |     @bundler = LanguagePack::Helpers::BundlerWrapper.new
 82 |   end
 83 | 
 84 |   after(:each) do
 85 |     if ENV['RUBYOPT']
 86 |       ENV['RUBYOPT'] = @original_rubyopt
 87 |     end
 88 | 
 89 |     @bundler.clean
 90 |   end
 91 | 
 92 |   it "handles windows BUNDLED WITH" do
 93 |     Dir.mktmpdir do |dir|
 94 |       tmp_dir = Pathname(dir)
 95 |       FileUtils.cp_r(fixture_path("windows_lockfile/."), tmp_dir)
 96 | 
 97 |       tmp_gemfile_path = tmp_dir.join("Gemfile")
 98 |       tmp_gemfile_lock_path = tmp_dir.join("Gemfile.lock")
 99 | 
100 |       expect(tmp_gemfile_lock_path.read).to match("BUNDLED")
101 | 
102 |       wrapper = LanguagePack::Helpers::BundlerWrapper.new(gemfile_path: tmp_gemfile_path )
103 | 
104 |       expect(wrapper.version).to eq(LanguagePack::Helpers::BundlerWrapper::BLESSED_BUNDLER_VERSIONS["2"])
105 | 
106 |       def wrapper.topic(*args); end # Silence output in tests
107 |       wrapper.bundler_version_escape_valve!
108 | 
109 |       expect(tmp_gemfile_lock_path.read).to_not match("BUNDLED")
110 |     end
111 |   end
112 | 
113 |   describe "when executing bundler" do
114 |     it "handles JRuby pre gemfiles" do
115 |       Hatchet::App.new("jruby-minimal").in_directory_fork do |dir|
116 |         require "bundler"
117 |         Bundler.with_unbundled_env do
118 |           @bundler.install
119 | 
120 |           expect(@bundler.ruby_version).to eq("ruby-2.3.1-p0-jruby-9.1.7.0")
121 |         end
122 |       end
123 |     end
124 |   end
125 | end
126 | 


--------------------------------------------------------------------------------
/spec/helpers/outdated_ruby_version_spec.rb:
--------------------------------------------------------------------------------
  1 | require "spec_helper"
  2 | 
  3 | describe LanguagePack::Helpers::OutdatedRubyVersion do
  4 |   let(:stack) { "heroku-16" }
  5 |   let(:fetcher) {
  6 |     LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL, stack: stack)
  7 |   }
  8 | 
  9 |   it "handles amd ↗️ architecture on heroku-24" do
 10 |     ruby_version = LanguagePack::RubyVersion.bundle_platform_ruby(bundler_output: "ruby-3.1.0")
 11 |     fetcher = LanguagePack::Fetcher.new(
 12 |       LanguagePack::Base::VENDOR_URL,
 13 |       stack: "heroku-24",
 14 |       arch: "amd64"
 15 |     )
 16 |     outdated = LanguagePack::Helpers::OutdatedRubyVersion.new(
 17 |       current_ruby_version: ruby_version,
 18 |       fetcher: fetcher
 19 |     )
 20 | 
 21 |     outdated.call
 22 |     expect(outdated.suggested_ruby_minor_version).to eq("3.1.7")
 23 |   end
 24 | 
 25 |   it "handles arm 💪 architecture on heroku-24" do
 26 |     ruby_version = LanguagePack::RubyVersion::bundle_platform_ruby(bundler_output: "ruby-3.1.0")
 27 |     fetcher = LanguagePack::Fetcher.new(
 28 |       LanguagePack::Base::VENDOR_URL,
 29 |       stack: "heroku-24",
 30 |       arch: "arm64"
 31 |     )
 32 |     outdated = LanguagePack::Helpers::OutdatedRubyVersion.new(
 33 |       current_ruby_version: ruby_version,
 34 |       fetcher: fetcher
 35 |     )
 36 | 
 37 |     outdated.call
 38 |     expect(outdated.suggested_ruby_minor_version).to eq("3.1.7")
 39 |   end
 40 | 
 41 |   it "finds the latest version on a stack" do
 42 |     ruby_version = LanguagePack::RubyVersion::bundle_platform_ruby(bundler_output: "ruby-2.2.5")
 43 |     outdated = LanguagePack::Helpers::OutdatedRubyVersion.new(
 44 |       current_ruby_version: ruby_version,
 45 |       fetcher: fetcher
 46 |     )
 47 | 
 48 |     outdated.call
 49 |     expect(outdated.suggested_ruby_minor_version).to eq("2.2.10")
 50 |     expect(outdated.eol?).to eq(true)
 51 |     expect(outdated.maybe_eol?).to eq(true)
 52 |   end
 53 | 
 54 |   it "detects returns original ruby version when using the latest" do
 55 |     ruby_version = LanguagePack::RubyVersion::bundle_platform_ruby(bundler_output: "ruby-2.2.10")
 56 |     outdated = LanguagePack::Helpers::OutdatedRubyVersion.new(
 57 |       current_ruby_version: ruby_version,
 58 |       fetcher: fetcher
 59 |     )
 60 | 
 61 |     outdated.call
 62 |     expect(outdated.suggested_ruby_minor_version).to eq("2.2.10")
 63 |     expect(outdated.latest_minor_version?).to be_truthy
 64 |   end
 65 | 
 66 |   it "recommends a non EOL version of Ruby" do
 67 |     ruby_version_one = LanguagePack::RubyVersion::bundle_platform_ruby(bundler_output: "ruby-2.1.10")
 68 |     ruby_version_two = LanguagePack::RubyVersion::bundle_platform_ruby(bundler_output: "ruby-2.2.10")
 69 | 
 70 |     outdated_one = LanguagePack::Helpers::OutdatedRubyVersion.new(
 71 |       current_ruby_version: ruby_version_one,
 72 |       fetcher: fetcher
 73 |     )
 74 |     outdated_two = LanguagePack::Helpers::OutdatedRubyVersion.new(
 75 |       current_ruby_version: ruby_version_two,
 76 |       fetcher: fetcher
 77 |     )
 78 | 
 79 |     outdated_one.call
 80 |     outdated_two.call
 81 | 
 82 |     expect(outdated_one.eol?).to be_truthy
 83 |     expect(outdated_one.maybe_eol?).to be_truthy
 84 | 
 85 |     expect(outdated_two.eol?).to be_truthy
 86 |     expect(outdated_one.maybe_eol?).to be_truthy
 87 | 
 88 |     suggested_one = outdated_one.suggest_ruby_eol_version
 89 |     expect(suggested_one).to eq(outdated_two.suggest_ruby_eol_version)
 90 |     expect(suggested_one.chars.last).to eq("x") # i.e. 2.5.x
 91 | 
 92 |     actual = Gem::Version.new(suggested_one)
 93 |     expect(actual).to be > Gem::Version.new("2.4.x")
 94 |   end
 95 | 
 96 |   it "does not recommend EOL for recent ruby version" do
 97 |     ruby_version = LanguagePack::RubyVersion::bundle_platform_ruby(bundler_output: "ruby-2.2.10")
 98 | 
 99 |     outdated = LanguagePack::Helpers::OutdatedRubyVersion.new(
100 |       current_ruby_version: ruby_version,
101 |       fetcher: fetcher
102 |     )
103 | 
104 |     outdated.call
105 | 
106 |     good_version = outdated.suggest_ruby_eol_version.sub("x", "0")
107 |     ruby_version = LanguagePack::RubyVersion::bundle_platform_ruby(bundler_output: "ruby-#{good_version}")
108 | 
109 |     outdated = LanguagePack::Helpers::OutdatedRubyVersion.new(
110 |       current_ruby_version: ruby_version,
111 |       fetcher: fetcher
112 |     )
113 |     outdated.call
114 | 
115 |     expect(outdated.eol?).to be_falsey
116 |     expect(outdated.maybe_eol?).to be_falsey
117 |   end
118 | 
119 |   it "can call eol? on the latest Ruby version" do
120 |     ruby_version = LanguagePack::RubyVersion::bundle_platform_ruby(bundler_output: "bundler_output: ruby-2.6.0")
121 | 
122 |     new_fetcher = fetcher.dup
123 |     def new_fetcher.exists?(value); false; end
124 | 
125 |     outdated = LanguagePack::Helpers::OutdatedRubyVersion.new(
126 |       current_ruby_version: ruby_version,
127 |       fetcher: new_fetcher
128 |     )
129 | 
130 |     outdated.call
131 | 
132 |     expect(outdated.eol?).to be_falsey
133 |     expect(outdated.maybe_eol?).to be_falsey
134 |   end
135 | end
136 | 


--------------------------------------------------------------------------------
/spec/helpers/rails_runner_spec.rb:
--------------------------------------------------------------------------------
  1 | require 'spec_helper'
  2 | 
  3 | describe "Rails Runner" do
  4 |   def isolate(&block)
  5 |     # Process and disk isolation
  6 |     Hatchet::App.new("default_ruby").in_directory_fork do
  7 |       original_path = ENV["PATH"]
  8 |       ENV["PATH"] = "./bin/:#{ENV['PATH']}"
  9 | 
 10 |       Dir.mktmpdir do |tmpdir|
 11 |         Dir.chdir(tmpdir) do
 12 |           block.call
 13 |         end
 14 |       end
 15 |     end
 16 |   end
 17 | 
 18 |   it "config objects build propperly formatted commands" do
 19 |     isolate do
 20 |       rails_runner  = LanguagePack::Helpers::RailsRunner.new
 21 |       local_storage = rails_runner.detect("active_storage.service")
 22 | 
 23 |       expected = 'rails runner "begin; puts %Q{heroku.detecting.config.for.active_storage.service=#{Rails.application.config.try(:active_storage).try(:service)}}; rescue => e; end;"'
 24 |       expect(rails_runner.command).to eq(expected)
 25 | 
 26 |       rails_runner.detect("assets.compile")
 27 | 
 28 |       expected = 'rails runner "begin; puts %Q{heroku.detecting.config.for.active_storage.service=#{Rails.application.config.try(:active_storage).try(:service)}}; rescue => e; end; begin; puts %Q{heroku.detecting.config.for.assets.compile=#{Rails.application.config.try(:assets).try(:compile)}}; rescue => e; end;"'
 29 |       expect(rails_runner.command).to eq(expected)
 30 |     end
 31 |   end
 32 | 
 33 |   it "calls run through child object" do
 34 |     isolate do
 35 |       rails_runner  = LanguagePack::Helpers::RailsRunner.new
 36 |       def rails_runner.call; @called ||= 0 ; @called += 1; "" end
 37 |       def rails_runner.called; @called; end
 38 | 
 39 |       local_storage = rails_runner.detect("active_storage.service")
 40 |       local_storage.success?
 41 |       expect(rails_runner.called).to eq(1)
 42 | 
 43 |       local_storage.success?
 44 |       local_storage.did_match?("foo")
 45 |       expect(rails_runner.called).to eq(1)
 46 |     end
 47 |   end
 48 | 
 49 |   it "calls a mock interface" do
 50 |     isolate do
 51 |       mock_rails_runner
 52 |       expect(File.executable?("bin/rails")).to eq(true)
 53 | 
 54 |       rails_runner  = LanguagePack::Helpers::RailsRunner.new
 55 |       local_storage = rails_runner.detect("active_storage.service")
 56 |       local_storage = rails_runner.detect("foo.bar")
 57 | 
 58 |       expect(rails_runner.output).to match("heroku.detecting.config.for.active_storage.service=active_storage.service")
 59 |       expect(rails_runner.output).to match("heroku.detecting.config.for.foo.bar=foo.bar")
 60 |       expect(rails_runner.success?).to be(true)
 61 |     end
 62 |   end
 63 | 
 64 |   it "timeout works as expected" do
 65 |     isolate do
 66 |       mock_rails_runner("pid = Process.spawn('sleep 5'); Process.wait(pid)")
 67 | 
 68 |       diff = time_it do
 69 |         rails_runner  = LanguagePack::Helpers::RailsRunner.new(false, 0.01)
 70 |         local_storage = rails_runner.detect("active_storage.service")
 71 |         expect(rails_runner.success?).to eq(false)
 72 |         expect(rails_runner.timeout?).to eq(true)
 73 |       end
 74 | 
 75 |       expect(diff < 1).to eq(true), "expected time difference #{diff} to be less than 1 second, but was longer"
 76 |     end
 77 |   end
 78 | 
 79 |   it "failure in one task does not cause another to fail" do
 80 |     isolate do
 81 |       mock_rails_runner('raise "bad" if value == :bad')
 82 | 
 83 |       rails_runner  = LanguagePack::Helpers::RailsRunner.new(false, 1)
 84 |       bad_value     = rails_runner.detect("bad.value")
 85 |       local_storage = rails_runner.detect("active_storage.service")
 86 | 
 87 |       expect(!!bad_value.success?).to     eq(false)
 88 |       expect(!!local_storage.success?).to eq(true)
 89 |     end
 90 |   end
 91 | 
 92 |   it "does not fail when there is an invalid byte sequence" do
 93 |     isolate do
 94 |       mock_rails_runner('puts "hi \255"')
 95 | 
 96 |       rails_runner  = LanguagePack::Helpers::RailsRunner.new
 97 |       local_storage = rails_runner.detect("active_storage.service")
 98 | 
 99 |       expect(local_storage.success?).to be_truthy
100 |     end
101 |   end
102 | 
103 |   def time_it
104 |     start = Time.now
105 |     yield
106 |     return Time.now - start
107 |   end
108 | 
109 |   def mock_rails_runner(try_code = "")
110 |     executable_contents = <<~FILE
111 |       #!/usr/bin/env ruby
112 |       require 'ostruct'
113 | 
114 |       module Rails; end
115 |       def Rails.application
116 |         OpenStruct.new(config: TryMock.new) # Rails.application.config #=> TryMock instance
117 |       end
118 | 
119 |       # Mock object used to record calls
120 |       # for example:
121 |       #
122 |       #   obj = Try.new
123 |       #   obj.try(:active_storage).try(:service)
124 |       #   puts obj.to_s # => "active_storage.service"
125 |       #
126 |       class TryMock
127 |         def initialize(array = [])
128 |           @try_array = array
129 |         end
130 | 
131 |         def try(value)
132 |           @try_array << value
133 |           #{try_code}
134 |           return TryMock.new(@try_array)
135 |         end
136 | 
137 |         def to_s
138 |           @try_array.join(".")
139 |         end
140 |       end
141 | 
142 |       ARGV.shift           # remove "runner"
143 |       eval(ARGV.join(" ")) # Execute command passed in
144 |     FILE
145 |     FileUtils.mkdir("bin")
146 |     File.open("bin/rails", "w") { |f| f << executable_contents }
147 |     File.chmod(0777, "bin/rails")
148 |   end
149 | end
150 | 


--------------------------------------------------------------------------------
/lib/language_pack/helpers/outdated_ruby_version.rb:
--------------------------------------------------------------------------------
  1 | # Queries S3 in the background to determine
  2 | # what versions are supported so they can be recommended
  3 | # to the user
  4 | #
  5 | # Example:
  6 | #
  7 | #   ruby_version = LanguagePack::RubyVersion.bundle_platform_ruby(bundler_output: "ruby-2.2.5")
  8 | #   outdated = LanguagePack::Helpers::OutdatedRubyVersion.new(
  9 | #     current_ruby_version: ruby_version,
 10 | #     fetcher: LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL, stack: "heroku-22")
 11 | #     fetcher: LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL, stack: "heroku-22", arch: "amd64")
 12 | #   )
 13 | #
 14 | #   outdated.call
 15 | #   puts outdated.suggested_ruby_minor_version
 16 | #   #=> "ruby-2.2.10"
 17 | class LanguagePack::Helpers::OutdatedRubyVersion
 18 |   DEFAULT_RANGE = 1..5
 19 |   attr_reader :current_ruby_version
 20 | 
 21 |   def initialize(current_ruby_version: , fetcher:)
 22 |     @current_ruby_version = current_ruby_version
 23 |     @fetcher      = fetcher
 24 |     @already_joined = false
 25 | 
 26 |     @minor_versions = []
 27 |     @eol_versions = []
 28 | 
 29 |     @minor_version_threads = []
 30 |     @eol_version_threads = []
 31 | 
 32 |     @suggested_eol_version = nil
 33 |     @suggested_minor_version = nil
 34 |   end
 35 | 
 36 |   def can_check?
 37 |     return false if current_ruby_version.jruby?
 38 | 
 39 |     true
 40 |   end
 41 | 
 42 |   # Enqueues checks in the background
 43 |   def call
 44 |     return unless can_check?
 45 |     raise "Cannot `call()` twice" unless @minor_version_threads.empty?
 46 | 
 47 |     check_minor_versions
 48 |     check_eol_versions_major
 49 |     check_eol_versions_minor
 50 |     self
 51 |   end
 52 | 
 53 |   def join
 54 |     return unless can_check?
 55 |     return true if @already_joined
 56 |     raise "Must initalize threads with `call()` before joining" if @minor_version_threads.empty?
 57 | 
 58 |     @eol_versions = @eol_version_threads.map(&:value).compact
 59 |     @minor_versions = @minor_version_threads.map(&:value).compact
 60 |     @minor_versions << current_ruby_version.ruby_version
 61 | 
 62 | 
 63 |     @suggested_minor_version = @minor_versions
 64 |       .map { |v| v.sub('ruby-', '') }
 65 |       .sort_by { |v| Gem::Version.new(v) }
 66 |       .last
 67 | 
 68 |     @suggested_eol_version = @eol_versions
 69 |       .map { |v| v.sub('ruby-', '') }
 70 |       .sort_by { |v| Gem::Version.new(v) }
 71 |       .last(3)
 72 |       .first
 73 | 
 74 |     @already_joined = true
 75 |   end
 76 | 
 77 |   def suggested_ruby_minor_version
 78 |     join
 79 |     @suggested_minor_version
 80 |   end
 81 | 
 82 |   def latest_minor_version?
 83 |     join
 84 |     @suggested_minor_version == current_ruby_version.ruby_version
 85 |   end
 86 | 
 87 |   def eol?
 88 |     join
 89 | 
 90 |     return @eol_versions.length > 3
 91 |   end
 92 | 
 93 |   # Account for preview releases
 94 |   def maybe_eol?
 95 |     join
 96 | 
 97 |     return @eol_versions.length > 2
 98 |   end
 99 | 
100 |   def suggest_ruby_eol_version
101 |     return false unless maybe_eol?
102 |     @suggested_eol_version
103 |       .sub(/0$/, 'x')
104 |   end
105 | 
106 |   # Checks for a range of "tiny" versions in parallel
107 |   #
108 |   # For example if 2.5.0 is given it will check for the existance of
109 |   # - 2.5.1
110 |   # - 2.5.2
111 |   # - 2.5.3
112 |   # - 2.5.4
113 |   # - 2.5.5
114 |   #
115 |   # If the last elment in the series exists, it will continue to
116 |   # search by enqueuing additional numbers until the final
117 |   # value in the series is found
118 |   private def check_minor_versions(range: DEFAULT_RANGE, base_version: current_ruby_version, &block)
119 |     range.each do |i|
120 |       @minor_version_threads << Thread.new do
121 |         version = base_version.next_logical_version(i)
122 |         next if !@fetcher.exists?("#{version}.tgz")
123 | 
124 |         if i == range.last
125 |           check_minor_versions(
126 |             range: Range.new(i+1, i+i),
127 |             base_version: base_version
128 |           )
129 |         end
130 | 
131 |         version
132 |       end
133 |     end
134 |   end
135 | 
136 |   # Checks to see if 3 minor versions exist above current version
137 |   #
138 |   # for example 2.4.0 would check for existance of:
139 |   #   - 2.5.0
140 |   #   - 2.6.0
141 |   #   - 2.7.0
142 |   #   - 2.8.0
143 |   private def check_eol_versions_minor(range: DEFAULT_RANGE, base_version: current_ruby_version)
144 |     range.each do |i|
145 |       @eol_version_threads << Thread.new do
146 |         version = base_version.next_minor_version(i)
147 | 
148 |         next if !@fetcher.exists?("#{version}.tgz")
149 | 
150 |         if i == range.last
151 |           check_eol_versions_minor(
152 |             range: Range.new(i+1, i+i),
153 |             base_version: base_version
154 |           )
155 |         end
156 | 
157 |         version
158 |       end
159 |     end
160 |   end
161 | 
162 |   # Checks to see if one major version exists above current version
163 |   # if it does, then it will check for minor versions of that version
164 |   #
165 |   # For checking 2.5. it would check for the existance of 3.0.0
166 |   #
167 |   # If 3.0.0 exists then it will check for:
168 |   #   - 3.1.0
169 |   #   - 3.2.0
170 |   #   - 3.3.0
171 |   private def check_eol_versions_major
172 |     @eol_version_threads << Thread.new do
173 |       version = current_ruby_version.next_major_version(1)
174 | 
175 |       next if !@fetcher.exists?("#{version}.tgz")
176 | 
177 |       check_eol_versions_minor(
178 |         base_version: LanguagePack::RubyVersion.bundle_platform_ruby(bundler_output: version)
179 |       )
180 | 
181 |       version
182 |     end
183 |   end
184 | end
185 | 


--------------------------------------------------------------------------------
/lib/language_pack/base.rb:
--------------------------------------------------------------------------------
  1 | require "language_pack"
  2 | require "pathname"
  3 | require "yaml"
  4 | require "digest/sha1"
  5 | require "language_pack/shell_helpers"
  6 | require "language_pack/cache"
  7 | require "language_pack/helpers/bundler_cache"
  8 | require "language_pack/metadata"
  9 | require "language_pack/fetcher"
 10 | 
 11 | Encoding.default_external = Encoding::UTF_8 if defined?(Encoding)
 12 | 
 13 | # abstract class that all the Ruby based Language Packs inherit from
 14 | class LanguagePack::Base
 15 |   include LanguagePack::ShellHelpers
 16 |   extend LanguagePack::ShellHelpers
 17 | 
 18 |   VENDOR_URL           = ENV['BUILDPACK_VENDOR_URL'] || "https://heroku-buildpack-ruby.s3.us-east-1.amazonaws.com"
 19 |   ROOT_DIR             = File.expand_path("../../..", __FILE__)
 20 |   MULTI_ARCH_STACKS    = ["heroku-24"]
 21 |   KNOWN_ARCHITECTURES  = ["amd64", "arm64"]
 22 | 
 23 |   attr_reader :app_path, :cache, :stack
 24 | 
 25 |   def initialize(app_path: , cache_path: , gemfile_lock: )
 26 |     @app_path = app_path
 27 |     @stack         = ENV.fetch("STACK")
 28 |     @cache         = LanguagePack::Cache.new(cache_path)
 29 |     @metadata      = LanguagePack::Metadata.new(@cache)
 30 |     @bundler_cache = LanguagePack::BundlerCache.new(@cache, @stack)
 31 |     @fetchers      = {:buildpack => LanguagePack::Fetcher.new(VENDOR_URL) }
 32 |     @arch = get_arch
 33 |     @report = HerokuBuildReport::GLOBAL
 34 |   end
 35 | 
 36 |   def get_arch
 37 |     command = "dpkg --print-architecture"
 38 |     arch = run!(command, silent: true).strip
 39 | 
 40 |     if !KNOWN_ARCHITECTURES.include?(arch)
 41 |       raise <<~EOF
 42 |         Architecture '#{arch}' returned from command `#{command}` is unknown.
 43 |         Known architectures include: #{KNOWN_ARCHITECTURES.inspect}"
 44 |       EOF
 45 |     end
 46 | 
 47 |     arch
 48 |   end
 49 | 
 50 |   def self.===(app_path)
 51 |     raise "must subclass"
 52 |   end
 53 | 
 54 |   # name of the Language Pack
 55 |   # @return [String] the result
 56 |   def name
 57 |     raise "must subclass"
 58 |   end
 59 | 
 60 |   # list of default addons to install
 61 |   def default_addons
 62 |     raise "must subclass"
 63 |   end
 64 | 
 65 |   # config vars to be set on first push.
 66 |   # @return [Hash] the result
 67 |   # @not: this is only set the first time an app is pushed to.
 68 |   def default_config_vars
 69 |     raise "must subclass"
 70 |   end
 71 | 
 72 |   # process types to provide for the app
 73 |   # Ex. for rails we provide a web process
 74 |   # @return [Hash] the result
 75 |   def default_process_types
 76 |     raise "must subclass"
 77 |   end
 78 | 
 79 |   # this is called to build the slug
 80 |   def compile
 81 |     write_release_yaml
 82 |     Kernel.puts ""
 83 |     warnings.each do |warning|
 84 |       Kernel.puts "\e[1m\e[33m###### WARNING:\e[0m"# Bold yellow
 85 |       Kernel.puts ""
 86 |       puts warning
 87 |       Kernel.puts ""
 88 |     end
 89 |     if deprecations.any?
 90 |       topic "DEPRECATIONS:"
 91 |       puts @deprecations.join("\n")
 92 |     end
 93 |     Kernel.puts ""
 94 |   end
 95 | 
 96 |   def build_release
 97 |     release = {}
 98 |     release["addons"]                = default_addons
 99 |     release["config_vars"]           = default_config_vars
100 |     release["default_process_types"] = default_process_types
101 | 
102 |     release
103 |   end
104 | 
105 |   def write_release_yaml
106 |     release = build_release
107 |     FileUtils.mkdir("tmp") unless File.exist?("tmp")
108 |     File.open("tmp/heroku-buildpack-release-step.yml", 'w') do |f|
109 |       f.write(release.to_yaml)
110 |     end
111 | 
112 |     warn_webserver
113 |   end
114 | 
115 |   def warn_webserver
116 |     return if File.exist?("Procfile")
117 |     msg =  "No Procfile detected, using the default web server.\n"
118 |     msg << "We recommend explicitly declaring how to boot your server process via a Procfile.\n"
119 |     msg << "https://devcenter.heroku.com/articles/ruby-default-web-server"
120 |     warn msg
121 |   end
122 | 
123 | private ##################################
124 | 
125 |   # sets up the environment variables for the build process
126 |   def setup_language_pack_environment
127 |   end
128 | 
129 |   def add_to_profiled(string, filename: "ruby.sh", mode: "a")
130 |     profiled_path = "#{app_path}/.profile.d/"
131 | 
132 |     FileUtils.mkdir_p profiled_path
133 |     File.open("#{profiled_path}/#{filename}", mode) do |file|
134 |       file.puts string
135 |     end
136 |   end
137 | 
138 |   def set_env_default(key, val)
139 |     add_to_profiled "export #{key}=${#{key}:-#{val}}"
140 |   end
141 | 
142 |   def set_env_override(key, val)
143 |     add_to_profiled %{export #{key}="#{val.gsub('"','\"')}"}
144 |   end
145 | 
146 |   def add_to_export(string)
147 |     export = File.join(ROOT_DIR, "export")
148 |     File.open(export, "a") do |file|
149 |       file.puts string
150 |     end
151 |   end
152 | 
153 |   # option can be :path, :default, :override
154 |   # https://github.com/buildpacks/spec/blob/366ac1aa0be59d11010cc21aa06c16d81d8d43e7/buildpack.md#environment-variable-modification-rules
155 |   def export(key, val, option: nil)
156 |     string =
157 |       if option == :default
158 |         %{export #{key}="${#{key}:-#{val}}"}
159 |       elsif option == :path
160 |         %{export #{key}="#{val}:$#{key}"}
161 |       else
162 |         %{export #{key}="#{val.gsub('"','\"')}"}
163 |       end
164 | 
165 |     export = File.join(ROOT_DIR, "export")
166 |     File.open(export, "a") do |file|
167 |       file.puts string
168 |     end
169 |   end
170 | 
171 |   def set_export_default(key, val)
172 |     export key, val, option: :default
173 |   end
174 | 
175 |   def set_export_override(key, val)
176 |     export key, val, option: :override
177 |   end
178 | 
179 |   def set_export_path(key, val)
180 |     export key, val, option: :path
181 |   end
182 | end
183 | 


--------------------------------------------------------------------------------
/spec/rake/deploy_check_spec.rb:
--------------------------------------------------------------------------------
  1 | require "spec_helper"
  2 | require "rake/deploy_check"
  3 | 
  4 | describe "A helper class for deploying" do
  5 |   describe "tests that hit github" do
  6 |     it "know remote tags" do
  7 |       Dir.mktmpdir do |dir|
  8 |         Dir.chdir(dir) do
  9 |           run!("touch foo; git init; git add .; git commit -m first")
 10 | 
 11 |           deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby")
 12 |           expect(deploy.remote_tag_array.class).to eq(Array)
 13 |           expect(deploy.remote_tag_array).to include("v218")
 14 | 
 15 |           expect(deploy.remote_tag_matches?(local_sha: "nope")).to be_falsey
 16 |           expect(deploy.remote_tag_matches?(remote_sha: "nope")).to be_falsey
 17 |         end
 18 |       end
 19 |     end
 20 | 
 21 |     it "remote sha" do
 22 |       deploy = DeployCheck.new(github: "sharpstone/do_not_delete_or_modify")
 23 |       expect(deploy.remote_commit_sha).to eq("3a9ff6433a05560acfd06dda03a11605a96ae133")
 24 |     end
 25 | 
 26 |     it "local_commit_sha" do
 27 |       Dir.mktmpdir do |dir|
 28 |         Dir.chdir(dir) do
 29 |           run!("git clone https://github.com/sharpstone/default_ruby #{dir} 2>&1 && cd #{dir} && git checkout 6e642963acec0ff64af51bd6fba8db3c4176ed6e 2>&1 && git checkout -b mybranch 2>&1")
 30 |           deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby")
 31 |           expect(deploy.local_commit_sha).to eq("6e642963acec0ff64af51bd6fba8db3c4176ed6e")
 32 |         end
 33 |       end
 34 |     end
 35 |   end
 36 | 
 37 |   describe "tests that do NOT hit github" do
 38 |     it "checks multiple things" do
 39 |       deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby")
 40 |       deploy.instance_variable_set("@methods_called", [])
 41 | 
 42 |       def deploy.check_version!; @methods_called << :check_version! ;end
 43 |       def deploy.check_unstaged!; @methods_called << :check_unstaged! ;end
 44 |       def deploy.check_branch!; @methods_called << :check_branch! ;end
 45 |       def deploy.check_changelog!; @methods_called << :check_changelog! ;end
 46 |       def deploy.check_sync!; @methods_called << :check_sync! ;end
 47 | 
 48 |       deploy.check!
 49 | 
 50 |       methods_called = deploy.instance_variable_get("@methods_called")
 51 |       expect(methods_called).to include(:check_version!)
 52 |       expect(methods_called).to include(:check_unstaged!)
 53 |       expect(methods_called).to include(:check_branch!)
 54 |       expect(methods_called).to include(:check_changelog!)
 55 |       expect(methods_called).to include(:check_sync!)
 56 |     end
 57 | 
 58 |     it "checks version" do
 59 |       ["v123abc", "123", "V123"].each do |bad_version|
 60 |         deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby", next_version: bad_version)
 61 |         expect {
 62 |           deploy.check_version!
 63 |         }.to raise_error(/Must look like a version/)
 64 |       end
 65 |     end
 66 | 
 67 |     it "checks local sha and remote sha match" do
 68 |       deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby")
 69 |       expect {
 70 |         deploy.check_sync!(local_sha: "a", remote_sha: "not a")
 71 |       }.to raise_error(/Must be in-sync/)
 72 | 
 73 |       deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby")
 74 |       deploy.check_sync!(
 75 |         local_sha: "cbe100933b1e50953f0da35aafc50374ae2a31f9",
 76 |         remote_sha: "cbe100933b1e50953f0da35aafc50374ae2a31f9"
 77 |       )
 78 |     end
 79 | 
 80 |     it "github url" do
 81 |       deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby")
 82 |       expect(deploy.github_url).to eq("https://github.com/heroku/heroku-buildpack-ruby")
 83 |     end
 84 | 
 85 |     it "knows the next version" do
 86 |       deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby", next_version: "v123")
 87 | 
 88 |       def deploy.remote_tag_array; [ "v123" ] ; end
 89 | 
 90 |       expect(deploy.tag_exists_on_remote?).to be_truthy
 91 | 
 92 |       deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby", next_version: "v124")
 93 | 
 94 |       def deploy.remote_tag_array; [ "v123" ] ; end
 95 | 
 96 |       expect(deploy.tag_exists_on_remote?).to be_falsey
 97 |     end
 98 | 
 99 |     it "checks remote tags for existance" do
100 |       deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby")
101 | 
102 |       def deploy.remote_tag_array; [ "v123" ] ; end
103 | 
104 |       expect(deploy.next_version).to eq("v124")
105 |     end
106 | 
107 |     it "checks unstaged" do
108 |       Dir.mktmpdir do |dir|
109 |         Dir.chdir(dir) do
110 |           deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby")
111 |           run!("echo 'blerg' >> foo.txt")
112 |           run!("git init .; git add . ; git commit -m first")
113 |           deploy.check_unstaged!
114 | 
115 |           run!("echo 'foo' >> foo.txt")
116 |           expect { deploy.check_unstaged! }.to raise_error(/Must have all changes committed/)
117 |         end
118 |       end
119 |     end
120 | 
121 |     it "checks branch" do
122 |       Dir.mktmpdir do |dir|
123 |         Dir.chdir(dir) do
124 |           deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby")
125 |           run!("echo 'blerg' >> foo.txt")
126 |           run!("git init .; git add . ; git commit -m first")
127 |           run!("git checkout -B main")
128 |           deploy.check_branch!
129 | 
130 |           expect { deploy.check_branch!("not_main") }.to raise_error(/Must be on main branch/)
131 |         end
132 |       end
133 |     end
134 | 
135 |     it "checks CHANGELOG" do
136 |       Dir.mktmpdir do |dir|
137 |         Dir.chdir(dir) do
138 |           deploy = DeployCheck.new(github: "heroku/heroku-buildpack-ruby", next_version: "v999")
139 |           run!("touch CHANGELOG.md")
140 |           expect {
141 |             deploy.check_changelog!
142 |           }.to raise_error(/Expected CHANGELOG.md to include \[v999\]/)
143 | 
144 |           run!("echo '## [v999]' >> CHANGELOG.md")
145 |           deploy.check_changelog!
146 |         end
147 |       end
148 |     end
149 |   end
150 | end
151 | 


--------------------------------------------------------------------------------
/spec/helpers/gemfile_lock_spec.rb:
--------------------------------------------------------------------------------
  1 | require "spec_helper"
  2 | 
  3 | describe LanguagePack::Helpers::GemfileLock do
  4 |   it "parses empty gemfile without error" do
  5 |     report = HerokuBuildReport.dev_null
  6 |     gemfile_lock = LanguagePack::Helpers::GemfileLock.new(
  7 |       report: report,
  8 |       contents: ""
  9 |     )
 10 |     expect(gemfile_lock.ruby.ruby_version).to eq(nil)
 11 |     expect(gemfile_lock.ruby.pre).to eq(nil)
 12 |     expect(gemfile_lock.ruby.engine).to eq(:ruby)
 13 |     expect(gemfile_lock.ruby.empty?).to eq(true)
 14 |     expect(gemfile_lock.ruby.engine_version).to eq(nil)
 15 | 
 16 |     expect(gemfile_lock.bundler.version).to eq(nil)
 17 |     expect(gemfile_lock.bundler.empty?).to eq(true)
 18 |     expect(report.data).to be_empty
 19 |   end
 20 | 
 21 |   it "records invalid parsing" do
 22 |     report = HerokuBuildReport.dev_null
 23 |     gemfile_lock = LanguagePack::Helpers::GemfileLock.new(
 24 |       report: report,
 25 |       contents: <<~EOF
 26 |         RUBY VERSION
 27 |         ruby 3.3.5p100
 28 |         BUNDLED WITH
 29 |         2.3.4
 30 |       EOF
 31 |     )
 32 |     expect(
 33 |       report.data["gemfile_lock.bundler_version.failed_parse"]
 34 |     ).to eq(true)
 35 |     expect(
 36 |       report.data["gemfile_lock.bundler_version.failed_contents"]
 37 |     ).to eq(<<~EOF.strip)
 38 |       BUNDLED WITH
 39 |       2.3.4
 40 |     EOF
 41 | 
 42 |     expect(
 43 |       report.data["gemfile_lock.ruby_version.failed_parse"]
 44 |     ).to eq(true)
 45 |     expect(
 46 |       report.data["gemfile_lock.ruby_version.failed_contents"]
 47 |     ).to eq(<<~EOF.strip)
 48 |       RUBY VERSION
 49 |       ruby 3.3.5p100
 50 |     EOF
 51 |   end
 52 | 
 53 |   it "captures MRI version and ignores patchlevel" do
 54 |     gemfile_lock = LanguagePack::Helpers::GemfileLock.new(
 55 |       contents: <<~EOF
 56 |         RUBY VERSION
 57 |            ruby 3.3.5p100
 58 |         BUNDLED WITH
 59 |            2.3.4
 60 |       EOF
 61 |     )
 62 |     expect(gemfile_lock.ruby.ruby_version).to eq("3.3.5")
 63 |     expect(gemfile_lock.ruby.pre).to eq(nil)
 64 |     expect(gemfile_lock.ruby.engine).to eq(:ruby)
 65 |     expect(gemfile_lock.ruby.empty?).to eq(false)
 66 |     expect(gemfile_lock.ruby.engine_version).to eq("3.3.5")
 67 | 
 68 |     expect(gemfile_lock.bundler.version).to eq("2.3.4")
 69 |     expect(gemfile_lock.bundler.empty?).to eq(false)
 70 |   end
 71 | 
 72 |   it "works with windows line endings" do
 73 |     gemfile_lock = LanguagePack::Helpers::GemfileLock.new(
 74 |       contents: <<~EOF.gsub("\n", "\r\n")
 75 |         RUBY VERSION
 76 |            ruby 3.3.5p100
 77 |         BUNDLED WITH
 78 |            2.3.4
 79 |       EOF
 80 |     )
 81 |     expect(gemfile_lock.ruby.ruby_version).to eq("3.3.5")
 82 |     expect(gemfile_lock.ruby.pre).to eq(nil)
 83 |     expect(gemfile_lock.ruby.engine).to eq(:ruby)
 84 |     expect(gemfile_lock.ruby.empty?).to eq(false)
 85 |     expect(gemfile_lock.ruby.engine_version).to eq("3.3.5")
 86 | 
 87 |     expect(gemfile_lock.bundler.version).to eq("2.3.4")
 88 |     expect(gemfile_lock.bundler.empty?).to eq(false)
 89 |   end
 90 | 
 91 |   it "captures jruby version" do
 92 |     gemfile_lock = LanguagePack::Helpers::GemfileLock.new(
 93 |       contents: <<~EOF
 94 |         GEM
 95 |           remote: https://rubygems.org/
 96 |           specs:
 97 |         PLATFORMS
 98 |           java
 99 |         RUBY VERSION
100 |            ruby 2.5.7p001 (jruby 9.2.13.0)
101 |       EOF
102 |     )
103 |     expect(gemfile_lock.ruby.ruby_version).to eq("2.5.7")
104 |     expect(gemfile_lock.ruby.pre).to eq(nil)
105 |     expect(gemfile_lock.ruby.engine).to eq(:jruby)
106 |     expect(gemfile_lock.ruby.empty?).to eq(false)
107 |     expect(gemfile_lock.ruby.engine_version).to eq("9.2.13.0")
108 |   end
109 | 
110 |   it "is resiliant to gemfile.lock format changes" do
111 |     gemfile_lock = LanguagePack::Helpers::GemfileLock.new(
112 |       contents: <<~EOF
113 |         GEM
114 |           remote: https://rubygems.org/
115 |           specs:
116 |         # Pretend format change
117 |         METADATA
118 |           (jruby 9)
119 |         PLATFORMS
120 |           java
121 |         RUBY VERSION
122 |            ruby 2.5.7p001 (jruby 9.2.13.0)
123 |       EOF
124 |     )
125 |     expect(gemfile_lock.ruby.ruby_version).to eq("2.5.7")
126 |     expect(gemfile_lock.ruby.pre).to eq(nil)
127 |     expect(gemfile_lock.ruby.engine).to eq(:jruby)
128 |     expect(gemfile_lock.ruby.empty?).to eq(false)
129 |     expect(gemfile_lock.ruby.engine_version).to eq("9.2.13.0")
130 |   end
131 | 
132 |   it "handles RC dot syntax" do
133 |     gemfile_lock = LanguagePack::Helpers::GemfileLock.new(
134 |       contents: <<~EOF
135 |         RUBY VERSION
136 |            ruby 3.4.0.rc1
137 |         BUNDLED WITH
138 |            2.3.4
139 |       EOF
140 |     )
141 |     expect(gemfile_lock.ruby.ruby_version).to eq("3.4.0")
142 |     expect(gemfile_lock.ruby.pre).to eq("rc1")
143 |     expect(gemfile_lock.ruby.engine).to eq(:ruby)
144 |     expect(gemfile_lock.ruby.empty?).to eq(false)
145 |     expect(gemfile_lock.ruby.engine_version).to eq("3.4.0")
146 |   end
147 | 
148 |   it "handles pre without a number" do
149 |     gemfile_lock = LanguagePack::Helpers::GemfileLock.new(
150 |       contents: <<~EOF
151 |         RUBY VERSION
152 |            ruby 3.4.0.lol
153 |         BUNDLED WITH
154 |            2.3.4
155 |       EOF
156 |     )
157 |     expect(gemfile_lock.ruby.ruby_version).to eq("3.4.0")
158 |     expect(gemfile_lock.ruby.pre).to eq("lol")
159 |     expect(gemfile_lock.ruby.engine).to eq(:ruby)
160 |     expect(gemfile_lock.ruby.empty?).to eq(false)
161 |     expect(gemfile_lock.ruby.engine_version).to eq("3.4.0")
162 |   end
163 | 
164 |   it "handles preview dot syntax" do
165 |     gemfile_lock = LanguagePack::Helpers::GemfileLock.new(
166 |       contents: <<~EOF
167 |         RUBY VERSION
168 |            ruby 3.4.0.preview2
169 |         BUNDLED WITH
170 |            2.3.4
171 |       EOF
172 |     )
173 |     expect(gemfile_lock.ruby.ruby_version).to eq("3.4.0")
174 |     expect(gemfile_lock.ruby.pre).to eq("preview2")
175 |     expect(gemfile_lock.ruby.engine).to eq(:ruby)
176 |     expect(gemfile_lock.ruby.empty?).to eq(false)
177 |     expect(gemfile_lock.ruby.engine_version).to eq("3.4.0")
178 |   end
179 | end
180 | 


--------------------------------------------------------------------------------
/lib/language_pack/rails3.rb:
--------------------------------------------------------------------------------
  1 | require "language_pack"
  2 | require "language_pack/rails2"
  3 | 
  4 | # Rails 3 Language Pack. This is for all Rails 3.x apps.
  5 | class LanguagePack::Rails3 < LanguagePack::Rails2
  6 |   # detects if this is a Rails 3.x app
  7 |   # @return [Boolean] true if it's a Rails 3.x app
  8 |   def self.use?
  9 |     rails_version = bundler.gem_version('railties')
 10 |     return false unless rails_version
 11 |     is_rails3 = rails_version >= Gem::Version.new('3.0.0') &&
 12 |                 rails_version <  Gem::Version.new('4.0.0')
 13 |     return is_rails3
 14 |   end
 15 | 
 16 |   def name
 17 |     "Ruby/Rails"
 18 |   end
 19 | 
 20 |   def default_process_types
 21 |     # let's special case thin here
 22 |     web_process = bundler.has_gem?("thin") ?
 23 |       "bundle exec thin start -R config.ru -e $RAILS_ENV -p ${PORT:-5000}" :
 24 |       "bundle exec rails server -p ${PORT:-5000}"
 25 | 
 26 |     super.merge({
 27 |       "web" => web_process,
 28 |       "console" => "bundle exec rails console"
 29 |     })
 30 |   end
 31 | 
 32 |   def rake_env
 33 |     default_env_vars.merge("RAILS_GROUPS" => "assets").merge(super)
 34 |   end
 35 | 
 36 |   def compile
 37 |     super
 38 |   end
 39 | 
 40 |   def config_detect
 41 |     super
 42 |     @assets_compile_config = @rails_runner.detect("assets.compile")
 43 |     @x_sendfile_config     = @rails_runner.detect("action_dispatch.x_sendfile_header")
 44 |   end
 45 | 
 46 |   def best_practice_warnings
 47 |     super
 48 |     warn_x_sendfile_use!
 49 | 
 50 |     if assets_compile_enabled?
 51 |       safe_sprockets_version_needed = sprocket_version_upgrade_needed
 52 |       if safe_sprockets_version_needed
 53 |         message = < Gem::Version.new("3") &&
137 |           sprockets_version < Gem::Version.new("3.7.2")
138 |       return "3.7.2"
139 |     elsif sprockets_version > Gem::Version.new("4") &&
140 |           sprockets_version < Gem::Version.new("4.0.0.beta8")
141 |       return "4.0.0.beta8"
142 |     else
143 |       return false
144 |     end
145 |   end
146 | 
147 |   def assets_compile_enabled?
148 |     return false unless @assets_compile_config.success?
149 |     @assets_compile_config.did_match?("true")
150 |   end
151 | 
152 |   def install_plugins
153 |     return false if bundler.has_gem?('rails_12factor')
154 |     plugins = {"rails_log_stdout" => "rails_stdout_logging", "rails3_serve_static_assets" => "rails_serve_static_assets" }.
155 |                 reject { |plugin, gem| bundler.has_gem?(gem) }
156 |     return false if plugins.empty?
157 |     plugins.each do |plugin, gem|
158 |       warn "Injecting plugin '#{plugin}'"
159 |     end
160 |     warn "Add 'rails_12factor' gem to your Gemfile to skip plugin injection"
161 |     LanguagePack::Helpers::PluginsInstaller.new(plugins.keys).install
162 |   end
163 | 
164 |   # runs the tasks for the Rails 3.1 asset pipeline
165 |   def run_assets_precompile_rake_task
166 |     if File.exist?("public/assets/manifest.yml")
167 |       puts "Detected manifest.yml, assuming assets were compiled locally"
168 |       return true
169 |     end
170 | 
171 |     precompile = rake.task("assets:precompile")
172 |     return true unless precompile.is_defined?
173 | 
174 |     topic("Preparing app for Rails asset pipeline")
175 | 
176 |     precompile.invoke(env: rake_env)
177 | 
178 |     if precompile.success?
179 |       puts "Asset precompilation completed (#{"%.2f" % precompile.time}s)"
180 |     else
181 |       precompile_fail(precompile.output)
182 |     end
183 |   end
184 | 
185 |   # generate a dummy database_url
186 |   def database_url
187 |     # need to use a dummy DATABASE_URL here, so rails can load the environment
188 |     return env("DATABASE_URL") if env("DATABASE_URL")
189 |     scheme =
190 |       if bundler.has_gem?("pg") || bundler.has_gem?("jdbc-postgres")
191 |         "postgres"
192 |     elsif bundler.has_gem?("mysql")
193 |       "mysql"
194 |     elsif bundler.has_gem?("mysql2")
195 |       "mysql2"
196 |     elsif bundler.has_gem?("sqlite3") || bundler.has_gem?("sqlite3-ruby")
197 |       "sqlite3"
198 |     end
199 |     "#{scheme}://user:pass@127.0.0.1/dbname"
200 |   end
201 | end
202 | 


--------------------------------------------------------------------------------