Skip to content

Commit

Permalink
Merge pull request #106 from github/ruby_packer_ruby_version
Browse files Browse the repository at this point in the history
Use ruby version from host for enumerating gems with licensed executable
  • Loading branch information
jonabc authored Oct 30, 2018
2 parents 520fd2a + e1b9cd6 commit 2d966bb
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 5 deletions.
12 changes: 12 additions & 0 deletions docs/sources/bundler.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

The bundler source will detect dependencies `Gemfile` and `Gemfile.lock` files are found at an apps `source_path`. The source uses the `Bundler` API to enumerate dependencies from `Gemfile` and `Gemfile.lock`.

### Enumerating bundler dependencies when using the licensed executable

**Note** this content only applies to running licensed from an executable. It does not apply when using licensed as a gem.

_It is strongly recommended that ruby is always available when running the licensed executable._

The licensed executable contains and runs a version of ruby. When using the Bundler APIs, a mismatch between the version of ruby built into the licensed executable and the version of licensed used during `bundle install` can occur. This mismatch can lead to licensed raising errors due to not finding dependencies.

For example, if `bundle install` was run with ruby 2.5.0 then the bundler specification path would be `<bundle path>/ruby/2.5.0/specifications`. However, if the licensed executable contains ruby 2.4.0, then licensed will be looking for specifications at `<bundle path>/ruby/2.4.0/specifications`. That path may not exist, or it may contain invalid or stale content.

If ruby is available when running licensed, licensed will determine the ruby version used during `bundle install` and prefer that version string over the version contained in the executable. If bundler is also available, then the ruby command will be run from a `bundle exec` context.

### Excluding gem groups

The bundler source determines which gem groups to include or exclude with the following logic, in order of precedence.
Expand Down
8 changes: 4 additions & 4 deletions lib/licensed/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ class AppConfiguration < Hash
].freeze
SOURCE_TYPES = Source.constants.map { |c| Source.const_get(c) }.freeze

attr_reader :ui

def initialize(options = {}, inherited_options = {})
super()

@ui = Licensed::UI::Shell.new

# update order:
# 1. anything inherited from root config
# 2. app defaults
Expand Down Expand Up @@ -120,8 +124,6 @@ def verify_arg(property)
class Configuration < AppConfiguration
class LoadError < StandardError; end

attr_accessor :ui

# Loads and returns a Licensed::Configuration object from the given path.
# The path can be relative or absolute, and can point at a file or directory.
# If the path given is a directory, the directory will be searched for a
Expand All @@ -133,8 +135,6 @@ def self.load_from(path)
end

def initialize(options = {})
@ui = Licensed::UI::Shell.new

apps = options.delete("apps") || []
super(default_options.merge(options))

Expand Down
45 changes: 44 additions & 1 deletion lib/licensed/source/bundler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def bundle_exec_gem_spec(name)
return unless Licensed::Shell.tool_available?("gem")

# use `gem specification` with a clean ENV to get gem specification YAML
yaml = ::Bundler.with_original_env { Licensed::Shell.execute("gem", "specification", name) }
yaml = ::Bundler.with_original_env { Licensed::Shell.execute(*ruby_command_args("gem", "specification", name)) }
Gem::Specification.from_yaml(yaml)
end

Expand Down Expand Up @@ -177,6 +177,23 @@ def lockfile_path
@lockfile_path ||= gemfile_path.dirname.join("#{gemfile_path.basename}.lock")
end

# Returns the configured bundler executable to use, or "bundle" by default.
def bundler_exe
@bundler_exe ||= begin
exe = @config.dig("rubygem", "bundler_exe")
return "bundle" unless exe
return exe if Licensed::Shell.tool_available?(exe)
@config.root.join(exe)
end
end

# Determines if the configured bundler executable is available and returns
# shell command args with or without `bundle exec` depending on availability.
def ruby_command_args(*args)
return Array(args) unless Licensed::Shell.tool_available?(bundler_exe)
[bundler_exe, "exec", *args]
end

private

# helper to clear all bundler environment around a yielded block
Expand All @@ -191,6 +208,26 @@ def with_local_configuration
# of the ruby-packer filesystem. this is needed to find spec sources
# from the host filesystem
ENV["ENCLOSE_IO_RUBYC_1ST_PASS"] = "1"
ruby_version = Gem::ConfigMap[:ruby_version]

if Licensed::Shell.tool_available?("ruby")
# set the ruby version in Gem::ConfigMap to the ruby version from the host.
# this helps Bundler find the correct spec sources and paths
Gem::ConfigMap[:ruby_version] = host_ruby_version
else
# running a ruby-packer-built licensed exe when ruby and bundler aren't available
# is possible but could lead to errors if the host ruby version doesn't
# match the built executable's ruby version
@config.ui.warn <<~WARNING
Ruby wasn't found when enumerating bundler
dependencies using the licensed executable. This can cause a
ruby mismatch between licensed and bundled dependencies and a
failure to find gem specifications.
If licensed is unable to find gem specifications that you believe are present,
please ensure that ruby and bundler are available and try again.
WARNING
end
end

# reset all bundler configuration
Expand All @@ -203,6 +240,7 @@ def with_local_configuration
if ruby_packer?
# if running under ruby-packer, restore environment after block is finished
ENV.delete("ENCLOSE_IO_RUBYC_1ST_PASS")
Gem::ConfigMap[:ruby_version] = ruby_version
end

ENV["BUNDLE_GEMFILE"] = original_bundle_gemfile
Expand All @@ -215,6 +253,11 @@ def with_local_configuration
def ruby_packer?
@ruby_packer ||= RbConfig::TOPDIR =~ /__enclose_io_memfs__/
end

# Returns the ruby version found in the bundler environment
def host_ruby_version
Licensed::Shell.execute(*ruby_command_args("ruby", "-e", "puts Gem::ConfigMap[:ruby_version]"))
end
end
end
end
30 changes: 30 additions & 0 deletions test/source/bundler_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -196,5 +196,35 @@
end
end
end

describe "bundler_exe" do
it "returns bundle if not configured" do
assert_equal "bundle", source.bundler_exe
end

it "returns the configured value if specifying an available tool" do
(config["rubygem"] ||= {})["bundler_exe"] = "ruby"
assert_equal "ruby", source.bundler_exe
end

it "returns the configured value relative to the configuration root" do
(config["rubygem"] ||= {})["bundler_exe"] = "lib/licensed.rb"
assert_equal config.root.join("lib/licensed.rb"), source.bundler_exe
end
end

describe "ruby_command_args" do
it "returns 'bundle exec args' when bundler exe is available'" do
Licensed::Shell.stub(:tool_available?, true) do
assert_equal "bundle exec test", source.ruby_command_args("test").join(" ")
end
end

it "returns args when bundler exe is not available'" do
Licensed::Shell.stub(:tool_available?, false) do
assert_equal "test", source.ruby_command_args("test").join(" ")
end
end
end
end
end

0 comments on commit 2d966bb

Please sign in to comment.