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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
## 2.1.2

- #649: fix: further improve isolation of warbled jars/wars from host bundler configuration
- #648: fix: further improve isolation of warbled jars/wars from user/host gem paths
- #465: fix: remove duplication of bundler path gems in warbled jars/wars
- #331: fix: correct support for `gem_excludes` inside bundler git specs
- chore: relax jruby-rack requirement to allow compatibility with upcoming 2.0.x

## 2.1.1
Expand Down
6 changes: 4 additions & 2 deletions lib/warbler/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ class Config
# Whether to include dependent gems (default true)
attr_accessor :gem_dependencies

# Array of regular expressions matching relative paths in gems to
# be excluded from the war. Default contains no exclusions.
# Array of regular expressions matching paths *inside* packed gems to be
# excluded from the archive. Paths are matched relative to each gem's root
# (for git-sourced gems: the repository checkout root). Default contains no
# exclusions.
attr_accessor :gem_excludes

# Whether to exclude **/*.log files (default is true)
Expand Down
2 changes: 1 addition & 1 deletion lib/warbler/jar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ def find_single_gem_files(config, spec)
# OK if the gem does not exists as its un-packed on the "shared" path
# ... at least gem spec.spec_file should exists although not crucial
if JRUBY_VERSION != JRubyJars::VERSION
warn "skipping #{spec.name} default gem (assuming its part of jruby-jars #{JRubyJars::VERSION})" unless silent?
warn "skipping #{spec.name} default gem (assuming its part of jruby-jars #{JRubyJars::VERSION})"
end
else
warn "skipping #{spec.name} gem (#{full_gem_path.to_s} does not exist)"
Expand Down
3 changes: 0 additions & 3 deletions lib/warbler/templates/bundler.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
ENV['BUNDLE_WITHOUT'] = '<%= config.bundle_without.join(':') %>'
<% if config.bundler[:frozen] -%>
ENV['BUNDLE_FROZEN'] = '1'
<% end -%>
117 changes: 75 additions & 42 deletions lib/warbler/traits/bundler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ class Bundler
include PathmapHelper
include BundlerHelper

# Bundler settings enforced in the packed application via a generated
# *.bundle/config*, the highest-precedence documented configuration level:
BUNDLE_CONFIG_DEFAULTS = {
# BUNDLE_VERSION: disable bundler's "auto-switch" to Gemfile.lock's BUNDLED WITH bundler version
# (bundler restarts the process to do so, which cannot work inside a servlet container or self-contained executable jar)
'BUNDLE_VERSION' => 'system',
# BUNDLE_FROZEN: fail fast with a descriptive error on Gemfile vs Gemfile.lock drift instead of attempting
# a runtime re-resolution
'BUNDLE_FROZEN' => 'true',
# BUNDLE_PATH__SYSTEM (path.system): pin bundler to the "system" gems - which are the packed gems, via
# GEM_HOME/GEM_PATH from *init.rb* - immune to a BUNDLE_PATH/BUNDLE_DEPLOYMENT leaking in from the host's
# environment or ~/.bundle/config
'BUNDLE_PATH__SYSTEM' => 'true',
# BUNDLE_AUTO_INSTALL: never install gems at boot time, even if the host's environment or global config enables it
'BUNDLE_AUTO_INSTALL' => 'false'
}.freeze

def self.detect?
File.exist?(ENV['BUNDLE_GEMFILE'] || 'Gemfile')
end
Expand All @@ -36,6 +53,8 @@ def add_bundler_gems; require 'bundler'
config.gem_dependencies = false # Bundler takes care of these
config.bundler = {} if config.bundler == true

warn_on_bundler_version_mismatch

bundler_specs.each do |spec|
spec = to_spec(spec)

Expand All @@ -45,25 +64,25 @@ def add_bundler_gems; require 'bundler'
config.bundler[:git_specs] << spec
when ::Bundler::Source::Path
unless bundler_source_is_warbled_gem_itself?(spec.source)
if spec.source.path && spec.source.path.relative?
# include (assuming) relative *[APP_ROOT]/gem/path*
# NOTE: might be tuned to only add gemspec.files ...
if spec.source.path&.relative?
# pack the gem at its relative *[APP_ROOT]/gem/path* location - at runtime bundler resolves the path
# source relative to the packed Gemfile.
# Deliberately NOT also added to config.gems: bundler never materializes path gems from the gem
# repository; doing so would cause duplication.
config.includes += FileList[File.join(spec.source.path, '**/*')]
config.gems << spec # probably not really needed
else
warn("Bundler `path' components are not fully supported.\n" +
"The `#{spec.full_name}' component was not bundled.\n" +
"Your application may fail to boot!")
end
end
else
config.gems << spec
config.gems << spec unless spec.respond_to?(:default_gem?) && spec.default_gem?
end
end
config.bundler[:gemfile] = ::Bundler.default_gemfile
config.bundler[:gemfile_path] = apply_pathmaps(config, relative_from_pwd(::Bundler.default_gemfile), :application)
config.bundler[:lockfile] = ::Bundler.default_lockfile
config.bundler[:frozen] = ::Bundler.settings[:frozen]
path = ::Bundler.settings[:path]
config.excludes += [path, "#{path}/**/*"] if path
config.init_contents << "#{config.warbler_templates}/bundler.erb"
Expand All @@ -73,42 +92,41 @@ def update_archive(jar)
add_bundler_files(jar) if config.bundler
end

