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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 98 additions & 26 deletions lib/language_pack/helpers/bundler_wrapper.rb
Original file line number Diff line number Diff line change
@@ -1,37 +1,89 @@
# frozen_string_literal: true

require 'language_pack/fetcher'

# This class is responsible for installing and maintaining a
# reference to bundler. It contains access to bundler internals
# that are used to introspect a project such as detecting presence
# of gems and their versions.
#
# Example:
#
# bundler = LanguagePack::Helpers::BundlerWrapper.new
# bundler.install
# bundler.version => "1.15.2"
# bundler.dir_name => "bundler-1.15.2"
# bundler.has_gem?("railties") => true
# bundler.gem_version("railties") => "5.2.2"
# bundler.clean
#
# Also used to determine the version of Ruby that a project is using
# based on `bundle platform --ruby`
#
# bundler.ruby_version # => "ruby-2.5.1"
# bundler.clean
#
# IMPORTANT: Calling `BundlerWrapper#install` on this class mutates the environment variable
# ENV['BUNDLE_GEMFILE']. If you're calling in a test context (or anything outside)
# of an isolated dyno, you must call `BundlerWrapper#clean`. To reset the environment
# variable:
#
# bundler = LanguagePack::Helpers::BundlerWrapper.new
# bundler.install
# bundler.clean # <========== IMPORTANT =============
#
class LanguagePack::Helpers::BundlerWrapper
include LanguagePack::ShellHelpers

BLESSED_BUNDLER_VERSIONS = {}
BLESSED_BUNDLER_VERSIONS["1"] = "1.15.2"
BLESSED_BUNDLER_VERSIONS["2"] = "2.0.1"
private_constant :BLESSED_BUNDLER_VERSIONS

class GemfileParseError < BuildpackError
def initialize(error)
msg = "There was an error parsing your Gemfile, we cannot continue\n"
msg = String.new("There was an error parsing your Gemfile, we cannot continue\n")
msg << error
super msg
end
end

VENDOR_URL = LanguagePack::Base::VENDOR_URL # coupling
DEFAULT_FETCHER = LanguagePack::Fetcher.new(VENDOR_URL) # coupling
BUNDLER_DIR_NAME = LanguagePack::Ruby::BUNDLER_GEM_PATH # coupling
BUNDLER_PATH = File.expand_path("../../../../tmp/#{BUNDLER_DIR_NAME}", __FILE__)
GEMFILE_PATH = Pathname.new "./Gemfile"
class UnsupportedBundlerVersion < BuildpackError
def initialize(version_hash, major)
msg = String.new("Your Gemfile.lock indicates you need bundler `#{major}.x`\n")
msg << "which is not currently supported. You can deploy with bundler version:\n"
version_hash.keys.each do |v|
msg << " - `#{v}.x`\n"
end
msg << "\nTo use another version of bundler, update your `Gemfile.lock` to point\n"
msg << "to a supported version. For example:\n"
msg << "\n"
msg << "```\n"
msg << "BUNDLED WITH\n"
msg << " #{version_hash["1"]}\n"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why index 1 instead of 0?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the major version. So this will be "1.15.2"

msg << "```\n"
super msg
end
end

attr_reader :bundler_path
attr_reader :bundler_path

def initialize(options = {})
@fetcher = options[:fetcher] || DEFAULT_FETCHER
@bundler_tmp = Dir.mktmpdir
@bundler_path = options[:bundler_path] || File.join(@bundler_tmp, "#{BUNDLER_DIR_NAME}")
@gemfile_path = options[:gemfile_path] || GEMFILE_PATH
@bundler_tar = options[:bundler_tar] || "#{BUNDLER_DIR_NAME}.tgz"
@gemfile_lock_path = "#{@gemfile_path}.lock"
@bundler_tmp = Pathname.new(Dir.mktmpdir)
@fetcher = options[:fetcher] || LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL) # coupling
@gemfile_path = options[:gemfile_path] || Pathname.new("./Gemfile")
@gemfile_lock_path = Pathname.new("#{@gemfile_path}.lock")
detect_bundler_version_and_dir_name!

