summaryrefslogtreecommitdiff
path: root/lib/bundler/cli
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bundler/cli')
-rw-r--r--lib/bundler/cli/add.rb59
-rw-r--r--lib/bundler/cli/binstubs.rb57
-rw-r--r--lib/bundler/cli/cache.rb32
-rw-r--r--lib/bundler/cli/check.rb40
-rw-r--r--lib/bundler/cli/clean.rb25
-rw-r--r--lib/bundler/cli/common.rb155
-rw-r--r--lib/bundler/cli/config.rb208
-rw-r--r--lib/bundler/cli/console.rb47
-rw-r--r--lib/bundler/cli/doctor.rb33
-rw-r--r--lib/bundler/cli/doctor/diagnose.rb167
-rw-r--r--lib/bundler/cli/doctor/ssl.rb249
-rw-r--r--lib/bundler/cli/exec.rb114
-rw-r--r--lib/bundler/cli/fund.rb36
-rw-r--r--lib/bundler/cli/gem.rb476
-rw-r--r--lib/bundler/cli/info.rb83
-rw-r--r--lib/bundler/cli/init.rb51
-rw-r--r--lib/bundler/cli/install.rb124
-rw-r--r--lib/bundler/cli/issue.rb41
-rw-r--r--lib/bundler/cli/list.rb97
-rw-r--r--lib/bundler/cli/lock.rb94
-rw-r--r--lib/bundler/cli/open.rb29
-rw-r--r--lib/bundler/cli/outdated.rb319
-rw-r--r--lib/bundler/cli/platform.rb48
-rw-r--r--lib/bundler/cli/plugin.rb39
-rw-r--r--lib/bundler/cli/pristine.rb64
-rw-r--r--lib/bundler/cli/remove.rb17
-rw-r--r--lib/bundler/cli/show.rb71
-rw-r--r--lib/bundler/cli/update.rb123
28 files changed, 2898 insertions, 0 deletions
diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb
new file mode 100644
index 0000000000..d65ed68b4a
--- /dev/null
+++ b/lib/bundler/cli/add.rb
@@ -0,0 +1,59 @@
+# 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]
+
+ 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
+ pessimistic: options[:pessimistic],
+ 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 `--pessimistic` at the same time." if options[:strict] && options[:pessimistic]
+
+ # 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
diff --git a/lib/bundler/cli/binstubs.rb b/lib/bundler/cli/binstubs.rb
new file mode 100644
index 0000000000..8ce138df96
--- /dev/null
+++ b/lib/bundler/cli/binstubs.rb
@@ -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
diff --git a/lib/bundler/cli/cache.rb b/lib/bundler/cli/cache.rb
new file mode 100644
index 0000000000..59605df847
--- /dev/null
+++ b/lib/bundler/cli/cache.rb
@@ -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
diff --git a/lib/bundler/cli/check.rb b/lib/bundler/cli/check.rb
new file mode 100644
index 0000000000..493eb3ec6a
--- /dev/null
+++ b/lib/bundler/cli/check.rb
@@ -0,0 +1,40 @@
+# 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.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
+ elsif !Bundler.default_lockfile.file? && Bundler.frozen_bundle?
+ Bundler.ui.error "This bundle has been frozen, but there is no #{SharedHelpers.relative_lockfile_path} present"
+ exit 1
+ else
+ definition.lock(true) unless options[:"dry-run"]
+ Bundler.ui.info "The Gemfile's dependencies are satisfied"
+ end
+ end
+ end
+end
diff --git a/lib/bundler/cli/clean.rb b/lib/bundler/cli/clean.rb
new file mode 100644
index 0000000000..c6b0968e3e
--- /dev/null
+++ b/lib/bundler/cli/clean.rb
@@ -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
diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb
new file mode 100644
index 0000000000..2f332ff364
--- /dev/null
+++ b/lib/bundler/cli/common.rb
@@ -0,0 +1,155 @@
+# frozen_string_literal: true
+
+module Bundler
+ module CLI::Common
+ 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_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
diff --git a/lib/bundler/cli/config.rb b/lib/bundler/cli/config.rb
new file mode 100644
index 0000000000..976cda7484
--- /dev/null
+++ b/lib/bundler/cli/config.rb
@@ -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
diff --git a/lib/bundler/cli/console.rb b/lib/bundler/cli/console.rb
new file mode 100644
index 0000000000..2d1a2ce458
--- /dev/null
+++ b/lib/bundler/cli/console.rb
@@ -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
diff --git a/lib/bundler/cli/doctor.rb b/lib/bundler/cli/doctor.rb
new file mode 100644
index 0000000000..5fd6a73d91
--- /dev/null
+++ b/lib/bundler/cli/doctor.rb
@@ -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
diff --git a/lib/bundler/cli/doctor/diagnose.rb b/lib/bundler/cli/doctor/diagnose.rb
new file mode 100644
index 0000000000..a878025dda
--- /dev/null
+++ b/lib/bundler/cli/doctor/diagnose.rb
@@ -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)
+ Dir.glob("#{spec.full_gem_path}/**/*.bundle")
+ 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
diff --git a/lib/bundler/cli/doctor/ssl.rb b/lib/bundler/cli/doctor/ssl.rb
new file mode 100644
index 0000000000..21fc4edf2d
--- /dev/null
+++ b/lib/bundler/cli/doctor/ssl.rb
@@ -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
diff --git a/lib/bundler/cli/exec.rb b/lib/bundler/cli/exec.rb
new file mode 100644
index 0000000000..2fdc416286
--- /dev/null
+++ b/lib/bundler/cli/exec.rb
@@ -0,0 +1,114 @@
+# 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 = "./" + bin_path unless File.absolute_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 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
diff --git a/lib/bundler/cli/fund.rb b/lib/bundler/cli/fund.rb
new file mode 100644
index 0000000000..ad7f31f3d6
--- /dev/null
+++ b/lib/bundler/cli/fund.rb
@@ -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
diff --git a/lib/bundler/cli/gem.rb b/lib/bundler/cli/gem.rb
new file mode 100644
index 0000000000..c8c24c8e66
--- /dev/null
+++ b/lib/bundler/cli/gem.rb
@@ -0,0 +1,476 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI
+ Bundler.require_thor_actions
+ include Thor::Actions
+ end
+
+ class CLI::Gem
+ 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
+
+ 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"
+ Bundler.ui.info "RuboCop enabled in config"
+ templates.merge!("rubocop.yml.tt" => ".rubocop.yml")
+ config[:ignore_paths] << ".rubocop.yml"
+ when "standard"
+ 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}"
+ require "shellwords"
+ `git init #{target.to_s.shellescape}`
+
+ 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 github_username(git_username)
+ if options[:github_username].nil?
+ git_username
+ elsif options[:github_username] == false
+ ""
+ else
+ options[:github_username]
+ end
+ end
+ end
+end
diff --git a/lib/bundler/cli/info.rb b/lib/bundler/cli/info.rb
new file mode 100644
index 0000000000..cd01d4949b
--- /dev/null
+++ b/lib/bundler/cli/info.rb
@@ -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
diff --git a/lib/bundler/cli/init.rb b/lib/bundler/cli/init.rb
new file mode 100644
index 0000000000..246b9d6460
--- /dev/null
+++ b/lib/bundler/cli/init.rb
@@ -0,0 +1,51 @@
+# 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)
+
+ 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
diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb
new file mode 100644
index 0000000000..67feba84bd
--- /dev/null
+++ b/lib/bundler/cli/install.rb
@@ -0,0 +1,124 @@
+# 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
+
+ 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.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
diff --git a/lib/bundler/cli/issue.rb b/lib/bundler/cli/issue.rb
new file mode 100644
index 0000000000..cbfb7da2d8
--- /dev/null
+++ b/lib/bundler/cli/issue.rb
@@ -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://bundler.io/
+
+ 3. Information about each Bundler command can be found in the Bundler
+ man pages: https://bundler.io/man/bundle.1.html
+
+ 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
diff --git a/lib/bundler/cli/list.rb b/lib/bundler/cli/list.rb
new file mode 100644
index 0000000000..6a467f45a9
--- /dev/null
+++ b/lib/bundler/cli/list.rb
@@ -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
diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb
new file mode 100644
index 0000000000..2f78868936
--- /dev/null
+++ b/lib/bundler/cli/lock.rb
@@ -0,0 +1,94 @@
+# 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
+
+ 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]
+ 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"].each do |platform_string|
+ platform = Gem::Platform.new(platform_string)
+ definition.remove_platform(platform)
+ end
+
+ options["add-platform"].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
+ 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
diff --git a/lib/bundler/cli/open.rb b/lib/bundler/cli/open.rb
new file mode 100644
index 0000000000..f24693b843
--- /dev/null
+++ b/lib/bundler/cli/open.rb
@@ -0,0 +1,29 @@
+# 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
+ require "shellwords"
+ command = Shellwords.split(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
+ end
+end
diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb
new file mode 100644
index 0000000000..ae827dbb4b
--- /dev/null
+++ b/lib/bundler/cli/outdated.rb
@@ -0,0 +1,319 @@
+# 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.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
+
+ 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.last
+ 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?
+ 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}"
+ 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 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
diff --git a/lib/bundler/cli/platform.rb b/lib/bundler/cli/platform.rb
new file mode 100644
index 0000000000..32d68abbb1
--- /dev/null
+++ b/lib/bundler/cli/platform.rb
@@ -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
diff --git a/lib/bundler/cli/plugin.rb b/lib/bundler/cli/plugin.rb
new file mode 100644
index 0000000000..32fa660fe0
--- /dev/null
+++ b/lib/bundler/cli/plugin.rb
@@ -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
diff --git a/lib/bundler/cli/pristine.rb b/lib/bundler/cli/pristine.rb
new file mode 100644
index 0000000000..f463f0bce8
--- /dev/null
+++ b/lib/bundler/cli/pristine.rb
@@ -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
diff --git a/lib/bundler/cli/remove.rb b/lib/bundler/cli/remove.rb
new file mode 100644
index 0000000000..44a4d891dd
--- /dev/null
+++ b/lib/bundler/cli/remove.rb
@@ -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
diff --git a/lib/bundler/cli/show.rb b/lib/bundler/cli/show.rb
new file mode 100644
index 0000000000..67fdcc797e
--- /dev/null
+++ b/lib/bundler/cli/show.rb
@@ -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
diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb
new file mode 100644
index 0000000000..9cc90acc58
--- /dev/null
+++ b/lib/bundler/cli/update.rb
@@ -0,0 +1,123 @@
+# 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) 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.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_fund_metadata_summary
+ end
+ end
+end