diff options
Diffstat (limited to 'tasks/gemfile_util.rb')
-rw-r--r-- | tasks/gemfile_util.rb | 263 |
1 files changed, 191 insertions, 72 deletions
diff --git a/tasks/gemfile_util.rb b/tasks/gemfile_util.rb index 60d1e2ff31..96dfcd78a2 100644 --- a/tasks/gemfile_util.rb +++ b/tasks/gemfile_util.rb @@ -1,99 +1,218 @@ require "bundler" +require "set" module GemfileUtil # - # Given a set of dependencies with groups in them, and a resolved set of - # gemspecs (with dependency info in them), creates a full set of specs - # with group information on it. If A is in groups x and y, and A depends on - # B and C, then B and C are also in groups x and y. + # Adds `override: true`, which allows your statement to override any other + # gem statement about the same gem in the Gemfile. # - class GemGroups < Hash - def initialize(resolved) - @resolved = resolved - end - attr_reader :resolved + def gem(name, *args) + Bundler.ui.debug "gem #{name}, #{args.join(", ")}" + current_dep = dependencies.find { |dep| dep.name == name } - def add_dependency(dep) - add_gem_groups(dep.name, dep.groups) + # Set path to absolute in case this is an included Gemfile in bundler 1.11.2 and below + options = args[-1].is_a?(Hash) ? args[-1] : {} + if options[:path] + # path sourced gems are assumed to be overrides. + options[:override] = true + # options[:path] = File.expand_path(options[:path], Bundler.default_gemfile.dirname) end - - private - - def add_gem_groups(name, groups) - self[name] ||= [] - difference = groups - self[name] - unless difference.empty? - self[name] += difference - spec = resolved.find { |spec| spec.name == name } - if spec - spec.dependencies.each do |spec| - add_gem_groups(spec.name, difference) - end + # Handle override + if options[:override] + override = true + options.delete(:override) + if current_dep + dependencies.delete(current_dep) + end + else + # If an override gem already exists, and we're not an override gem, + # ignore this gem in favor of the override (but warn if they don't match) + if overridden_gems.include?(name) + args.pop if args[-1].is_a?(Hash) + version = args || [">=0"] + desired_dep = Bundler::Dependency.new(name, version, options.dup) + if desired_dep =~ current_dep + Bundler.ui.debug "Replaced Gemfile dependency #{desired_dep} (#{desired_dep.source}) with override gem #{current_dep} (#{current_dep.source})" + else + Bundler.ui.warn "Replaced Gemfile dependency #{desired_dep} (#{desired_dep.source}) with incompatible override gem #{current_dep} (#{current_dep.source})" end + return end end + + # Add the gem normally + super + + overridden_gems << name if override + + # Emit a warning if we're replacing a dep that doesn't match + if current_dep && override + added_dep = dependencies.find { |dep| dep.name == name } + if added_dep =~ current_dep + Bundler.ui.debug "Replaced Gemfile dependency #{current_dep} (#{current_dep.source}) with override gem #{added_dep} (#{added_dep.source})" + else + Bundler.ui.warn "Replaced Gemfile dependency #{current_dep} (#{current_dep.source}) with incompatible override gem #{added_dep} (#{added_dep.source})" + end + end + end + + def overridden_gems + @overridden_gems ||= Set.new end - def calculate_dependents(spec_set) - dependents = {} - spec_set.each do |spec| - dependents[spec] ||= [] + # + # Include all gems in the locked gemfile. + # + # @param gemfile Path to the Gemfile to load (relative to your Gemfile) + # @param groups A list of groups to include (whitelist). If not passed (or set + # to nil), all gems will be selected. + # @param without_groups A list of groups to ignore. Gems will be excluded from + # the results if all groups they belong to are ignored. + # This matches bundler's `without` behavior. + # @param gems A list of gems to include above and beyond the given groups. + # Gems in this list must be explicitly included in the Gemfile + # with a `gem "gem_name", ...` line or they will be silently + # ignored. + # + def include_locked_gemfile(gemfile, groups: nil, without_groups: nil, gems: []) + gemfile = File.expand_path(gemfile, Bundler.default_gemfile.dirname) + gems = Set.new(gems) + GemfileUtil.select_gems(gemfile, groups: nil, without_groups: nil) + specs = GemfileUtil.locked_gems("#{gemfile}.lock", gems) + specs.each do |name, version: nil, **options| + options = options.merge(override: true) + Bundler.ui.debug("Adding gem #{name}, #{version}, #{options} from #{gemfile}") + gem name, version, options end - spec_set.each do |spec| - spec.dependencies.each do |dep| - puts "#{dep.class} -> #{spec.class}" - dependents[dep] << spec - end + rescue + puts "ERROR: #{$!}" + puts $!.backtrace + raise + end + + # + # Include all gems in the locked gemfile. + # + # @param current_gemfile The Gemfile you are currently loading (`self`). + # @param gemfile Path to the Gemfile to load (relative to your Gemfile) + # @param groups A list of groups to include (whitelist). If not passed (or set + # to nil), all gems will be selected. + # @param without_groups A list of groups to ignore. Gems will be excluded from + # the results if all groups they belong to are ignored. + # This matches bundler's `without` behavior. + # @param gems A list of gems to include above and beyond the given groups. + # Gems in this list must be explicitly included in the Gemfile + # with a `gem "gem_name", ...` line or they will be silently + # ignored. + # + def self.include_locked_gemfile(current_gemfile, gemfile, groups: nil, without_groups: nil, gems: []) + current_gemfile.instance_eval do + extend GemfileUtil + include_locked_gemfile(gemfile, groups: groups, without_groups: without_groups, gems: []) end - dependents end - def include_locked_gemfile(gemfile) - # - # Read the gemfile and inject its locks as first-class dependencies - # - current_source = nil - bundle = Bundler::Definition.build(gemfile, "#{gemfile}.lock", nil) - - # Go through and create the actual gemfile from the given locks and - # groups. - bundle.resolve.sort_by { |spec| spec.name }.each do |spec| - # bundler can't be installed by bundler so don't pin it. - next if spec.name == "bundler" - dep = bundle.dependencies.find { |d| d.name == spec.name } - gem_metadata = "" - if dep - gem_metadata << ", groups: #{dep.groups.inspect}" if dep.groups != [:default] - gem_metadata << ", platforms: #{dep.platforms.inspect}" if dep.platforms && !dep.platforms.empty? + # + # Select the desired gems, sans dependencies, from the gemfile. + # + # @param gemfile Path to the Gemfile to load + # @param groups A list of groups to include (whitelist). If not passed (or set + # to nil), all gems will be selected. + # @param without_groups A list of groups to ignore. Gems will be excluded from + # the results if all groups they belong to are ignored. + # This matches bundler's `without` behavior. + # + # @return An array of strings with the names of the given gems. + # + def self.select_gems(gemfile, groups: nil, without_groups: nil) + Bundler.with_clean_env do + # Set BUNDLE_GEMFILE to the new gemfile temporarily so all bundler's things work + # This works around some issues in bundler 1.11.2. + ENV["BUNDLE_GEMFILE"] = gemfile + + parsed_gemfile = Bundler::Dsl.new + parsed_gemfile.eval_gemfile(gemfile) + deps = parsed_gemfile.dependencies.select do |dep| + dep_groups = dep.groups + dep_groups = dep_groups & groups if groups + dep_groups = dep_groups - without_groups if without_groups + dep_groups.any? end - case spec.source - when Bundler::Source::Rubygems - if current_source - if current_source != spec.source - raise "Gem #{spec.name} has source #{spec.source}, but other gems have #{current_source}. Multiple rubygems sources are not supported." + deps.map { |dep| dep.name } + end + end + + # + # Get all gems in the locked gemfile that start from the given gem set. + # + # @param lockfile Path to the Gemfile to load + # @param groups A list of groups to include (whitelist). If not passed (or set + # to nil), all gems will be selected. + # @param without_groups A list of groups to ignore. Gems will be excluded from + # the results if all groups they belong to are ignored. + # This matches bundler's `without` behavior. + # @param gems A list of gems to include above and beyond the given groups. + # Gems in this list must be explicitly included in the Gemfile + # with a `gem "gem_name", ...` line or they will be silently + # ignored. + # @param include_development_deps Whether to include development dependencies + # or runtime only. + # + # @return Hash[String, Hash] A hash from gem_name -> { version: <version>, source: <source>, git: <git>, path: <path>, ref: <ref> } + # + def self.locked_gems(lockfile, gems, include_development_deps: false) + # Grab all the specs from the lockfile + parsed_lockfile = Bundler::LockfileParser.new(IO.read(lockfile)) + specs = {} + parsed_lockfile.specs.each { |s| specs[s.name] = s } + + # Select the desired gems, as well as their dependencies + to_process = Array(gems) + results = {} + while to_process.any? + gem_name = to_process.pop + next if gem_name == "bundler" # can't be bundled. Messes things up. Stop it. + # Only process each gem once + unless results.has_key?(gem_name) + spec = specs[gem_name] + unless spec + raise "Gem #{gem_name.inspect} was requested but was not in #{lockfile}! Gems in lockfile: #{specs.keys}" + end + results[gem_name] = gem_metadata(spec, lockfile) + spec.dependencies.each do |dep| + if dep.type == :runtime || include_development_deps + to_process << dep.name end - else - current_source = spec.source - add_gemfile_line("source #{spec.source.remotes.first.to_s.inspect}", __LINE__) end - add_gemfile_line("gem #{spec.name.inspect}, #{spec.version.to_s.inspect}#{gem_metadata}", __LINE__) - when Bundler::Source::Git - add_gemfile_line("gem #{spec.name.inspect}, git: #{spec.source.uri.to_s.inspect}, ref: #{spec.source.revision.inspect}#{gem_metadata}", __LINE__) - when Bundler::Source::Path - add_gemfile_line("gem #{spec.name.inspect}, path: #{spec.source.path.to_s.inspect}#{gem_metadata}", __LINE__) - else - raise "Unknown source #{spec.source} for gem #{spec.name}" end end - rescue - puts $! - puts $!.backtrace - raise + + results end private - def add_gemfile_line(line, lineno) - instance_eval(line, __FILE__, lineno) + # + # Get metadata for the given Bundler spec (coming from a lockfile). + # + # @return Hash { version: <version>, git: <git>, path: <path>, source: <source>, ref: <ref> } + # + def self.gem_metadata(spec, lockfile) + # Copy source information from included Gemfile + result = {} + case spec.source + when Bundler::Source::Rubygems + result[:source] = spec.source.remotes.first.to_s + result[:version] = spec.version.to_s + when Bundler::Source::Git + result[:git] = spec.source.uri.to_s + result[:ref] = spec.source.revision + when Bundler::Source::Path + # Path is relative to the lockfile (if it's relative at all) + result[:path] = File.expand_path(spec.source.path.to_s, File.dirname(lockfile)) + else + raise "Unknown source #{spec.source} for gem #{spec.name}" + end + result end + end |