@bundler_path = options[:bundler_path] || @bundler_tmp.join(dir_name)
@bundler_tar = options[:bundler_tar] || "bundler/#{dir_name}.tgz"
@orig_bundle_gemfile = ENV['BUNDLE_GEMFILE']
ENV['BUNDLE_GEMFILE'] = @gemfile_path.to_s
@path = Pathname.new "#{@bundler_path}/gems/#{BUNDLER_DIR_NAME}/lib"
@path = Pathname.new("#{@bundler_path}/gems/#{dir_name}/lib")
end

def install
ENV['BUNDLE_GEMFILE'] = @gemfile_path.to_s

fetch_bundler
$LOAD_PATH << @path
require "bundler"
Expand All @@ -40,14 +92,7 @@ def install

def clean
ENV['BUNDLE_GEMFILE'] = @orig_bundle_gemfile
FileUtils.remove_entry_secure(@bundler_tmp) if Dir.exist?(@bundler_tmp)

if LanguagePack::Ruby::BUNDLER_VERSION == "1.7.12"
# Hack to cleanup after pre 1.8 versions of bundler. See https://github.com/bundler/bundler/pull/3277/
Dir["#{Dir.tmpdir}/bundler*"].each do |dir|
FileUtils.remove_entry_secure(dir) if Dir.exist?(dir) && File.stat(dir).writable?
end
end
@bundler_tmp.rmtree if @bundler_tmp.directory?
end

def has_gem?(name)
Expand All @@ -71,15 +116,19 @@ def windows_gemfile_lock?
end

def specs
@specs ||= lockfile_parser.specs.each_with_object({}) {|spec, hash| hash[spec.name] = spec }
@specs ||= lockfile_parser.specs.each_with_object({}) {|spec, hash| hash[spec.name] = spec }
end

def platforms
@platforms ||= lockfile_parser.platforms
end

def version
Bundler::VERSION
@version
end

def dir_name
"bundler-#{version}"
end

def instrument(*args, &block)
Expand All @@ -89,7 +138,7 @@ def instrument(*args, &block)
def ruby_version
instrument 'detect_ruby_version' do
env = { "PATH" => "#{bundler_path}/bin:#{ENV['PATH']}",
"RUBYLIB" => File.join(bundler_path, "gems", BUNDLER_DIR_NAME, "lib"),
"RUBYLIB" => File.join(bundler_path, "gems", dir_name, "lib"),
"GEM_PATH" => "#{bundler_path}:#{ENV["GEM_PATH"]}",
"BUNDLE_DISABLE_VERSION_CHECK" => "true"
}
Expand Down Expand Up @@ -130,4 +179,27 @@ def parse_gemfile_lock
Bundler::LockfileParser.new(gemfile_contents)
end
end

def major_bundler_version
# https://rubular.com/r/jt9yj0aY7fU3hD
bundler_version_match = @gemfile_lock_path.read.match(/^BUNDLED WITH$(\r?\n) (?<major>\d+)\.\d+\.\d+/m)

if bundler_version_match
bundler_version_match[:major]
else
"1"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we actually fail instead of defaulting?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

People can still deploy without having a BUNDLED WITH in their Gemfile.lock. For example in some of our hatchet test apps https://github.com/sharpstone/default_ruby/blob/master/Gemfile.lock.

end
end

# You cannot use Bundler 2.x with a Gemfile.lock that points to a 1.x bundler
# version. The solution here is to read in the value set in the Gemfile.lock
# and download the "blessed" version with the same major version.
def detect_bundler_version_and_dir_name!
major = major_bundler_version
if BLESSED_BUNDLER_VERSIONS.key?(major)
@version = BLESSED_BUNDLER_VERSIONS[major]
else
raise UnsupportedBundlerVersion.new(BLESSED_BUNDLER_VERSIONS, major)
end
end
end
14 changes: 6 additions & 8 deletions lib/language_pack/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ class LanguagePack::Ruby < LanguagePack::Base
NAME = "ruby"
LIBYAML_VERSION = "0.1.7"
LIBYAML_PATH = "libyaml-#{LIBYAML_VERSION}"
BUNDLER_VERSION = "1.15.2"
BUNDLER_GEM_PATH = "bundler-#{BUNDLER_VERSION}"
RBX_BASE_URL = "http://binaries.rubini.us/heroku"
NODE_BP_PATH = "vendor/node/bin"

Expand Down Expand Up @@ -126,9 +124,9 @@ def config_detect
def warn_bundler_upgrade
old_bundler_version = @metadata.read("bundler_version").chomp if @metadata.exists?("bundler_version")

