This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Add
|
||||
attr_reader :gems, :options, :version
|
||||
|
||||
def initialize(options, gems)
|
||||
@gems = gems
|
||||
@options = options
|
||||
@options[:group] = options[:group].split(",").map(&:strip) unless options[:group].nil?
|
||||
@version = options[:version].split(",").map(&:strip) unless options[:version].nil?
|
||||
end
|
||||
|
||||
def run
|
||||
Bundler.ui.level = "warn" if options[:quiet]
|
||||
|
||||
Bundler::CLI::Common.validate_cooldown!(options[:cooldown])
|
||||
Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown]
|
||||
|
||||
validate_options!
|
||||
inject_dependencies
|
||||
perform_bundle_install unless options["skip-install"]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def perform_bundle_install
|
||||
Installer.install(Bundler.root, Bundler.definition)
|
||||
Bundler.load.cache if Bundler.app_cache.exist?
|
||||
end
|
||||
|
||||
def inject_dependencies
|
||||
dependencies = gems.map {|g| Bundler::Dependency.new(g, version, options) }
|
||||
|
||||
Injector.inject(dependencies,
|
||||
conservative_versioning: options[:version].nil?, # Perform conservative versioning only when version is not specified
|
||||
optimistic: options[:optimistic],
|
||||
strict: options[:strict])
|
||||
end
|
||||
|
||||
def validate_options!
|
||||
raise InvalidOption, "You cannot specify `--git` and `--github` at the same time." if options["git"] && options["github"]
|
||||
|
||||
unless options["git"] || options["github"]
|
||||
raise InvalidOption, "You cannot specify `--branch` unless `--git` or `--github` is specified." if options["branch"]
|
||||
|
||||
raise InvalidOption, "You cannot specify `--ref` unless `--git` or `--github` is specified." if options["ref"]
|
||||
end
|
||||
|
||||
raise InvalidOption, "You cannot specify `--branch` and `--ref` at the same time." if options["branch"] && options["ref"]
|
||||
|
||||
raise InvalidOption, "You cannot specify `--strict` and `--optimistic` at the same time." if options[:strict] && options[:optimistic]
|
||||
|
||||
# raise error when no gems are specified
|
||||
raise InvalidOption, "Please specify gems to add." if gems.empty?
|
||||
|
||||
version.to_a.each do |v|
|
||||
raise InvalidOption, "Invalid gem requirement pattern '#{v}'" unless Gem::Requirement::PATTERN.match?(v.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Binstubs
|
||||
attr_reader :options, :gems
|
||||
def initialize(options, gems)
|
||||
@options = options
|
||||
@gems = gems
|
||||
end
|
||||
|
||||
def run
|
||||
Bundler.definition.validate_runtime!
|
||||
path_option = options["path"]
|
||||
path_option = nil if path_option&.empty?
|
||||
Bundler.settings.set_command_option :bin, path_option if options["path"]
|
||||
Bundler.settings.set_command_option_if_given :shebang, options["shebang"]
|
||||
installer = Installer.new(Bundler.root, Bundler.definition)
|
||||
|
||||
installer_opts = {
|
||||
force: options[:force],
|
||||
binstubs_cmd: true,
|
||||
all_platforms: options["all-platforms"],
|
||||
}
|
||||
|
||||
if options[:all]
|
||||
raise InvalidOption, "Cannot specify --all with specific gems" unless gems.empty?
|
||||
@gems = Bundler.definition.specs.map(&:name)
|
||||
installer_opts.delete(:binstubs_cmd)
|
||||
elsif gems.empty?
|
||||
Bundler.ui.error "`bundle binstubs` needs at least one gem to run."
|
||||
exit 1
|
||||
end
|
||||
|
||||
gems.each do |gem_name|
|
||||
spec = Bundler.definition.specs.find {|s| s.name == gem_name }
|
||||
unless spec
|
||||
raise GemNotFound, Bundler::CLI::Common.gem_not_found_message(
|
||||
gem_name, Bundler.definition.specs
|
||||
)
|
||||
end
|
||||
|
||||
if options[:standalone]
|
||||
if gem_name == "bundler"
|
||||
Bundler.ui.warn("Sorry, Bundler can only be run via RubyGems.") unless options[:all]
|
||||
next
|
||||
end
|
||||
|
||||
Bundler.settings.temporary(path: Bundler.settings[:path] || Bundler.root) do
|
||||
installer.generate_standalone_bundler_executable_stubs(spec, installer_opts)
|
||||
end
|
||||
else
|
||||
installer.generate_bundler_executable_stubs(spec, installer_opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Cache
|
||||
attr_reader :options
|
||||
|
||||
def initialize(options)
|
||||
@options = options
|
||||
end
|
||||
|
||||
def run
|
||||
Bundler.ui.level = "warn" if options[:quiet]
|
||||
Bundler.settings.set_command_option_if_given :cache_path, options["cache-path"]
|
||||
|
||||
install
|
||||
|
||||
Bundler.settings.temporary(cache_all_platforms: options["all-platforms"]) do
|
||||
Bundler.load.cache
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def install
|
||||
require_relative "install"
|
||||
options = self.options.dup
|
||||
options["local"] = false if Bundler.settings[:cache_all_platforms]
|
||||
options["no-cache"] = true
|
||||
Bundler::CLI::Install.new(options).run
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Check
|
||||
attr_reader :options
|
||||
|
||||
def initialize(options)
|
||||
@options = options
|
||||
end
|
||||
|
||||
def run
|
||||
Bundler.settings.set_command_option_if_given :path, options[:path]
|
||||
|
||||
definition = Bundler.definition
|
||||
definition.ensure_equivalent_gemfile_and_lockfile
|
||||
definition.validate_runtime!
|
||||
|
||||
begin
|
||||
definition.check!
|
||||
not_installed = definition.missing_specs
|
||||
rescue GemNotFound, GitError, SolveFailure
|
||||
Bundler.ui.error "Bundler can't satisfy your Gemfile's dependencies."
|
||||
Bundler.ui.warn "Install missing gems with `bundle install`."
|
||||
exit 1
|
||||
end
|
||||
|
||||
if not_installed.any?
|
||||
Bundler.ui.error "The following gems are missing"
|
||||
not_installed.each {|s| Bundler.ui.error " * #{s.name} (#{s.version})" }
|
||||
Bundler.ui.warn "Install missing gems with `bundle install`"
|
||||
exit 1
|
||||
else
|
||||
definition.lock(true) unless options[:"dry-run"]
|
||||
Bundler.ui.info "The Gemfile's dependencies are satisfied"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Clean
|
||||
attr_reader :options
|
||||
|
||||
def initialize(options)
|
||||
@options = options
|
||||
end
|
||||
|
||||
def run
|
||||
require_path_or_force unless options[:"dry-run"]
|
||||
Bundler.load.clean(options[:"dry-run"])
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def require_path_or_force
|
||||
return unless Bundler.use_system_gems? && !options[:force]
|
||||
raise InvalidOption, "Cleaning all the gems on your system is dangerous! " \
|
||||
"If you're sure you want to remove every system gem not in this " \
|
||||
"bundle, run `bundle clean --force`."
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,172 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
module CLI::Common
|
||||
def self.validate_cooldown!(value)
|
||||
return if value.nil?
|
||||
return if value.is_a?(Integer) && value >= 0
|
||||
raise InvalidOption, "Expected `--cooldown` to be a non-negative integer, got #{value.inspect}"
|
||||
end
|
||||
|
||||
def self.output_post_install_messages(messages)
|
||||
return if Bundler.settings["ignore_messages"]
|
||||
messages.to_a.each do |name, msg|
|
||||
print_post_install_message(name, msg) unless Bundler.settings["ignore_messages.#{name}"]
|
||||
end
|
||||
end
|
||||
|
||||
def self.print_post_install_message(name, msg)
|
||||
Bundler.ui.confirm "Post-install message from #{name}:"
|
||||
Bundler.ui.info msg
|
||||
end
|
||||
|
||||
def self.output_cooldown_skipped_summary(definition = Bundler.definition)
|
||||
skipped = definition.cooldown_skipped
|
||||
return if skipped.empty?
|
||||
|
||||
Bundler.ui.info "The following gem versions were skipped by the cooldown setting:"
|
||||
skipped.each do |entry|
|
||||
days = entry[:available_in_days]
|
||||
Bundler.ui.info " * #{entry[:name]} #{entry[:version]} (available in #{days} #{days == 1 ? "day" : "days"}), resolved #{entry[:resolved]} instead"
|
||||
end
|
||||
end
|
||||
|
||||
def self.output_fund_metadata_summary
|
||||
return if Bundler.settings["ignore_funding_requests"]
|
||||
definition = Bundler.definition
|
||||
current_dependencies = definition.requested_dependencies
|
||||
current_specs = definition.specs
|
||||
|
||||
count = current_dependencies.count {|dep| current_specs[dep.name].first.metadata.key?("funding_uri") }
|
||||
|
||||
return if count.zero?
|
||||
|
||||
intro = count > 1 ? "#{count} installed gems you directly depend on are" : "#{count} installed gem you directly depend on is"
|
||||
message = "#{intro} looking for funding.\n Run `bundle fund` for details"
|
||||
Bundler.ui.info message
|
||||
end
|
||||
|
||||
def self.output_without_groups_message(command)
|
||||
return if Bundler.settings[:without].empty?
|
||||
Bundler.ui.confirm without_groups_message(command)
|
||||
end
|
||||
|
||||
def self.without_groups_message(command)
|
||||
command_in_past_tense = command == :install ? "installed" : "updated"
|
||||
groups = Bundler.settings[:without]
|
||||
"Gems in the #{verbalize_groups(groups)} were not #{command_in_past_tense}."
|
||||
end
|
||||
|
||||
def self.verbalize_groups(groups)
|
||||
groups.map! {|g| "'#{g}'" }
|
||||
group_list = [groups[0...-1].join(", "), groups[-1..-1]].
|
||||
reject {|s| s.to_s.empty? }.join(" and ")
|
||||
group_str = groups.size == 1 ? "group" : "groups"
|
||||
"#{group_str} #{group_list}"
|
||||
end
|
||||
|
||||
def self.select_spec(name, regex_match = nil)
|
||||
specs = []
|
||||
regexp = Regexp.new(name) if regex_match
|
||||
|
||||
Bundler.definition.specs.each do |spec|
|
||||
return spec if spec.name == name
|
||||
specs << spec if regexp && spec.name.match?(regexp)
|
||||
end
|
||||
|
||||
default_spec = default_gem_spec(name)
|
||||
specs << default_spec if default_spec
|
||||
|
||||
case specs.count
|
||||
when 0
|
||||
dep_in_other_group = Bundler.definition.current_dependencies.find {|dep|dep.name == name }
|
||||
|
||||
if dep_in_other_group
|
||||
raise GemNotFound, "Could not find gem '#{name}', because it's in the #{verbalize_groups(dep_in_other_group.groups)}, configured to be ignored."
|
||||
else
|
||||
raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies)
|
||||
end
|
||||
when 1
|
||||
specs.first
|
||||
else
|
||||
ask_for_spec_from(specs)
|
||||
end
|
||||
rescue RegexpError
|
||||
raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies)
|
||||
end
|
||||
|
||||
def self.default_gem_spec(name)
|
||||
gem_spec = Gem::Specification.find_all_by_name(name).last
|
||||
gem_spec if gem_spec&.default_gem?
|
||||
end
|
||||
|
||||
def self.ask_for_spec_from(specs)
|
||||
specs.each_with_index do |spec, index|
|
||||
Bundler.ui.info "#{index.succ} : #{spec.name}", true
|
||||
end
|
||||
Bundler.ui.info "0 : - exit -", true
|
||||
|
||||
num = Bundler.ui.ask("> ").to_i
|
||||
num > 0 ? specs[num - 1] : nil
|
||||
end
|
||||
|
||||
def self.gem_not_found_message(missing_gem_name, alternatives)
|
||||
message = "Could not find gem '#{missing_gem_name}'."
|
||||
alternate_names = alternatives.map {|a| a.respond_to?(:name) ? a.name : a }
|
||||
if alternate_names.include?(missing_gem_name.downcase)
|
||||
message += "\nDid you mean '#{missing_gem_name.downcase}'?"
|
||||
elsif defined?(DidYouMean::SpellChecker)
|
||||
suggestions = DidYouMean::SpellChecker.new(dictionary: alternate_names).correct(missing_gem_name)
|
||||
message += "\nDid you mean #{word_list(suggestions)}?" unless suggestions.empty?
|
||||
end
|
||||
message
|
||||
end
|
||||
|
||||
def self.ensure_all_gems_in_lockfile!(names, locked_gems = Bundler.locked_gems)
|
||||
return unless locked_gems
|
||||
|
||||
locked_names = locked_gems.specs.map(&:name).uniq
|
||||
names.-(locked_names).each do |g|
|
||||
raise GemNotFound, gem_not_found_message(g, locked_names)
|
||||
end
|
||||
end
|
||||
|
||||
def self.configure_gem_version_promoter(definition, options)
|
||||
patch_level = patch_level_options(options)
|
||||
patch_level << :patch if patch_level.empty? && Bundler.settings[:prefer_patch]
|
||||
raise InvalidOption, "Provide only one of the following options: #{patch_level.join(", ")}" unless patch_level.length <= 1
|
||||
|
||||
definition.gem_version_promoter.tap do |gvp|
|
||||
gvp.level = patch_level.first || :major
|
||||
gvp.strict = options[:strict] || options["filter-strict"]
|
||||
gvp.pre = options[:pre]
|
||||
end
|
||||
end
|
||||
|
||||
def self.patch_level_options(options)
|
||||
[:major, :minor, :patch].select {|v| options.keys.include?(v.to_s) }
|
||||
end
|
||||
|
||||
def self.clean_after_install?
|
||||
clean = Bundler.settings[:clean]
|
||||
return clean unless clean.nil?
|
||||
clean ||= Bundler.feature_flag.bundler_5_mode? && Bundler.settings[:path].nil?
|
||||
clean &&= !Bundler.use_system_gems?
|
||||
clean
|
||||
end
|
||||
|
||||
def self.word_list(words)
|
||||
if words.empty?
|
||||
return ""
|
||||
end
|
||||
|
||||
words = words.map {|word| "'#{word}'" }
|
||||
|
||||
if words.length == 1
|
||||
return words[0]
|
||||
end
|
||||
|
||||
[words[0..-2].join(", "), words[-1]].join(" or ")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,208 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Config < Thor
|
||||
class_option :parseable, type: :boolean, banner: "Use minimal formatting for more parseable output"
|
||||
|
||||
def self.scope_options
|
||||
method_option :global, type: :boolean, banner: "Only change the global config"
|
||||
method_option :local, type: :boolean, banner: "Only change the local config"
|
||||
end
|
||||
private_class_method :scope_options
|
||||
|
||||
desc "base NAME [VALUE]", "The Bundler 1 config interface", hide: true
|
||||
scope_options
|
||||
method_option :delete, type: :boolean, banner: "delete"
|
||||
def base(name = nil, *value)
|
||||
new_args =
|
||||
if ARGV.size == 1
|
||||
["config", "list"]
|
||||
elsif ARGV.include?("--delete")
|
||||
ARGV.map {|arg| arg == "--delete" ? "unset" : arg }
|
||||
elsif ARGV.include?("--global") || ARGV.include?("--local") || ARGV.size == 3
|
||||
["config", "set", *ARGV[1..-1]]
|
||||
else
|
||||
["config", "get", ARGV[1]]
|
||||
end
|
||||
|
||||
message = "Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle #{new_args.join(" ")}` instead."
|
||||
SharedHelpers.feature_deprecated! message
|
||||
|
||||
Base.new(options, name, value, self).run
|
||||
end
|
||||
|
||||
desc "list", "List out all configured settings"
|
||||
def list
|
||||
Base.new(options, nil, nil, self).run
|
||||
end
|
||||
|
||||
desc "get NAME", "Returns the value for the given key"
|
||||
def get(name)
|
||||
Base.new(options, name, nil, self).run
|
||||
end
|
||||
|
||||
desc "set NAME VALUE", "Sets the given value for the given key"
|
||||
scope_options
|
||||
def set(name, value, *value_)
|
||||
Base.new(options, name, value_.unshift(value), self).run
|
||||
end
|
||||
|
||||
desc "unset NAME", "Unsets the value for the given key"
|
||||
scope_options
|
||||
def unset(name)
|
||||
options[:delete] = true
|
||||
Base.new(options, name, nil, self).run
|
||||
end
|
||||
|
||||
default_task :base
|
||||
|
||||
class Base
|
||||
attr_reader :name, :value, :options, :scope, :thor
|
||||
|
||||
def initialize(options, name, value, thor)
|
||||
@options = options
|
||||
@name = name
|
||||
value = Array(value)
|
||||
@value = value.empty? ? nil : value.join(" ")
|
||||
@thor = thor
|
||||
validate_scope!
|
||||
end
|
||||
|
||||
def run
|
||||
unless name
|
||||
warn_unused_scope "Ignoring --#{scope}"
|
||||
confirm_all
|
||||
return
|
||||
end
|
||||
|
||||
if options[:delete]
|
||||
if !explicit_scope? || scope != "global"
|
||||
Bundler.settings.set_local(name, nil)
|
||||
end
|
||||
if !explicit_scope? || scope != "local"
|
||||
Bundler.settings.set_global(name, nil)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if value.nil?
|
||||
warn_unused_scope "Ignoring --#{scope} since no value to set was given"
|
||||
current_value = Bundler.settings[name]
|
||||
|
||||
if options[:parseable]
|
||||
if value = Bundler.settings[name]
|
||||
Bundler.ui.info("#{name}=#{value}")
|
||||
end
|
||||
else
|
||||
confirm(name)
|
||||
end
|
||||
|
||||
if current_value.nil?
|
||||
exit 1
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
Bundler.ui.info(message) if message
|
||||
Bundler.settings.send("set_#{scope}", name, new_value)
|
||||
end
|
||||
|
||||
def confirm_all
|
||||
if @options[:parseable]
|
||||
thor.with_padding do
|
||||
Bundler.settings.all.each do |setting|
|
||||
val = Bundler.settings[setting]
|
||||
Bundler.ui.info "#{setting}=#{val}"
|
||||
end
|
||||
end
|
||||
else
|
||||
Bundler.ui.confirm "Settings are listed in order of priority. The top value will be used.\n"
|
||||
Bundler.settings.all.each do |setting|
|
||||
Bundler.ui.confirm setting
|
||||
show_pretty_values_for(setting)
|
||||
Bundler.ui.confirm ""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def confirm(name)
|
||||
Bundler.ui.confirm "Settings for `#{name}` in order of priority. The top value will be used"
|
||||
show_pretty_values_for(name)
|
||||
end
|
||||
|
||||
def new_value
|
||||
pathname = Pathname.new(value)
|
||||
if name.start_with?("local.") && pathname.directory?
|
||||
pathname.expand_path.to_s
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
def message
|
||||
locations = Bundler.settings.locations(name)
|
||||
if @options[:parseable]
|
||||
"#{name}=#{new_value}" if new_value
|
||||
elsif scope == "global"
|
||||
if !locations[:local].nil?
|
||||
"Your application has set #{name} to #{locations[:local].inspect}. " \
|
||||
"This will override the global value you are currently setting"
|
||||
elsif locations[:env]
|
||||
"You have a bundler environment variable for #{name} set to " \
|
||||
"#{locations[:env].inspect}. This will take precedence over the global value you are setting"
|
||||
elsif !locations[:global].nil? && locations[:global] != value
|
||||
"You are replacing the current global value of #{name}, which is currently " \
|
||||
"#{locations[:global].inspect}"
|
||||
end
|
||||
elsif scope == "local" && !locations[:local].nil? && locations[:local] != value
|
||||
"You are replacing the current local value of #{name}, which is currently " \
|
||||
"#{locations[:local].inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def show_pretty_values_for(setting)
|
||||
thor.with_padding do
|
||||
Bundler.settings.pretty_values_for(setting).each do |line|
|
||||
Bundler.ui.info line
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def explicit_scope?
|
||||
@explicit_scope
|
||||
end
|
||||
|
||||
def warn_unused_scope(msg)
|
||||
return unless explicit_scope?
|
||||
return if options[:parseable]
|
||||
|
||||
Bundler.ui.warn(msg)
|
||||
end
|
||||
|
||||
def validate_scope!
|
||||
@explicit_scope = true
|
||||
scopes = %w[global local].select {|s| options[s] }
|
||||
case scopes.size
|
||||
when 0
|
||||
@scope = inside_app? ? "local" : "global"
|
||||
@explicit_scope = false
|
||||
when 1
|
||||
@scope = scopes.first
|
||||
else
|
||||
raise InvalidOption,
|
||||
"The options #{scopes.join " and "} were specified. Please only use one of the switches at a time."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def inside_app?
|
||||
Bundler.root
|
||||
true
|
||||
rescue GemfileNotFound
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Console
|
||||
attr_reader :options, :group
|
||||
def initialize(options, group)
|
||||
@options = options
|
||||
@group = group
|
||||
end
|
||||
|
||||
def run
|
||||
group ? Bundler.require(:default, *group.split(" ").map!(&:to_sym)) : Bundler.require
|
||||
ARGV.clear
|
||||
|
||||
console = get_console(Bundler.settings[:console] || "irb")
|
||||
console.start
|
||||
end
|
||||
|
||||
def get_console(name)
|
||||
require name
|
||||
get_constant(name)
|
||||
rescue LoadError
|
||||
if name == "irb"
|
||||
if defined?(Gem::BUNDLED_GEMS) && Gem::BUNDLED_GEMS.respond_to?(:force_activate)
|
||||
Gem::BUNDLED_GEMS.force_activate "irb"
|
||||
require name
|
||||
return get_constant(name)
|
||||
end
|
||||
Bundler.ui.error "#{name} is not available"
|
||||
exit 1
|
||||
else
|
||||
Bundler.ui.error "Couldn't load console #{name}, falling back to irb"
|
||||
name = "irb"
|
||||
retry
|
||||
end
|
||||
end
|
||||
|
||||
def get_constant(name)
|
||||
const_name = {
|
||||
"pry" => :Pry,
|
||||
"ripl" => :Ripl,
|
||||
"irb" => :IRB,
|
||||
}[name]
|
||||
Object.const_get(const_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Doctor < Thor
|
||||
default_command(:diagnose)
|
||||
|
||||
desc "diagnose [OPTIONS]", "Checks the bundle for common problems"
|
||||
long_desc <<-D
|
||||
Doctor scans the OS dependencies of each of the gems requested in the Gemfile. If
|
||||
missing dependencies are detected, Bundler prints them and exits status 1.
|
||||
Otherwise, Bundler prints a success message and exits with a status of 0.
|
||||
D
|
||||
method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile"
|
||||
method_option "quiet", type: :boolean, banner: "Only output warnings and errors."
|
||||
method_option "ssl", type: :boolean, default: false, banner: "Diagnose SSL problems."
|
||||
def diagnose
|
||||
require_relative "doctor/diagnose"
|
||||
Diagnose.new(options).run
|
||||
end
|
||||
|
||||
desc "ssl [OPTIONS]", "Diagnose SSL problems"
|
||||
long_desc <<-D
|
||||
Diagnose SSL problems, especially related to certificates or TLS version while connecting to https://rubygems.org.
|
||||
D
|
||||
method_option "host", type: :string, banner: "The host to diagnose."
|
||||
method_option "tls-version", type: :string, banner: "Specify the SSL/TLS version when running the diagnostic. Accepts either <1.1> or <1.2>"
|
||||
method_option "verify-mode", type: :string, banner: "Specify the mode used for certification verification. Accepts either <peer> or <none>"
|
||||
def ssl
|
||||
require_relative "doctor/ssl"
|
||||
SSL.new(options).run
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,167 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rbconfig"
|
||||
require "shellwords"
|
||||
|
||||
module Bundler
|
||||
class CLI::Doctor::Diagnose
|
||||
DARWIN_REGEX = /\s+(.+) \(compatibility /
|
||||
LDD_REGEX = /\t\S+ => (\S+) \(\S+\)/
|
||||
|
||||
attr_reader :options
|
||||
|
||||
def initialize(options)
|
||||
@options = options
|
||||
end
|
||||
|
||||
def otool_available?
|
||||
Bundler.which("otool")
|
||||
end
|
||||
|
||||
def ldd_available?
|
||||
Bundler.which("ldd")
|
||||
end
|
||||
|
||||
def dylibs_darwin(path)
|
||||
output = `/usr/bin/otool -L #{path.shellescape}`.chomp
|
||||
dylibs = output.split("\n")[1..-1].filter_map {|l| l.match(DARWIN_REGEX)&.match(1) }.uniq
|
||||
# ignore @rpath and friends
|
||||
dylibs.reject {|dylib| dylib.start_with? "@" }
|
||||
end
|
||||
|
||||
def dylibs_ldd(path)
|
||||
output = `/usr/bin/ldd #{path.shellescape}`.chomp
|
||||
output.split("\n").filter_map do |l|
|
||||
match = l.match(LDD_REGEX)
|
||||
next if match.nil?
|
||||
match.captures[0]
|
||||
end
|
||||
end
|
||||
|
||||
def dylibs(path)
|
||||
case RbConfig::CONFIG["host_os"]
|
||||
when /darwin/
|
||||
return [] unless otool_available?
|
||||
dylibs_darwin(path)
|
||||
when /(linux|solaris|bsd)/
|
||||
return [] unless ldd_available?
|
||||
dylibs_ldd(path)
|
||||
else # Windows, etc.
|
||||
Bundler.ui.warn("Dynamic library check not supported on this platform.")
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def bundles_for_gem(spec)
|
||||
SharedHelpers.glob_files_in_dir("**/*.bundle", spec.full_gem_path)
|
||||
end
|
||||
|
||||
def lookup_with_fiddle(path)
|
||||
require "fiddle"
|
||||
Fiddle.dlopen(path)
|
||||
false
|
||||
rescue Fiddle::DLError
|
||||
true
|
||||
end
|
||||
|
||||
def check!
|
||||
require_relative "../check"
|
||||
Bundler::CLI::Check.new({}).run
|
||||
end
|
||||
|
||||
def diagnose_ssl
|
||||
require_relative "ssl"
|
||||
Bundler::CLI::Doctor::SSL.new({}).run
|
||||
end
|
||||
|
||||
def run
|
||||
Bundler.ui.level = "warn" if options[:quiet]
|
||||
Bundler.settings.validate!
|
||||
check!
|
||||
diagnose_ssl if options[:ssl]
|
||||
|
||||
definition = Bundler.definition
|
||||
broken_links = {}
|
||||
|
||||
definition.specs.each do |spec|
|
||||
bundles_for_gem(spec).each do |bundle|
|
||||
bad_paths = dylibs(bundle).select do |f|
|
||||
lookup_with_fiddle(f)
|
||||
end
|
||||
if bad_paths.any?
|
||||
broken_links[spec] ||= []
|
||||
broken_links[spec].concat(bad_paths)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
permissions_valid = check_home_permissions
|
||||
|
||||
if broken_links.any?
|
||||
message = "The following gems are missing OS dependencies:"
|
||||
broken_links.flat_map do |spec, paths|
|
||||
paths.uniq.map do |path|
|
||||
"\n * #{spec.name}: #{path}"
|
||||
end
|
||||
end.sort.each {|m| message += m }
|
||||
raise ProductionError, message
|
||||
elsif permissions_valid
|
||||
Bundler.ui.info "No issues found with the installed bundle"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_home_permissions
|
||||
require "find"
|
||||
files_not_readable = []
|
||||
files_not_readable_and_owned_by_different_user = []
|
||||
files_not_owned_by_current_user_but_still_readable = []
|
||||
broken_symlinks = []
|
||||
Find.find(Bundler.bundle_path.to_s).each do |f|
|
||||
if !File.exist?(f)
|
||||
broken_symlinks << f
|
||||
elsif !File.readable?(f)
|
||||
if File.stat(f).uid != Process.uid
|
||||
files_not_readable_and_owned_by_different_user << f
|
||||
else
|
||||
files_not_readable << f
|
||||
end
|
||||
elsif File.stat(f).uid != Process.uid
|
||||
files_not_owned_by_current_user_but_still_readable << f
|
||||
end
|
||||
end
|
||||
|
||||
ok = true
|
||||
|
||||
if broken_symlinks.any?
|
||||
Bundler.ui.warn "Broken links exist in the Bundler home. Please report them to the offending gem's upstream repo. These files are:\n - #{broken_symlinks.join("\n - ")}"
|
||||
|
||||
ok = false
|
||||
end
|
||||
|
||||
if files_not_owned_by_current_user_but_still_readable.any?
|
||||
Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \
|
||||
"user, but are still readable. These files are:\n - #{files_not_owned_by_current_user_but_still_readable.join("\n - ")}"
|
||||
|
||||
ok = false
|
||||
end
|
||||
|
||||
if files_not_readable_and_owned_by_different_user.any?
|
||||
Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \
|
||||
"user, and are not readable. These files are:\n - #{files_not_readable_and_owned_by_different_user.join("\n - ")}"
|
||||
|
||||
ok = false
|
||||
end
|
||||
|
||||
if files_not_readable.any?
|
||||
Bundler.ui.warn "Files exist in the Bundler home that are not " \
|
||||
"readable by the current user. These files are:\n - #{files_not_readable.join("\n - ")}"
|
||||
|
||||
ok = false
|
||||
end
|
||||
|
||||
ok
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,249 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rubygems/remote_fetcher"
|
||||
require "uri"
|
||||
|
||||
module Bundler
|
||||
class CLI::Doctor::SSL
|
||||
attr_reader :options
|
||||
|
||||
def initialize(options)
|
||||
@options = options
|
||||
end
|
||||
|
||||
def run
|
||||
return unless openssl_installed?
|
||||
|
||||
output_ssl_environment
|
||||
bundler_success = bundler_connection_successful?
|
||||
rubygem_success = rubygem_connection_successful?
|
||||
|
||||
return unless net_http_connection_successful?
|
||||
|
||||
Explanation.summarize(bundler_success, rubygem_success, host)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def host
|
||||
@options[:host] || "rubygems.org"
|
||||
end
|
||||
|
||||
def tls_version
|
||||
@options[:"tls-version"].then do |version|
|
||||
"TLS#{version.sub(".", "_")}".to_sym if version
|
||||
end
|
||||
end
|
||||
|
||||
def verify_mode
|
||||
mode = @options[:"verify-mode"] || :peer
|
||||
|
||||
@verify_mode ||= mode.then {|mod| OpenSSL::SSL.const_get("verify_#{mod}".upcase) }
|
||||
end
|
||||
|
||||
def uri
|
||||
@uri ||= URI("https://#{host}")
|
||||
end
|
||||
|
||||
def openssl_installed?
|
||||
require "openssl"
|
||||
|
||||
true
|
||||
rescue LoadError
|
||||
Bundler.ui.warn(<<~MSG)
|
||||
Oh no! Your Ruby doesn't have OpenSSL, so it can't connect to #{host}.
|
||||
You'll need to recompile or reinstall Ruby with OpenSSL support and try again.
|
||||
MSG
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def output_ssl_environment
|
||||
Bundler.ui.info(<<~MESSAGE)
|
||||
Here's your OpenSSL environment:
|
||||
|
||||
OpenSSL: #{OpenSSL::VERSION}
|
||||
Compiled with: #{OpenSSL::OPENSSL_VERSION}
|
||||
Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
|
||||
MESSAGE
|
||||
end
|
||||
|
||||
def bundler_connection_successful?
|
||||
Bundler.ui.info("\nTrying connections to #{uri}:\n")
|
||||
|
||||
bundler_uri = Gem::URI(uri.to_s)
|
||||
Bundler::Fetcher.new(
|
||||
Bundler::Source::Rubygems::Remote.new(bundler_uri)
|
||||
).send(:connection).request(bundler_uri)
|
||||
|
||||
Bundler.ui.info("Bundler: success")
|
||||
|
||||
true
|
||||
rescue StandardError => error
|
||||
Bundler.ui.warn("Bundler: failed (#{Explanation.explain_bundler_or_rubygems_error(error)})")
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def rubygem_connection_successful?
|
||||
Gem::RemoteFetcher.fetcher.fetch_path(uri)
|
||||
Bundler.ui.info("RubyGems: success")
|
||||
|
||||
true
|
||||
rescue StandardError => error
|
||||
Bundler.ui.warn("RubyGems: failed (#{Explanation.explain_bundler_or_rubygems_error(error)})")
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def net_http_connection_successful?
|
||||
::Gem::Net::HTTP.new(uri.host, uri.port).tap do |http|
|
||||
http.use_ssl = true
|
||||
http.min_version = tls_version
|
||||
http.max_version = tls_version
|
||||
http.verify_mode = verify_mode
|
||||
end.start
|
||||
|
||||
Bundler.ui.info("Ruby net/http: success")
|
||||
warn_on_unsupported_tls12
|
||||
|
||||
true
|
||||
rescue StandardError => error
|
||||
Bundler.ui.warn(<<~MSG)
|
||||
Ruby net/http: failed
|
||||
|
||||
Unfortunately, this Ruby can't connect to #{host}.
|
||||
|
||||
#{Explanation.explain_net_http_error(error, host, tls_version)}
|
||||
MSG
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def warn_on_unsupported_tls12
|
||||
ctx = OpenSSL::SSL::SSLContext.new
|
||||
supported = true
|
||||
|
||||
if ctx.respond_to?(:min_version=)
|
||||
begin
|
||||
ctx.min_version = ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
|
||||
rescue OpenSSL::SSL::SSLError, NameError
|
||||
supported = false
|
||||
end
|
||||
else
|
||||
supported = OpenSSL::SSL::SSLContext::METHODS.include?(:TLSv1_2) # rubocop:disable Naming/VariableNumber
|
||||
end
|
||||
|
||||
Bundler.ui.warn(<<~EOM) unless supported
|
||||
|
||||
WARNING: Although your Ruby can connect to #{host} today, your OpenSSL is very old!
|
||||
WARNING: You will need to upgrade OpenSSL to use #{host}.
|
||||
|
||||
EOM
|
||||
end
|
||||
|
||||
module Explanation
|
||||
extend self
|
||||
|
||||
def explain_bundler_or_rubygems_error(error)
|
||||
case error.message
|
||||
when /certificate verify failed/
|
||||
"certificate verification"
|
||||
when /read server hello A/
|
||||
"SSL/TLS protocol version mismatch"
|
||||
when /tlsv1 alert protocol version/
|
||||
"requested TLS version is too old"
|
||||
else
|
||||
error.message
|
||||
end
|
||||
end
|
||||
|
||||
def explain_net_http_error(error, host, tls_version)
|
||||
case error.message
|
||||
# Check for certificate errors
|
||||
when /certificate verify failed/
|
||||
<<~MSG
|
||||
#{show_ssl_certs}
|
||||
Your Ruby can't connect to #{host} because you are missing the certificate files OpenSSL needs to verify you are connecting to the genuine #{host} servers.
|
||||
MSG
|
||||
# Check for TLS version errors
|
||||
when /read server hello A/, /tlsv1 alert protocol version/
|
||||
if tls_version.to_s == "TLS1_3"
|
||||
"Your Ruby can't connect to #{host} because #{tls_version} isn't supported yet.\n"
|
||||
else
|
||||
<<~MSG
|
||||
Your Ruby can't connect to #{host} because your version of OpenSSL is too old.
|
||||
You'll need to upgrade your OpenSSL install and/or recompile Ruby to use a newer OpenSSL.
|
||||
MSG
|
||||
end
|
||||
# OpenSSL doesn't support TLS version specified by argument
|
||||
when /unknown SSL method/
|
||||
"Your Ruby can't connect because #{tls_version} isn't supported by your version of OpenSSL."
|
||||
else
|
||||
<<~MSG
|
||||
Even worse, we're not sure why.
|
||||
|
||||
Here's the full error information:
|
||||
#{error.class}: #{error.message}
|
||||
#{error.backtrace.join("\n ")}
|
||||
|
||||
You might have more luck using Mislav's SSL doctor.rb script. You can get it here:
|
||||
https://github.com/mislav/ssl-tools/blob/8b3dec4/doctor.rb
|
||||
|
||||
Read more about the script and how to use it in this blog post:
|
||||
https://mislav.net/2013/07/ruby-openssl/
|
||||
MSG
|
||||
end
|
||||
end
|
||||
|
||||
def summarize(bundler_success, rubygems_success, host)
|
||||
guide_url = "http://ruby.to/ssl-check-failed"
|
||||
|
||||
message = if bundler_success && rubygems_success
|
||||
<<~MSG
|
||||
Hooray! This Ruby can connect to #{host}.
|
||||
You are all set to use Bundler and RubyGems.
|
||||
|
||||
MSG
|
||||
elsif !bundler_success && !rubygems_success
|
||||
<<~MSG
|
||||
For some reason, your Ruby installation can connect to #{host}, but neither RubyGems nor Bundler can.
|
||||
The most likely fix is to manually upgrade RubyGems by following the instructions at #{guide_url}.
|
||||
After you've done that, run `gem install bundler` to upgrade Bundler, and then run this script again to make sure everything worked. ❣
|
||||
|
||||
MSG
|
||||
elsif !bundler_success
|
||||
<<~MSG
|
||||
Although your Ruby installation and RubyGems can both connect to #{host}, Bundler is having trouble.
|
||||
The most likely way to fix this is to upgrade Bundler by running `gem install bundler`.
|
||||
Run this script again after doing that to make sure everything is all set.
|
||||
If you're still having trouble, check out the troubleshooting guide at #{guide_url}.
|
||||
|
||||
MSG
|
||||
else
|
||||
<<~MSG
|
||||
It looks like Ruby and Bundler can connect to #{host}, but RubyGems itself cannot.
|
||||
You can likely solve this by manually downloading and installing a RubyGems update.
|
||||
Visit #{guide_url} for instructions on how to manually upgrade RubyGems.
|
||||
|
||||
MSG
|
||||
end
|
||||
|
||||
Bundler.ui.info("\n#{message}")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def show_ssl_certs
|
||||
ssl_cert_file = ENV["SSL_CERT_FILE"] || OpenSSL::X509::DEFAULT_CERT_FILE
|
||||
ssl_cert_dir = ENV["SSL_CERT_DIR"] || OpenSSL::X509::DEFAULT_CERT_DIR
|
||||
|
||||
<<~MSG
|
||||
Below affect only Ruby net/http connections:
|
||||
SSL_CERT_FILE: #{File.exist?(ssl_cert_file) ? "exists #{ssl_cert_file}" : "is missing #{ssl_cert_file}"}
|
||||
SSL_CERT_DIR: #{Dir.exist?(ssl_cert_dir) ? "exists #{ssl_cert_dir}" : "is missing #{ssl_cert_dir}"}
|
||||
MSG
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,128 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "../current_ruby"
|
||||
|
||||
module Bundler
|
||||
class CLI::Exec
|
||||
attr_reader :options, :args, :cmd
|
||||
|
||||
TRAPPED_SIGNALS = %w[INT].freeze
|
||||
|
||||
def initialize(options, args)
|
||||
@options = options
|
||||
@cmd = args.shift
|
||||
@args = args
|
||||
@args << { close_others: !options.keep_file_descriptors? } unless Bundler.current_ruby.jruby?
|
||||
end
|
||||
|
||||
def run
|
||||
validate_cmd!
|
||||
SharedHelpers.set_bundle_environment
|
||||
if bin_path = Bundler.which(cmd)
|
||||
if !Bundler.settings[:disable_exec_load] && directly_loadable?(bin_path)
|
||||
bin_path.delete_suffix!(".bat") if Gem.win_platform?
|
||||
kernel_load(bin_path, *args)
|
||||
else
|
||||
bin_path = explicit_path(bin_path)
|
||||
kernel_exec(bin_path, *args)
|
||||
end
|
||||
else
|
||||
# exec using the given command
|
||||
kernel_exec(cmd, *args)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_cmd!
|
||||
return unless cmd.nil?
|
||||
Bundler.ui.error "bundler: exec needs a command to run"
|
||||
exit 128
|
||||
end
|
||||
|
||||
def kernel_exec(*args)
|
||||
Kernel.exec(*args)
|
||||
rescue Errno::EACCES, Errno::ENOEXEC
|
||||
Bundler.ui.error "bundler: not executable: #{cmd}"
|
||||
exit 126
|
||||
rescue Errno::ENOENT
|
||||
Bundler.ui.error "bundler: command not found: #{cmd}"
|
||||
Bundler.ui.warn "Install missing gem executables with `bundle install`"
|
||||
exit 127
|
||||
end
|
||||
|
||||
def kernel_load(file, *args)
|
||||
args.pop if args.last.is_a?(Hash)
|
||||
ARGV.replace(args)
|
||||
$0 = file
|
||||
Process.setproctitle(process_title(file, args)) if Process.respond_to?(:setproctitle)
|
||||
require_relative "../setup"
|
||||
TRAPPED_SIGNALS.each {|s| trap(s, "DEFAULT") }
|
||||
Kernel.load(file)
|
||||
rescue SystemExit, SignalException
|
||||
raise
|
||||
rescue Exception # rubocop:disable Lint/RescueException
|
||||
Bundler.ui.error "bundler: failed to load command: #{cmd} (#{file})"
|
||||
Bundler::FriendlyErrors.disable!
|
||||
raise
|
||||
end
|
||||
|
||||
def process_title(file, args)
|
||||
"#{file} #{args.join(" ")}".strip
|
||||
end
|
||||
|
||||
def explicit_path(path)
|
||||
if File.absolute_path?(path) || explicit_relative_path?(path)
|
||||
path
|
||||
else
|
||||
".#{File::SEPARATOR}#{path}"
|
||||
end
|
||||
end
|
||||
|
||||
def explicit_relative_path?(path)
|
||||
[File::SEPARATOR, File::ALT_SEPARATOR].compact.any? do |separator|
|
||||
path.start_with?(".#{separator}", "..#{separator}")
|
||||
end
|
||||
end
|
||||
|
||||
def directly_loadable?(file)
|
||||
if Gem.win_platform?
|
||||
script_wrapper?(file)
|
||||
else
|
||||
ruby_shebang?(file)
|
||||
end
|
||||
end
|
||||
|
||||
def script_wrapper?(file)
|
||||
script_file = file.delete_suffix(".bat")
|
||||
return false unless File.exist?(script_file)
|
||||
|
||||
if File.zero?(script_file)
|
||||
Bundler.ui.warn "#{script_file} is empty"
|
||||
return false
|
||||
end
|
||||
|
||||
header = File.open(file, "r") {|f| f.read(32) }
|
||||
ruby_exe = "#{RbConfig::CONFIG["RUBY_INSTALL_NAME"]}#{RbConfig::CONFIG["EXEEXT"]}"
|
||||
ruby_exe = "ruby.exe" if ruby_exe.empty?
|
||||
header.include?(ruby_exe)
|
||||
end
|
||||
|
||||
def ruby_shebang?(file)
|
||||
possibilities = [
|
||||
"#!/usr/bin/env ruby\n",
|
||||
"#!/usr/bin/env jruby\n",
|
||||
"#!/usr/bin/env truffleruby\n",
|
||||
"#!#{Gem.ruby}\n",
|
||||
]
|
||||
|
||||
if File.zero?(file)
|
||||
Bundler.ui.warn "#{file} is empty"
|
||||
return false
|
||||
end
|
||||
|
||||
first_line = File.open(file, "rb") {|f| f.read(possibilities.map(&:size).max) }
|
||||
possibilities.any? {|shebang| first_line.start_with?(shebang) }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Fund
|
||||
attr_reader :options
|
||||
|
||||
def initialize(options)
|
||||
@options = options
|
||||
end
|
||||
|
||||
def run
|
||||
Bundler.definition.validate_runtime!
|
||||
|
||||
groups = Array(options[:group]).map(&:to_sym)
|
||||
|
||||
deps = if groups.any?
|
||||
Bundler.definition.dependencies_for(groups)
|
||||
else
|
||||
Bundler.definition.requested_dependencies
|
||||
end
|
||||
|
||||
fund_info = deps.each_with_object([]) do |dep, arr|
|
||||
spec = Bundler.definition.specs[dep.name].first
|
||||
if spec.metadata.key?("funding_uri")
|
||||
arr << "* #{spec.name} (#{spec.version})\n Funding: #{spec.metadata["funding_uri"]}"
|
||||
end
|
||||
end
|
||||
|
||||
if fund_info.empty?
|
||||
Bundler.ui.info "None of the installed gems you directly depend on are looking for funding."
|
||||
else
|
||||
Bundler.ui.info fund_info.join("\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,492 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI
|
||||
Bundler.require_thor_actions
|
||||
include Thor::Actions
|
||||
end
|
||||
|
||||
class CLI::Gem
|
||||
TEST_FRAMEWORK_VERSIONS = {
|
||||
"rspec" => "3.0",
|
||||
"minitest" => "5.16",
|
||||
"test-unit" => "3.0",
|
||||
}.freeze
|
||||
|
||||
DEFAULT_GITHUB_USERNAME = "[USERNAME]"
|
||||
|
||||
attr_reader :options, :gem_name, :thor, :name, :target, :extension
|
||||
|
||||
def initialize(options, gem_name, thor)
|
||||
@options = options
|
||||
@gem_name = resolve_name(gem_name)
|
||||
|
||||
@thor = thor
|
||||
thor.behavior = :invoke
|
||||
thor.destination_root = nil
|
||||
|
||||
@name = @gem_name
|
||||
@target = Pathname.new(SharedHelpers.pwd).join(gem_name)
|
||||
|
||||
@extension = options[:ext]
|
||||
|
||||
validate_ext_name if @extension
|
||||
end
|
||||
|
||||
def run
|
||||
Bundler.ui.confirm "Creating gem '#{name}'..."
|
||||
|
||||
underscored_name = name.tr("-", "_")
|
||||
namespaced_path = name.tr("-", "/")
|
||||
constant_name = name.gsub(/-[_-]*(?![_-]|$)/) { "::" }.gsub(/([_-]+|(::)|^)(.|$)/) { $2.to_s + $3.upcase }
|
||||
constant_array = constant_name.split("::")
|
||||
minitest_constant_name = constant_array.clone.tap {|a| a[-1] = "Test#{a[-1]}" }.join("::") # Foo::Bar => Foo::TestBar
|
||||
|
||||
use_git = Bundler.git_present? && options[:git]
|
||||
|
||||
git_author_name = use_git ? `git config user.name`.chomp : ""
|
||||
git_username = use_git ? `git config github.user`.chomp : ""
|
||||
git_user_email = use_git ? `git config user.email`.chomp : ""
|
||||
github_username = github_username(git_username)
|
||||
|
||||
if github_username.empty?
|
||||
homepage_uri = "TODO: Put your gem's website or public repo URL here."
|
||||
source_code_uri = "TODO: Put your gem's public repo URL here."
|
||||
changelog_uri = "TODO: Put your gem's CHANGELOG.md URL here."
|
||||
else
|
||||
homepage_uri = "https://github.com/#{github_username}/#{name}"
|
||||
source_code_uri = "https://github.com/#{github_username}/#{name}"
|
||||
changelog_uri = "https://github.com/#{github_username}/#{name}/blob/main/CHANGELOG.md"
|
||||
end
|
||||
|
||||
config = {
|
||||
name: name,
|
||||
underscored_name: underscored_name,
|
||||
namespaced_path: namespaced_path,
|
||||
makefile_path: "#{underscored_name}/#{underscored_name}",
|
||||
constant_name: constant_name,
|
||||
constant_array: constant_array,
|
||||
author: git_author_name.empty? ? "TODO: Write your name" : git_author_name,
|
||||
email: git_user_email.empty? ? "TODO: Write your email address" : git_user_email,
|
||||
test: options[:test],
|
||||
ext: extension,
|
||||
exe: options[:exe],
|
||||
bundle: options[:bundle],
|
||||
bundler_version: bundler_dependency_version,
|
||||
git: use_git,
|
||||
github_username: github_username.empty? ? DEFAULT_GITHUB_USERNAME : github_username,
|
||||
required_ruby_version: required_ruby_version,
|
||||
rust_builder_required_rubygems_version: rust_builder_required_rubygems_version,
|
||||
minitest_constant_name: minitest_constant_name,
|
||||
ignore_paths: %w[bin/],
|
||||
homepage_uri: homepage_uri,
|
||||
source_code_uri: source_code_uri,
|
||||
changelog_uri: changelog_uri,
|
||||
}
|
||||
ensure_safe_gem_name(name, constant_array)
|
||||
|
||||
templates = {
|
||||
"Gemfile.tt" => Bundler.preferred_gemfile_name,
|
||||
"lib/newgem.rb.tt" => "lib/#{namespaced_path}.rb",
|
||||
"lib/newgem/version.rb.tt" => "lib/#{namespaced_path}/version.rb",
|
||||
"sig/newgem.rbs.tt" => "sig/#{namespaced_path}.rbs",
|
||||
"newgem.gemspec.tt" => "#{name}.gemspec",
|
||||
"Rakefile.tt" => "Rakefile",
|
||||
"README.md.tt" => "README.md",
|
||||
"bin/console.tt" => "bin/console",
|
||||
"bin/setup.tt" => "bin/setup",
|
||||
}
|
||||
|
||||
executables = %w[
|
||||
bin/console
|
||||
bin/setup
|
||||
]
|
||||
|
||||
case Bundler.preferred_gemfile_name
|
||||
when "Gemfile"
|
||||
config[:ignore_paths] << "Gemfile"
|
||||
when "gems.rb"
|
||||
config[:ignore_paths] << "gems.rb"
|
||||
config[:ignore_paths] << "gems.locked"
|
||||
end
|
||||
|
||||
if use_git
|
||||
templates.merge!("gitignore.tt" => ".gitignore")
|
||||
config[:ignore_paths] << ".gitignore"
|
||||
end
|
||||
|
||||
if test_framework = ask_and_set_test_framework
|
||||
config[:test] = test_framework
|
||||
config[:test_framework_version] = TEST_FRAMEWORK_VERSIONS[test_framework]
|
||||
|
||||
case test_framework
|
||||
when "rspec"
|
||||
templates.merge!(
|
||||
"rspec.tt" => ".rspec",
|
||||
"spec/spec_helper.rb.tt" => "spec/spec_helper.rb",
|
||||
"spec/newgem_spec.rb.tt" => "spec/#{namespaced_path}_spec.rb"
|
||||
)
|
||||
config[:test_task] = :spec
|
||||
config[:ignore_paths] << ".rspec"
|
||||
config[:ignore_paths] << "spec/"
|
||||
when "minitest"
|
||||
# Generate path for minitest target file (FileList["test/**/test_*.rb"])
|
||||
# foo => test/test_foo.rb
|
||||
# foo-bar => test/foo/test_bar.rb
|
||||
# foo_bar => test/test_foo_bar.rb
|
||||
paths = namespaced_path.rpartition("/")
|
||||
paths[2] = "test_#{paths[2]}"
|
||||
minitest_namespaced_path = paths.join("")
|
||||
|
||||
templates.merge!(
|
||||
"test/minitest/test_helper.rb.tt" => "test/test_helper.rb",
|
||||
"test/minitest/test_newgem.rb.tt" => "test/#{minitest_namespaced_path}.rb"
|
||||
)
|
||||
config[:test_task] = :test
|
||||
config[:ignore_paths] << "test/"
|
||||
when "test-unit"
|
||||
templates.merge!(
|
||||
"test/test-unit/test_helper.rb.tt" => "test/test_helper.rb",
|
||||
"test/test-unit/newgem_test.rb.tt" => "test/#{namespaced_path}_test.rb"
|
||||
)
|
||||
config[:test_task] = :test
|
||||
config[:ignore_paths] << "test/"
|
||||
end
|
||||
end
|
||||
|
||||
config[:ci] = ask_and_set_ci
|
||||
case config[:ci]
|
||||
when "github"
|
||||
templates.merge!("github/workflows/main.yml.tt" => ".github/workflows/main.yml")
|
||||
if extension == "rust"
|
||||
templates.merge!("github/workflows/build-gems.yml.tt" => ".github/workflows/build-gems.yml")
|
||||
end
|
||||
config[:ignore_paths] << ".github/"
|
||||
when "gitlab"
|
||||
templates.merge!("gitlab-ci.yml.tt" => ".gitlab-ci.yml")
|
||||
config[:ignore_paths] << ".gitlab-ci.yml"
|
||||
when "circle"
|
||||
templates.merge!("circleci/config.yml.tt" => ".circleci/config.yml")
|
||||
config[:ignore_paths] << ".circleci/"
|
||||
end
|
||||
|
||||
if ask_and_set(:mit, "Do you want to license your code permissively under the MIT license?",
|
||||
"Using a MIT license means that any other developer or company will be legally allowed " \
|
||||
"to use your code for free as long as they admit you created it. You can read more about " \
|
||||
"the MIT license at https://choosealicense.com/licenses/mit.")
|
||||
config[:mit] = true
|
||||
Bundler.ui.info "MIT License enabled in config"
|
||||
templates.merge!("LICENSE.txt.tt" => "LICENSE.txt")
|
||||
end
|
||||
|
||||
if ask_and_set(:coc, "Do you want to include a code of conduct in gems you generate?",
|
||||
"Codes of conduct can increase contributions to your project by contributors who " \
|
||||
"prefer safe, respectful, productive, and collaborative spaces. \n" \
|
||||
"See https://github.com/ruby/rubygems/blob/master/CODE_OF_CONDUCT.md")
|
||||
config[:coc] = true
|
||||
Bundler.ui.info "Code of conduct enabled in config"
|
||||
templates.merge!("CODE_OF_CONDUCT.md.tt" => "CODE_OF_CONDUCT.md")
|
||||
end
|
||||
|
||||
if ask_and_set(:changelog, "Do you want to include a changelog?",
|
||||
"A changelog is a file which contains a curated, chronologically ordered list of notable " \
|
||||
"changes for each version of a project. To make it easier for users and contributors to" \
|
||||
" see precisely what notable changes have been made between each release (or version) of" \
|
||||
" the project. Whether consumers or developers, the end users of software are" \
|
||||
" human beings who care about what's in the software. When the software changes, people " \
|
||||
"want to know why and how. see https://keepachangelog.com")
|
||||
config[:changelog] = true
|
||||
Bundler.ui.info "Changelog enabled in config"
|
||||
templates.merge!("CHANGELOG.md.tt" => "CHANGELOG.md")
|
||||
end
|
||||
|
||||
config[:linter] = ask_and_set_linter
|
||||
case config[:linter]
|
||||
when "rubocop"
|
||||
config[:linter_version] = rubocop_version
|
||||
Bundler.ui.info "RuboCop enabled in config"
|
||||
templates.merge!("rubocop.yml.tt" => ".rubocop.yml")
|
||||
config[:ignore_paths] << ".rubocop.yml"
|
||||
when "standard"
|
||||
config[:linter_version] = standard_version
|
||||
Bundler.ui.info "Standard enabled in config"
|
||||
templates.merge!("standard.yml.tt" => ".standard.yml")
|
||||
config[:ignore_paths] << ".standard.yml"
|
||||
end
|
||||
|
||||
if config[:exe]
|
||||
templates.merge!("exe/newgem.tt" => "exe/#{name}")
|
||||
executables.push("exe/#{name}")
|
||||
end
|
||||
|
||||
if extension == "c"
|
||||
templates.merge!(
|
||||
"ext/newgem/extconf-c.rb.tt" => "ext/#{name}/extconf.rb",
|
||||
"ext/newgem/newgem.h.tt" => "ext/#{name}/#{underscored_name}.h",
|
||||
"ext/newgem/newgem.c.tt" => "ext/#{name}/#{underscored_name}.c"
|
||||
)
|
||||
end
|
||||
|
||||
if extension == "rust"
|
||||
templates.merge!(
|
||||
"Cargo.toml.tt" => "Cargo.toml",
|
||||
"ext/newgem/Cargo.toml.tt" => "ext/#{name}/Cargo.toml",
|
||||
"ext/newgem/build.rs.tt" => "ext/#{name}/build.rs",
|
||||
"ext/newgem/extconf-rust.rb.tt" => "ext/#{name}/extconf.rb",
|
||||
"ext/newgem/src/lib.rs.tt" => "ext/#{name}/src/lib.rs",
|
||||
)
|
||||
end
|
||||
|
||||
if extension == "go"
|
||||
templates.merge!(
|
||||
"ext/newgem/go.mod.tt" => "ext/#{name}/go.mod",
|
||||
"ext/newgem/extconf-go.rb.tt" => "ext/#{name}/extconf.rb",
|
||||
"ext/newgem/newgem.h.tt" => "ext/#{name}/#{underscored_name}.h",
|
||||
"ext/newgem/newgem.go.tt" => "ext/#{name}/#{underscored_name}.go",
|
||||
"ext/newgem/newgem-go.c.tt" => "ext/#{name}/#{underscored_name}.c",
|
||||
)
|
||||
|
||||
config[:go_module_username] = config[:github_username] == DEFAULT_GITHUB_USERNAME ? "username" : config[:github_username]
|
||||
end
|
||||
|
||||
if target.exist? && !target.directory?
|
||||
Bundler.ui.error "Couldn't create a new gem named `#{gem_name}` because there's an existing file named `#{gem_name}`."
|
||||
exit Bundler::BundlerError.all_errors[Bundler::GenericSystemCallError]
|
||||
end
|
||||
|
||||
if use_git
|
||||
Bundler.ui.info "\nInitializing git repo in #{target}"
|
||||
IO.popen(["git", "init", target.to_s], &:read)
|
||||
|
||||
config[:git_default_branch] = File.read("#{target}/.git/HEAD").split("/").last.chomp
|
||||
end
|
||||
|
||||
templates.each do |src, dst|
|
||||
destination = target.join(dst)
|
||||
thor.template("newgem/#{src}", destination, config)
|
||||
end
|
||||
|
||||
executables.each do |file|
|
||||
path = target.join(file)
|
||||
executable = (path.stat.mode | 0o111)
|
||||
path.chmod(executable)
|
||||
end
|
||||
|
||||
if use_git
|
||||
IO.popen(%w[git add .], { chdir: target }, &:read)
|
||||
end
|
||||
|
||||
if config[:bundle]
|
||||
Bundler.ui.info "Running bundle install in the new gem directory."
|
||||
Dir.chdir(target) do
|
||||
system("bundle install")
|
||||
end
|
||||
end
|
||||
|
||||
# Open gemspec in editor
|
||||
open_editor(options["edit"], target.join("#{name}.gemspec")) if options[:edit]
|
||||
|
||||
Bundler.ui.info "\nGem '#{name}' was successfully created. " \
|
||||
"For more information on making a RubyGem visit https://guides.rubygems.org/make-your-own-gem/"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resolve_name(name)
|
||||
Pathname.new(SharedHelpers.pwd).join(name).basename.to_s
|
||||
end
|
||||
|
||||
def ask_and_set(key, prompt, explanation)
|
||||
choice = options[key]
|
||||
choice = Bundler.settings["gem.#{key}"] if choice.nil?
|
||||
|
||||
if choice.nil?
|
||||
Bundler.ui.info "\n#{explanation}"
|
||||
choice = Bundler.ui.yes? "#{prompt} y/(n):"
|
||||
Bundler.settings.set_global("gem.#{key}", choice)
|
||||
end
|
||||
|
||||
choice
|
||||
end
|
||||
|
||||
def validate_ext_name
|
||||
return unless gem_name.index("-")
|
||||
|
||||
Bundler.ui.error "You have specified a gem name which does not conform to the \n" \
|
||||
"naming guidelines for C extensions. For more information, \n" \
|
||||
"see the 'Extension Naming' section at the following URL:\n" \
|
||||
"https://guides.rubygems.org/gems-with-extensions/\n"
|
||||
exit 1
|
||||
end
|
||||
|
||||
def ask_and_set_test_framework
|
||||
return if skip?(:test)
|
||||
test_framework = options[:test] || Bundler.settings["gem.test"]
|
||||
|
||||
if test_framework.to_s.empty?
|
||||
Bundler.ui.info "\nDo you want to generate tests with your gem?"
|
||||
Bundler.ui.info hint_text("test")
|
||||
|
||||
result = Bundler.ui.ask "Enter a test framework. rspec/minitest/test-unit/(none):"
|
||||
if /rspec|minitest|test-unit/.match?(result)
|
||||
test_framework = result
|
||||
else
|
||||
test_framework = false
|
||||
end
|
||||
end
|
||||
|
||||
if Bundler.settings["gem.test"].nil?
|
||||
Bundler.settings.set_global("gem.test", test_framework)
|
||||
end
|
||||
|
||||
if options[:test] == Bundler.settings["gem.test"]
|
||||
Bundler.ui.info "#{options[:test]} is already configured, ignoring --test flag."
|
||||
end
|
||||
|
||||
test_framework
|
||||
end
|
||||
|
||||
def skip?(option)
|
||||
options.key?(option) && options[option].nil?
|
||||
end
|
||||
|
||||
def hint_text(setting)
|
||||
if Bundler.settings["gem.#{setting}"] == false
|
||||
"Your choice will only be applied to this gem."
|
||||
else
|
||||
"Future `bundle gem` calls will use your choice. " \
|
||||
"This setting can be changed anytime with `bundle config gem.#{setting}`."
|
||||
end
|
||||
end
|
||||
|
||||
def ask_and_set_ci
|
||||
return if skip?(:ci)
|
||||
ci_template = options[:ci] || Bundler.settings["gem.ci"]
|
||||
|
||||
if ci_template.to_s.empty?
|
||||
Bundler.ui.info "\nDo you want to set up continuous integration for your gem? " \
|
||||
"Supported services:\n" \
|
||||
"* CircleCI: https://circleci.com/\n" \
|
||||
"* GitHub Actions: https://github.com/features/actions\n" \
|
||||
"* GitLab CI: https://docs.gitlab.com/ee/ci/\n"
|
||||
Bundler.ui.info hint_text("ci")
|
||||
|
||||
result = Bundler.ui.ask "Enter a CI service. github/gitlab/circle/(none):"
|
||||
if /github|gitlab|circle/.match?(result)
|
||||
ci_template = result
|
||||
else
|
||||
ci_template = false
|
||||
end
|
||||
end
|
||||
|
||||
if Bundler.settings["gem.ci"].nil?
|
||||
Bundler.settings.set_global("gem.ci", ci_template)
|
||||
end
|
||||
|
||||
if options[:ci] == Bundler.settings["gem.ci"]
|
||||
Bundler.ui.info "#{options[:ci]} is already configured, ignoring --ci flag."
|
||||
end
|
||||
|
||||
ci_template
|
||||
end
|
||||
|
||||
def ask_and_set_linter
|
||||
return if skip?(:linter)
|
||||
linter_template = options[:linter] || Bundler.settings["gem.linter"]
|
||||
|
||||
if linter_template.to_s.empty?
|
||||
Bundler.ui.info "\nDo you want to add a code linter and formatter to your gem? " \
|
||||
"Supported Linters:\n" \
|
||||
"* RuboCop: https://rubocop.org\n" \
|
||||
"* Standard: https://github.com/standardrb/standard\n"
|
||||
Bundler.ui.info hint_text("linter")
|
||||
|
||||
result = Bundler.ui.ask "Enter a linter. rubocop/standard/(none):"
|
||||
if /rubocop|standard/.match?(result)
|
||||
linter_template = result
|
||||
else
|
||||
linter_template = false
|
||||
end
|
||||
end
|
||||
|
||||
if Bundler.settings["gem.linter"].nil?
|
||||
Bundler.settings.set_global("gem.linter", linter_template)
|
||||
end
|
||||
|
||||
# Once gem.linter safely set, unset the deprecated gem.rubocop
|
||||
unless Bundler.settings["gem.rubocop"].nil?
|
||||
Bundler.settings.set_global("gem.rubocop", nil)
|
||||
end
|
||||
|
||||
if options[:linter] == Bundler.settings["gem.linter"]
|
||||
Bundler.ui.info "#{options[:linter]} is already configured, ignoring --linter flag."
|
||||
end
|
||||
|
||||
linter_template
|
||||
end
|
||||
|
||||
def bundler_dependency_version
|
||||
v = Gem::Version.new(Bundler::VERSION)
|
||||
req = v.segments[0..1]
|
||||
req << "a" if v.prerelease?
|
||||
req.join(".")
|
||||
end
|
||||
|
||||
def ensure_safe_gem_name(name, constant_array)
|
||||
if /^\d/.match?(name)
|
||||
Bundler.ui.error "Invalid gem name #{name} Please give a name which does not start with numbers."
|
||||
exit 1
|
||||
end
|
||||
|
||||
if /[A-Z]/.match?(name)
|
||||
Bundler.ui.warn "Gem names with capital letters are not recommended. Please use only lowercase letters, numbers, and hyphens."
|
||||
end
|
||||
|
||||
constant_name = constant_array.join("::")
|
||||
|
||||
existing_constant = constant_array.inject(Object) do |c, s|
|
||||
defined = begin
|
||||
c.const_defined?(s)
|
||||
rescue NameError
|
||||
Bundler.ui.error "Invalid gem name #{name} -- `#{constant_name}` is an invalid constant name"
|
||||
exit 1
|
||||
end
|
||||
(defined && c.const_get(s)) || break
|
||||
end
|
||||
|
||||
return unless existing_constant
|
||||
Bundler.ui.error "Invalid gem name #{name} constant #{constant_name} is already in use. Please choose another gem name."
|
||||
exit 1
|
||||
end
|
||||
|
||||
def open_editor(editor, file)
|
||||
thor.run(%(#{editor} "#{file}"))
|
||||
end
|
||||
|
||||
def rust_builder_required_rubygems_version
|
||||
"3.3.11"
|
||||
end
|
||||
|
||||
def required_ruby_version
|
||||
"3.2.0"
|
||||
end
|
||||
|
||||
def rubocop_version
|
||||
"1.21"
|
||||
end
|
||||
|
||||
def standard_version
|
||||
"1.3"
|
||||
end
|
||||
|
||||
def github_username(git_username)
|
||||
if options[:github_username].nil?
|
||||
git_username
|
||||
elsif options[:github_username] == false
|
||||
""
|
||||
else
|
||||
options[:github_username]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,83 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Info
|
||||
attr_reader :gem_name, :options
|
||||
def initialize(options, gem_name)
|
||||
@options = options
|
||||
@gem_name = gem_name
|
||||
end
|
||||
|
||||
def run
|
||||
Bundler.ui.silence do
|
||||
Bundler.definition.validate_runtime!
|
||||
Bundler.load.lock
|
||||
end
|
||||
|
||||
spec = spec_for_gem(gem_name)
|
||||
|
||||
if spec
|
||||
return print_gem_path(spec) if @options[:path]
|
||||
return print_gem_version(spec) if @options[:version]
|
||||
print_gem_info(spec)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def spec_for_gem(name)
|
||||
Bundler::CLI::Common.select_spec(name, :regex_match)
|
||||
end
|
||||
|
||||
def print_gem_version(spec)
|
||||
Bundler.ui.info spec.version.to_s
|
||||
end
|
||||
|
||||
def print_gem_path(spec)
|
||||
name = spec.name
|
||||
if name == "bundler"
|
||||
path = File.expand_path("../../..", __dir__)
|
||||
else
|
||||
path = spec.full_gem_path
|
||||
if spec.installation_missing?
|
||||
return Bundler.ui.warn "The gem #{name} is missing. It should be installed at #{path}, but was not found"
|
||||
end
|
||||
end
|
||||
|
||||
Bundler.ui.info path
|
||||
end
|
||||
|
||||
def print_gem_info(spec)
|
||||
metadata = spec.metadata
|
||||
name = spec.name
|
||||
gem_info = String.new
|
||||
gem_info << " * #{name} (#{spec.version}#{spec.git_version})\n"
|
||||
gem_info << "\tSummary: #{spec.summary}\n" if spec.summary
|
||||
gem_info << "\tHomepage: #{spec.homepage}\n" if spec.homepage
|
||||
gem_info << "\tDocumentation: #{metadata["documentation_uri"]}\n" if metadata.key?("documentation_uri")
|
||||
gem_info << "\tSource Code: #{metadata["source_code_uri"]}\n" if metadata.key?("source_code_uri")
|
||||
gem_info << "\tFunding: #{metadata["funding_uri"]}\n" if metadata.key?("funding_uri")
|
||||
gem_info << "\tWiki: #{metadata["wiki_uri"]}\n" if metadata.key?("wiki_uri")
|
||||
gem_info << "\tChangelog: #{metadata["changelog_uri"]}\n" if metadata.key?("changelog_uri")
|
||||
gem_info << "\tBug Tracker: #{metadata["bug_tracker_uri"]}\n" if metadata.key?("bug_tracker_uri")
|
||||
gem_info << "\tMailing List: #{metadata["mailing_list_uri"]}\n" if metadata.key?("mailing_list_uri")
|
||||
gem_info << "\tPath: #{spec.full_gem_path}\n"
|
||||
gem_info << "\tDefault Gem: yes\n" if spec.respond_to?(:default_gem?) && spec.default_gem?
|
||||
gem_info << "\tReverse Dependencies: \n\t\t#{gem_dependencies.join("\n\t\t")}" if gem_dependencies.any?
|
||||
|
||||
if name != "bundler" && spec.installation_missing?
|
||||
return Bundler.ui.warn "The gem #{name} is missing. Gemspec information is still available though:\n#{gem_info}"
|
||||
end
|
||||
|
||||
Bundler.ui.info gem_info
|
||||
end
|
||||
|
||||
def gem_dependencies
|
||||
@gem_dependencies ||= Bundler.definition.specs.filter_map do |spec|
|
||||
dependency = spec.dependencies.find {|dep| dep.name == gem_name }
|
||||
next unless dependency
|
||||
"#{spec.name} (#{spec.version}) depends on #{gem_name} (#{dependency.requirements_list.join(", ")})"
|
||||
end.sort
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,55 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Init
|
||||
attr_reader :options
|
||||
def initialize(options)
|
||||
@options = options
|
||||
end
|
||||
|
||||
def run
|
||||
if File.exist?(gemfile)
|
||||
Bundler.ui.error "#{gemfile} already exists at #{File.expand_path(gemfile)}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
unless File.writable?(Dir.pwd)
|
||||
Bundler.ui.error "Can not create #{gemfile} as the current directory is not writable."
|
||||
exit 1
|
||||
end
|
||||
|
||||
if options[:gemspec]
|
||||
gemspec = File.expand_path(options[:gemspec])
|
||||
unless File.exist?(gemspec)
|
||||
Bundler.ui.error "Gem specification #{gemspec} doesn't exist"
|
||||
exit 1
|
||||
end
|
||||
|
||||
spec = Bundler.load_gemspec_uncached(gemspec)
|
||||
unless spec
|
||||
Bundler.ui.error "Gem specification #{gemspec} did not produce a specification"
|
||||
exit 1
|
||||
end
|
||||
|
||||
File.open(gemfile, "wb") do |file|
|
||||
file << "# Generated from #{gemspec}\n"
|
||||
file << spec.to_gemfile
|
||||
end
|
||||
else
|
||||
File.open(File.expand_path("../templates/Gemfile", __dir__), "r") do |template|
|
||||
File.open(gemfile, "wb") do |destination|
|
||||
IO.copy_stream(template, destination)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
puts "Writing new #{gemfile} to #{SharedHelpers.pwd}/#{gemfile}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def gemfile
|
||||
@gemfile ||= options[:gemfile] || Bundler.preferred_gemfile_name
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,128 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Install
|
||||
attr_reader :options
|
||||
def initialize(options)
|
||||
@options = options
|
||||
end
|
||||
|
||||
def run
|
||||
Bundler.ui.level = "warn" if options[:quiet]
|
||||
|
||||
warn_if_root
|
||||
|
||||
if options[:local]
|
||||
Bundler.self_manager.restart_with_locked_bundler_if_needed
|
||||
else
|
||||
Bundler.self_manager.install_locked_bundler_and_restart_with_it_if_needed
|
||||
end
|
||||
|
||||
Bundler::SharedHelpers.set_env "RB_USER_INSTALL", "1" if Gem.freebsd_platform?
|
||||
|
||||
if target_rbconfig_path = options[:"target-rbconfig"]
|
||||
Bundler.rubygems.set_target_rbconfig(target_rbconfig_path)
|
||||
end
|
||||
|
||||
check_trust_policy
|
||||
|
||||
if Bundler.frozen_bundle? && !Bundler.default_lockfile.exist?
|
||||
flag = "deployment setting" if Bundler.settings[:deployment]
|
||||
flag = "frozen setting" if Bundler.settings[:frozen]
|
||||
raise ProductionError, "The #{flag} requires a lockfile. Please make " \
|
||||
"sure you have checked your #{SharedHelpers.relative_lockfile_path} into version control " \
|
||||
"before deploying."
|
||||
end
|
||||
|
||||
normalize_settings
|
||||
|
||||
Bundler::Fetcher.disable_endpoint = options["full-index"]
|
||||
|
||||
Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins]
|
||||
|
||||
# For install we want to enable strict validation
|
||||
# (rather than some optimizations we perform at app runtime).
|
||||
definition = Bundler.definition(strict: true)
|
||||
definition.validate_runtime!
|
||||
definition.lockfile = options["lockfile"] if options["lockfile"]
|
||||
definition.lockfile = false if options["no-lock"]
|
||||
|
||||
installer = Installer.install(Bundler.root, definition, options)
|
||||
|
||||
Bundler.settings.temporary(cache_all_platforms: options[:local] ? false : Bundler.settings[:cache_all_platforms]) do
|
||||
Bundler.load.cache(nil, options[:local]) if Bundler.app_cache.exist? && !options["no-cache"] && !Bundler.frozen_bundle?
|
||||
end
|
||||
|
||||
Bundler.ui.confirm "Bundle complete! #{dependencies_count_for(definition)}, #{gems_installed_for(definition)}."
|
||||
Bundler::CLI::Common.output_without_groups_message(:install)
|
||||
|
||||
if Bundler.use_system_gems?
|
||||
Bundler.ui.confirm "Use `bundle info [gemname]` to see where a bundled gem is installed."
|
||||
else
|
||||
relative_path = Bundler.configured_bundle_path.base_path_relative_to_pwd
|
||||
Bundler.ui.confirm "Bundled gems are installed into `#{relative_path}`"
|
||||
end
|
||||
|
||||
Bundler::CLI::Common.output_post_install_messages installer.post_install_messages
|
||||
Bundler::CLI::Common.output_cooldown_skipped_summary(definition)
|
||||
|
||||
if CLI::Common.clean_after_install?
|
||||
require_relative "clean"
|
||||
Bundler::CLI::Clean.new(options).run
|
||||
end
|
||||
|
||||
Bundler::CLI::Common.output_fund_metadata_summary
|
||||
rescue Gem::InvalidSpecificationException
|
||||
Bundler.ui.warn "You have one or more invalid gemspecs that need to be fixed."
|
||||
raise
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def warn_if_root
|
||||
return if Bundler.settings[:silence_root_warning] || Gem.win_platform? || !Process.uid.zero?
|
||||
Bundler.ui.warn "Don't run Bundler as root. Installing your bundle as root " \
|
||||
"will break this application for all non-root users on this machine.", wrap: true
|
||||
end
|
||||
|
||||
def dependencies_count_for(definition)
|
||||
count = definition.dependencies.count
|
||||
"#{count} Gemfile #{count == 1 ? "dependency" : "dependencies"}"
|
||||
end
|
||||
|
||||
def gems_installed_for(definition)
|
||||
count = definition.specs.count {|spec| spec.name != "bundler" }
|
||||
"#{count} #{count == 1 ? "gem" : "gems"} now installed"
|
||||
end
|
||||
|
||||
def check_trust_policy
|
||||
trust_policy = options["trust-policy"]
|
||||
unless Bundler.rubygems.security_policies.keys.unshift(nil).include?(trust_policy)
|
||||
raise InvalidOption, "RubyGems doesn't know about trust policy '#{trust_policy}'. " \
|
||||
"The known policies are: #{Bundler.rubygems.security_policies.keys.join(", ")}."
|
||||
end
|
||||
Bundler.settings.set_command_option_if_given :"trust-policy", trust_policy
|
||||
end
|
||||
|
||||
def normalize_settings
|
||||
if options["standalone"] && Bundler.settings[:path].nil? && !options["local"]
|
||||
Bundler.settings.set_command_option :path, "bundle"
|
||||
end
|
||||
|
||||
Bundler.settings.set_command_option_if_given :shebang, options["shebang"]
|
||||
|
||||
Bundler.settings.set_command_option_if_given :jobs, options["jobs"]
|
||||
|
||||
Bundler::CLI::Common.validate_cooldown!(options["cooldown"])
|
||||
Bundler.settings.set_command_option_if_given :cooldown, options["cooldown"]
|
||||
|
||||
Bundler.settings.set_command_option_if_given :no_prune, options["no-prune"]
|
||||
|
||||
Bundler.settings.set_command_option_if_given :no_install, options["no-install"]
|
||||
|
||||
Bundler.settings.set_command_option_if_given :clean, options["clean"]
|
||||
|
||||
options[:force] = options[:redownload] if options[:redownload]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rbconfig"
|
||||
|
||||
module Bundler
|
||||
class CLI::Issue
|
||||
def run
|
||||
Bundler.ui.info <<~EOS
|
||||
Did you find an issue with Bundler? Before filing a new issue,
|
||||
be sure to check out these resources:
|
||||
|
||||
1. Check out our troubleshooting guide for quick fixes to common issues:
|
||||
https://github.com/ruby/rubygems/blob/master/doc/bundler/TROUBLESHOOTING.md
|
||||
|
||||
2. Instructions for common Bundler uses can be found on the documentation
|
||||
site: https://guides.rubygems.org/
|
||||
|
||||
3. Information about each Bundler command can be found in the Bundler
|
||||
man pages: https://guides.rubygems.org/command-reference/bundle/
|
||||
|
||||
Hopefully the troubleshooting steps above resolved your problem! If things
|
||||
still aren't working the way you expect them to, please let us know so
|
||||
that we can diagnose and help fix the problem you're having, by filling
|
||||
in the new issue form located at
|
||||
https://github.com/ruby/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md,
|
||||
and copy and pasting the information below.
|
||||
|
||||
EOS
|
||||
|
||||
Bundler.ui.info Bundler::Env.report
|
||||
|
||||
Bundler.ui.info "\n## Bundle Doctor"
|
||||
doctor
|
||||
end
|
||||
|
||||
def doctor
|
||||
require_relative "doctor/diagnose"
|
||||
Bundler::CLI::Doctor::Diagnose.new({}).run
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,97 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "json"
|
||||
|
||||
module Bundler
|
||||
class CLI::List
|
||||
def initialize(options)
|
||||
@options = options
|
||||
@without_group = options["without-group"].map(&:to_sym)
|
||||
@only_group = options["only-group"].map(&:to_sym)
|
||||
@format = options["format"]
|
||||
end
|
||||
|
||||
def run
|
||||
raise InvalidOption, "The `--only-group` and `--without-group` options cannot be used together" if @only_group.any? && @without_group.any?
|
||||
|
||||
raise InvalidOption, "The `--name-only` and `--paths` options cannot be used together" if @options["name-only"] && @options[:paths]
|
||||
|
||||
specs = if @only_group.any? || @without_group.any?
|
||||
filtered_specs_by_groups
|
||||
else
|
||||
begin
|
||||
Bundler.load.specs
|
||||
rescue GemNotFound => e
|
||||
Bundler.ui.error e.message
|
||||
Bundler.ui.warn "Install missing gems with `bundle install`."
|
||||
exit 1
|
||||
end
|
||||
end.reject {|s| s.name == "bundler" }.sort_by(&:name)
|
||||
|
||||
case @format
|
||||
when "json"
|
||||
print_json(specs: specs)
|
||||
when nil
|
||||
print_human(specs: specs)
|
||||
else
|
||||
raise InvalidOption, "Unknown option`--format=#{@format}`. Supported formats: `json`"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def print_json(specs:)
|
||||
gems = if @options["name-only"]
|
||||
specs.map {|s| { name: s.name } }
|
||||
else
|
||||
specs.map do |s|
|
||||
{
|
||||
name: s.name,
|
||||
version: s.version.to_s,
|
||||
git_version: s.git_version&.strip,
|
||||
}.tap do |h|
|
||||
h[:path] = s.full_gem_path if @options["paths"]
|
||||
end
|
||||
end
|
||||
end
|
||||
Bundler.ui.info({ gems: gems }.to_json)
|
||||
end
|
||||
|
||||
def print_human(specs:)
|
||||
return Bundler.ui.info "No gems in the Gemfile" if specs.empty?
|
||||
|
||||
return specs.each {|s| Bundler.ui.info s.name } if @options["name-only"]
|
||||
return specs.each {|s| Bundler.ui.info s.full_gem_path } if @options["paths"]
|
||||
|
||||
Bundler.ui.info "Gems included by the bundle:"
|
||||
|
||||
specs.each {|s| Bundler.ui.info " * #{s.name} (#{s.version}#{s.git_version})" }
|
||||
|
||||
Bundler.ui.info "Use `bundle info` to print more detailed information about a gem"
|
||||
end
|
||||
|
||||
def verify_group_exists(groups)
|
||||
(@without_group + @only_group).each do |group|
|
||||
raise InvalidOption, "`#{group}` group could not be found." unless groups.include?(group)
|
||||
end
|
||||
end
|
||||
|
||||
def filtered_specs_by_groups
|
||||
definition = Bundler.definition
|
||||
groups = definition.groups
|
||||
|
||||
verify_group_exists(groups)
|
||||
|
||||
show_groups =
|
||||
if @without_group.any?
|
||||
groups.reject {|g| @without_group.include?(g) }
|
||||
elsif @only_group.any?
|
||||
groups.select {|g| @only_group.include?(g) }
|
||||
else
|
||||
groups
|
||||
end.map(&:to_sym)
|
||||
|
||||
definition.specs_for(show_groups)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,102 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Lock
|
||||
attr_reader :options
|
||||
|
||||
def initialize(options)
|
||||
@options = options
|
||||
end
|
||||
|
||||
def run
|
||||
unless Bundler.default_gemfile
|
||||
Bundler.ui.error "Unable to find a Gemfile to lock"
|
||||
exit 1
|
||||
end
|
||||
|
||||
check_for_conflicting_options
|
||||
|
||||
Bundler::CLI::Common.validate_cooldown!(options[:cooldown])
|
||||
Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown]
|
||||
|
||||
print = options[:print]
|
||||
previous_output_stream = Bundler.ui.output_stream
|
||||
Bundler.ui.output_stream = :stderr if print
|
||||
|
||||
Bundler::Fetcher.disable_endpoint = options["full-index"]
|
||||
|
||||
update = options[:update]
|
||||
# --update is repeatable, so it parses as an array with one entry per
|
||||
# occurrence, where a bare `--update` produces a `true` entry
|
||||
update = update.include?(true) ? true : update.flatten if update.is_a?(Array)
|
||||
conservative = options[:conservative]
|
||||
bundler = options[:bundler]
|
||||
|
||||
if update.is_a?(Array) # unlocking specific gems
|
||||
Bundler::CLI::Common.ensure_all_gems_in_lockfile!(update)
|
||||
update = { gems: update, conservative: conservative }
|
||||
elsif update && conservative
|
||||
update = { conservative: conservative }
|
||||
elsif update && bundler
|
||||
update = { bundler: bundler }
|
||||
end
|
||||
|
||||
Bundler.settings.temporary(frozen: false) do
|
||||
definition = Bundler.definition(update, Bundler.default_lockfile)
|
||||
definition.add_checksums if options["add-checksums"]
|
||||
|
||||
Bundler::CLI::Common.configure_gem_version_promoter(definition, options) if options[:update]
|
||||
|
||||
options["remove-platform"].flatten.each do |platform_string|
|
||||
platform = Gem::Platform.new(platform_string)
|
||||
definition.remove_platform(platform)
|
||||
end
|
||||
|
||||
options["add-platform"].flatten.each do |platform_string|
|
||||
platform = Gem::Platform.new(platform_string)
|
||||
if platform.to_s == "unknown"
|
||||
Bundler.ui.error "The platform `#{platform_string}` is unknown to RubyGems and can't be added to the lockfile."
|
||||
exit 1
|
||||
end
|
||||
definition.add_platform(platform)
|
||||
end
|
||||
|
||||
if definition.platforms.empty?
|
||||
raise InvalidOption, "Removing all platforms from the bundle is not allowed"
|
||||
end
|
||||
|
||||
definition.remotely! unless options[:local]
|
||||
|
||||
if options["normalize-platforms"]
|
||||
definition.normalize_platforms
|
||||
end
|
||||
|
||||
if print
|
||||
puts definition.to_lock
|
||||
else
|
||||
file = options[:lockfile]
|
||||
file = file ? Pathname.new(file).expand_path : Bundler.default_lockfile
|
||||
|
||||
puts "Writing lockfile to #{file}"
|
||||
definition.write_lock(file, false)
|
||||
end
|
||||
|
||||
Bundler::CLI::Common.output_cooldown_skipped_summary(definition)
|
||||
end
|
||||
|
||||
Bundler.ui.output_stream = previous_output_stream
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_for_conflicting_options
|
||||
if options["normalize-platforms"] && options["add-platform"].any?
|
||||
raise InvalidOption, "--normalize-platforms can't be used with --add-platform"
|
||||
end
|
||||
|
||||
if options["normalize-platforms"] && options["remove-platform"].any?
|
||||
raise InvalidOption, "--normalize-platforms can't be used with --remove-platform"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Open
|
||||
attr_reader :options, :name, :path
|
||||
def initialize(options, name)
|
||||
@options = options
|
||||
@name = name
|
||||
@path = options[:path] unless options[:path].nil?
|
||||
end
|
||||
|
||||
def run
|
||||
raise InvalidOption, "Cannot specify `--path` option without a value" if !@path.nil? && @path.empty?
|
||||
editor = [ENV["BUNDLER_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find {|e| !e.nil? && !e.empty? }
|
||||
return Bundler.ui.info("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR") unless editor
|
||||
return unless spec = Bundler::CLI::Common.select_spec(name, :regex_match)
|
||||
if spec.default_gem?
|
||||
Bundler.ui.info "Unable to open #{name} because it's a default gem, so the directory it would normally be installed to does not exist."
|
||||
else
|
||||
root_path = spec.full_gem_path
|
||||
command = editor_command(editor) << File.join([root_path, path].compact)
|
||||
Bundler.with_original_env do
|
||||
system(*command, { chdir: root_path })
|
||||
end || Bundler.ui.info("Could not run '#{command.join(" ")}'")
|
||||
end
|
||||
end
|
||||
|
||||
def editor_command(editor)
|
||||
# On Windows an editor is often configured with a full path such as
|
||||
# C:\Program Files\Microsoft VS Code\Code.exe, which shell splitting
|
||||
# would corrupt. Take a value that names an existing file as a
|
||||
# single word.
|
||||
return [editor] if Gem.win_platform? && File.file?(editor)
|
||||
|
||||
require "shellwords"
|
||||
Shellwords.split(editor)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,367 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Outdated
|
||||
attr_reader :options, :gems, :options_include_groups, :filter_options_patch, :sources, :strict
|
||||
attr_accessor :outdated_gems
|
||||
|
||||
def initialize(options, gems)
|
||||
@options = options
|
||||
@gems = gems
|
||||
@sources = Array(options[:source])
|
||||
|
||||
@filter_options_patch = options.keys & %w[filter-major filter-minor filter-patch]
|
||||
|
||||
@outdated_gems = []
|
||||
|
||||
@options_include_groups = [:group, :groups].any? do |v|
|
||||
options.keys.include?(v.to_s)
|
||||
end
|
||||
|
||||
# the patch level options imply strict is also true. It wouldn't make
|
||||
# sense otherwise.
|
||||
@strict = options["filter-strict"] || Bundler::CLI::Common.patch_level_options(options).any?
|
||||
end
|
||||
|
||||
def run
|
||||
check_for_deployment_mode!
|
||||
|
||||
Bundler::CLI::Common.validate_cooldown!(options[:cooldown])
|
||||
Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown]
|
||||
|
||||
Bundler.definition.validate_runtime!
|
||||
current_specs = Bundler.ui.silence { Bundler.definition.resolve }
|
||||
|
||||
gems.each do |gem_name|
|
||||
if current_specs[gem_name].empty?
|
||||
raise GemNotFound, "Could not find gem '#{gem_name}'."
|
||||
end
|
||||
end
|
||||
|
||||
current_dependencies = Bundler.ui.silence do
|
||||
Bundler.load.dependencies.map {|dep| [dep.name, dep] }.to_h
|
||||
end
|
||||
|
||||
definition = if gems.empty? && sources.empty?
|
||||
# We're doing a full update
|
||||
Bundler.definition(true)
|
||||
else
|
||||
Bundler.definition(gems: gems, sources: sources)
|
||||
end
|
||||
|
||||
Bundler::CLI::Common.configure_gem_version_promoter(
|
||||
Bundler.definition,
|
||||
options.merge(strict: @strict)
|
||||
)
|
||||
|
||||
definition_resolution = proc do
|
||||
options[:local] ? definition.resolve_with_cache! : definition.resolve_remotely!
|
||||
end
|
||||
|
||||
if options[:parseable]
|
||||
Bundler.ui.progress(&definition_resolution)
|
||||
else
|
||||
definition_resolution.call
|
||||
end
|
||||
|
||||
Bundler.ui.info ""
|
||||
|
||||
# Loop through the current specs
|
||||
gemfile_specs, dependency_specs = current_specs.partition do |spec|
|
||||
current_dependencies.key? spec.name
|
||||
end
|
||||
|
||||
specs = if options["only-explicit"]
|
||||
gemfile_specs
|
||||
else
|
||||
gemfile_specs + dependency_specs
|
||||
end
|
||||
|
||||
specs.sort_by(&:name).uniq(&:name).each do |current_spec|
|
||||
next unless gems.empty? || gems.include?(current_spec.name)
|
||||
|
||||
active_spec = retrieve_active_spec(definition, current_spec)
|
||||
next unless active_spec
|
||||
|
||||
next unless filter_options_patch.empty? || update_present_via_semver_portions(current_spec, active_spec, options)
|
||||
|
||||
gem_outdated = Gem::Version.new(active_spec.version) > Gem::Version.new(current_spec.version)
|
||||
next unless gem_outdated || (current_spec.git_version != active_spec.git_version)
|
||||
|
||||
dependency = current_dependencies[current_spec.name]
|
||||
groups = ""
|
||||
if dependency && !options[:parseable]
|
||||
groups = dependency.groups.join(", ")
|
||||
end
|
||||
|
||||
outdated_gems << {
|
||||
active_spec: active_spec,
|
||||
current_spec: current_spec,
|
||||
dependency: dependency,
|
||||
groups: groups,
|
||||
}
|
||||
end
|
||||
|
||||
relevant_outdated_gems = if options_include_groups
|
||||
outdated_gems.group_by {|g| g[:groups] }.sort.flat_map do |groups, gems|
|
||||
contains_group = groups.split(", ").include?(options[:group])
|
||||
next unless options[:groups] || contains_group
|
||||
|
||||
gems
|
||||
end.compact
|
||||
else
|
||||
outdated_gems
|
||||
end
|
||||
|
||||
if relevant_outdated_gems.empty?
|
||||
unless options[:parseable]
|
||||
Bundler.ui.info(nothing_outdated_message)
|
||||
end
|
||||
else
|
||||
if options[:parseable]
|
||||
print_gems(relevant_outdated_gems)
|
||||
else
|
||||
print_gems_table(relevant_outdated_gems)
|
||||
end
|
||||
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def loaded_from_for(spec)
|
||||
return unless spec.respond_to?(:loaded_from)
|
||||
|
||||
spec.loaded_from
|
||||
end
|
||||
|
||||
def groups_text(group_text, groups)
|
||||
"#{group_text}#{groups.split(",").size > 1 ? "s" : ""} \"#{groups}\""
|
||||
end
|
||||
|
||||
def nothing_outdated_message
|
||||
if filter_options_patch.any?
|
||||
display = filter_options_patch.map do |o|
|
||||
o.sub("filter-", "")
|
||||
end.join(" or ")
|
||||
|
||||
"No #{display} updates to display.\n"
|
||||
else
|
||||
"Bundle up to date!\n"
|
||||
end
|
||||
end
|
||||
|
||||
def retrieve_active_spec(definition, current_spec)
|
||||
active_spec = definition.resolve.find_by_name_and_platform(current_spec.name, current_spec.platform)
|
||||
return unless active_spec
|
||||
|
||||
return active_spec if strict
|
||||
|
||||
matching_specs(active_spec, current_spec).last
|
||||
end
|
||||
|
||||
def matching_specs(active_spec, current_spec)
|
||||
@matching_specs ||= {}
|
||||
@matching_specs[[active_spec.source, current_spec.name, current_spec.platform]] ||= begin
|
||||
active_specs = active_spec.source.specs.search(current_spec.name).select {|spec| spec.installable_on_platform?(current_spec.platform) }.sort_by(&:version)
|
||||
if !current_spec.version.prerelease? && !options[:pre] && active_specs.size > 1
|
||||
active_specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? }
|
||||
end
|
||||
active_specs
|
||||
end
|
||||
end
|
||||
|
||||
# The newest version the cooldown setting would let bundler adopt right
|
||||
# now, when the newest overall version is still inside the window. Only a
|
||||
# version strictly between the installed one and the newest one is an
|
||||
# adoptable update worth showing.
|
||||
def newest_out_of_cooldown(active_spec, current_spec)
|
||||
newest = matching_specs(active_spec, current_spec).reverse_each.find {|spec| cooldown_days_remaining(spec).nil? }
|
||||
return unless newest
|
||||
return if newest.version >= active_spec.version || newest.version <= current_spec.version
|
||||
newest
|
||||
end
|
||||
|
||||
def print_gems(gems_list)
|
||||
gems_list.each do |gem|
|
||||
print_gem(
|
||||
gem[:current_spec],
|
||||
gem[:active_spec],
|
||||
gem[:dependency],
|
||||
gem[:groups],
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def print_gems_table(gems_list)
|
||||
data = gems_list.map do |gem|
|
||||
gem_column_for(
|
||||
gem[:current_spec],
|
||||
gem[:active_spec],
|
||||
gem[:dependency],
|
||||
gem[:groups],
|
||||
)
|
||||
end
|
||||
|
||||
print_indented([table_header] + data)
|
||||
end
|
||||
|
||||
def print_gem(current_spec, active_spec, dependency, groups)
|
||||
spec_version = "#{active_spec.version}#{active_spec.git_version}"
|
||||
if Bundler.ui.debug?
|
||||
loaded_from = loaded_from_for(active_spec)
|
||||
spec_version += " (from #{loaded_from})" if loaded_from
|
||||
end
|
||||
current_version = "#{current_spec.version}#{current_spec.git_version}"
|
||||
|
||||
if dependency&.specific?
|
||||
dependency_version = %(, requested #{dependency.requirement})
|
||||
end
|
||||
|
||||
spec_outdated_info = "#{active_spec.name} (newest #{spec_version}, " \
|
||||
"installed #{current_version}#{dependency_version}"
|
||||
|
||||
release_date = release_date_for(active_spec)
|
||||
spec_outdated_info += ", released #{release_date}" unless release_date.empty?
|
||||
|
||||
remaining = cooldown_days_remaining(active_spec)
|
||||
if remaining
|
||||
spec_outdated_info += ", in cooldown for #{remaining} more day#{"s" if remaining > 1}"
|
||||
adoptable = newest_out_of_cooldown(active_spec, current_spec)
|
||||
spec_outdated_info += ", newest out of cooldown #{adoptable.version}" if adoptable
|
||||
end
|
||||
|
||||
spec_outdated_info += ")"
|
||||
|
||||
output_message = if options[:parseable]
|
||||
spec_outdated_info.to_s
|
||||
elsif options_include_groups || groups.empty?
|
||||
" * #{spec_outdated_info}"
|
||||
else
|
||||
" * #{spec_outdated_info} in #{groups_text("group", groups)}"
|
||||
end
|
||||
|
||||
Bundler.ui.info output_message.rstrip
|
||||
end
|
||||
|
||||
def gem_column_for(current_spec, active_spec, dependency, groups)
|
||||
current_version = "#{current_spec.version}#{current_spec.git_version}"
|
||||
spec_version = "#{active_spec.version}#{active_spec.git_version}"
|
||||
remaining = cooldown_days_remaining(active_spec)
|
||||
if remaining
|
||||
adoptable = newest_out_of_cooldown(active_spec, current_spec)
|
||||
adoptable_note = adoptable ? ", #{adoptable.version} out of cooldown" : ""
|
||||
spec_version += " (cooldown #{remaining}d#{adoptable_note})"
|
||||
end
|
||||
dependency = dependency.requirement if dependency
|
||||
|
||||
ret_val = [active_spec.name, current_version, spec_version, dependency.to_s, groups.to_s]
|
||||
ret_val << release_date_for(active_spec)
|
||||
ret_val << loaded_from_for(active_spec).to_s if Bundler.ui.debug?
|
||||
ret_val
|
||||
end
|
||||
|
||||
def cooldown_days_remaining(spec, now = cooldown_now)
|
||||
return nil unless spec.respond_to?(:created_at) && spec.created_at
|
||||
return nil unless spec.respond_to?(:remote) && spec.remote
|
||||
days = spec.remote.effective_cooldown
|
||||
return nil if days.nil? || days <= 0
|
||||
remaining = days - ((now - spec.created_at) / 86_400.0)
|
||||
remaining > 0 ? remaining.ceil : nil
|
||||
end
|
||||
|
||||
def cooldown_now
|
||||
@cooldown_now ||= Time.now
|
||||
end
|
||||
|
||||
def check_for_deployment_mode!
|
||||
return unless Bundler.frozen_bundle?
|
||||
suggested_command = if Bundler.settings.locations("frozen").keys.&([:global, :local]).any?
|
||||
"bundle config unset frozen"
|
||||
elsif Bundler.settings.locations("deployment").keys.&([:global, :local]).any?
|
||||
"bundle config unset deployment"
|
||||
end
|
||||
raise ProductionError, "You are trying to check outdated gems in " \
|
||||
"deployment mode. Run `bundle outdated` elsewhere.\n" \
|
||||
"\nIf this is a development machine, remove the " \
|
||||
"#{Bundler.default_gemfile} freeze" \
|
||||
"\nby running `#{suggested_command}`."
|
||||
end
|
||||
|
||||
def update_present_via_semver_portions(current_spec, active_spec, options)
|
||||
current_major = current_spec.version.segments.first
|
||||
active_major = active_spec.version.segments.first
|
||||
|
||||
update_present = false
|
||||
update_present = active_major > current_major if options["filter-major"]
|
||||
|
||||
if !update_present && (options["filter-minor"] || options["filter-patch"]) && current_major == active_major
|
||||
current_minor = get_version_semver_portion_value(current_spec, 1)
|
||||
active_minor = get_version_semver_portion_value(active_spec, 1)
|
||||
|
||||
update_present = active_minor > current_minor if options["filter-minor"]
|
||||
|
||||
if !update_present && options["filter-patch"] && current_minor == active_minor
|
||||
current_patch = get_version_semver_portion_value(current_spec, 2)
|
||||
active_patch = get_version_semver_portion_value(active_spec, 2)
|
||||
|
||||
update_present = active_patch > current_patch
|
||||
end
|
||||
end
|
||||
|
||||
update_present
|
||||
end
|
||||
|
||||
def get_version_semver_portion_value(spec, version_portion_index)
|
||||
version_section = spec.version.segments[version_portion_index, 1]
|
||||
version_section.to_a[0].to_i
|
||||
end
|
||||
|
||||
def print_indented(matrix)
|
||||
header = matrix[0]
|
||||
data = matrix[1..-1]
|
||||
|
||||
column_sizes = Array.new(header.size) do |index|
|
||||
matrix.max_by {|row| row[index].length }[index].length
|
||||
end
|
||||
|
||||
Bundler.ui.info justify(header, column_sizes)
|
||||
|
||||
data.sort_by! {|row| row[0] }
|
||||
|
||||
data.each do |row|
|
||||
Bundler.ui.info justify(row, column_sizes)
|
||||
end
|
||||
end
|
||||
|
||||
def table_header
|
||||
header = ["Gem", "Current", "Latest", "Requested", "Groups", "Release Date"]
|
||||
header << "Path" if Bundler.ui.debug?
|
||||
header
|
||||
end
|
||||
|
||||
def release_date_for(spec)
|
||||
return "" unless spec.respond_to?(:date)
|
||||
|
||||
date = spec.date
|
||||
return "" unless date
|
||||
|
||||
return "" unless Gem.const_defined?(:DEFAULT_SOURCE_DATE_EPOCH)
|
||||
default_date = Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc
|
||||
default_date = Time.utc(default_date.year, default_date.month, default_date.day)
|
||||
|
||||
date = date.utc if date.respond_to?(:utc)
|
||||
|
||||
return "" if date == default_date
|
||||
|
||||
date.strftime("%Y-%m-%d")
|
||||
end
|
||||
|
||||
def justify(row, sizes)
|
||||
row.each_with_index.map do |element, index|
|
||||
element.ljust(sizes[index])
|
||||
end.join(" ").strip + "\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Platform
|
||||
attr_reader :options
|
||||
def initialize(options)
|
||||
@options = options
|
||||
end
|
||||
|
||||
def run
|
||||
ruby_version = if Bundler.locked_gems
|
||||
Bundler.locked_gems.ruby_version&.gsub(/p\d+\Z/, "")
|
||||
else
|
||||
Bundler.definition.ruby_version&.single_version_string
|
||||
end
|
||||
|
||||
output = []
|
||||
|
||||
if options[:ruby]
|
||||
if ruby_version
|
||||
output << ruby_version
|
||||
else
|
||||
output << "No ruby version specified"
|
||||
end
|
||||
else
|
||||
platforms = Bundler.definition.platforms.map {|p| "* #{p}" }
|
||||
|
||||
output << "Your platform is: #{Gem::Platform.local}"
|
||||
output << "Your app has gems that work on these platforms:\n#{platforms.join("\n")}"
|
||||
|
||||
if ruby_version
|
||||
output << "Your Gemfile specifies a Ruby version requirement:\n* #{ruby_version}"
|
||||
|
||||
begin
|
||||
Bundler.definition.validate_runtime!
|
||||
output << "Your current platform satisfies the Ruby version requirement."
|
||||
rescue RubyVersionMismatch => e
|
||||
output << e.message
|
||||
end
|
||||
else
|
||||
output << "Your Gemfile does not specify a Ruby version requirement."
|
||||
end
|
||||
end
|
||||
|
||||
Bundler.ui.info output.join("\n\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "../vendored_thor"
|
||||
module Bundler
|
||||
class CLI::Plugin < Thor
|
||||
desc "install PLUGINS", "Install the plugin from the source"
|
||||
long_desc <<-D
|
||||
Install plugins either from the rubygems source provided (with --source option), from a git source provided with --git, or a local path provided with --path. If no sources are provided, it uses Gem.sources
|
||||
D
|
||||
method_option "source", type: :string, default: nil, banner: "URL of the RubyGems source to fetch the plugin from"
|
||||
method_option "version", type: :string, default: nil, banner: "The version of the plugin to fetch"
|
||||
method_option "git", type: :string, default: nil, banner: "URL of the git repo to fetch from"
|
||||
method_option "local_git", type: :string, default: nil, banner: "Path of the local git repo to fetch from (removed)"
|
||||
method_option "branch", type: :string, default: nil, banner: "The git branch to checkout"
|
||||
method_option "ref", type: :string, default: nil, banner: "The git revision to check out"
|
||||
method_option "path", type: :string, default: nil, banner: "Path of a local gem to directly use"
|
||||
def install(*plugins)
|
||||
if options.key?(:local_git)
|
||||
raise InvalidOption, "--local_git has been removed, use --git"
|
||||
end
|
||||
|
||||
Bundler::Plugin.install(plugins, options)
|
||||
end
|
||||
|
||||
desc "uninstall PLUGINS", "Uninstall the plugins"
|
||||
long_desc <<-D
|
||||
Uninstall given list of plugins. To uninstall all the plugins, use -all option.
|
||||
D
|
||||
method_option "all", type: :boolean, default: nil, banner: "Uninstall all the installed plugins. If no plugin is installed, then it does nothing."
|
||||
def uninstall(*plugins)
|
||||
Bundler::Plugin.uninstall(plugins, options)
|
||||
end
|
||||
|
||||
desc "list", "List the installed plugins and available commands"
|
||||
def list
|
||||
Bundler::Plugin.list
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Pristine
|
||||
def initialize(gems)
|
||||
@gems = gems
|
||||
end
|
||||
|
||||
def run
|
||||
CLI::Common.ensure_all_gems_in_lockfile!(@gems)
|
||||
definition = Bundler.definition
|
||||
definition.validate_runtime!
|
||||
installer = Bundler::Installer.new(Bundler.root, definition)
|
||||
git_sources = []
|
||||
|
||||
ProcessLock.lock do
|
||||
installed_specs = definition.specs.reject do |spec|
|
||||
next if spec.name == "bundler" # Source::Rubygems doesn't install bundler
|
||||
next if !@gems.empty? && !@gems.include?(spec.name)
|
||||
|
||||
gem_name = "#{spec.name} (#{spec.version}#{spec.git_version})"
|
||||
gem_name += " (#{spec.platform})" if !spec.platform.nil? && spec.platform != Gem::Platform::RUBY
|
||||
|
||||
case source = spec.source
|
||||
when Source::Rubygems
|
||||
cached_gem = spec.cache_file
|
||||
unless File.exist?(cached_gem)
|
||||
Bundler.ui.error("Failed to pristine #{gem_name}. Cached gem #{cached_gem} does not exist.")
|
||||
next
|
||||
end
|
||||
|
||||
FileUtils.rm_rf spec.full_gem_path
|
||||
when Source::Git
|
||||
if source.local?
|
||||
Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is locally overridden.")
|
||||
next
|
||||
end
|
||||
|
||||
source.remote!
|
||||
if extension_cache_path = source.extension_cache_path(spec)
|
||||
FileUtils.rm_rf extension_cache_path
|
||||
end
|
||||
FileUtils.rm_rf spec.extension_dir
|
||||
FileUtils.rm_rf spec.full_gem_path
|
||||
|
||||
next if git_sources.include?(source)
|
||||
git_sources << source
|
||||
else
|
||||
Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is sourced from local path.")
|
||||
next
|
||||
end
|
||||
|
||||
true
|
||||
end.map(&:name)
|
||||
|
||||
jobs = Bundler.settings.installation_parallelization
|
||||
pristine_count = definition.specs.count - installed_specs.count
|
||||
# allow a pristining a single gem to skip the parallel worker
|
||||
jobs = [jobs, pristine_count].min
|
||||
ParallelInstaller.call(installer, definition.specs, jobs, false, true, skip: installed_specs)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Remove
|
||||
def initialize(gems, options)
|
||||
@gems = gems
|
||||
@options = options
|
||||
end
|
||||
|
||||
def run
|
||||
raise InvalidOption, "Please specify gems to remove." if @gems.empty?
|
||||
|
||||
Injector.remove(@gems, {})
|
||||
Installer.install(Bundler.root, Bundler.definition)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,71 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Show
|
||||
attr_reader :options, :gem_name, :latest_specs
|
||||
def initialize(options, gem_name)
|
||||
@options = options
|
||||
@gem_name = gem_name
|
||||
@verbose = options[:verbose]
|
||||
@latest_specs = fetch_latest_specs if @verbose
|
||||
end
|
||||
|
||||
def run
|
||||
Bundler.ui.silence do
|
||||
Bundler.definition.validate_runtime!
|
||||
Bundler.load.lock
|
||||
end
|
||||
|
||||
if gem_name
|
||||
if gem_name == "bundler"
|
||||
path = File.expand_path("../../..", __dir__)
|
||||
else
|
||||
spec = Bundler::CLI::Common.select_spec(gem_name, :regex_match)
|
||||
return unless spec
|
||||
path = spec.full_gem_path
|
||||
unless File.directory?(path)
|
||||
return Bundler.ui.warn "The gem #{gem_name} is missing. It should be installed at #{path}, but was not found"
|
||||
end
|
||||
end
|
||||
return Bundler.ui.info(path)
|
||||
end
|
||||
|
||||
if options[:paths]
|
||||
Bundler.load.specs.sort_by(&:name).map do |s|
|
||||
Bundler.ui.info s.full_gem_path
|
||||
end
|
||||
else
|
||||
Bundler.ui.info "Gems included by the bundle:"
|
||||
Bundler.load.specs.sort_by(&:name).each do |s|
|
||||
desc = " * #{s.name} (#{s.version}#{s.git_version})"
|
||||
if @verbose
|
||||
latest = latest_specs.find {|l| l.name == s.name }
|
||||
Bundler.ui.info <<~END
|
||||
#{desc.lstrip}
|
||||
\tSummary: #{s.summary || "No description available."}
|
||||
\tHomepage: #{s.homepage || "No website available."}
|
||||
\tStatus: #{outdated?(s, latest) ? "Outdated - #{s.version} < #{latest.version}" : "Up to date"}
|
||||
END
|
||||
else
|
||||
Bundler.ui.info desc
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_latest_specs
|
||||
definition = Bundler.definition(true)
|
||||
Bundler.ui.info "Fetching remote specs for outdated check...\n\n"
|
||||
Bundler.ui.silence { definition.remotely! }
|
||||
Bundler.reset!
|
||||
definition.specs
|
||||
end
|
||||
|
||||
def outdated?(current, latest)
|
||||
return false unless latest
|
||||
Gem::Version.new(current.version) < Gem::Version.new(latest.version)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,126 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Update
|
||||
attr_reader :options, :gems
|
||||
def initialize(options, gems)
|
||||
@options = options
|
||||
@gems = gems
|
||||
end
|
||||
|
||||
def run
|
||||
Bundler.ui.level = "warn" if options[:quiet]
|
||||
|
||||
update_bundler = options[:bundler]
|
||||
|
||||
Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler, pre: options[:pre]) if update_bundler
|
||||
|
||||
Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins]
|
||||
|
||||
sources = Array(options[:source])
|
||||
groups = Array(options[:group]).map(&:to_sym)
|
||||
|
||||
full_update = gems.empty? && sources.empty? && groups.empty? && !options[:ruby] && !update_bundler
|
||||
|
||||
if full_update && !options[:all]
|
||||
if Bundler.settings[:update_requires_all_flag]
|
||||
raise InvalidOption, "To update everything, pass the `--all` flag."
|
||||
end
|
||||
SharedHelpers.feature_deprecated! "Pass --all to `bundle update` to update everything"
|
||||
elsif !full_update && options[:all]
|
||||
raise InvalidOption, "Cannot specify --all along with specific options."
|
||||
end
|
||||
|
||||
conservative = options[:conservative]
|
||||
|
||||
if full_update
|
||||
if conservative
|
||||
Bundler.definition(conservative: conservative)
|
||||
else
|
||||
Bundler.definition(true)
|
||||
end
|
||||
else
|
||||
unless Bundler.default_lockfile.exist?
|
||||
raise GemfileLockNotFound, "This Bundle hasn't been installed yet. " \
|
||||
"Run `bundle install` to update and install the bundled gems."
|
||||
end
|
||||
Bundler::CLI::Common.ensure_all_gems_in_lockfile!(gems)
|
||||
|
||||
if groups.any?
|
||||
deps = Bundler.definition.dependencies.select {|d| (d.groups & groups).any? }
|
||||
gems.concat(deps.map(&:name))
|
||||
end
|
||||
|
||||
Bundler.definition(gems: gems, sources: sources, ruby: options[:ruby],
|
||||
conservative: conservative,
|
||||
bundler: update_bundler)
|
||||
end
|
||||
|
||||
Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options)
|
||||
|
||||
Bundler::Fetcher.disable_endpoint = options["full-index"]
|
||||
|
||||
opts = options.dup
|
||||
opts["update"] = true
|
||||
opts["local"] = options[:local]
|
||||
opts["force"] = options[:redownload] if options[:redownload]
|
||||
|
||||
Bundler.settings.set_command_option_if_given :jobs, opts["jobs"]
|
||||
Bundler::CLI::Common.validate_cooldown!(options[:cooldown])
|
||||
Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown]
|
||||
|
||||
Bundler.definition.validate_runtime!
|
||||
|
||||
if locked_gems = Bundler.definition.locked_gems
|
||||
previous_locked_info = locked_gems.specs.reduce({}) do |h, s|
|
||||
h[s.name] = { spec: s, version: s.version, source: s.source.identifier }
|
||||
h
|
||||
end
|
||||
end
|
||||
|
||||
installer = Installer.install Bundler.root, Bundler.definition, opts
|
||||
Bundler.load.cache if Bundler.app_cache.exist?
|
||||
|
||||
if CLI::Common.clean_after_install?
|
||||
require_relative "clean"
|
||||
Bundler::CLI::Clean.new(options).run
|
||||
end
|
||||
|
||||
if locked_gems
|
||||
gems.each do |name|
|
||||
locked_info = previous_locked_info[name]
|
||||
next unless locked_info
|
||||
|
||||
locked_spec = locked_info[:spec]
|
||||
new_spec = Bundler.definition.specs[name].first
|
||||
unless new_spec
|
||||
unless locked_spec.installable_on_platform?(Bundler.local_platform)
|
||||
Bundler.ui.warn "Bundler attempted to update #{name} but it was not considered because it is for a different platform from the current one"
|
||||
end
|
||||
|
||||
next
|
||||
end
|
||||
|
||||
locked_source = locked_info[:source]
|
||||
new_source = new_spec.source.identifier
|
||||
next if locked_source != new_source
|
||||
|
||||
new_version = new_spec.version
|
||||
locked_version = locked_info[:version]
|
||||
if new_version < locked_version
|
||||
Bundler.ui.warn "Note: #{name} version regressed from #{locked_version} to #{new_version}"
|
||||
elsif new_version == locked_version
|
||||
Bundler.ui.warn "Bundler attempted to update #{name} but its version stayed the same"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Bundler.ui.confirm "Bundle updated!"
|
||||
Bundler::CLI::Common.output_without_groups_message(:update)
|
||||
Bundler::CLI::Common.output_post_install_messages installer.post_install_messages
|
||||
Bundler::CLI::Common.output_cooldown_skipped_summary
|
||||
|
||||
Bundler::CLI::Common.output_fund_metadata_summary
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user