# Add Bundler Gemfiles and git repositories to the archive.
# Add Bundler Gemfiles, .bundle/config and git repositories to the archive.
def add_bundler_files(jar)
gemfile = relative_from_pwd(config.bundler[:gemfile])
lockfile = relative_from_pwd(config.bundler[:lockfile])
bundle_config = File.join('.bundle', 'config')

jar.files[apply_pathmaps(config, gemfile, :application)] = config.bundler[:gemfile].to_s
if File.exist?(lockfile)
jar.files[apply_pathmaps(config, lockfile, :application)] = config.bundler[:lockfile].to_s
end
if config.bundler[:git_specs]
pathmap = "#{config.relative_gem_path}/bundler/gems/%p"
pathmap.sub!(%r{^/+}, '')
config.pathmaps.git = [pathmap]
config.bundler[:git_specs].each do |spec|
full_gem_path = Pathname.new(spec.full_gem_path)

gem_relative_path = full_gem_path.relative_path_from(::Bundler.install_path)
filenames = []
gem_relative_path.each_filename { |f| filenames << f }

exclude_gems = true
unless filenames.empty?
full_gem_path = Pathname.new(::Bundler.install_path) + filenames.first
exclude_gems = false
end
jar.files[apply_pathmaps(config, lockfile, :application)] = config.bundler[:lockfile].to_s if File.exist?(lockfile)
# NOTE: in-memory content must be an IO - jar creation treats plain Strings as source file paths
jar.files[apply_pathmaps(config, bundle_config, :application)] = StringIO.new(bundle_config_contents)

if spec.groups.include?(:warbler_excluded)
pattern = "#{full_gem_path.to_s}/**/#{spec.name}.gemspec" # #42: gemspec only to avert Bundler error
else
pattern = "#{full_gem_path.to_s}/**/*"
end
add_bundler_git_specs(config.bundler[:git_specs], jar) if config.bundler[:git_specs]
end

FileList[pattern].each do |src|
f = Pathname.new(src).relative_path_from(full_gem_path).to_s
next if exclude_gems && config.gem_excludes && config.gem_excludes.any? {|rx| f =~ rx }
jar.files[apply_pathmaps(config, File.join(full_gem_path.basename, f), :git)] = src
end
private

def add_bundler_git_specs(git_specs, jar)
config.pathmaps.git = ["#{config.relative_gem_path}/bundler/gems/%p".sub(%r{^/+}, '')]

# a git source checkout may contain multiple gems (spec.full_gem_path being a sub-directory) - bundler expects
# the complete repository checkout under bundler/gems/<repo>-<ref>, so pack from its root (once per repository,
# even when several specs share the checkout)
checkout_paths = git_specs.map do |spec|
full_gem_path = Pathname.new(spec.full_gem_path)
filenames = full_gem_path.relative_path_from(::Bundler.install_path).each_filename.to_a
filenames.empty? ? full_gem_path : Pathname.new(::Bundler.install_path) + filenames.first
end.uniq

checkout_paths.each do |checkout_path|
FileList["#{checkout_path.to_s}/**/*"].each do |src|
f = Pathname.new(src).relative_path_from(checkout_path).to_s
# NOTE: for git sources the excludes match relative to the packed repository checkout root (the gem root,
# except multi-gem repos)
next if config.gem_excludes && config.gem_excludes.any? { |rx| f =~ rx }
jar.files[apply_pathmaps(config, File.join(checkout_path.basename, f), :git)] = src
end
end
end
Expand All @@ -121,17 +139,32 @@ def relative_from_pwd(path)
end
end

private
def warn_on_bundler_version_mismatch
lockfile = ::Bundler.default_lockfile
return unless lockfile && File.exist?(lockfile)
locked = ::Bundler::LockfileParser.new(File.read(lockfile)).bundler_version rescue nil
if locked && locked.to_s != ::Bundler::VERSION
warn("Gemfile.lock BUNDLED WITH (#{locked}) does not match the bundler running warbler (#{::Bundler::VERSION}).\n" +
"The packed application will boot with the default bundler of the packed JRuby (jruby-jars),\n" +
"consider re-generating Gemfile.lock with a matching bundler version.")
end
end

# Contents for the packed *.bundle/config*: only warbler's deployment settings, deliberately independent of the
# application's build-time bundler configuration.
def bundle_config_contents
require 'yaml'
settings = BUNDLE_CONFIG_DEFAULTS.dup
# frozen mode errors without a lockfile - do not force it upon applications packed without a Gemfile.lock
settings.delete('BUNDLE_FROZEN') if config.bundler[:frozen] == false || !File.exist?(config.bundler[:lockfile].to_s)
settings.to_yaml
end

def bundler_specs
bundle_without = config.bundle_without.map { |s| s.to_sym }
definition = ::Bundler.definition
all = definition.specs.to_a
requested_groups = definition.groups - bundle_without
requested = requested_groups.empty? ? [] : definition.specs_for(requested_groups).to_a
excluded_git_specs = (all - requested).select { |spec| ::Bundler::Source::Git === spec.source }
excluded_git_specs.each { |spec| spec.groups << :warbler_excluded }
requested + excluded_git_specs
requested_groups.empty? ? [] : definition.specs_for(requested_groups).to_a
end

def bundler_source_is_warbled_gem_itself?(source)
Expand Down
Loading