if old_bundler_version && old_bundler_version != BUNDLER_VERSION
if old_bundler_version && old_bundler_version != bundler.version
puts(<<-WARNING)
Your app was upgraded to bundler #{ BUNDLER_VERSION }.
Your app was upgraded to bundler #{ bundler.version }.
Previously you had a successful deploy with bundler #{ old_bundler_version }.

If you see problems related to the bundler version please refer to:
Expand Down Expand Up @@ -596,7 +594,7 @@ def bundler_binstubs_path
end

def bundler_path
@bundler_path ||= "#{slug_vendor_base}/gems/#{BUNDLER_GEM_PATH}"
@bundler_path ||= "#{slug_vendor_base}/gems/#{bundler.dir_name}"
end

def write_bundler_shim(path)
Expand All @@ -607,7 +605,7 @@ def write_bundler_shim(path)
#!/usr/bin/env ruby
require 'rubygems'

version = "#{BUNDLER_VERSION}"
version = "#{bundler.version}"

if ARGV.first
str = ARGV.first
Expand Down Expand Up @@ -678,7 +676,7 @@ def build_bundler(default_bundle_without)
yaml_include = File.expand_path("#{libyaml_dir}/include").shellescape
yaml_lib = File.expand_path("#{libyaml_dir}/lib").shellescape
pwd = Dir.pwd
bundler_path = "#{pwd}/#{slug_vendor_base}/gems/#{BUNDLER_GEM_PATH}/lib"
bundler_path = "#{pwd}/#{slug_vendor_base}/gems/#{bundler.dir_name}/lib"
# we need to set BUNDLE_CONFIG and BUNDLE_GEMFILE for
# codon since it uses bundler.
env_vars = {
Expand Down Expand Up @@ -1081,7 +1079,7 @@ def load_bundler_cache
FileUtils.mkdir_p(heroku_metadata)
@metadata.write(ruby_version_cache, full_ruby_version, false)
@metadata.write(buildpack_version_cache, BUILDPACK_VERSION, false)
@metadata.write(bundler_version_cache, BUNDLER_VERSION, false)
@metadata.write(bundler_version_cache, bundler.version, false)
@metadata.write(rubygems_version_cache, rubygems_version, false)
@metadata.write(stack_cache, @stack, false)
@metadata.save
Expand Down
11 changes: 11 additions & 0 deletions spec/hatchet/bundler_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'spec_helper'

describe "Bundler" do
it "deploys with version 2.x" do
before_deploy = -> { run!(%Q{printf "\nBUNDLED WITH\n 2.0.1\n" >> Gemfile.lock}) }

Hatchet::Runner.new("default_ruby", before_deploy: before_deploy).deploy do |app|
expect(app.output).to match("Installing dependencies using bundler 2.")
end
end
end
6 changes: 3 additions & 3 deletions spec/helpers/fetcher_spec.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
require 'spec_helper'

describe "Fetches" do

it "bundler" do
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
FileUtils.touch("Gemfile.lock")

fetcher = LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL)
fetcher.fetch_untar("#{LanguagePack::Ruby::BUNDLER_GEM_PATH}.tgz")
fetcher.fetch_untar("#{LanguagePack::Helpers::BundlerWrapper.new.dir_name}.tgz")
expect(`ls bin`).to match("bundle")
end
end
end
end

7 changes: 5 additions & 2 deletions spec/helpers/rails_runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

describe "Rails Runner" do
around(:each) do |test|
original_path = ENV["PATH"]
ENV["PATH"] = "./bin/:#{ENV['PATH']}"

Dir.mktmpdir do |tmpdir|
@tmpdir = tmpdir
Dir.chdir(tmpdir) do
test.run
end
end
ensure
ENV["PATH"] = original_path if original_path
end

it "config objects build propperly formatted commands" do
Expand Down Expand Up @@ -119,7 +123,6 @@ def to_s
FileUtils.mkdir("bin")
File.open("bin/rails", "w") { |f| f << executable_contents }
File.chmod(0777, "bin/rails")
ENV["PATH"] = "./bin/:#{ENV['PATH']}" unless ENV["PATH"].include?("./bin:")

# BUILDPACK_LOG_FILE support for logging
FileUtils.mkdir("tmp")
Expand Down