Skip to content

Commit

Permalink
Merge pull request #392 from github/reload-bundler-runtime
Browse files Browse the repository at this point in the history
Remove more custom bundler logic and reload the bundler runtime for each app
  • Loading branch information
jonabc authored Sep 6, 2021
2 parents bee6d16 + dffc7f4 commit 8754a56
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 157 deletions.
102 changes: 31 additions & 71 deletions lib/licensed/sources/bundler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
begin
require "bundler"
require "licensed/sources/bundler/missing_specification"
require "licensed/sources/bundler/definition"
rescue LoadError
end

Expand Down Expand Up @@ -37,23 +38,27 @@ def spec_file
end
end

GEMFILES = { "Gemfile" => "Gemfile.lock", "gems.rb" => "gems.locked" }
DEFAULT_WITHOUT_GROUPS = %i{development test}
RUBY_PACKER_ERROR = "The bundler source cannot be used from the executable built with ruby-packer. Please install licensed using `gem install` or using bundler."

def enabled?
# running a ruby-packer-built licensed exe when ruby isn't available
# could lead to errors if the host ruby doesn't exist
return false if ruby_packer? && !Licensed::Shell.tool_available?("ruby")
defined?(::Bundler) && lockfile_path && lockfile_path.exist?

# if Bundler isn't loaded, this enumerator won't work!
return false unless defined?(::Bundler)

with_application_environment { ::Bundler.default_lockfile&.exist? }
rescue ::Bundler::GemfileNotFound
false
end

def enumerate_dependencies
raise Licensed::Sources::Source::Error.new(RUBY_PACKER_ERROR) if ruby_packer?

with_local_configuration do
specs.map do |spec|
next if spec.name == "bundler" && !include_bundler?
with_application_environment do
definition.specs.map do |spec|
next if spec.name == config["name"]

error = spec.error if spec.respond_to?(:error)
Expand All @@ -73,41 +78,13 @@ def enumerate_dependencies
end
end

# Returns an array of Gem::Specifications for all gem dependencies
def specs
@specs ||= definition.specs_for(groups)
end

# Returns whether to include bundler as a listed dependency of the project
def include_bundler?
@include_bundler ||= begin
# include if bundler is listed as a direct dependency that should be included
requested_dependencies = definition.dependencies.select { |d| (d.groups & groups).any? && d.should_include? }
return true if requested_dependencies.any? { |d| d.name == "bundler" }
# include if bundler is an indirect dependency
return true if specs.flat_map(&:dependencies).any? { |d| d.name == "bundler" }
false
end
end

# Build the bundler definition
def definition
@definition ||= ::Bundler::Definition.build(gemfile_path, lockfile_path, nil)
end

# Returns the bundle definition groups, removing "without" groups,
# and including "with" groups
def groups
@groups ||= definition.groups - bundler_setting_array(:without) + bundler_setting_array(:with) - exclude_groups
end

# Returns a bundler setting as an array.
# Depending on the version of bundler, array values are either returned as
# a raw string ("a:b:c") or as an array ([:a, :b, :c])
def bundler_setting_array(key)
setting = ::Bundler.settings[key]
setting = setting.split(":").map(&:to_sym) if setting.is_a?(String)
Array(setting)
@definition ||= begin
definition = ::Bundler::Definition.build(::Bundler.default_gemfile, ::Bundler.default_lockfile, nil)
definition.extend Licensed::Bundler::DefinitionExtensions
definition.force_exclude_groups = exclude_groups
definition
end
end

# Returns any groups to exclude specified from both licensed configuration
Expand All @@ -121,46 +98,29 @@ def exclude_groups
end
end

# Returns the path to the Bundler Gemfile
def gemfile_path
@gemfile_path ||= GEMFILES.keys
.map { |g| config.pwd.join g }
.find { |f| f.exist? }
end

# Returns the path to the Bundler Gemfile.lock
def lockfile_path
return unless gemfile_path
@lockfile_path ||= gemfile_path.dirname.join(GEMFILES[gemfile_path.basename.to_s])
end

# helper to clear all bundler environment around a yielded block
def with_local_configuration
# silence any bundler warnings while running licensed
bundler_ui, ::Bundler.ui = ::Bundler.ui, ::Bundler::UI::Silent.new
def with_application_environment
backup = nil

original_bundle_gemfile = nil
if gemfile_path.to_s != ENV["BUNDLE_GEMFILE"]
# force bundler to use the local gem file
original_bundle_gemfile, ENV["BUNDLE_GEMFILE"] = ENV["BUNDLE_GEMFILE"], gemfile_path.to_s
::Bundler.ui.silence do
if ::Bundler.root != config.source_path
backup = ENV.to_hash
ENV.replace(::Bundler.original_env)

# reset all bundler configuration
::Bundler.reset!
# and re-configure with settings for current directory
::Bundler.configure
end
# reset bundler to load from the current app's source path
::Bundler.reset!
::Bundler.load
end

yield
yield
end
ensure
if original_bundle_gemfile
ENV["BUNDLE_GEMFILE"] = original_bundle_gemfile

if backup
# restore bundler configuration
ENV.replace(backup)
::Bundler.reset!
::Bundler.configure
::Bundler.load
end

::Bundler.ui = bundler_ui
end

