From 59c8d50653480bef3f24517296e6ddf937fdf6bc Mon Sep 17 00:00:00 2001 From: hsbt Date: Fri, 2 Nov 2018 23:07:56 +0000 Subject: Added bundler as default gems. Revisit [Feature #12733] * bin/*, lib/bundler/*, lib/bundler.rb, spec/bundler, man/*: Merge from latest stable branch of bundler/bundler repository and added workaround patches. I will backport them into upstream. * common.mk, defs/gmake.mk: Added `test-bundler` task for test suite of bundler. * tool/sync_default_gems.rb: Added sync task for bundler. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@65509 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- lib/bundler/injector.rb | 253 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 lib/bundler/injector.rb (limited to 'lib/bundler/injector.rb') diff --git a/lib/bundler/injector.rb b/lib/bundler/injector.rb new file mode 100644 index 0000000000..1bb29f0b36 --- /dev/null +++ b/lib/bundler/injector.rb @@ -0,0 +1,253 @@ +# frozen_string_literal: true + +module Bundler + class Injector + INJECTED_GEMS = "injected gems".freeze + + def self.inject(new_deps, options = {}) + injector = new(new_deps, options) + injector.inject(Bundler.default_gemfile, Bundler.default_lockfile) + end + + def self.remove(gems, options = {}) + injector = new(gems, options) + injector.remove(Bundler.default_gemfile, Bundler.default_lockfile) + end + + def initialize(deps, options = {}) + @deps = deps + @options = options + end + + # @param [Pathname] gemfile_path The Gemfile in which to inject the new dependency. + # @param [Pathname] lockfile_path The lockfile in which to inject the new dependency. + # @return [Array] + def inject(gemfile_path, lockfile_path) + if Bundler.frozen_bundle? + # ensure the lock and Gemfile are synced + Bundler.definition.ensure_equivalent_gemfile_and_lockfile(true) + end + + # temporarily unfreeze + Bundler.settings.temporary(:deployment => false, :frozen => false) do + # evaluate the Gemfile we have now + builder = Dsl.new + builder.eval_gemfile(gemfile_path) + + # don't inject any gems that are already in the Gemfile + @deps -= builder.dependencies + + # add new deps to the end of the in-memory Gemfile + # Set conservative versioning to false because + # we want to let the resolver resolve the version first + builder.eval_gemfile(INJECTED_GEMS, build_gem_lines(false)) if @deps.any? + + # resolve to see if the new deps broke anything + @definition = builder.to_definition(lockfile_path, {}) + @definition.resolve_remotely! + + # since nothing broke, we can add those gems to the gemfile + append_to(gemfile_path, build_gem_lines(@options[:conservative_versioning])) if @deps.any? + + # since we resolved successfully, write out the lockfile + @definition.lock(Bundler.default_lockfile) + + # invalidate the cached Bundler.definition + Bundler.reset_paths! + + # return an array of the deps that we added + @deps + end + end + + # @param [Pathname] gemfile_path The Gemfile from which to remove dependencies. + # @param [Pathname] lockfile_path The lockfile from which to remove dependencies. + # @return [Array] + def remove(gemfile_path, lockfile_path) + # remove gems from each gemfiles we have + Bundler.definition.gemfiles.each do |path| + deps = remove_deps(path) + + show_warning("No gems were removed from the gemfile.") if deps.empty? + + deps.each {|dep| Bundler.ui.confirm "#{SharedHelpers.pretty_dependency(dep, false)} was removed." } + end + end + + private + + def conservative_version(spec) + version = spec.version + return ">= 0" if version.nil? + segments = version.segments + seg_end_index = version >= Gem::Version.new("1.0") ? 1 : 2 + + prerelease_suffix = version.to_s.gsub(version.release.to_s, "") if version.prerelease? + "#{version_prefix}#{segments[0..seg_end_index].join(".")}#{prerelease_suffix}" + end + + def version_prefix + if @options[:strict] + "= " + elsif @options[:optimistic] + ">= " + else + "~> " + end + end + + def build_gem_lines(conservative_versioning) + @deps.map do |d| + name = d.name.dump + + requirement = if conservative_versioning + ", \"#{conservative_version(@definition.specs[d.name][0])}\"" + else + ", #{d.requirement.as_list.map(&:dump).join(", ")}" + end + + if d.groups != Array(:default) + group = d.groups.size == 1 ? ", :group => #{d.groups.first.inspect}" : ", :groups => #{d.groups.inspect}" + end + + source = ", :source => \"#{d.source}\"" unless d.source.nil? + + %(gem #{name}#{requirement}#{group}#{source}) + end.join("\n") + end + + def append_to(gemfile_path, new_gem_lines) + gemfile_path.open("a") do |f| + f.puts + f.puts new_gem_lines + end + end + + # evalutes a gemfile to remove the specified gem + # from it. + def remove_deps(gemfile_path) + initial_gemfile = IO.readlines(gemfile_path) + + Bundler.ui.info "Removing gems from #{gemfile_path}" + + # evaluate the Gemfile we have + builder = Dsl.new + builder.eval_gemfile(gemfile_path) + + removed_deps = remove_gems_from_dependencies(builder, @deps, gemfile_path) + + # abort the opertion if no gems were removed + # no need to operate on gemfile furthur + return [] if removed_deps.empty? + + cleaned_gemfile = remove_gems_from_gemfile(@deps, gemfile_path) + + SharedHelpers.write_to_gemfile(gemfile_path, cleaned_gemfile) + + # check for errors + # including extra gems being removed + # or some gems not being removed + # and return the actual removed deps + cross_check_for_errors(gemfile_path, builder.dependencies, removed_deps, initial_gemfile) + end + + # @param [Dsl] builder Dsl object of current Gemfile. + # @param [Array] gems Array of names of gems to be removed. + # @param [Pathname] path of the Gemfile + # @return [Array] removed_deps Array of removed dependencies. + def remove_gems_from_dependencies(builder, gems, gemfile_path) + removed_deps = [] + + gems.each do |gem_name| + deleted_dep = builder.dependencies.find {|d| d.name == gem_name } + + if deleted_dep.nil? + raise GemfileError, "`#{gem_name}` is not specified in #{gemfile_path} so it could not be removed." + end + + builder.dependencies.delete(deleted_dep) + + removed_deps << deleted_dep + end + + removed_deps + end + + # @param [Array] gems Array of names of gems to be removed. + # @param [Pathname] gemfile_path The Gemfile from which to remove dependencies. + def remove_gems_from_gemfile(gems, gemfile_path) + patterns = /gem\s+(['"])#{Regexp.union(gems)}\1|gem\s*\((['"])#{Regexp.union(gems)}\2\)/ + + # remove lines which match the regex + new_gemfile = IO.readlines(gemfile_path).reject {|line| line.match(patterns) } + + # remove lone \n and append them with other strings + new_gemfile.each_with_index do |_line, index| + if new_gemfile[index + 1] == "\n" + new_gemfile[index] += new_gemfile[index + 1] + new_gemfile.delete_at(index + 1) + end + end + + %w[group source env install_if].each {|block| remove_nested_blocks(new_gemfile, block) } + + new_gemfile.join.chomp + end + + # @param [Array] gemfile Array of gemfile contents. + # @param [String] block_name Name of block name to look for. + def remove_nested_blocks(gemfile, block_name) + nested_blocks = 0 + + # count number of nested blocks + gemfile.each_with_index {|line, index| nested_blocks += 1 if !gemfile[index + 1].nil? && gemfile[index + 1].include?(block_name) && line.include?(block_name) } + + while nested_blocks >= 0 + nested_blocks -= 1 + + gemfile.each_with_index do |line, index| + next unless !line.nil? && line.include?(block_name) + if gemfile[index + 1] =~ /^\s*end\s*$/ + gemfile[index] = nil + gemfile[index + 1] = nil + end + end + + gemfile.compact! + end + end + + # @param [Pathname] gemfile_path The Gemfile from which to remove dependencies. + # @param [Array] original_deps Array of original dependencies. + # @param [Array] removed_deps Array of removed dependencies. + # @param [Array] initial_gemfile Contents of original Gemfile before any operation. + def cross_check_for_errors(gemfile_path, original_deps, removed_deps, initial_gemfile) + # evalute the new gemfile to look for any failure cases + builder = Dsl.new + builder.eval_gemfile(gemfile_path) + + # record gems which were removed but not requested + extra_removed_gems = original_deps - builder.dependencies + + # if some extra gems were removed then raise error + # and revert Gemfile to original + unless extra_removed_gems.empty? + SharedHelpers.write_to_gemfile(gemfile_path, initial_gemfile.join) + + raise InvalidOption, "Gems could not be removed. #{extra_removed_gems.join(", ")} would also have been removed. Bundler cannot continue." + end + + # record gems which could not be removed due to some reasons + errored_deps = builder.dependencies.select {|d| d.gemfile == gemfile_path } & removed_deps.select {|d| d.gemfile == gemfile_path } + + show_warning "#{errored_deps.map(&:name).join(", ")} could not be removed." unless errored_deps.empty? + + # return actual removed dependencies + removed_deps - errored_deps + end + + def show_warning(message) + Bundler.ui.info Bundler.ui.add_color(message, :yellow) + end + end +end -- cgit v1.2.3