2018-11-02 19:07:56 -04:00
|
|
|
# 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?
|
2019-06-01 05:49:40 -04:00
|
|
|
git = ", :git => \"#{d.git}\"" unless d.git.nil?
|
|
|
|
branch = ", :branch => \"#{d.branch}\"" unless d.branch.nil?
|
2018-11-02 19:07:56 -04:00
|
|
|
|
2019-06-01 05:49:40 -04:00
|
|
|
%(gem #{name}#{requirement}#{group}#{source}#{git}#{branch})
|
2018-11-02 19:07:56 -04:00
|
|
|
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
|
|
|
|
|
2019-04-14 02:01:35 -04:00
|
|
|
# evaluates a gemfile to remove the specified gem
|
2018-11-02 19:07:56 -04:00
|
|
|
# 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)
|
|
|
|
|
2019-04-14 02:01:35 -04:00
|
|
|
# abort the operation if no gems were removed
|
|
|
|
# no need to operate on gemfile further
|
2018-11-02 19:07:56 -04:00
|
|
|
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.
|
2019-04-14 02:01:35 -04:00
|
|
|
# @param [Pathname] gemfile_path Path of the Gemfile.
|
|
|
|
# @return [Array] Array of removed dependencies.
|
2018-11-02 19:07:56 -04:00
|
|
|
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|
|
2019-04-14 02:01:35 -04:00
|
|
|
next unless !line.nil? && line.strip.start_with?(block_name)
|
2018-11-02 19:07:56 -04:00
|
|
|
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)
|
2019-04-14 02:01:35 -04:00
|
|
|
# evaluate the new gemfile to look for any failure cases
|
2018-11-02 19:07:56 -04:00
|
|
|
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
|