diff options
Diffstat (limited to 'lib/bundler/cli')
28 files changed, 979 insertions, 667 deletions
diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb index 5bcf30d82d..d65ed68b4a 100644 --- a/lib/bundler/cli/add.rb +++ b/lib/bundler/cli/add.rb @@ -12,6 +12,8 @@ module Bundler end def run + Bundler.ui.level = "warn" if options[:quiet] + validate_options! inject_dependencies perform_bundle_install unless options["skip-install"] @@ -28,19 +30,29 @@ module Bundler 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]) + 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 can not specify `--strict` and `--optimistic` at the same time." if options[:strict] && options[:optimistic] + 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 =~ v.to_s + raise InvalidOption, "Invalid gem requirement pattern '#{v}'" unless Gem::Requirement::PATTERN.match?(v.to_s) end end end diff --git a/lib/bundler/cli/binstubs.rb b/lib/bundler/cli/binstubs.rb index 639c01ff39..8ce138df96 100644 --- a/lib/bundler/cli/binstubs.rb +++ b/lib/bundler/cli/binstubs.rb @@ -11,15 +11,15 @@ module Bundler def run Bundler.definition.validate_runtime! path_option = options["path"] - path_option = nil if path_option && path_option.empty? + 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"], + force: options[:force], + binstubs_cmd: true, + all_platforms: options["all-platforms"], } if options[:all] @@ -40,8 +40,12 @@ module Bundler end if options[:standalone] - next Bundler.ui.warn("Sorry, Bundler can only be run via RubyGems.") if gem_name == "bundler" - Bundler.settings.temporary(:path => (Bundler.settings[:path] || Bundler.root)) do + 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 diff --git a/lib/bundler/cli/cache.rb b/lib/bundler/cli/cache.rb index c8698ed7e3..59605df847 100644 --- a/lib/bundler/cli/cache.rb +++ b/lib/bundler/cli/cache.rb @@ -10,17 +10,12 @@ module Bundler def run Bundler.ui.level = "warn" if options[:quiet] - Bundler.settings.set_command_option_if_given :path, options[:path] Bundler.settings.set_command_option_if_given :cache_path, options["cache-path"] - setup_cache_all install - # TODO: move cache contents here now that all bundles are locked - custom_path = Bundler.settings[:path] if options[:path] - - Bundler.settings.temporary(:cache_all_platforms => options["all-platforms"]) do - Bundler.load.cache(custom_path) + Bundler.settings.temporary(cache_all_platforms: options["all-platforms"]) do + Bundler.load.cache end end @@ -33,11 +28,5 @@ module Bundler options["no-cache"] = true Bundler::CLI::Install.new(options).run end - - def setup_cache_all - all = options.fetch(:all, Bundler.feature_flag.cache_all? || nil) - - Bundler.settings.set_command_option_if_given :cache_all, all - end end end diff --git a/lib/bundler/cli/check.rb b/lib/bundler/cli/check.rb index 65c51337d2..493eb3ec6a 100644 --- a/lib/bundler/cli/check.rb +++ b/lib/bundler/cli/check.rb @@ -15,9 +15,9 @@ module Bundler definition.validate_runtime! begin - definition.resolve_only_locally! + definition.check! not_installed = definition.missing_specs - rescue GemNotFound, VersionConflict + 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 @@ -29,10 +29,10 @@ module Bundler 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 #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} present" + Bundler.ui.error "This bundle has been frozen, but there is no #{SharedHelpers.relative_lockfile_path} present" exit 1 else - Bundler.load.lock(:preserve_unknown_sections => true) unless options[:"dry-run"] + definition.lock(true) unless options[:"dry-run"] Bundler.ui.info "The Gemfile's dependencies are satisfied" end end diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb index 0d83a1c07e..2f332ff364 100644 --- a/lib/bundler/cli/common.rb +++ b/lib/bundler/cli/common.rb @@ -54,9 +54,12 @@ module Bundler Bundler.definition.specs.each do |spec| return spec if spec.name == name - specs << spec if regexp && spec.name =~ regexp + 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 } @@ -75,6 +78,11 @@ module Bundler 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 @@ -86,11 +94,14 @@ module Bundler end def self.gem_not_found_message(missing_gem_name, alternatives) - require_relative "../similarity_detector" message = "Could not find gem '#{missing_gem_name}'." alternate_names = alternatives.map {|a| a.respond_to?(:name) ? a.name : a } - suggestions = SimilarityDetector.new(alternate_names).similar_word_list(missing_gem_name) - message += "\nDid you mean #{suggestions}?" if suggestions + 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 @@ -111,6 +122,7 @@ module Bundler 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 @@ -121,9 +133,23 @@ module Bundler def self.clean_after_install? clean = Bundler.settings[:clean] return clean unless clean.nil? - clean ||= Bundler.feature_flag.auto_clean_without_path? && Bundler.settings[:path].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 index e1222c75dd..976cda7484 100644 --- a/lib/bundler/cli/config.rb +++ b/lib/bundler/cli/config.rb @@ -2,17 +2,17 @@ module Bundler class CLI::Config < Thor - class_option :parseable, :type => :boolean, :banner => "Use minimal formatting for more parseable output" + 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" + 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 + desc "base NAME [VALUE]", "The Bundler 1 config interface", hide: true scope_options - method_option :delete, :type => :boolean, :banner => "delete" + method_option :delete, type: :boolean, banner: "delete" def base(name = nil, *value) new_args = if ARGV.size == 1 @@ -25,8 +25,8 @@ module Bundler ["config", "get", ARGV[1]] end - SharedHelpers.major_deprecation 3, - "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." + 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 @@ -87,16 +87,21 @@ module Bundler 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 - return + else + confirm(name) end - confirm(name) - return + if current_value.nil? + exit 1 + else + return + end end Bundler.ui.info(message) if message diff --git a/lib/bundler/cli/console.rb b/lib/bundler/cli/console.rb index 97b8dc0663..2d1a2ce458 100644 --- a/lib/bundler/cli/console.rb +++ b/lib/bundler/cli/console.rb @@ -9,9 +9,6 @@ module Bundler end def run - Bundler::SharedHelpers.major_deprecation 2, "bundle console will be replaced " \ - "by `bin/console` generated by `bundle gem <name>`" - group ? Bundler.require(:default, *group.split(" ").map!(&:to_sym)) : Bundler.require ARGV.clear @@ -23,21 +20,28 @@ module Bundler require name get_constant(name) rescue LoadError - Bundler.ui.error "Couldn't load console #{name}, falling back to irb" - require "irb" - get_constant("irb") + 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, + "pry" => :Pry, "ripl" => :Ripl, - "irb" => :IRB, + "irb" => :IRB, }[name] Object.const_get(const_name) - rescue NameError - Bundler.ui.error "Could not find constant #{const_name}" - exit 1 end end end diff --git a/lib/bundler/cli/doctor.rb b/lib/bundler/cli/doctor.rb index 74444ad0ce..5fd6a73d91 100644 --- a/lib/bundler/cli/doctor.rb +++ b/lib/bundler/cli/doctor.rb @@ -1,159 +1,33 @@ # frozen_string_literal: true -require "rbconfig" -require "shellwords" -require "fiddle" - module Bundler - class CLI::Doctor - DARWIN_REGEX = /\s+(.+) \(compatibility /.freeze - LDD_REGEX = /\t\S+ => (\S+) \(\S+\)/.freeze - - 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].map {|l| l.match(DARWIN_REGEX).captures[0] }.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").map do |l| - match = l.match(LDD_REGEX) - next if match.nil? - match.captures[0] - end.compact - 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 check! - require_relative "check" - Bundler::CLI::Check.new({}).run - end - - def run - Bundler.ui.level = "warn" if options[:quiet] - Bundler.settings.validate! - check! - - definition = Bundler.definition - broken_links = {} - - definition.specs.each do |spec| - bundles_for_gem(spec).each do |bundle| - bad_paths = dylibs(bundle).select do |f| - begin - Fiddle.dlopen(f) - false - rescue Fiddle::DLError - true - end - 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.map do |spec, paths| - paths.uniq.map do |path| - "\n * #{spec.name}: #{path}" - end - end.flatten.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_or_writable = [] - files_not_rw_and_owned_by_different_user = [] - files_not_owned_by_current_user_but_still_rw = [] - broken_symlinks = [] - Find.find(Bundler.bundle_path.to_s).each do |f| - if !File.exist?(f) - broken_symlinks << f - elsif !File.writable?(f) || !File.readable?(f) - if File.stat(f).uid != Process.uid - files_not_rw_and_owned_by_different_user << f - else - files_not_readable_or_writable << f - end - elsif File.stat(f).uid != Process.uid - files_not_owned_by_current_user_but_still_rw << 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_rw.any? - Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \ - "user, but are still readable/writable. These files are:\n - #{files_not_owned_by_current_user_but_still_rw.join("\n - ")}" - - ok = false - end - - if files_not_rw_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/writable. These files are:\n - #{files_not_rw_and_owned_by_different_user.join("\n - ")}" - - ok = false - end - - if files_not_readable_or_writable.any? - Bundler.ui.warn "Files exist in the Bundler home that are not " \ - "readable/writable by the current user. These files are:\n - #{files_not_readable_or_writable.join("\n - ")}" - - ok = false - end - - ok + 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 index 42b602a055..2fdc416286 100644 --- a/lib/bundler/cli/exec.rb +++ b/lib/bundler/cli/exec.rb @@ -12,17 +12,20 @@ module Bundler @options = options @cmd = args.shift @args = args - @args << { :close_others => !options.keep_file_descriptors? } unless Bundler.current_ruby.jruby? + @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] && ruby_shebang?(bin_path) - return kernel_load(bin_path, *args) + 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 - kernel_exec(bin_path, *args) else # exec using the given command kernel_exec(cmd, *args) @@ -68,6 +71,29 @@ module Bundler "#{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", diff --git a/lib/bundler/cli/fund.rb b/lib/bundler/cli/fund.rb index 52db5aef68..ad7f31f3d6 100644 --- a/lib/bundler/cli/fund.rb +++ b/lib/bundler/cli/fund.rb @@ -16,7 +16,7 @@ module Bundler deps = if groups.any? Bundler.definition.dependencies_for(groups) else - Bundler.definition.current_dependencies + Bundler.definition.requested_dependencies end fund_info = deps.each_with_object([]) do |dep, arr| diff --git a/lib/bundler/cli/gem.rb b/lib/bundler/cli/gem.rb index c4c76d1b69..c8c24c8e66 100644 --- a/lib/bundler/cli/gem.rb +++ b/lib/bundler/cli/gem.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "pathname" - module Bundler class CLI Bundler.require_thor_actions @@ -9,13 +7,9 @@ module Bundler end class CLI::Gem - TEST_FRAMEWORK_VERSIONS = { - "rspec" => "3.0", - "minitest" => "5.0", - "test-unit" => "3.0", - }.freeze + DEFAULT_GITHUB_USERNAME = "[USERNAME]" - attr_reader :options, :gem_name, :thor, :name, :target + attr_reader :options, :gem_name, :thor, :name, :target, :extension def initialize(options, gem_name, thor) @options = options @@ -26,9 +20,11 @@ module Bundler thor.destination_root = nil @name = @gem_name - @target = SharedHelpers.pwd.join(gem_name) + @target = Pathname.new(SharedHelpers.pwd).join(gem_name) + + @extension = options[:ext] - validate_ext_name if options[:ext] + validate_ext_name if @extension end def run @@ -45,37 +41,46 @@ module Bundler 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) - github_username = if options[:github_username].nil? - git_username - elsif options[:github_username] == false - "" + 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 - options[:github_username] + 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 => options[:ext], - :exe => options[:exe], - :bundler_version => bundler_dependency_version, - :git => use_git, - :github_username => github_username.empty? ? "[USERNAME]" : github_username, - :required_ruby_version => required_ruby_version, - :minitest_constant_name => minitest_constant_name, + 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 = { - "#{Bundler.preferred_gemfile_name}.tt" => Bundler.preferred_gemfile_name, + "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", @@ -91,11 +96,21 @@ module Bundler bin/setup ] - templates.merge!("gitignore.tt" => ".gitignore") if use_git + 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" @@ -105,6 +120,8 @@ module Bundler "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 @@ -119,12 +136,14 @@ module Bundler "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 @@ -132,18 +151,22 @@ module Bundler case config[:ci] when "github" templates.merge!("github/workflows/main.yml.tt" => ".github/workflows/main.yml") - when "travis" - templates.merge!("travis.yml.tt" => ".travis.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?", - "This 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.") + "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") @@ -151,12 +174,8 @@ module Bundler 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 collaborative, safe spaces. You can read more about the code of conduct at " \ - "contributor-covenant.org. Having a code of conduct means agreeing to the responsibility " \ - "of enforcing it, so be sure that you are prepared to do that. Be sure that your email " \ - "address is specified as a contact in the generated code of conduct so that people know " \ - "who to contact in case of a violation. For suggestions about " \ - "how to enforce codes of conduct, see https://bit.ly/coc-enforcement.") + "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") @@ -177,32 +196,57 @@ module Bundler 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 - templates.merge!("exe/newgem.tt" => "exe/#{name}") if config[:exe] + if config[:exe] + templates.merge!("exe/newgem.tt" => "exe/#{name}") + executables.push("exe/#{name}") + end - if options[:ext] + if extension == "c" templates.merge!( - "ext/newgem/extconf.rb.tt" => "ext/#{name}/extconf.rb", + "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 "Initializing git repo in #{target}" + Bundler.ui.info "\nInitializing git repo in #{target}" require "shellwords" `git init #{target.to_s.shellescape}` @@ -221,31 +265,36 @@ module Bundler 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 - `git add .` + system("bundle install") end end # Open gemspec in editor open_editor(options["edit"], target.join("#{name}.gemspec")) if options[:edit] - Bundler.ui.info "Gem '#{name}' was successfully created. " \ - "For more information on making a RubyGem visit https://bundler.io/guides/creating_gem.html" + 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) - SharedHelpers.pwd.join(name).basename.to_s + Pathname.new(SharedHelpers.pwd).join(name).basename.to_s end - def ask_and_set(key, header, message) + def ask_and_set(key, prompt, explanation) choice = options[key] choice = Bundler.settings["gem.#{key}"] if choice.nil? if choice.nil? - Bundler.ui.confirm header - choice = Bundler.ui.yes? "#{message} y/(n):" + Bundler.ui.info "\n#{explanation}" + choice = Bundler.ui.yes? "#{prompt} y/(n):" Bundler.settings.set_global("gem.#{key}", choice) end @@ -263,14 +312,15 @@ module Bundler 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.confirm "Do you want to generate tests with your gem?" + 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 result =~ /rspec|minitest|test-unit/ + if /rspec|minitest|test-unit/.match?(result) test_framework = result else test_framework = false @@ -288,6 +338,10 @@ module Bundler 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." @@ -298,20 +352,19 @@ module Bundler 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.confirm "Do you want to set up continuous integration for your gem? " \ + 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" \ - "* Travis CI: https://travis-ci.org/\n" \ - "\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/travis/gitlab/circle/(none):" - if result =~ /github|travis|gitlab|circle/ + 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 @@ -330,19 +383,18 @@ module Bundler end def ask_and_set_linter + return if skip?(:linter) linter_template = options[:linter] || Bundler.settings["gem.linter"] - linter_template = deprecated_rubocop_option if linter_template.nil? if linter_template.to_s.empty? - Bundler.ui.confirm "Do you want to add a code linter and formatter to your gem? " \ + 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/testdouble/standard\n" \ - "\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 result =~ /rubocop|standard/ + if /rubocop|standard/.match?(result) linter_template = result else linter_template = false @@ -365,22 +417,6 @@ module Bundler linter_template end - def deprecated_rubocop_option - if !options[:rubocop].nil? - if options[:rubocop] - Bundler::SharedHelpers.major_deprecation 2, "--rubocop is deprecated, use --linter=rubocop" - "rubocop" - else - Bundler::SharedHelpers.major_deprecation 2, "--no-rubocop is deprecated, use --linter" - false - end - elsif !Bundler.settings["gem.rubocop"].nil? - Bundler::SharedHelpers.major_deprecation 2, - "config gem.rubocop is deprecated; we've updated your config to use gem.linter instead" - Bundler.settings["gem.rubocop"] ? "rubocop" : false - end - end - def bundler_dependency_version v = Gem::Version.new(Bundler::VERSION) req = v.segments[0..1] @@ -389,11 +425,15 @@ module Bundler end def ensure_safe_gem_name(name, constant_array) - if name =~ /^\d/ + 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| @@ -415,28 +455,21 @@ module Bundler thor.run(%(#{editor} "#{file}")) end - def required_ruby_version - if Gem.ruby_version < Gem::Version.new("2.4.a") then "2.3.0" - elsif Gem.ruby_version < Gem::Version.new("2.5.a") then "2.4.0" - elsif Gem.ruby_version < Gem::Version.new("2.6.a") then "2.5.0" - else - "2.6.0" - end + def rust_builder_required_rubygems_version + "3.3.11" end - def rubocop_version - if Gem.ruby_version < Gem::Version.new("2.4.a") then "0.81.0" - elsif Gem.ruby_version < Gem::Version.new("2.5.a") then "1.12" - else - "1.21" - end + def required_ruby_version + "3.2.0" end - def standard_version - if Gem.ruby_version < Gem::Version.new("2.4.a") then "0.2.5" - elsif Gem.ruby_version < Gem::Version.new("2.5.a") then "1.0" + def github_username(git_username) + if options[:github_username].nil? + git_username + elsif options[:github_username] == false + "" else - "1.3" + options[:github_username] end end end diff --git a/lib/bundler/cli/info.rb b/lib/bundler/cli/info.rb index 0545ce8c75..cd01d4949b 100644 --- a/lib/bundler/cli/info.rb +++ b/lib/bundler/cli/info.rb @@ -25,19 +25,8 @@ module Bundler private - def spec_for_gem(gem_name) - spec = Bundler.definition.specs.find {|s| s.name == gem_name } - spec || default_gem_spec(gem_name) || Bundler::CLI::Common.select_spec(gem_name, :regex_match) - end - - def default_gem_spec(gem_name) - return unless Gem::Specification.respond_to?(:find_all_by_name) - gem_spec = Gem::Specification.find_all_by_name(gem_name).last - return gem_spec if gem_spec && gem_spec.respond_to?(:default_gem?) && gem_spec.default_gem? - end - - def spec_not_found(gem_name) - raise GemNotFound, Bundler::CLI::Common.gem_not_found_message(gem_name, Bundler.definition.dependencies) + def spec_for_gem(name) + Bundler::CLI::Common.select_spec(name, :regex_match) end def print_gem_version(spec) @@ -50,8 +39,8 @@ module Bundler path = File.expand_path("../../..", __dir__) else path = spec.full_gem_path - if spec.deleted_gem? - return Bundler.ui.warn "The gem #{name} has been deleted. It was installed at: #{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 @@ -76,19 +65,19 @@ module Bundler 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.deleted_gem? - return Bundler.ui.warn "The gem #{name} has been deleted. Gemspec information is still available though:\n#{gem_info}" + 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.map do |spec| + @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.compact.sort + end.sort end end end diff --git a/lib/bundler/cli/init.rb b/lib/bundler/cli/init.rb index e4f8229c48..246b9d6460 100644 --- a/lib/bundler/cli/init.rb +++ b/lib/bundler/cli/init.rb @@ -32,7 +32,11 @@ module Bundler file << spec.to_gemfile end else - FileUtils.cp(File.expand_path("../templates/#{gemfile}", __dir__), gemfile) + 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}" @@ -41,7 +45,7 @@ module Bundler private def gemfile - @gemfile ||= Bundler.preferred_gemfile_name + @gemfile ||= options[:gemfile] || Bundler.preferred_gemfile_name end end end diff --git a/lib/bundler/cli/inject.rb b/lib/bundler/cli/inject.rb deleted file mode 100644 index 8093a85283..0000000000 --- a/lib/bundler/cli/inject.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -module Bundler - class CLI::Inject - attr_reader :options, :name, :version, :group, :source, :gems - def initialize(options, name, version) - @options = options - @name = name - @version = version || last_version_number - @group = options[:group].split(",") unless options[:group].nil? - @source = options[:source] - @gems = [] - end - - def run - # The required arguments allow Thor to give useful feedback when the arguments - # are incorrect. This adds those first two arguments onto the list as a whole. - gems.unshift(source).unshift(group).unshift(version).unshift(name) - - # Build an array of Dependency objects out of the arguments - deps = [] - # when `inject` support addition of more than one gem, then this loop will - # help. Currently this loop is running once. - gems.each_slice(4) do |gem_name, gem_version, gem_group, gem_source| - ops = Gem::Requirement::OPS.map {|key, _val| key } - has_op = ops.any? {|op| gem_version.start_with? op } - gem_version = "~> #{gem_version}" unless has_op - deps << Bundler::Dependency.new(gem_name, gem_version, "group" => gem_group, "source" => gem_source) - end - - added = Injector.inject(deps, options) - - if added.any? - Bundler.ui.confirm "Added to Gemfile:" - Bundler.ui.confirm(added.map do |d| - name = "'#{d.name}'" - requirement = ", '#{d.requirement}'" - group = ", :group => #{d.groups.inspect}" if d.groups != Array(:default) - source = ", :source => '#{d.source}'" unless d.source.nil? - %(gem #{name}#{requirement}#{group}#{source}) - end.join("\n")) - else - Bundler.ui.confirm "All gems were already present in the Gemfile" - end - end - - private - - def last_version_number - definition = Bundler.definition(true) - definition.resolve_remotely! - specs = definition.index[name].sort_by(&:version) - unless options[:pre] - specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? } - end - spec = specs.last - spec.version.to_s - end - end -end diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb index 851ae9b840..67feba84bd 100644 --- a/lib/bundler/cli/install.rb +++ b/lib/bundler/cli/install.rb @@ -12,56 +12,44 @@ module Bundler warn_if_root - Bundler.self_manager.install_locked_bundler_and_restart_with_it_if_needed - - Bundler::SharedHelpers.set_env "RB_USER_INSTALL", "1" if Bundler::FREEBSD + 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 - # Disable color in deployment mode - Bundler.ui.shell = Thor::Shell::Basic.new if options[:deployment] + Bundler::SharedHelpers.set_env "RB_USER_INSTALL", "1" if Gem.freebsd_platform? - check_for_options_conflicts + if target_rbconfig_path = options[:"target-rbconfig"] + Bundler.rubygems.set_target_rbconfig(target_rbconfig_path) + end check_trust_policy - if options[:deployment] || options[:frozen] || Bundler.frozen_bundle? - unless Bundler.default_lockfile.exist? - flag = "--deployment flag" if options[:deployment] - flag ||= "--frozen flag" if options[:frozen] - flag ||= "deployment setting" - raise ProductionError, "The #{flag} requires a #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}. Please make " \ - "sure you have checked your #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} into version control " \ - "before deploying." - end - - options[:local] = true if Bundler.app_cache.exist? - - Bundler.settings.set_command_option :deployment, true if options[:deployment] - Bundler.settings.set_command_option :frozen, true if options[:frozen] - end - - # When install is called with --no-deployment, disable deployment mode - if options[:deployment] == false - Bundler.settings.set_command_option :frozen, nil - options[:system] = true + 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"] - if options["binstubs"] - Bundler::SharedHelpers.major_deprecation 2, - "The --binstubs option will be removed in favor of `bundle binstubs --all`" - end - - Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.feature_flag.plugins? + Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins] - definition = Bundler.definition + # 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.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 @@ -77,8 +65,6 @@ module Bundler Bundler::CLI::Common.output_post_install_messages installer.post_install_messages - warn_ambiguous_gems - if CLI::Common.clean_after_install? require_relative "clean" Bundler::CLI::Clean.new(options).run @@ -94,9 +80,8 @@ module Bundler 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. Bundler can ask for sudo " \ - "if it is needed, and installing your bundle as root will break this " \ - "application for all non-root users on this machine.", :wrap => true + 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) @@ -105,26 +90,10 @@ module Bundler end def gems_installed_for(definition) - count = definition.specs.count + count = definition.specs.count {|spec| spec.name != "bundler" } "#{count} #{count == 1 ? "gem" : "gems"} now installed" end - def check_for_group_conflicts_in_cli_options - conflicting_groups = Array(options[:without]) & Array(options[:with]) - return if conflicting_groups.empty? - raise InvalidOption, "You can't list a group in both with and without." \ - " The offending groups are: #{conflicting_groups.join(", ")}." - end - - def check_for_options_conflicts - if (options[:path] || options[:deployment]) && options[:system] - error_message = String.new - error_message << "You have specified both --path as well as --system. Please choose only one option.\n" if options[:path] - error_message << "You have specified both --deployment as well as --system. Please choose only one option.\n" if options[:deployment] - raise InvalidOption.new(error_message) - end - end - def check_trust_policy trust_policy = options["trust-policy"] unless Bundler.rubygems.security_policies.keys.unshift(nil).include?(trust_policy) @@ -134,30 +103,11 @@ module Bundler Bundler.settings.set_command_option_if_given :"trust-policy", trust_policy end - def normalize_groups - check_for_group_conflicts_in_cli_options - - # need to nil them out first to get around validation for backwards compatibility - Bundler.settings.set_command_option :without, nil - Bundler.settings.set_command_option :with, nil - Bundler.settings.set_command_option :without, options[:without] - Bundler.settings.set_command_option :with, options[:with] - end - def normalize_settings - Bundler.settings.set_command_option :path, nil if options[:system] - Bundler.settings.set_command_option_if_given :path, options[:path] - if options["standalone"] && Bundler.settings[:path].nil? && !options["local"] - Bundler.settings.temporary(:path_relative_to_cwd => false) do - Bundler.settings.set_command_option :path, "bundle" - end + Bundler.settings.set_command_option :path, "bundle" end - bin_option = options["binstubs"] - bin_option = nil if bin_option && bin_option.empty? - Bundler.settings.set_command_option :bin, bin_option if options["binstubs"] - Bundler.settings.set_command_option_if_given :shebang, options["shebang"] Bundler.settings.set_command_option_if_given :jobs, options["jobs"] @@ -168,23 +118,7 @@ module Bundler Bundler.settings.set_command_option_if_given :clean, options["clean"] - normalize_groups if options[:without] || options[:with] - - options[:force] = options[:redownload] - end - - def warn_ambiguous_gems - # TODO: remove this when we drop Bundler 1.x support - Installer.ambiguous_gems.to_a.each do |name, installed_from_uri, *also_found_in_uris| - Bundler.ui.warn "Warning: the gem '#{name}' was found in multiple sources." - Bundler.ui.warn "Installed from: #{installed_from_uri}" - Bundler.ui.warn "Also found in:" - also_found_in_uris.each {|uri| Bundler.ui.warn " * #{uri}" } - Bundler.ui.warn "You should add a source requirement to restrict this gem to your preferred source." - Bundler.ui.warn "For example:" - Bundler.ui.warn " gem '#{name}', :source => '#{installed_from_uri}'" - Bundler.ui.warn "Then uninstall the gem '#{name}' (or delete all bundled gems) and then install again." - end + options[:force] = options[:redownload] if options[:redownload] end end end diff --git a/lib/bundler/cli/issue.rb b/lib/bundler/cli/issue.rb index b891ecb1d2..cbfb7da2d8 100644 --- a/lib/bundler/cli/issue.rb +++ b/lib/bundler/cli/issue.rb @@ -5,12 +5,12 @@ require "rbconfig" module Bundler class CLI::Issue def run - Bundler.ui.info <<-EOS.gsub(/^ {8}/, "") + 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/rubygems/rubygems/blob/master/bundler/doc/TROUBLESHOOTING.md + 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/ @@ -22,7 +22,7 @@ module Bundler 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/rubygems/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md, + https://github.com/ruby/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md, and copy and pasting the information below. EOS @@ -34,8 +34,8 @@ module Bundler end def doctor - require_relative "doctor" - Bundler::CLI::Doctor.new({}).run + 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 index f56bf5b86a..6a467f45a9 100644 --- a/lib/bundler/cli/list.rb +++ b/lib/bundler/cli/list.rb @@ -1,11 +1,14 @@ # 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 @@ -25,6 +28,36 @@ module Bundler 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"] @@ -37,8 +70,6 @@ module Bundler Bundler.ui.info "Use `bundle info` to print more detailed information about a gem" end - private - def verify_group_exists(groups) (@without_group + @only_group).each do |group| raise InvalidOption, "`#{group}` group could not be found." unless groups.include?(group) diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb index 7d613a6644..2f78868936 100644 --- a/lib/bundler/cli/lock.rb +++ b/lib/bundler/cli/lock.rb @@ -14,54 +14,81 @@ module Bundler exit 1 end + check_for_conflicting_options + print = options[:print] - ui = Bundler.ui - Bundler.ui = UI::Silent.new if 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 - update = { :conservative => conservative } if conservative + update = { gems: update, conservative: conservative } + elsif update && conservative + update = { conservative: conservative } + elsif update && bundler + update = { bundler: bundler } end - definition = Bundler.definition(update) - Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options) if options[:update] + Bundler.settings.temporary(frozen: false) do + definition = Bundler.definition(update, Bundler.default_lockfile) + definition.add_checksums if options["add-checksums"] - options["remove-platform"].each do |platform| - definition.remove_platform(platform) - end + Bundler::CLI::Common.configure_gem_version_promoter(definition, options) if options[:update] - options["add-platform"].each do |platform_string| - platform = Gem::Platform.new(platform_string) - if platform.to_s == "unknown" - Bundler.ui.warn "The platform `#{platform_string}` is unknown to RubyGems " \ - "and adding it will likely lead to resolution errors" + options["remove-platform"].each do |platform_string| + platform = Gem::Platform.new(platform_string) + definition.remove_platform(platform) end - definition.add_platform(platform) - end - if definition.platforms.empty? - raise InvalidOption, "Removing all platforms from the bundle is not allowed" + 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 - definition.resolve_remotely! unless options[:local] + Bundler.ui.output_stream = previous_output_stream + end + + private - if print - puts definition.to_lock - else - file = options[:lockfile] - file = file ? File.expand_path(file) : Bundler.default_lockfile - puts "Writing lockfile to #{file}" - definition.lock(file) + 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 - Bundler.ui = ui + 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 index ea504344f3..f24693b843 100644 --- a/lib/bundler/cli/open.rb +++ b/lib/bundler/cli/open.rb @@ -2,27 +2,27 @@ module Bundler class CLI::Open - attr_reader :options, :name + 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 - path = spec.full_gem_path - Dir.chdir(path) do - require "shellwords" - command = Shellwords.split(editor) + [path] - Bundler.with_original_env do - system(*command) - end || Bundler.ui.info("Could not run '#{command.join(" ")}'") - end + 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 diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb index e9f93fec39..ae827dbb4b 100644 --- a/lib/bundler/cli/outdated.rb +++ b/lib/bundler/cli/outdated.rb @@ -26,13 +26,15 @@ module Bundler def run check_for_deployment_mode! - gems.each do |gem_name| - Bundler::CLI::Common.select_spec(gem_name) - end - 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 @@ -41,12 +43,12 @@ module Bundler # We're doing a full update Bundler.definition(true) else - Bundler.definition(:gems => gems, :sources => sources) + Bundler.definition(gems: gems, sources: sources) end Bundler::CLI::Common.configure_gem_version_promoter( Bundler.definition, - options.merge(:strict => @strict) + options.merge(strict: @strict) ) definition_resolution = proc do @@ -54,7 +56,7 @@ module Bundler end if options[:parseable] - Bundler.ui.silence(&definition_resolution) + Bundler.ui.progress(&definition_resolution) else definition_resolution.call end @@ -90,37 +92,33 @@ module Bundler end outdated_gems << { - :active_spec => active_spec, - :current_spec => current_spec, - :dependency => dependency, - :groups => groups, + active_spec: active_spec, + current_spec: current_spec, + dependency: dependency, + groups: groups, } end - if outdated_gems.empty? + 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_include_groups - relevant_outdated_gems = 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 - - if options[:parseable] - relevant_outdated_gems.each do |gems| - print_gems(gems) - end - else - print_gems_table(relevant_outdated_gems) - end - elsif options[:parseable] - print_gems(outdated_gems) + if options[:parseable] + print_gems(relevant_outdated_gems) else - print_gems_table(outdated_gems) + print_gems_table(relevant_outdated_gems) end exit 1 @@ -157,7 +155,7 @@ module Bundler return active_spec if strict - active_specs = active_spec.source.specs.search(current_spec.name).select {|spec| spec.match_platform(current_spec.platform) }.sort_by(&:version) + 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 @@ -196,12 +194,16 @@ module Bundler end current_version = "#{current_spec.version}#{current_spec.git_version}" - if dependency && dependency.specific? + if dependency&.specific? dependency_version = %(, requested #{dependency.requirement}) end spec_outdated_info = "#{active_spec.name} (newest #{spec_version}, " \ - "installed #{current_version}#{dependency_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 @@ -220,6 +222,7 @@ module Bundler 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 @@ -285,11 +288,28 @@ module Bundler end def table_header - header = ["Gem", "Current", "Latest", "Requested", "Groups"] + 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]) diff --git a/lib/bundler/cli/platform.rb b/lib/bundler/cli/platform.rb index 73da8cf80e..32d68abbb1 100644 --- a/lib/bundler/cli/platform.rb +++ b/lib/bundler/cli/platform.rb @@ -8,12 +8,12 @@ module Bundler end def run - platforms, ruby_version = Bundler.ui.silence do - locked_ruby_version = Bundler.locked_gems && Bundler.locked_gems.ruby_version&.gsub(/p\d+\Z/, "") - gemfile_ruby_version = Bundler.definition.ruby_version && Bundler.definition.ruby_version.single_version_string - [Bundler.definition.platforms.map {|p| "* #{p}" }, - locked_ruby_version || gemfile_ruby_version] + 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] @@ -23,6 +23,8 @@ module Bundler 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")}" diff --git a/lib/bundler/cli/plugin.rb b/lib/bundler/cli/plugin.rb index fe3f4412fa..32fa660fe0 100644 --- a/lib/bundler/cli/plugin.rb +++ b/lib/bundler/cli/plugin.rb @@ -5,21 +5,20 @@ 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) or from a git source provided with --git (for remote repos) or --local_git (for local repos). If no sources are provided, it uses Gem.sources + 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" - 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 "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 @@ -27,8 +26,7 @@ module Bundler 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." + 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 diff --git a/lib/bundler/cli/pristine.rb b/lib/bundler/cli/pristine.rb index d6654f8053..f463f0bce8 100644 --- a/lib/bundler/cli/pristine.rb +++ b/lib/bundler/cli/pristine.rb @@ -11,41 +11,53 @@ module Bundler definition = Bundler.definition definition.validate_runtime! installer = Bundler::Installer.new(Bundler.root, definition) + git_sources = [] - Bundler.load.specs.each do |spec| - next if spec.name == "bundler" # Source::Rubygems doesn't install bundler - next if !@gems.empty? && !@gems.include?(spec.name) + 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 + 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 + 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 - FileUtils.rm_rf spec.full_gem_path - when Source::Git - if source.local? - Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is locally overridden.") + 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 - 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 - else - Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is sourced from local path.") - next - end - - Bundler::GemInstaller.new(spec, installer, false, 0, true).install_from_spec + 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 diff --git a/lib/bundler/cli/show.rb b/lib/bundler/cli/show.rb index 2df13db1fa..67fdcc797e 100644 --- a/lib/bundler/cli/show.rb +++ b/lib/bundler/cli/show.rb @@ -6,7 +6,7 @@ module Bundler def initialize(options, gem_name) @options = options @gem_name = gem_name - @verbose = options[:verbose] || options[:outdated] + @verbose = options[:verbose] @latest_specs = fetch_latest_specs if @verbose end @@ -24,7 +24,7 @@ module Bundler return unless spec path = spec.full_gem_path unless File.directory?(path) - return Bundler.ui.warn "The gem #{gem_name} has been deleted. It was installed at: #{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) @@ -40,8 +40,8 @@ module Bundler desc = " * #{s.name} (#{s.version}#{s.git_version})" if @verbose latest = latest_specs.find {|l| l.name == s.name } - Bundler.ui.info <<-END.gsub(/^ +/, "") - #{desc} + 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"} @@ -57,12 +57,8 @@ module Bundler def fetch_latest_specs definition = Bundler.definition(true) - if options[:outdated] - Bundler.ui.info "Fetching remote specs for outdated check...\n\n" - Bundler.ui.silence { definition.resolve_remotely! } - else - definition.resolve_with_cache! - end + Bundler.ui.info "Fetching remote specs for outdated check...\n\n" + Bundler.ui.silence { definition.remotely! } Bundler.reset! definition.specs end diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb index b49182655b..9cc90acc58 100644 --- a/lib/bundler/cli/update.rb +++ b/lib/bundler/cli/update.rb @@ -15,7 +15,7 @@ module 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.feature_flag.plugins? + Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins] sources = Array(options[:source]) groups = Array(options[:group]).map(&:to_sym) @@ -23,10 +23,10 @@ module Bundler full_update = gems.empty? && sources.empty? && groups.empty? && !options[:ruby] && !update_bundler if full_update && !options[:all] - if Bundler.feature_flag.update_requires_all_flag? + if Bundler.settings[:update_requires_all_flag] raise InvalidOption, "To update everything, pass the `--all` flag." end - SharedHelpers.major_deprecation 3, "Pass --all to `bundle update` to update everything" + 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 @@ -35,7 +35,7 @@ module Bundler if full_update if conservative - Bundler.definition(:conservative => conservative) + Bundler.definition(conservative: conservative) else Bundler.definition(true) end @@ -51,9 +51,9 @@ module Bundler gems.concat(deps.map(&:name)) end - Bundler.definition(:gems => gems, :sources => sources, :ruby => options[:ruby], - :conservative => conservative, - :bundler => update_bundler) + 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) @@ -63,6 +63,7 @@ module Bundler 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"] @@ -70,7 +71,7 @@ module Bundler 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[s.name] = { spec: s, version: s.version, source: s.source.identifier } h end end @@ -91,7 +92,7 @@ module Bundler locked_spec = locked_info[:spec] new_spec = Bundler.definition.specs[name].first unless new_spec - unless locked_spec.match_platform(Bundler.local_platform) + 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 diff --git a/lib/bundler/cli/viz.rb b/lib/bundler/cli/viz.rb deleted file mode 100644 index 5c09e00995..0000000000 --- a/lib/bundler/cli/viz.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -module Bundler - class CLI::Viz - attr_reader :options, :gem_name - def initialize(options) - @options = options - end - - def run - # make sure we get the right `graphviz`. There is also a `graphviz` - # gem we're not built to support - gem "ruby-graphviz" - require "graphviz" - - options[:without] = options[:without].join(":").tr(" ", ":").split(":") - output_file = File.expand_path(options[:file]) - - graph = Graph.new(Bundler.load, output_file, options[:version], options[:requirements], options[:format], options[:without]) - graph.viz - rescue LoadError => e - Bundler.ui.error e.inspect - Bundler.ui.warn "Make sure you have the graphviz ruby gem. You can install it with:" - Bundler.ui.warn "`gem install ruby-graphviz`" - rescue StandardError => e - raise unless e.message.to_s.include?("GraphViz not installed or dot not in PATH") - Bundler.ui.error e.message - Bundler.ui.warn "Please install GraphViz. On a Mac with Homebrew, you can run `brew install graphviz`." - end - end -end |