# Returns whether the current licensed execution is running ruby-packer
Expand Down
36 changes: 36 additions & 0 deletions lib/licensed/sources/bundler/definition.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module Licensed
module Bundler
module DefinitionExtensions
attr_accessor :force_exclude_groups

# Override specs to avoid logic that would raise Gem::NotFound
# which is handled in this ./missing_specification.rb, and to not add
# bundler as a dependency if it's not a user-requested gem.
#
# Newer versions of Bundler have changed the implementation of specs_for
# as well which no longer calls this function. Overriding this function
# gives a stable access point for licensed
def specs
@specs ||= begin
specs = resolve.materialize(requested_dependencies)

all_dependencies = requested_dependencies.concat(specs.flat_map(&:dependencies))
if all_dependencies.any? { |d| d.name == "bundler" } && !specs["bundler"].any?
bundler = sources.metadata_source.specs.search(Gem::Dependency.new("bundler", ::Bundler::VERSION)).last
specs["bundler"] = bundler
end

specs
end
end

# Override requested_groups to also exclude any groups that are
# in the "bundler.without" section of the licensed configuration file.
def requested_groups
super - Array(force_exclude_groups)
end
end
end
end
100 changes: 14 additions & 86 deletions test/sources/bundler_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,88 +34,6 @@
end
end

describe "gemfile_path" do
bundle_gemfile = ENV["BUNDLE_GEMFILE"]
after do
ENV["BUNDLE_GEMFILE"] = bundle_gemfile
end

it "returns a the path to Gemfile local to the current directory" do
Dir.mktmpdir do |tmp|
bundle_gemfile_path = File.join(tmp, "gems.rb")
File.write(bundle_gemfile_path, "")
ENV["BUNDLE_GEMFILE"] = bundle_gemfile_path

path = File.join(tmp, "bundler")
Dir.mkdir(path)
Dir.chdir(path) do
File.write("Gemfile", "")
assert_equal Pathname.pwd.join("Gemfile"), source.gemfile_path
end
end
end

it "returns a the path to gems.rb local to the current directory" do
Dir.mktmpdir do |tmp|
bundle_gemfile_path = File.join(tmp, "Gemfile")
File.write(bundle_gemfile_path, "")
ENV["BUNDLE_GEMFILE"] = bundle_gemfile_path

path = File.join(tmp, "bundler")
Dir.mkdir(path)
Dir.chdir(path) do
File.write("gems.rb", "")
assert_equal Pathname.pwd.join("gems.rb"), source.gemfile_path
end
end
end

it "prefers Gemfile over gems.rb" do
Dir.mktmpdir do |tmp|
Dir.chdir(tmp) do
File.write("Gemfile", "")
File.write("gems.rb", "")
assert_equal Pathname.pwd.join("Gemfile"), source.gemfile_path
end
end
end

it "returns nil if a gem file can't be found" do
ENV["BUNDLE_GEMFILE"] = nil
Dir.mktmpdir do |tmp|
Dir.chdir(tmp) do
assert_nil source.gemfile_path
end
end
end
end

describe "lockfile_path" do
it "returns nil if gemfile_path is nil" do
source.stub(:gemfile_path, nil) do
assert_nil source.lockfile_path
end
end

it "returns Gemfile.lock for Gemfile gemfile_path" do
Dir.mktmpdir do |tmp|
Dir.chdir(tmp) do
File.write("Gemfile", "")
assert_equal Pathname.pwd.join("Gemfile.lock"), source.lockfile_path
end
end
end

it "returns gems.locked for gems.rb gemfile_path" do
Dir.mktmpdir do |tmp|
Dir.chdir(tmp) do
File.write("gems.rb", "")
assert_equal Pathname.pwd.join("gems.locked"), source.lockfile_path
end
end
end
end

describe "dependencies" do
it "does not include the source project" do
Dir.chdir(fixtures) do
Expand Down Expand Up @@ -258,12 +176,12 @@
end
end

describe "#with_local_configuration" do
describe "#with_application_environment" do
it "resets the Bundler environment" do
begin
original_gem_home, ENV["GEM_HOME"] = ENV["GEM_HOME"], "foo"
Dir.chdir(fixtures) do
source.with_local_configuration do
source.with_application_environment do
refute_equal "foo", ENV["GEM_HOME"]
end
end
Expand All @@ -275,9 +193,9 @@
it "does not reset Bundler environment when the correct environment is already set" do
begin
original_gem_home, ENV["GEM_HOME"] = ENV["GEM_HOME"], "foo"
original_bundle_gemfile, ENV["BUNDLE_GEMFILE"] = ENV["BUNDLE_GEMFILE"], source.gemfile_path.to_s
original_bundle_gemfile, ENV["BUNDLE_GEMFILE"] = ENV["BUNDLE_GEMFILE"], source.config.source_path.join("Gemfile").to_s
Dir.chdir(fixtures) do
source.with_local_configuration do
source.with_application_environment do
assert_equal "foo", ENV["GEM_HOME"]
end
end
Expand All @@ -286,6 +204,16 @@
ENV["GEM_HOME"] = original_gem_home
end
end

it "reloads the Bundler runtime to the applications configured source_path" do
Dir.chdir(fixtures) do
refute_equal config.source_path, ::Bundler.load.root
source.with_application_environment do
assert_equal config.source_path, ::Bundler.load.root
end
refute_equal config.source_path, ::Bundler.load.root
end
end
end
end
end

0 comments on commit 8754a56

Please sign in to comment.