diff options
author | hsbt <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2017-11-01 23:29:38 +0000 |
---|---|---|
committer | hsbt <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2017-11-01 23:29:38 +0000 |
commit | be7b5929126cb3e696ef222339237faba9b8fe5a (patch) | |
tree | 51eae376f93c09bc82dde5a657a91df2c89062e4 /lib/bundler | |
parent | ae49dbd392083f69026f2a0fff4a1d5f42d172a7 (diff) | |
download | ruby-be7b5929126cb3e696ef222339237faba9b8fe5a.tar.gz |
Update bundled bundler to 1.16.0.
* lib/bundler, spec/bundler: Merge bundler-1.16.0.
* common.mk: rspec examples of bundler-1.16.0 needs require option.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@60603 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib/bundler')
154 files changed, 4879 insertions, 1262 deletions
diff --git a/lib/bundler/build_metadata.rb b/lib/bundler/build_metadata.rb new file mode 100644 index 0000000000..54436f982d --- /dev/null +++ b/lib/bundler/build_metadata.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Bundler + # Represents metadata from when the Bundler gem was built. + module BuildMetadata + # begin ivars + @release = false + # end ivars + + # A hash representation of the build metadata. + def self.to_h + { + "Built At" => built_at, + "Git SHA" => git_commit_sha, + "Released Version" => release?, + } + end + + # A string representing the date the bundler gem was built. + def self.built_at + @built_at ||= Time.now.utc.strftime("%Y-%m-%d").freeze + end + + # The SHA for the git commit the bundler gem was built from. + def self.git_commit_sha + @git_commit_sha ||= Dir.chdir(File.expand_path("..", __FILE__)) do + `git rev-parse --short HEAD`.strip.freeze + end + end + + # Whether this is an official release build of Bundler. + def self.release? + @release + end + end +end diff --git a/lib/bundler/capistrano.rb b/lib/bundler/capistrano.rb index 7b0bbbd6d2..1b7145b72b 100644 --- a/lib/bundler/capistrano.rb +++ b/lib/bundler/capistrano.rb @@ -1,4 +1,9 @@ # frozen_string_literal: true + +require "bundler/shared_helpers" +Bundler::SharedHelpers.major_deprecation 2, + "The Bundler task for Capistrano. Please use http://github.com/capistrano/bundler" + # Capistrano task for Bundler. # # Add "require 'bundler/capistrano'" in your Capistrano deploy.rb, and diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 03e08e25a1..05e1851c18 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -1,13 +1,18 @@ # frozen_string_literal: true + require "bundler" require "bundler/vendored_thor" module Bundler class CLI < Thor - AUTO_INSTALL_CMDS = %w(show binstubs outdated exec open console licenses clean).freeze - PARSEABLE_COMMANDS = %w( + require "bundler/cli/common" + + package_name "Bundler" + + AUTO_INSTALL_CMDS = %w[show binstubs outdated exec open console licenses clean].freeze + PARSEABLE_COMMANDS = %w[ check config help exec platform show version - ).freeze + ].freeze def self.start(*) super @@ -30,11 +35,11 @@ module Bundler custom_gemfile = options[:gemfile] || Bundler.settings[:gemfile] if custom_gemfile && !custom_gemfile.empty? - ENV["BUNDLE_GEMFILE"] = File.expand_path(custom_gemfile) + Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", File.expand_path(custom_gemfile) Bundler.reset_paths! end - Bundler.settings[:retry] = options[:retry] if options[:retry] + Bundler.settings.set_command_option_if_given :retry, options[:retry] current_cmd = args.last[:current_command].name auto_install if AUTO_INSTALL_CMDS.include?(current_cmd) @@ -42,7 +47,6 @@ module Bundler raise InvalidOption, e.message ensure self.options ||= {} - Bundler.settings.cli_flags_given = !options.empty? unprinted_warnings = Bundler.ui.unprinted_warnings Bundler.ui = UI::Shell.new(options) Bundler.ui.level = "debug" if options["verbose"] @@ -57,10 +61,41 @@ module Bundler end end + def self.deprecated_option(*args, &blk) + return if Bundler.feature_flag.forget_cli_options? + method_option(*args, &blk) + end + check_unknown_options!(:except => [:config, :exec]) stop_on_unknown_option! :exec - default_task :install + desc "cli_help", "Prints a summary of bundler commands", :hide => true + def cli_help + version + Bundler.ui.info "\n" + + primary_commands = ["install", "update", + Bundler.feature_flag.cache_command_is_package? ? "cache" : "package", + "exec", "config", "help"] + + list = self.class.printable_commands(true) + by_name = list.group_by {|name, _message| name.match(/^bundle (\w+)/)[1] } + utilities = by_name.keys.sort - primary_commands + primary_commands.map! {|name| (by_name[name] || raise("no primary command #{name}")).first } + utilities.map! {|name| by_name[name].first } + + shell.say "Bundler commands:\n\n" + + shell.say " Primary commands:\n" + shell.print_table(primary_commands, :indent => 4, :truncate => true) + shell.say + shell.say " Utilities:\n" + shell.print_table(utilities, :indent => 4, :truncate => true) + shell.say + self.class.send(:class_options_help, shell) + end + default_task(Bundler.feature_flag.default_cli_command) + class_option "no-color", :type => :boolean, :desc => "Disable colorization in output" class_option "retry", :type => :numeric, :aliases => "-r", :banner => "NUM", :desc => "Specify the number of times you wish to attempt network commands" @@ -107,7 +142,7 @@ module Bundler Gemfile to a gem with a gemspec, the --gemspec option will automatically add each dependency listed in the gemspec file to the newly created Gemfile. D - method_option "gemspec", :type => :string, :banner => "Use the specified .gemspec to create the Gemfile" + deprecated_option "gemspec", :type => :string, :banner => "Use the specified .gemspec to create the Gemfile" def init require "bundler/cli/init" Init.new(options.dup).run @@ -124,7 +159,7 @@ module Bundler method_option "gemfile", :type => :string, :banner => "Use the specified gemfile instead of Gemfile" method_option "path", :type => :string, :banner => - "Specify a different path than the system default ($BUNDLE_PATH or $GEM_HOME). Bundler will remember this value for future installs on this machine" + "Specify a different path than the system default ($BUNDLE_PATH or $GEM_HOME).#{" Bundler will remember this value for future installs on this machine" unless Bundler.feature_flag.forget_cli_options?}" map "c" => "check" def check require "bundler/cli/check" @@ -142,13 +177,13 @@ module Bundler If the bundle has already been installed, bundler will tell you so and then exit. D - method_option "binstubs", :type => :string, :lazy_default => "bin", :banner => + deprecated_option "binstubs", :type => :string, :lazy_default => "bin", :banner => "Generate bin stubs for bundled gems to ./bin" - method_option "clean", :type => :boolean, :banner => + deprecated_option "clean", :type => :boolean, :banner => "Run bundle clean automatically after install" - method_option "deployment", :type => :boolean, :banner => + deprecated_option "deployment", :type => :boolean, :banner => "Install using defaults tuned for deployment environments" - method_option "frozen", :type => :boolean, :banner => + deprecated_option "frozen", :type => :boolean, :banner => "Do not allow the Gemfile.lock to be updated after this install" method_option "full-index", :type => :boolean, :banner => "Fall back to using the single-file index of all gems" @@ -158,28 +193,29 @@ module Bundler "Specify the number of jobs to run in parallel" method_option "local", :type => :boolean, :banner => "Do not attempt to fetch gems remotely and use the gem cache instead" - method_option "no-cache", :type => :boolean, :banner => + deprecated_option "no-cache", :type => :boolean, :banner => "Don't update the existing gem cache." - method_option "force", :type => :boolean, :banner => + method_option "redownload", :type => :boolean, :aliases => + [Bundler.feature_flag.forget_cli_options? ? nil : "--force"].compact, :banner => "Force downloading every gem." - method_option "no-prune", :type => :boolean, :banner => + deprecated_option "no-prune", :type => :boolean, :banner => "Don't remove stale gems from the cache." - method_option "path", :type => :string, :banner => + deprecated_option "path", :type => :string, :banner => "Specify a different path than the system default ($BUNDLE_PATH or $GEM_HOME). Bundler will remember this value for future installs on this machine" method_option "quiet", :type => :boolean, :banner => "Only output warnings and errors." - method_option "shebang", :type => :string, :banner => + deprecated_option "shebang", :type => :string, :banner => "Specify a different shebang executable name than the default (usually 'ruby')" method_option "standalone", :type => :array, :lazy_default => [], :banner => "Make a bundle that can work without the Bundler runtime" - method_option "system", :type => :boolean, :banner => + deprecated_option "system", :type => :boolean, :banner => "Install to the system location ($BUNDLE_PATH or $GEM_HOME) even if the bundle was previously installed somewhere else for this application" method_option "trust-policy", :alias => "P", :type => :string, :banner => "Gem trust policy (like gem install -P). Must be one of " + Bundler.rubygems.security_policy_keys.join("|") - method_option "without", :type => :array, :banner => + deprecated_option "without", :type => :array, :banner => "Exclude gems that are part of the specified named group." - method_option "with", :type => :array, :banner => + deprecated_option "with", :type => :array, :banner => "Include gems that are part of the specified named group." map "i" => "install" def install @@ -189,7 +225,7 @@ module Bundler end end - desc "update [OPTIONS]", "update the current environment" + desc "update [OPTIONS]", "Update the current environment" long_desc <<-D Update will install the newest versions of the gems listed in the Gemfile. Use update when you have changed the Gemfile, or if you want to get the newest @@ -223,6 +259,8 @@ module Bundler "Do not allow any gem to be updated past latest --patch | --minor | --major" method_option "conservative", :type => :boolean, :banner => "Use bundle install conservative update behavior and do not allow shared dependencies to be updated." + method_option "all", :type => :boolean, :banner => + "Update everything." def update(*gems) require "bundler/cli/update" Update.new(options, gems).run @@ -238,12 +276,24 @@ module Bundler method_option "outdated", :type => :boolean, :banner => "Show verbose output including whether gems are outdated." def show(gem_name = nil) - Bundler::SharedHelpers.major_deprecation("use `bundle show` instead of `bundle list`") if ARGV[0] == "list" + Bundler::SharedHelpers.major_deprecation(2, "use `bundle list` instead of `bundle show`") if ARGV[0] == "show" require "bundler/cli/show" Show.new(options, gem_name).run end - # TODO: 2.0 remove `bundle list` - map %w(list) => "show" + # TODO: 2.0 remove `bundle show` + + if Bundler.feature_flag.list_command? + desc "list", "List all gems in the bundle" + method_option "name-only", :type => :boolean, :banner => "print only the gem names" + def list + require "bundler/cli/list" + List.new(options).run + end + + map %w[ls] => "list" + else + map %w[list] => "show" + end desc "info GEM [OPTIONS]", "Show information for the given gem" method_option "path", :type => :boolean, :banner => "Print full path to gem" @@ -262,6 +312,8 @@ module Bundler "Overwrite existing binstubs if they exist" method_option "path", :type => :string, :lazy_default => "bin", :banner => "Binstub destination directory (default bin)" + method_option "shebang", :type => :string, :banner => + "Specify a different shebang executable name than the default (usually 'ruby')" method_option "standalone", :type => :boolean, :banner => "Make binstubs that can work without the Bundler runtime" def binstubs(*gems) @@ -282,7 +334,7 @@ module Bundler Add.new(options.dup, gem_name).run end - desc "outdated GEM [OPTIONS]", "list installed gems with newer versions available" + desc "outdated GEM [OPTIONS]", "List installed gems with newer versions available" long_desc <<-D Outdated lists the names and versions of gems that have a newer version available in the given source. Calling outdated with [GEM [GEM]] will only check for newer @@ -292,8 +344,8 @@ module Bundler For more information on patch level options (--major, --minor, --patch, --update-strict) see documentation on the same options on the update command. D - method_option "group", :aliases => "--group", :type => :string, :banner => "List gems from a specific group" - method_option "groups", :aliases => "--groups", :type => :boolean, :banner => "List gems organized by groups" + method_option "group", :type => :string, :banner => "List gems from a specific group" + method_option "groups", :type => :boolean, :banner => "List gems organized by groups" method_option "local", :type => :boolean, :banner => "Do not attempt to fetch gems remotely and use the gem cache instead" method_option "pre", :type => :boolean, :banner => "Check for newer pre-release gems" @@ -315,17 +367,27 @@ module Bundler Outdated.new(options, gems).run end - desc "cache [OPTIONS]", "Cache all the gems to vendor/cache", :hide => true - method_option "all", :type => :boolean, :banner => "Include all sources (including path and git)." - method_option "all-platforms", :type => :boolean, :banner => "Include gems for all platforms present in the lockfile, not only the current one" - method_option "no-prune", :type => :boolean, :banner => "Don't remove stale gems from the cache." - def cache - require "bundler/cli/cache" - Cache.new(options).run + if Bundler.feature_flag.cache_command_is_package? + map %w[cache] => :package + else + desc "cache [OPTIONS]", "Cache all the gems to vendor/cache", :hide => true + unless Bundler.feature_flag.cache_command_is_package? + method_option "all", :type => :boolean, + :banner => "Include all sources (including path and git)." + end + method_option "all-platforms", :type => :boolean, :banner => "Include gems for all platforms present in the lockfile, not only the current one" + method_option "no-prune", :type => :boolean, :banner => "Don't remove stale gems from the cache." + def cache + require "bundler/cli/cache" + Cache.new(options).run + end end - desc "package [OPTIONS]", "Locks and then caches all of the gems into vendor/cache" - method_option "all", :type => :boolean, :banner => "Include all sources (including path and git)." + desc "#{Bundler.feature_flag.cache_command_is_package? ? :cache : :package} [OPTIONS]", "Locks and then caches all of the gems into vendor/cache" + unless Bundler.feature_flag.cache_command_is_package? + method_option "all", :type => :boolean, + :banner => "Include all sources (including path and git)." + end method_option "all-platforms", :type => :boolean, :banner => "Include gems for all platforms present in the lockfile, not only the current one" method_option "cache-path", :type => :string, :banner => "Specify a different cache path than the default (vendor/cache)." @@ -347,14 +409,14 @@ module Bundler require "bundler/cli/package" Package.new(options).run end - map %w(pack) => :package + map %w[pack] => :package desc "exec [OPTIONS]", "Run the command in context of the bundle" method_option :keep_file_descriptors, :type => :boolean, :default => false long_desc <<-D Exec runs a command, providing it access to the gems in the bundle. While using bundle exec you can require and call the bundled gems as if they were installed - into the system wide Rubygems repository. + into the system wide RubyGems repository. D map "e" => "exec" def exec(*args) @@ -362,7 +424,7 @@ module Bundler Exec.new(options, args).run end - desc "config NAME [VALUE]", "retrieve or set a configuration value" + desc "config NAME [VALUE]", "Retrieve or set a configuration value" long_desc <<-D Retrieves or sets a configuration value. If only one parameter is provided, retrieve the value. If two parameters are provided, replace the existing value with the newly provided one. @@ -386,18 +448,28 @@ module Bundler Open.new(options, name).run end - desc "console [GROUP]", "Opens an IRB session with the bundle pre-loaded" - def console(group = nil) - # TODO: Remove for 2.0 - require "bundler/cli/console" - Console.new(options, group).run + if Bundler.feature_flag.console_command? + desc "console [GROUP]", "Opens an IRB session with the bundle pre-loaded" + def console(group = nil) + require "bundler/cli/console" + Console.new(options, group).run + end end desc "version", "Prints the bundler's version information" def version - Bundler.ui.info "Bundler version #{Bundler::VERSION}" + cli_help = current_command.name == "cli_help" + if cli_help || ARGV.include?("version") + build_info = " (#{BuildMetadata.built_at} commit #{BuildMetadata.git_commit_sha})" + end + + if !cli_help && Bundler.feature_flag.print_only_version_number? + Bundler.ui.info "#{Bundler::VERSION}#{build_info}" + else + Bundler.ui.info "Bundler version #{Bundler::VERSION}#{build_info}" + end end - map %w(-v --version) => :version + map %w[-v --version] => :version desc "licenses", "Prints the license of all gems in the bundle" def licenses @@ -413,7 +485,7 @@ module Bundler end end - desc "viz [OPTIONS]", "Generates a visual dependency graph" + desc "viz [OPTIONS]", "Generates a visual dependency graph", :hide => true long_desc <<-D Viz generates a PNG file of the current Gemfile as a dependency graph. Viz requires the ruby-graphviz gem (and its dependencies). @@ -431,7 +503,7 @@ module Bundler old_gem = instance_method(:gem) - desc "gem GEM [OPTIONS]", "Creates a skeleton for creating a rubygem" + desc "gem NAME [OPTIONS]", "Creates a skeleton for creating a rubygem" method_option :exe, :type => :boolean, :default => false, :aliases => ["--bin", "-b"], :desc => "Generate a binary executable for your library." method_option :coc, :type => :boolean, :desc => "Generate a code of conduct file. Set a default with `bundle config gem.coc true`." method_option :edit, :type => :string, :aliases => "-e", :required => false, :banner => "EDITOR", @@ -470,7 +542,7 @@ module Bundler File.expand_path(File.join(File.dirname(__FILE__), "templates")) end - desc "clean [OPTIONS]", "Cleans up unused gems in your bundler directory" + desc "clean [OPTIONS]", "Cleans up unused gems in your bundler directory", :hide => true method_option "dry-run", :type => :boolean, :default => false, :banner => "Only print out changes, do not clean gems" method_option "force", :type => :boolean, :default => false, :banner => @@ -488,13 +560,13 @@ module Bundler Platform.new(options).run end - desc "inject GEM VERSION", "Add the named gem, with version requirements, to the resolved Gemfile" + desc "inject GEM VERSION", "Add the named gem, with version requirements, to the resolved Gemfile", :hide => true method_option "source", :type => :string, :banner => "Install gem from the given source" method_option "group", :type => :string, :banner => "Install gem into a bundler group" def inject(name, version) - SharedHelpers.major_deprecation "The `inject` command has been replaced by the `add` command" + SharedHelpers.major_deprecation 2, "The `inject` command has been replaced by the `add` command" require "bundler/cli/inject" Inject.new(options.dup, name, version).run end @@ -531,7 +603,7 @@ module Bundler desc "env", "Print information about the environment Bundler is running under" def env - Env.new.write($stdout) + Env.write($stdout) end desc "doctor [OPTIONS]", "Checks the bundle for common problems" @@ -555,15 +627,20 @@ module Bundler Issue.new.run end - desc "pristine", "Restores installed gems to pristine condition from files located in the gem cache. Gem installed from a git repository will be issued `git checkout --force`." - def pristine + desc "pristine [GEMS...]", "Restores installed gems to pristine condition" + long_desc <<-D + Restores installed gems to pristine condition from files located in the + gem cache. Gems installed from a git repository will be issued `git + checkout --force`. + D + def pristine(*gems) require "bundler/cli/pristine" - Pristine.new.run + Pristine.new(gems).run end if Bundler.feature_flag.plugins? require "bundler/cli/plugin" - desc "plugin SUBCOMMAND ...ARGS", "manage the bundler plugins" + desc "plugin", "Manage the bundler plugins" subcommand "plugin", Plugin end @@ -571,14 +648,14 @@ module Bundler # into the corresponding `bundle help #{command}` call def self.reformatted_help_args(args) bundler_commands = all_commands.keys - help_flags = %w(--help -h) - exec_commands = %w(e ex exe exec) + help_flags = %w[--help -h] + exec_commands = %w[e ex exe exec] help_used = args.index {|a| help_flags.include? a } exec_used = args.index {|a| exec_commands.include? a } command = args.find {|a| bundler_commands.include? a } if exec_used && help_used if exec_used + help_used == 1 - %w(help exec) + %w[help exec] else args end @@ -613,16 +690,20 @@ module Bundler end end + def current_command + _, _, config = @_initializer + config[:current_command] + end + def print_command return unless Bundler.ui.debug? - _, _, config = @_initializer - current_command = config[:current_command] - command_name = current_command.name + cmd = current_command + command_name = cmd.name return if PARSEABLE_COMMANDS.include?(command_name) command = ["bundle", command_name] + args options_to_print = options.dup options_to_print.delete_if do |k, v| - next unless o = current_command.options[k] + next unless o = cmd.options[k] o.default == v end command << Thor::Options.to_switches(options_to_print.sort_by(&:first)).strip @@ -633,8 +714,6 @@ module Bundler def warn_on_outdated_bundler return if Bundler.settings[:disable_version_check] - _, _, config = @_initializer - current_command = config[:current_command] command_name = current_command.name return if PARSEABLE_COMMANDS.include?(command_name) @@ -649,8 +728,17 @@ module Bundler current = Gem::Version.new(VERSION) return if current >= latest + latest_installed = Bundler.rubygems.find_name("bundler").map(&:version).max + + installation = "To install the latest version, run `gem install bundler#{" --pre" if latest.prerelease?}`" + if latest_installed && latest_installed > current + suggestion = "To update to the most recent installed version (#{latest_installed}), run `bundle update --bundler`" + suggestion = "#{installation}\n#{suggestion}" if latest_installed < latest + else + suggestion = installation + end - Bundler.ui.warn "The latest bundler is #{latest}, but you are currently running #{current}.\nTo update, run `gem install bundler#{" --pre" if latest.prerelease?}`" + Bundler.ui.warn "The latest bundler is #{latest}, but you are currently running #{current}.\n#{suggestion}" rescue nil end diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb index e80c775433..1fcbd22f28 100644 --- a/lib/bundler/cli/add.rb +++ b/lib/bundler/cli/add.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "bundler/cli/common" module Bundler class CLI::Add diff --git a/lib/bundler/cli/binstubs.rb b/lib/bundler/cli/binstubs.rb index 95103b7dd8..449204d821 100644 --- a/lib/bundler/cli/binstubs.rb +++ b/lib/bundler/cli/binstubs.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "bundler/cli/common" module Bundler class CLI::Binstubs @@ -11,8 +10,10 @@ module Bundler def run Bundler.definition.validate_runtime! - Bundler.settings[:bin] = options["path"] if options["path"] - Bundler.settings[:bin] = nil if options["path"] && options["path"].empty? + path_option = options["path"] + path_option = nil if path_option && 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) if gems.empty? @@ -28,10 +29,11 @@ module Bundler ) end - if spec.name == "bundler" - Bundler.ui.warn "Sorry, Bundler can only be run via Rubygems." - elsif options[:standalone] - installer.generate_standalone_bundler_executable_stubs(spec) + 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 + installer.generate_standalone_bundler_executable_stubs(spec) + end else installer.generate_bundler_executable_stubs(spec, :force => options[:force], :binstubs_cmd => true) end diff --git a/lib/bundler/cli/cache.rb b/lib/bundler/cli/cache.rb index 5ba105a31d..9d2ba87d34 100644 --- a/lib/bundler/cli/cache.rb +++ b/lib/bundler/cli/cache.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class CLI::Cache attr_reader :options @@ -10,9 +11,9 @@ module Bundler Bundler.definition.validate_runtime! Bundler.definition.resolve_with_cache! setup_cache_all - Bundler.settings[:cache_all_platforms] = options["all-platforms"] if options.key?("all-platforms") + Bundler.settings.set_command_option_if_given :cache_all_platforms, options["all-platforms"] Bundler.load.cache - Bundler.settings[:no_prune] = true if options["no-prune"] + Bundler.settings.set_command_option_if_given :no_prune, options["no-prune"] Bundler.load.lock rescue GemNotFound => e Bundler.ui.error(e.message) @@ -23,9 +24,9 @@ module Bundler private def setup_cache_all - Bundler.settings[:cache_all] = options[:all] if options.key?("all") + Bundler.settings.set_command_option_if_given :cache_all, options[:all] - if Bundler.definition.has_local_dependencies? && !Bundler.settings[:cache_all] + if Bundler.definition.has_local_dependencies? && !Bundler.feature_flag.cache_all? Bundler.ui.warn "Your Gemfile contains path and git dependencies. If you want " \ "to package them as well, please pass the --all flag. This will be the default " \ "on Bundler 2.0." diff --git a/lib/bundler/cli/check.rb b/lib/bundler/cli/check.rb index 057a7e5695..e572787dc4 100644 --- a/lib/bundler/cli/check.rb +++ b/lib/bundler/cli/check.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class CLI::Check attr_reader :options @@ -8,10 +9,7 @@ module Bundler end def run - if options[:path] - Bundler.settings[:path] = File.expand_path(options[:path]) - Bundler.settings[:disable_shared_gems] = true - end + Bundler.settings.set_command_option_if_given :path, options[:path] begin definition = Bundler.definition @@ -28,7 +26,7 @@ module Bundler 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.settings[:frozen] + elsif !Bundler.default_lockfile.file? && Bundler.frozen? Bundler.ui.error "This bundle has been frozen, but there is no #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} present" exit 1 else diff --git a/lib/bundler/cli/clean.rb b/lib/bundler/cli/clean.rb index 5eba09c6bc..4a407fbae7 100644 --- a/lib/bundler/cli/clean.rb +++ b/lib/bundler/cli/clean.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class CLI::Clean attr_reader :options @@ -15,12 +16,10 @@ module Bundler protected def require_path_or_force - if !Bundler.settings[:path] && !options[:force] - Bundler.ui.error "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`." - exit 1 - end + 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 index bacbb2edc5..9d40ee9dfd 100644 --- a/lib/bundler/cli/common.rb +++ b/lib/bundler/cli/common.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler module CLI::Common def self.output_post_install_messages(messages) @@ -14,12 +15,12 @@ module Bundler end def self.output_without_groups_message - return unless Bundler.settings.without.any? + return if Bundler.settings[:without].empty? Bundler.ui.confirm without_groups_message end def self.without_groups_message - groups = Bundler.settings.without + groups = Bundler.settings[:without] group_list = [groups[0...-1].join(", "), groups[-1..-1]]. reject {|s| s.to_s.empty? }.join(" and ") group_str = (groups.size == 1) ? "group" : "groups" @@ -89,5 +90,13 @@ module Bundler 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.auto_clean_without_path? && Bundler.settings[:path].nil? + clean &&= !Bundler.use_system_gems? + clean + end end end diff --git a/lib/bundler/cli/config.rb b/lib/bundler/cli/config.rb index e8f13620ec..12f71ea8fe 100644 --- a/lib/bundler/cli/config.rb +++ b/lib/bundler/cli/config.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class CLI::Config attr_reader :name, :options, :scope, :thor @@ -112,7 +113,7 @@ module Bundler end def valid_scope?(scope) - %w(delete local global).include?(scope) + %w[delete local global].include?(scope) end end end diff --git a/lib/bundler/cli/console.rb b/lib/bundler/cli/console.rb index 715abf2554..853eca8358 100644 --- a/lib/bundler/cli/console.rb +++ b/lib/bundler/cli/console.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class CLI::Console attr_reader :options, :group @@ -8,7 +9,7 @@ module Bundler end def run - Bundler::SharedHelpers.major_deprecation "bundle console will be replaced " \ + 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 diff --git a/lib/bundler/cli/doctor.rb b/lib/bundler/cli/doctor.rb index ae27983240..7f28a5eb13 100644 --- a/lib/bundler/cli/doctor.rb +++ b/lib/bundler/cli/doctor.rb @@ -62,6 +62,7 @@ module Bundler def run Bundler.ui.level = "error" if options[:quiet] + Bundler.settings.validate! check! definition = Bundler.definition diff --git a/lib/bundler/cli/exec.rb b/lib/bundler/cli/exec.rb index 62f7bc26cb..2fdc614fbb 100644 --- a/lib/bundler/cli/exec.rb +++ b/lib/bundler/cli/exec.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true + require "bundler/current_ruby" module Bundler class CLI::Exec attr_reader :options, :args, :cmd - RESERVED_SIGNALS = %w(SEGV BUS ILL FPE VTALRM KILL STOP).freeze + RESERVED_SIGNALS = %w[SEGV BUS ILL FPE VTALRM KILL STOP].freeze def initialize(options, args) @options = options @@ -72,7 +73,7 @@ module Bundler signals = Signal.list.keys - RESERVED_SIGNALS signals.each {|s| trap(s, "DEFAULT") } Kernel.load(file) - rescue SystemExit + rescue SystemExit, SignalException raise rescue Exception => e # rubocop:disable Lint/RescueException Bundler.ui = ui diff --git a/lib/bundler/cli/gem.rb b/lib/bundler/cli/gem.rb index fed904e9aa..885578e819 100644 --- a/lib/bundler/cli/gem.rb +++ b/lib/bundler/cli/gem.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "pathname" module Bundler @@ -71,10 +72,10 @@ module Bundler "bin/setup.tt" => "bin/setup" } - executables = %w( + executables = %w[ bin/console bin/setup - ) + ] templates.merge!("gitignore.tt" => ".gitignore") if Bundler.git_present? diff --git a/lib/bundler/cli/info.rb b/lib/bundler/cli/info.rb index 4465fba9d4..958b525067 100644 --- a/lib/bundler/cli/info.rb +++ b/lib/bundler/cli/info.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "bundler/cli/common" module Bundler class CLI::Info diff --git a/lib/bundler/cli/init.rb b/lib/bundler/cli/init.rb index 8ffd1db41a..50e01f54fb 100644 --- a/lib/bundler/cli/init.rb +++ b/lib/bundler/cli/init.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class CLI::Init attr_reader :options @@ -7,8 +8,8 @@ module Bundler end def run - if File.exist?("Gemfile") - Bundler.ui.error "Gemfile already exists at #{SharedHelpers.pwd}/Gemfile" + if File.exist?(gemfile) + Bundler.ui.error "#{gemfile} already exists at #{File.expand_path(gemfile)}" exit 1 end @@ -21,14 +22,24 @@ module Bundler spec = Bundler.load_gemspec_uncached(gemspec) - puts "Writing new Gemfile to #{SharedHelpers.pwd}/Gemfile" - File.open("Gemfile", "wb") do |file| + File.open(gemfile, "wb") do |file| file << "# Generated from #{gemspec}\n" file << spec.to_gemfile end else - puts "Writing new Gemfile to #{SharedHelpers.pwd}/Gemfile" - FileUtils.cp(File.expand_path("../../templates/Gemfile", __FILE__), "Gemfile") + FileUtils.cp(File.expand_path("../../templates/#{gemfile}", __FILE__), gemfile) + end + + puts "Writing new #{gemfile} to #{SharedHelpers.pwd}/#{gemfile}" + end + + private + + def gemfile + @gemfile ||= begin + Bundler.default_gemfile + rescue GemfileNotFound + Bundler.feature_flag.init_gems_rb? ? "gems.rb" : "Gemfile" end end end diff --git a/lib/bundler/cli/inject.rb b/lib/bundler/cli/inject.rb index b17292643f..b00675d348 100644 --- a/lib/bundler/cli/inject.rb +++ b/lib/bundler/cli/inject.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class CLI::Inject attr_reader :options, :name, :version, :group, :source, :gems diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb index ff6bedd9fd..f0b821ed84 100644 --- a/lib/bundler/cli/install.rb +++ b/lib/bundler/cli/install.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "bundler/cli/common" module Bundler class CLI::Install @@ -13,17 +12,9 @@ module Bundler warn_if_root - [:with, :without].each do |option| - if options[option] - options[option] = options[option].join(":").tr(" ", ":").split(":") - end - end - - check_for_group_conflicts - normalize_groups - ENV["RB_USER_INSTALL"] = "1" if Bundler::FREEBSD + Bundler::SharedHelpers.set_env "RB_USER_INSTALL", "1" if Bundler::FREEBSD # Disable color in deployment mode Bundler.ui.shell = Thor::Shell::Basic.new if options[:deployment] @@ -32,22 +23,28 @@ module Bundler check_trust_policy - if options[:deployment] || options[:frozen] + if options[:deployment] || options[:frozen] || Bundler.frozen? unless Bundler.default_lockfile.exist? - flag = options[:deployment] ? "--deployment" : "--frozen" - raise ProductionError, "The #{flag} flag requires a #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}. Please make " \ + 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[:frozen] = "1" + if Bundler.feature_flag.deployment_means_frozen? + Bundler.settings.set_command_option :deployment, true + else + Bundler.settings.set_command_option :frozen, true + end end # When install is called with --no-deployment, disable deployment mode if options[:deployment] == false - Bundler.settings.delete(:frozen) + Bundler.settings.set_command_option :frozen, nil options[:system] = true end @@ -56,7 +53,7 @@ module Bundler Bundler::Fetcher.disable_endpoint = options["full-index"] if options["binstubs"] - Bundler::SharedHelpers.major_deprecation \ + Bundler::SharedHelpers.major_deprecation 2, "The --binstubs option will be removed in favor of `bundle binstubs`" end @@ -66,24 +63,24 @@ module Bundler definition.validate_runtime! installer = Installer.install(Bundler.root, definition, options) - Bundler.load.cache if Bundler.app_cache.exist? && !options["no-cache"] && !Bundler.settings[:frozen] + Bundler.load.cache if Bundler.app_cache.exist? && !options["no-cache"] && !Bundler.frozen? Bundler.ui.confirm "Bundle complete! #{dependencies_count_for(definition)}, #{gems_installed_for(definition)}." Bundler::CLI::Common.output_without_groups_message - if Bundler.settings[:path] - absolute_path = File.expand_path(Bundler.settings[:path]) - relative_path = absolute_path.sub(File.expand_path(".") + File::SEPARATOR, "." + File::SEPARATOR) - Bundler.ui.confirm "Bundled gems are installed into #{relative_path}." - else + if Bundler.use_system_gems? Bundler.ui.confirm "Use `bundle info [gemname]` to see where a bundled gem is installed." + else + absolute_path = File.expand_path(Bundler.configured_bundle_path.base_path) + relative_path = absolute_path.sub(File.expand_path(".") + File::SEPARATOR, "." + File::SEPARATOR) + Bundler.ui.confirm "Bundled gems are installed into `#{relative_path}`" end Bundler::CLI::Common.output_post_install_messages installer.post_install_messages warn_ambiguous_gems - if Bundler.settings[:clean] && Bundler.settings[:path] + if CLI::Common.clean_after_install? require "bundler/cli/clean" Bundler::CLI::Clean.new(options).run end @@ -124,15 +121,11 @@ module Bundler "#{count} #{count == 1 ? "gem" : "gems"} now installed" end - def check_for_group_conflicts - if options[:without] && options[:with] - conflicting_groups = options[:without] & options[:with] - unless conflicting_groups.empty? - Bundler.ui.error "You can't list a group in both, --with and --without." \ - " The offending groups are: #{conflicting_groups.join(", ")}." - exit 1 - end - 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 @@ -145,28 +138,29 @@ module Bundler end def check_trust_policy - if options["trust-policy"] - unless Bundler.rubygems.security_policies.keys.include?(options["trust-policy"]) - Bundler.ui.error "Rubygems doesn't know about trust policy '#{options["trust-policy"]}'. " \ - "The known policies are: #{Bundler.rubygems.security_policies.keys.join(", ")}." - exit 1 - end - Bundler.settings["trust-policy"] = options["trust-policy"] - else - Bundler.settings["trust-policy"] = nil if Bundler.settings["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_groups - Bundler.settings.with = [] if options[:with] && options[:with].empty? - Bundler.settings.without = [] if options[:without] && options[:without].empty? + options[:with] &&= options[:with].join(":").tr(" ", ":").split(":") + options[:without] &&= options[:without].join(":").tr(" ", ":").split(":") + + check_for_group_conflicts_in_cli_options - with = options.fetch("with", []) - with |= Bundler.settings.with.map(&:to_s) + Bundler.settings.set_command_option :with, nil if options[:with] == [] + Bundler.settings.set_command_option :without, nil if options[:without] == [] + + with = options.fetch(:with, []) + with |= Bundler.settings[:with].map(&:to_s) with -= options[:without] if options[:without] - without = options.fetch("without", []) - without |= Bundler.settings.without.map(&:to_s) + without = options.fetch(:without, []) + without |= Bundler.settings[:without].map(&:to_s) without -= options[:with] if options[:with] options[:with] = with @@ -174,28 +168,34 @@ module Bundler end def normalize_settings - Bundler.settings[:path] = nil if options[:system] - Bundler.settings[:path] = "vendor/bundle" if options[:deployment] - Bundler.settings[:path] = options["path"] if options["path"] - Bundler.settings[:path] ||= "bundle" if options["standalone"] + Bundler.settings.set_command_option :path, nil if options[:system] + Bundler.settings.set_command_option :path, "vendor/bundle" if options[:deployment] + Bundler.settings.set_command_option_if_given :path, options["path"] + Bundler.settings.set_command_option :path, "bundle" if options["standalone"] && Bundler.settings[:path].nil? - Bundler.settings[:bin] = options["binstubs"] if options["binstubs"] - Bundler.settings[:bin] = nil if options["binstubs"] && options["binstubs"].empty? + 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[:shebang] = options["shebang"] if options["shebang"] + Bundler.settings.set_command_option_if_given :shebang, options["shebang"] - Bundler.settings[:jobs] = options["jobs"] if options["jobs"] + Bundler.settings.set_command_option_if_given :jobs, options["jobs"] - Bundler.settings[:no_prune] = true if options["no-prune"] + Bundler.settings.set_command_option_if_given :no_prune, options["no-prune"] - Bundler.settings[:no_install] = true if options["no-install"] + Bundler.settings.set_command_option_if_given :no_install, options["no-install"] - Bundler.settings[:clean] = options["clean"] if options["clean"] + Bundler.settings.set_command_option_if_given :clean, options["clean"] - Bundler.settings.without = options[:without] - Bundler.settings.with = options[:with] + unless Bundler.settings[:without] == options[:without] && Bundler.settings[:with] == options[:with] + # 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] - options[:with] + Bundler.settings.set_command_option :with, options[:with] + end - Bundler.settings[:disable_shared_gems] = Bundler.settings[:path] ? true : nil + options[:force] = options[:redownload] end def warn_ambiguous_gems diff --git a/lib/bundler/cli/issue.rb b/lib/bundler/cli/issue.rb index ace0f985a9..91f827ea99 100644 --- a/lib/bundler/cli/issue.rb +++ b/lib/bundler/cli/issue.rb @@ -26,7 +26,7 @@ module Bundler EOS - Bundler.ui.info Bundler::Env.new.report + Bundler.ui.info Bundler::Env.report Bundler.ui.info "\n## Bundle Doctor" doctor diff --git a/lib/bundler/cli/list.rb b/lib/bundler/cli/list.rb new file mode 100644 index 0000000000..b5e7c1e650 --- /dev/null +++ b/lib/bundler/cli/list.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Bundler + class CLI::List + def initialize(options) + @options = options + end + + def run + specs = Bundler.load.specs.reject {|s| s.name == "bundler" }.sort_by(&:name) + return specs.each {|s| Bundler.ui.info s.name } if @options["name-only"] + + return Bundler.ui.info "No gems in the Gemfile" if specs.empty? + Bundler.ui.info "Gems included by the bundle:" + specs.each do |s| + Bundler.ui.info " * #{s.name} (#{s.version}#{s.git_version})" + end + + Bundler.ui.info "Use `bundle info` to print more detailed information about a gem" + end + end +end diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb index 223db9419f..7dd078b1ef 100644 --- a/lib/bundler/cli/lock.rb +++ b/lib/bundler/cli/lock.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "bundler/cli/common" module Bundler class CLI::Lock diff --git a/lib/bundler/cli/open.rb b/lib/bundler/cli/open.rb index 9a21f6811c..552fe6f128 100644 --- a/lib/bundler/cli/open.rb +++ b/lib/bundler/cli/open.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require "bundler/cli/common" + require "shellwords" module Bundler @@ -17,7 +17,7 @@ module Bundler path = spec.full_gem_path Dir.chdir(path) do command = Shellwords.split(editor) + [path] - Bundler.with_clean_env do + Bundler.with_original_env do system(*command) end || Bundler.ui.info("Could not run '#{command.join(" ")}'") end diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb index 863d0dd388..5125cc710b 100644 --- a/lib/bundler/cli/outdated.rb +++ b/lib/bundler/cli/outdated.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "bundler/cli/common" module Bundler class CLI::Outdated @@ -46,7 +45,7 @@ module Bundler Bundler::CLI::Common.patch_level_options(options).any? filter_options_patch = options.keys & - %w(filter-major filter-minor filter-patch) + %w[filter-major filter-minor filter-patch] definition_resolution = proc do options[:local] ? definition.resolve_with_cache! : definition.resolve_remotely! @@ -214,13 +213,19 @@ module Bundler end def check_for_deployment_mode - if Bundler.settings[:frozen] - 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 `bundle install --no-deployment`." + return unless Bundler.frozen? + suggested_command = if Bundler.settings.locations("frozen")[:global] + "bundle config --delete frozen" + elsif Bundler.settings.locations("deployment").keys.&([:global, :local]).any? + "bundle config --delete deployment" + else + "bundle install --no-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) diff --git a/lib/bundler/cli/package.rb b/lib/bundler/cli/package.rb index cf65e8a68c..2dcd0e1e29 100644 --- a/lib/bundler/cli/package.rb +++ b/lib/bundler/cli/package.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class CLI::Package attr_reader :options @@ -9,15 +10,15 @@ module Bundler def run Bundler.ui.level = "error" if options[:quiet] - Bundler.settings[:path] = File.expand_path(options[:path]) if options[:path] - Bundler.settings[:cache_all_platforms] = options["all-platforms"] if options.key?("all-platforms") - Bundler.settings[:cache_path] = options["cache-path"] if options.key?("cache-path") + Bundler.settings.set_command_option_if_given :path, options[:path] + Bundler.settings.set_command_option_if_given :cache_all_platforms, options["all-platforms"] + 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 = Pathname.new(options[:path]) if options[:path] + custom_path = Bundler.settings[:path] if options[:path] Bundler.load.cache(custom_path) end @@ -34,9 +35,11 @@ module Bundler end def setup_cache_all - Bundler.settings[:cache_all] = options[:all] if options.key?("all") + all = options.fetch(:all, Bundler.feature_flag.cache_command_is_package? || nil) + + Bundler.settings.set_command_option_if_given :cache_all, all - if Bundler.definition.has_local_dependencies? && !Bundler.settings[:cache_all] + if Bundler.definition.has_local_dependencies? && !Bundler.feature_flag.cache_all? Bundler.ui.warn "Your Gemfile contains path and git dependencies. If you want " \ "to package them as well, please pass the --all flag. This will be the default " \ "on Bundler 2.0." diff --git a/lib/bundler/cli/platform.rb b/lib/bundler/cli/platform.rb index 9fdab0a53c..e97cad49a4 100644 --- a/lib/bundler/cli/platform.rb +++ b/lib/bundler/cli/platform.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class CLI::Platform attr_reader :options diff --git a/lib/bundler/cli/plugin.rb b/lib/bundler/cli/plugin.rb index 277822dafc..5488a9f28d 100644 --- a/lib/bundler/cli/plugin.rb +++ b/lib/bundler/cli/plugin.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "bundler/vendored_thor" module Bundler class CLI::Plugin < Thor diff --git a/lib/bundler/cli/pristine.rb b/lib/bundler/cli/pristine.rb index 10d03b4b41..9b9cdaa9b3 100644 --- a/lib/bundler/cli/pristine.rb +++ b/lib/bundler/cli/pristine.rb @@ -1,15 +1,20 @@ # frozen_string_literal: true -require "bundler/cli/common" 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) 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) gem_name = "#{spec.name} (#{spec.version}#{spec.git_version})" gem_name += " (#{spec.platform})" if !spec.platform.nil? && spec.platform != Gem::Platform::RUBY @@ -21,13 +26,15 @@ module Bundler 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 source.remote! + FileUtils.rm_rf spec.full_gem_path else Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is sourced from local path.") next end - FileUtils.rm_rf spec.full_gem_path Bundler::GemInstaller.new(spec, installer, false, 0, true).install_from_spec end diff --git a/lib/bundler/cli/show.rb b/lib/bundler/cli/show.rb index 47d4470aec..61756801b2 100644 --- a/lib/bundler/cli/show.rb +++ b/lib/bundler/cli/show.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "bundler/cli/common" module Bundler class CLI::Show diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb index df7524f004..5de11e84e4 100644 --- a/lib/bundler/cli/update.rb +++ b/lib/bundler/cli/update.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "bundler/cli/common" module Bundler class CLI::Update @@ -17,7 +16,18 @@ module Bundler sources = Array(options[:source]) groups = Array(options[:group]).map(&:to_sym) - if gems.empty? && sources.empty? && groups.empty? && !options[:ruby] && !options[:bundler] + full_update = gems.empty? && sources.empty? && groups.empty? && !options[:ruby] && !options[:bundler] + + if full_update && !options[:all] + if Bundler.feature_flag.update_requires_all_flag? + raise InvalidOption, "To update everything, pass the `--all` flag." + end + SharedHelpers.major_deprecation 2, "Pass --all to `bundle update` to update everything" + elsif !full_update && options[:all] + raise InvalidOption, "Cannot specify --all along with specific options." + end + + if full_update # We're doing a full update Bundler.definition(true) else @@ -33,7 +43,8 @@ module Bundler end Bundler.definition(:gems => gems, :sources => sources, :ruby => options[:ruby], - :lock_shared_dependencies => options[:conservative]) + :lock_shared_dependencies => options[:conservative], + :bundler => options[:bundler]) end Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options) @@ -44,17 +55,32 @@ module Bundler opts["update"] = true opts["local"] = options[:local] - Bundler.settings[:jobs] = opts["jobs"] if opts["jobs"] + Bundler.settings.set_command_option_if_given :jobs, opts["jobs"] Bundler.definition.validate_runtime! installer = Installer.install Bundler.root, Bundler.definition, opts Bundler.load.cache if Bundler.app_cache.exist? - if Bundler.settings[:clean] && Bundler.settings[:path] + if CLI::Common.clean_after_install? require "bundler/cli/clean" Bundler::CLI::Clean.new(options).run end + if locked_gems = Bundler.definition.locked_gems + gems.each do |name| + locked_version = locked_gems.specs.find {|s| s.name == name }.version + new_version = Bundler.definition.specs[name].first + new_version &&= new_version.version + if !new_version + Bundler.ui.warn "Bundler attempted to update #{name} but it was removed from the bundle" + elsif new_version < locked_version + Bundler.ui.warn "Bundler attempted to update #{name} but its 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 Bundler::CLI::Common.output_post_install_messages installer.post_install_messages diff --git a/lib/bundler/cli/viz.rb b/lib/bundler/cli/viz.rb index 767fe8f3de..644f9b25cf 100644 --- a/lib/bundler/cli/viz.rb +++ b/lib/bundler/cli/viz.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class CLI::Viz attr_reader :options, :gem_name diff --git a/lib/bundler/compact_index_client.rb b/lib/bundler/compact_index_client.rb index 3ed05ca484..6c241ca07a 100644 --- a/lib/bundler/compact_index_client.rb +++ b/lib/bundler/compact_index_client.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "pathname" require "set" diff --git a/lib/bundler/compact_index_client/cache.rb b/lib/bundler/compact_index_client/cache.rb index e44f05dc7e..f6105d3bb3 100644 --- a/lib/bundler/compact_index_client/cache.rb +++ b/lib/bundler/compact_index_client/cache.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "digest/md5" module Bundler class CompactIndexClient @@ -68,7 +67,7 @@ module Bundler def info_path(name) name = name.to_s if name =~ /[^a-z0-9_-]/ - name += "-#{Digest::MD5.hexdigest(name).downcase}" + name += "-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}" info_roots.last.join(name) else info_roots.first.join(name) diff --git a/lib/bundler/compact_index_client/updater.rb b/lib/bundler/compact_index_client/updater.rb index dc26095040..3a4e4441ca 100644 --- a/lib/bundler/compact_index_client/updater.rb +++ b/lib/bundler/compact_index_client/updater.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require "fileutils" + +require "bundler/vendored_fileutils" require "stringio" -require "tmpdir" require "zlib" module Bundler @@ -22,6 +22,7 @@ module Bundler def initialize(fetcher) @fetcher = fetcher + require "tmpdir" end def update(local_path, remote_path, retrying = nil) @@ -98,7 +99,7 @@ module Bundler # because we need to preserve \n line endings on windows when calculating # the checksum SharedHelpers.filesystem_access(path, :read) do - Digest::MD5.hexdigest(IO.read(path)) + SharedHelpers.digest(:MD5).hexdigest(IO.read(path)) end end end diff --git a/lib/bundler/compatibility_guard.rb b/lib/bundler/compatibility_guard.rb new file mode 100644 index 0000000000..750a1db04f --- /dev/null +++ b/lib/bundler/compatibility_guard.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: false + +require "rubygems" +require "bundler/version" + +if Bundler::VERSION.split(".").first.to_i >= 2 + if Gem::Version.new(Object::RUBY_VERSION.dup) < Gem::Version.new("2.3") + abort "Bundler 2 requires Ruby 2.3 or later. Either install bundler 1 or update to a supported Ruby version." + end + + if Gem::Version.new(Gem::VERSION.dup) < Gem::Version.new("2.5") + abort "Bundler 2 requires RubyGems 2.5 or later. Either install bundler 1 or update to a supported RubyGems version." + end +end diff --git a/lib/bundler/constants.rb b/lib/bundler/constants.rb index 5b1c0a8cb1..2e4ebb37ee 100644 --- a/lib/bundler/constants.rb +++ b/lib/bundler/constants.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler WINDOWS = RbConfig::CONFIG["host_os"] =~ /(msdos|mswin|djgpp|mingw)/ FREEBSD = RbConfig::CONFIG["host_os"] =~ /bsd/ diff --git a/lib/bundler/current_ruby.rb b/lib/bundler/current_ruby.rb index cca40100ad..31532d108d 100644 --- a/lib/bundler/current_ruby.rb +++ b/lib/bundler/current_ruby.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler # Returns current version of Ruby # @@ -8,7 +9,7 @@ module Bundler end class CurrentRuby - KNOWN_MINOR_VERSIONS = %w( + KNOWN_MINOR_VERSIONS = %w[ 1.8 1.9 2.0 @@ -17,11 +18,11 @@ module Bundler 2.3 2.4 2.5 - ).freeze + ].freeze KNOWN_MAJOR_VERSIONS = KNOWN_MINOR_VERSIONS.map {|v| v.split(".", 2).first }.uniq.freeze - KNOWN_PLATFORMS = %w( + KNOWN_PLATFORMS = %w[ jruby maglev mingw @@ -31,7 +32,7 @@ module Bundler rbx ruby x64_mingw - ).freeze + ].freeze def ruby? !mswin? && (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby" || RUBY_ENGINE == "rbx" || RUBY_ENGINE == "maglev") diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 3e5b1bc447..f93ed76226 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true + require "bundler/lockfile_parser" -require "digest/sha1" require "set" module Bundler @@ -14,7 +14,9 @@ module Bundler :locked_gems, :platforms, :requires, - :ruby_version + :ruby_version, + :lockfile, + :gemfiles ) # Given a gemfile and lockfile creates a Bundler definition @@ -51,8 +53,16 @@ module Bundler # to be updated or true if all gems should be updated # @param ruby_version [Bundler::RubyVersion, nil] Requested Ruby Version # @param optional_groups [Array(String)] A list of optional groups - def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = []) - @unlocking = unlock == true || !unlock.empty? + def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = [], gemfiles = []) + if [true, false].include?(unlock) + @unlocking_bundler = false + @unlocking = unlock + else + unlock = unlock.dup + @unlocking_bundler = unlock.delete(:bundler) + unlock.delete_if {|_k, v| Array(v).empty? } + @unlocking = !unlock.empty? + end @dependencies = dependencies @sources = sources @@ -61,6 +71,7 @@ module Bundler @remote = false @specs = nil @ruby_version = ruby_version + @gemfiles = gemfiles @lockfile = lockfile @lockfile_contents = String.new @@ -102,7 +113,7 @@ module Bundler end @unlocking ||= @unlock[:ruby] ||= (!@locked_ruby_version ^ !@ruby_version) - add_current_platform unless Bundler.settings[:frozen] + add_current_platform unless Bundler.frozen? converge_path_sources_to_gemspec_sources @path_changes = converge_paths @@ -167,9 +178,8 @@ module Bundler "to a different version of #{locked_gem} that hasn't been removed in order to install." end unless specs["bundler"].any? - local = Bundler.settings[:frozen] ? rubygems_index : index - bundler = local.search(Gem::Dependency.new("bundler", VERSION)).last - specs["bundler"] = bundler if bundler + bundler = sources.metadata_source.specs.search(Gem::Dependency.new("bundler", VERSION)).last + specs["bundler"] = bundler end specs @@ -194,10 +204,19 @@ module Bundler missing end - def missing_dependencies - missing = [] - resolve.materialize(current_dependencies, missing) - missing + def missing_specs? + missing = missing_specs + return false if missing.empty? + Bundler.ui.debug "The definition is missing #{missing.map(&:full_name)}" + true + rescue BundlerError => e + @index = nil + @resolve = nil + @specs = nil + @gem_version_promoter = create_gem_version_promoter + + Bundler.ui.debug "The definition is missing dependencies, failed to resolve & materialize locally (#{e})" + true end def requested_specs @@ -226,7 +245,10 @@ module Bundler def resolve @resolve ||= begin last_resolve = converge_locked_specs - if Bundler.settings[:frozen] || (!unlocking? && nothing_changed?) + if Bundler.frozen? + Bundler.ui.debug "Frozen, using resolution from the lockfile" + last_resolve + elsif !unlocking? && nothing_changed? Bundler.ui.debug("Found no changes, using resolution from the lockfile") last_resolve else @@ -242,25 +264,44 @@ module Bundler dependency_names = @dependencies.map(&:name) sources.all_sources.each do |source| - source.dependency_names = dependency_names.dup + source.dependency_names = dependency_names - pinned_spec_names(source) idx.add_source source.specs - dependency_names -= pinned_spec_names(source.specs) dependency_names.concat(source.unmet_deps).uniq! end - idx << Gem::Specification.new("ruby\0", RubyVersion.system.to_gem_version_with_patchlevel) - idx << Gem::Specification.new("rubygems\0", Gem::VERSION) - end - end - # used when frozen is enabled so we can find the bundler - # spec, even if (say) a git gem is not checked out. - def rubygems_index - @rubygems_index ||= Index.build do |idx| - sources.rubygems_sources.each do |rubygems| - idx.add_source rubygems.specs + double_check_for_index(idx, dependency_names) + end + end + + # Suppose the gem Foo depends on the gem Bar. Foo exists in Source A. Bar has some versions that exist in both + # sources A and B. At this point, the API request will have found all the versions of Bar in source A, + # but will not have found any versions of Bar from source B, which is a problem if the requested version + # of Foo specifically depends on a version of Bar that is only found in source B. This ensures that for + # each spec we found, we add all possible versions from all sources to the index. + def double_check_for_index(idx, dependency_names) + pinned_names = pinned_spec_names + loop do + idxcount = idx.size + + names = :names # do this so we only have to traverse to get dependency_names from the index once + unmet_dependency_names = lambda do + return names unless names == :names + new_names = sources.all_sources.map(&:dependency_names_to_double_check) + return names = nil if new_names.compact! + names = new_names.flatten(1).concat(dependency_names) + names.uniq! + names -= pinned_names + names end + + sources.all_sources.each do |source| + source.double_check_for(unmet_dependency_names, :override_dupes) + end + + break if idxcount == idx.size end end + private :double_check_for_index def has_rubygems_remotes? sources.rubygems_sources.any? {|s| s.remotes.any? } @@ -295,10 +336,10 @@ module Bundler end end - preserve_unknown_sections ||= !updating_major && (Bundler.settings[:frozen] || !unlocking?) + preserve_unknown_sections ||= !updating_major && (Bundler.frozen? || !(unlocking? || @unlocking_bundler)) return if lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections) - if Bundler.settings[:frozen] + if Bundler.frozen? Bundler.ui.error "Cannot write a changed lockfile while frozen." return end @@ -338,51 +379,8 @@ module Bundler end def to_lock - out = String.new - - sources.lock_sources.each do |source| - # Add the source header - out << source.to_lock - # Find all specs for this source - resolve. - select {|s| source.can_lock?(s) }. - # This needs to be sorted by full name so that - # gems with the same name, but different platform - # are ordered consistently - sort_by(&:full_name). - each do |spec| - next if spec.name == "bundler" - out << spec.to_lock - end - out << "\n" - end - - out << "PLATFORMS\n" - - platforms.map(&:to_s).sort.each do |p| - out << " #{p}\n" - end - - out << "\n" - out << "DEPENDENCIES\n" - - handled = [] - dependencies.sort_by(&:to_s).each do |dep| - next if handled.include?(dep.name) - out << dep.to_lock - handled << dep.name - end - - if locked_ruby_version - out << "\nRUBY VERSION\n" - out << " #{locked_ruby_version}\n" - end - - # Record the version of Bundler that was used to create the lockfile - out << "\nBUNDLED WITH\n" - out << " #{locked_bundler_version}\n" - - out + require "bundler/lockfile_generator" + LockfileGenerator.generate(self) end def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false) @@ -392,8 +390,13 @@ module Bundler "updated #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} to version control." unless explicit_flag - - suggested_command = Bundler.settings.locations("frozen")[:global] == "1" ? "bundle config --delete frozen" : "bundle install --no-deployment" + suggested_command = if Bundler.settings.locations("frozen")[:global] + "bundle config --delete frozen" + elsif Bundler.settings.locations("deployment").keys.&([:global, :local]).any? + "bundle config --delete deployment" + else + "bundle install --no-deployment" + end msg << "\n\nIf this is a development machine, remove the #{Bundler.default_gemfile} " \ "freeze \nby running `#{suggested_command}`." end @@ -417,8 +420,8 @@ module Bundler # Check if it is possible that the source is only changed thing if (new_deps.empty? && deleted_deps.empty?) && (!new_sources.empty? && !deleted_sources.empty?) - new_sources.reject! {|source| source.is_a_path? && source.path.exist? } - deleted_sources.reject! {|source| source.is_a_path? && source.path.exist? } + new_sources.reject! {|source| (source.path? && source.path.exist?) || equivalent_rubygems_remotes?(source) } + deleted_sources.reject! {|source| (source.path? && source.path.exist?) || equivalent_rubygems_remotes?(source) } end if @locked_sources != gemfile_sources @@ -511,7 +514,7 @@ module Bundler def add_current_platform current_platform = Bundler.local_platform - add_platform(current_platform) if Bundler.settings[:specific_platform] + add_platform(current_platform) if Bundler.feature_flag.specific_platform? add_platform(generic(current_platform)) end @@ -558,10 +561,7 @@ module Bundler end def pretty_dep(dep, source = false) - msg = String.new(dep.name) - msg << " (#{dep.requirement})" unless dep.requirement == Gem::Requirement.default - msg << " from the `#{dep.source}` source" if source && dep.source - msg + SharedHelpers.pretty_dependency(dep, source) end # Check if the specs of the given source changed @@ -585,6 +585,9 @@ module Bundler # order here matters, since Index#== is checking source.specs.include?(locked_index) locked_index != source.specs + rescue PathError, GitError => e + Bundler.ui.debug "Assuming that #{source} has not changed since fetching its specs errored (#{e})" + false end # Get all locals and override their matching sources. @@ -632,22 +635,32 @@ module Bundler end end - def converge_sources + def converge_rubygems_sources + return false if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + changes = false - # Get the Rubygems sources from the Gemfile.lock + # Get the RubyGems sources from the Gemfile.lock locked_gem_sources = @locked_sources.select {|s| s.is_a?(Source::Rubygems) } - # Get the Rubygems remotes from the Gemfile + # Get the RubyGems remotes from the Gemfile actual_remotes = sources.rubygems_remotes - # If there is a Rubygems source in both + # If there is a RubyGems source in both if !locked_gem_sources.empty? && !actual_remotes.empty? locked_gem_sources.each do |locked_gem| # Merge the remotes from the Gemfile into the Gemfile.lock - changes |= locked_gem.replace_remotes(actual_remotes) + changes |= locked_gem.replace_remotes(actual_remotes, Bundler.settings[:allow_deployment_source_credential_changes]) end end + changes + end + + def converge_sources + changes = false + + changes |= converge_rubygems_sources + # Replace the sources from the Gemfile with the sources from the Gemfile.lock, # if they exist in the Gemfile.lock and are `==`. If you can't find an equivalent # source in the Gemfile.lock, use the one from the Gemfile. @@ -669,7 +682,7 @@ module Bundler end def converge_dependencies - frozen = Bundler.settings[:frozen] + frozen = Bundler.frozen? (@dependencies + @locked_deps.values).each do |dep| locked_source = @locked_deps[dep.name] # This is to make sure that if bundler is installing in deployment mode and @@ -739,6 +752,8 @@ module Bundler end end + unlock_source_unlocks_spec = Bundler.feature_flag.unlock_source_unlocks_spec? + converged = [] @locked_specs.each do |s| # Replace the locked dependency's source with the equivalent source from the Gemfile @@ -746,21 +761,33 @@ module Bundler s.source = (dep && dep.source) || sources.get(s.source) # Don't add a spec to the list if its source is expired. For example, - # if you change a Git gem to Rubygems. + # if you change a Git gem to RubyGems. next if s.source.nil? next if @unlock[:sources].include?(s.source.name) # XXX This is a backwards-compatibility fix to preserve the ability to # unlock a single gem by passing its name via `--source`. See issue #3759 # TODO: delete in Bundler 2 - next if @unlock[:sources].include?(s.name) + next if unlock_source_unlocks_spec && @unlock[:sources].include?(s.name) # If the spec is from a path source and it doesn't exist anymore # then we unlock it. # Path sources have special logic if s.source.instance_of?(Source::Path) || s.source.instance_of?(Source::Gemspec) - other = s.source.specs[s].first + other_sources_specs = begin + s.source.specs + rescue PathError, GitError + # if we won't need the source (according to the lockfile), + # don't error if the path/git source isn't available + next if @locked_specs. + for(requested_dependencies, [], false, true, false). + none? {|locked_spec| locked_spec.source == s.source } + + raise + end + + other = other_sources_specs[s].first # If the spec is no longer in the path source, unlock it. This # commonly happens if the version changed in the gemspec @@ -807,17 +834,21 @@ module Bundler # the metadata dependencies here def expanded_dependencies @expanded_dependencies ||= begin + expand_dependencies(dependencies + metadata_dependencies, @remote) + end + end + + def metadata_dependencies + @metadata_dependencies ||= begin ruby_versions = concat_ruby_version_requirements(@ruby_version) if ruby_versions.empty? || !@ruby_version.exact? concat_ruby_version_requirements(RubyVersion.system) concat_ruby_version_requirements(locked_ruby_version_object) unless @unlock[:ruby] end - - metadata_dependencies = [ + [ Dependency.new("ruby\0", ruby_versions), Dependency.new("rubygems\0", Gem::VERSION), ] - expand_dependencies(dependencies + metadata_dependencies, @remote) end end @@ -838,11 +869,12 @@ module Bundler end def expand_dependencies(dependencies, remote = false) + sorted_platforms = Resolver.sort_platforms(@platforms) deps = [] dependencies.each do |dep| dep = Dependency.new(dep, ">= 0") unless dep.respond_to?(:name) next if !remote && !dep.current_platform? - platforms = dep.gem_platforms(@platforms) + platforms = dep.gem_platforms(sorted_platforms) if platforms.empty? mapped_platforms = dep.platforms.map {|p| Dependency::PLATFORM_MAP[p] } Bundler.ui.warn \ @@ -872,30 +904,33 @@ module Bundler # Record the specs available in each gem's source, so that those # specs will be available later when the resolver knows where to # look for that gemspec (or its dependencies) - source_requirements = {} + default = sources.default_source + source_requirements = { :default => default } + default = nil unless Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? dependencies.each do |dep| - next unless dep.source - source_requirements[dep.name] = dep.source.specs + next unless source = dep.source || default + source_requirements[dep.name] = source end + metadata_dependencies.each do |dep| + source_requirements[dep.name] = sources.metadata_source + end + source_requirements["bundler"] = sources.metadata_source # needs to come last to override source_requirements end - def pinned_spec_names(specs) - names = [] - specs.each do |s| - # TODO: when two sources without blocks is an error, we can change - # this check to !s.source.is_a?(Source::LocalRubygems). For now, - # we need to ask every Rubygems for every gem name. - if s.source.is_a?(Source::Git) || s.source.is_a?(Source::Path) - names << s.name - end + def pinned_spec_names(skip = nil) + pinned_names = [] + default = Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? && sources.default_source + @dependencies.each do |dep| + next unless dep_source = dep.source || default + next if dep_source == skip + pinned_names << dep.name end - names.uniq! - names + pinned_names end def requested_groups - groups - Bundler.settings.without - @optional_groups + Bundler.settings.with + groups - Bundler.settings[:without] - @optional_groups + Bundler.settings[:with] end def lockfiles_equal?(current, proposed, preserve_unknown_sections) @@ -930,11 +965,20 @@ module Bundler def additional_base_requirements_for_resolve return [] unless @locked_gems && Bundler.feature_flag.only_update_to_newer_versions? + dependencies_by_name = dependencies.group_by(&:name) @locked_gems.specs.reduce({}) do |requirements, locked_spec| - dep = Gem::Dependency.new(locked_spec.name, ">= #{locked_spec.version}") - requirements[locked_spec.name] = DepProxy.new(dep, locked_spec.platform) + name = locked_spec.name + next requirements if @locked_deps[name] != dependencies_by_name[name] + dep = Gem::Dependency.new(name, ">= #{locked_spec.version}") + requirements[name] = DepProxy.new(dep, locked_spec.platform) requirements end.values end + + def equivalent_rubygems_remotes?(source) + return false unless source.is_a?(Source::Rubygems) + + Bundler.settings[:allow_deployment_source_credential_changes] && source.equivalent_remotes?(sources.rubygems_remotes) + end end end diff --git a/lib/bundler/dep_proxy.rb b/lib/bundler/dep_proxy.rb index 998975bbaf..7a9423b14a 100644 --- a/lib/bundler/dep_proxy.rb +++ b/lib/bundler/dep_proxy.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class DepProxy attr_reader :__platform, :dep @@ -13,6 +14,7 @@ module Bundler end def ==(other) + return if other.nil? dep == other.dep && __platform == other.__platform end diff --git a/lib/bundler/dependency.rb b/lib/bundler/dependency.rb index d2bac66cdb..24257bc113 100644 --- a/lib/bundler/dependency.rb +++ b/lib/bundler/dependency.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "rubygems/dependency" require "bundler/shared_helpers" require "bundler/rubygems_ext" @@ -90,16 +91,14 @@ module Bundler @autorequire = Array(options["require"] || []) if options.key?("require") end + # Returns the platforms this dependency is valid for, in the same order as + # passed in the `valid_platforms` parameter def gem_platforms(valid_platforms) return valid_platforms if @platforms.empty? - platforms = [] - @platforms.each do |p| - platform = PLATFORM_MAP[p] - next unless valid_platforms.include?(platform) - platforms |= [platform] - end - platforms + @gem_platforms ||= @platforms.map {|pl| PLATFORM_MAP[pl] }.compact.uniq + + valid_platforms & @gem_platforms end def should_include? diff --git a/lib/bundler/deployment.rb b/lib/bundler/deployment.rb index 94f2fac620..291e158ca0 100644 --- a/lib/bundler/deployment.rb +++ b/lib/bundler/deployment.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "bundler/shared_helpers" -Bundler::SharedHelpers.major_deprecation "Bundler no longer integrates with " \ +Bundler::SharedHelpers.major_deprecation 2, "Bundler no longer integrates with " \ "Capistrano, but Capistrano provides its own integration with " \ "Bundler via the capistrano-bundler gem. Use it instead." diff --git a/lib/bundler/deprecate.rb b/lib/bundler/deprecate.rb index b978c0df6c..387f632a39 100644 --- a/lib/bundler/deprecate.rb +++ b/lib/bundler/deprecate.rb @@ -1,11 +1,22 @@ # frozen_string_literal: true + +begin + require "rubygems/deprecate" +rescue LoadError + # it's fine if it doesn't exist on the current RubyGems... + nil +end + module Bundler - if defined? ::Deprecate + if defined? Bundler::Deprecate + # nothing to do! + elsif defined? ::Deprecate Deprecate = ::Deprecate elsif defined? Gem::Deprecate Deprecate = Gem::Deprecate else - class Deprecate; end + class Deprecate + end end unless Deprecate.respond_to?(:skip_during) @@ -20,7 +31,7 @@ module Bundler unless Deprecate.respond_to?(:skip) def Deprecate.skip - @skip + @skip ||= false end end diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index e4c257d267..8681163277 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "bundler/dependency" require "bundler/ruby_dsl" @@ -14,6 +15,9 @@ module Bundler VALID_PLATFORMS = Bundler::Dependency::PLATFORM_MAP.keys.freeze + VALID_KEYS = %w[group groups git path glob name branch ref tag require submodules + platform platforms type source install_if].freeze + attr_reader :gemspecs attr_accessor :dependencies @@ -30,14 +34,16 @@ module Bundler @ruby_version = nil @gemspecs = [] @gemfile = nil + @gemfiles = [] add_git_sources end def eval_gemfile(gemfile, contents = nil) - expanded_gemfile_path = Pathname.new(gemfile).expand_path + expanded_gemfile_path = Pathname.new(gemfile).expand_path(@gemfile && @gemfile.parent) original_gemfile = @gemfile @gemfile = expanded_gemfile_path - contents ||= Bundler.read_file(gemfile.to_s) + @gemfiles << expanded_gemfile_path + contents ||= Bundler.read_file(@gemfile.to_s) instance_eval(contents.dup.untaint, gemfile.to_s, 1) rescue Exception => e message = "There was an error " \ @@ -95,10 +101,10 @@ module Bundler # if there's already a dependency with this name we try to prefer one if current = @dependencies.find {|d| d.name == dep.name } + deleted_dep = @dependencies.delete(current) if current.type == :development + if current.requirement != dep.requirement - if current.type == :development - @dependencies.delete current - else + unless deleted_dep return if dep.type == :development raise GemfileError, "You cannot specify the same gem twice with different version requirements.\n" \ "You specified: #{current.name} (#{current.requirement}) and #{dep.name} (#{dep.requirement})" @@ -111,9 +117,7 @@ module Bundler end if current.source != dep.source - if current.type == :development - @dependencies.delete current - else + unless deleted_dep return if dep.type == :development raise GemfileError, "You cannot specify the same gem twice coming from different sources.\n" \ "You specified that #{dep.name} (#{dep.requirement}) should come from " \ @@ -128,10 +132,12 @@ module Bundler def source(source, *args, &blk) options = args.last.is_a?(Hash) ? args.pop.dup : {} options = normalize_hash(options) + source = normalize_source(source) + if options.key?("type") options["type"] = options["type"].to_s unless Plugin.source?(options["type"]) - raise "No sources available for #{options["type"]}" + raise InvalidOption, "No plugin sources available for #{options["type"]}" end unless block_given? @@ -141,12 +147,10 @@ module Bundler source_opts = options.merge("uri" => source) with_source(@sources.add_plugin_source(options["type"], source_opts), &blk) elsif block_given? - source = normalize_source(source) with_source(@sources.add_rubygems_source("remotes" => source), &blk) else - source = normalize_source(source) check_primary_source_safety(@sources) - @sources.add_rubygems_remote(source) + @sources.global_rubygems_source = source end end @@ -164,6 +168,19 @@ module Bundler end def path(path, options = {}, &blk) + unless block_given? + msg = "You can no longer specify a path source by itself. Instead, \n" \ + "either use the :path option on a gem, or specify the gems that \n" \ + "bundler should find in the path source by passing a block to \n" \ + "the path method, like: \n\n" \ + " path 'dir/containing/rails' do\n" \ + " gem 'rails'\n" \ + " end\n\n" + + raise DeprecatedError, msg if Bundler.feature_flag.disable_multisource? + SharedHelpers.major_deprecation(2, msg.strip) + end + source_options = normalize_hash(options).merge( "path" => Pathname.new(path), "root_path" => gemfile_root, @@ -190,6 +207,7 @@ module Bundler def github(repo, options = {}) raise ArgumentError, "GitHub sources require a block" unless block_given? + raise DeprecatedError, "The #github method has been removed" if Bundler.feature_flag.skip_default_git_sources? github_uri = @git_sources["github"].call(repo) git_options = normalize_hash(options).merge("uri" => github_uri) git_source = @sources.add_git_source(git_options) @@ -197,16 +215,16 @@ module Bundler end def to_definition(lockfile, unlock) - Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups) + Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles) end def group(*args, &blk) - opts = Hash === args.last ? args.pop.dup : {} - normalize_group_options(opts, args) + options = args.last.is_a?(Hash) ? args.pop.dup : {} + normalize_group_options(options, args) @groups.concat args - if opts["optional"] + if options["optional"] optional_groups = args - @optional_groups @optional_groups.concat optional_groups end @@ -216,9 +234,9 @@ module Bundler args.each { @groups.pop } end - def install_if(*args, &blk) + def install_if(*args) @install_conditionals.concat args - blk.call + yield ensure args.each { @install_conditionals.pop } end @@ -250,7 +268,12 @@ module Bundler private def add_git_sources + return if Bundler.feature_flag.skip_default_git_sources? + git_source(:github) do |repo_name| + warn_deprecated_git_source(:github, <<-'RUBY'.strip, 'Change any "reponame" :github sources to "username/reponame".') +"https://github.com/#{repo_name}.git" + RUBY # It would be better to use https instead of the git protocol, but this # can break deployment of existing locked bundles when switching between # different versions of Bundler. The change will be made in 2.0, which @@ -267,23 +290,29 @@ module Bundler repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") # TODO: 2.0 upgrade this setting to the default if Bundler.settings["github.https"] + Bundler::SharedHelpers.major_deprecation 2, "The `github.https` setting will be removed" "https://github.com/#{repo_name}.git" else - warn_github_source_change(repo_name) "git://github.com/#{repo_name}.git" end end # TODO: 2.0 remove this deprecated git source git_source(:gist) do |repo_name| - warn_deprecated_git_source(:gist, 'https://gist.github.com/#{repo_name}.git') + warn_deprecated_git_source(:gist, '"https://gist.github.com/#{repo_name}.git"') + "https://gist.github.com/#{repo_name}.git" end # TODO: 2.0 remove this deprecated git source git_source(:bitbucket) do |repo_name| - user_name, repo_name = repo_name.split "/" - warn_deprecated_git_source(:bitbucket, 'https://#{user_name}@bitbucket.org/#{user_name}/#{repo_name}.git') + warn_deprecated_git_source(:bitbucket, <<-'RUBY'.strip) +user_name, repo_name = repo_name.split("/") +repo_name ||= user_name +"https://#{user_name}@bitbucket.org/#{user_name}/#{repo_name}.git" + RUBY + + user_name, repo_name = repo_name.split("/") repo_name ||= user_name "https://#{user_name}@bitbucket.org/#{user_name}/#{repo_name}.git" end @@ -308,7 +337,7 @@ module Bundler end def valid_keys - @valid_keys ||= %w(group groups git path glob name branch ref tag require submodules platform platforms type source install_if) + @valid_keys ||= VALID_KEYS end def normalize_options(name, version, opts) @@ -318,6 +347,9 @@ module Bundler if name =~ /\s/ raise GemfileError, %('#{name}' is not a valid gem name because it contains whitespace) end + if name.empty? + raise GemfileError, %(an empty gem name is not valid) + end normalize_hash(opts) @@ -355,7 +387,7 @@ module Bundler opts["git"] = @git_sources[git_name].call(opts[git_name]) end - %w(git path).each do |type| + %w[git path].each do |type| next unless param = opts[type] if version.first && version.first =~ /^\s*=?\s*(\d[^\s]*)\s*$/ options = opts.merge("name" => name, "version" => $1) @@ -366,8 +398,8 @@ module Bundler opts["source"] = source end - opts["source"] ||= @source - opts["env"] ||= @env + opts["source"] ||= @source + opts["env"] ||= @env opts["platforms"] = platforms.dup opts["group"] = groups opts["should_include"] = install_if @@ -377,7 +409,7 @@ module Bundler normalize_hash(opts) groups = groups.map {|group| ":#{group}" }.join(", ") - validate_keys("group #{groups}", opts, %w(optional)) + validate_keys("group #{groups}", opts, %w[optional]) opts["optional"] ||= false end @@ -390,25 +422,25 @@ module Bundler raise GemfileError, %(The `branch` option for `#{command}` is not allowed. Only gems with a git source can specify a branch) end - if invalid_keys.any? - message = String.new - message << "You passed #{invalid_keys.map {|k| ":" + k }.join(", ")} " - message << if invalid_keys.size > 1 - "as options for #{command}, but they are invalid." - else - "as an option for #{command}, but it is invalid." - end - - message << " Valid options are: #{valid_keys.join(", ")}." - message << " You may be able to resolve this by upgrading Bundler to the newest version." - raise InvalidOption, message - end + return true unless invalid_keys.any? + + message = String.new + message << "You passed #{invalid_keys.map {|k| ":" + k }.join(", ")} " + message << if invalid_keys.size > 1 + "as options for #{command}, but they are invalid." + else + "as an option for #{command}, but it is invalid." + end + + message << " Valid options are: #{valid_keys.join(", ")}." + message << " You may be able to resolve this by upgrading Bundler to the newest version." + raise InvalidOption, message end def normalize_source(source) case source when :gemcutter, :rubygems, :rubyforge - Bundler::SharedHelpers.major_deprecation "The source :#{source} is deprecated because HTTP " \ + Bundler::SharedHelpers.major_deprecation 2, "The source :#{source} is deprecated because HTTP " \ "requests are insecure.\nPlease change your source to 'https://" \ "rubygems.org' if possible, or 'http://rubygems.org' if not." "http://rubygems.org" @@ -419,17 +451,20 @@ module Bundler end end - def check_primary_source_safety(source) - return unless source.rubygems_primary_remotes.any? + def check_primary_source_safety(source_list) + return if source_list.rubygems_primary_remotes.empty? && source_list.global_rubygems_source.nil? - # TODO: 2.0 upgrade from setting to default - if Bundler.settings[:disable_multisource] - raise GemfileError, "Warning: this Gemfile contains multiple primary sources. " \ + if Bundler.feature_flag.disable_multisource? + msg = "This Gemfile contains multiple primary sources. " \ "Each source after the first must include a block to indicate which gems " \ - "should come from that source. To downgrade this error to a warning, run " \ - "`bundle config --delete disable_multisource`" + "should come from that source" + unless Bundler.feature_flag.bundler_2_mode? + msg += ". To downgrade this error to a warning, run " \ + "`bundle config --delete disable_multisource`" + end + raise GemfileEvalError, msg else - Bundler::SharedHelpers.major_deprecation "Your Gemfile contains multiple primary sources. " \ + Bundler::SharedHelpers.major_deprecation 2, "Your Gemfile contains multiple primary sources. " \ "Using `source` more than once without a block is a security risk, and " \ "may result in installing unexpected gems. To resolve this warning, use " \ "a block to indicate which gems should come from the secondary source. " \ @@ -438,20 +473,20 @@ module Bundler end end - def warn_github_source_change(repo_name) + def warn_deprecated_git_source(name, replacement, additional_message = nil) # TODO: 2.0 remove deprecation - Bundler::SharedHelpers.major_deprecation "The :github option uses the git: protocol, which is not secure. " \ - "Bundler 2.0 will use the https: protocol, which is secure. Enable this change now by " \ - "running `bundle config github.https true`." - end + additional_message &&= " #{additional_message}" + replacement = if replacement.count("\n").zero? + "{|repo_name| #{replacement} }" + else + "do |repo_name|\n#{replacement.to_s.gsub(/^/, " ")}\n end" + end + + Bundler::SharedHelpers.major_deprecation 2, <<-EOS +The :#{name} git source is deprecated, and will be removed in Bundler 2.0.#{additional_message} Add this code to the top of your Gemfile to ensure it continues to work: + + git_source(:#{name}) #{replacement} - def warn_deprecated_git_source(name, repo_string) - # TODO: 2.0 remove deprecation - Bundler::SharedHelpers.major_deprecation <<-EOS -The :#{name} git source is deprecated, and will be removed in Bundler 2.0. Add this code to your Gemfile to ensure it continues to work: - git_source(:#{name}) do |repo_name| - "#{repo_string}" - end EOS end @@ -530,7 +565,7 @@ The :#{name} git source is deprecated, and will be removed in Bundler 2.0. Add t lines = contents.lines.to_a indent = " # " indicator = indent.tr("#", ">") - first_line = (line_numer.zero?) + first_line = line_numer.zero? last_line = (line_numer == (lines.count - 1)) m << "\n" diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb index 5a1deeea47..8668c4ea7f 100644 --- a/lib/bundler/endpoint_specification.rb +++ b/lib/bundler/endpoint_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler # used for Creating Specifications from the Gemcutter Endpoint class EndpointSpecification < Gem::Specification @@ -9,11 +10,15 @@ module Bundler attr_accessor :source, :remote, :dependencies def initialize(name, version, platform, dependencies, metadata = nil) + super() @name = name @version = Gem::Version.create version @platform = platform @dependencies = dependencies.map {|dep, reqs| build_dependency(dep, reqs) } + @loaded_from = nil + @remote_specification = nil + parse_metadata(metadata) end @@ -71,6 +76,8 @@ module Bundler @remote_specification.post_install_message elsif _local_specification _local_specification.post_install_message + else + super end end @@ -80,6 +87,8 @@ module Bundler @remote_specification.extensions elsif _local_specification _local_specification.extensions + else + super end end diff --git a/lib/bundler/env.rb b/lib/bundler/env.rb index 8b990baf40..58fe20dbe7 100644 --- a/lib/bundler/env.rb +++ b/lib/bundler/env.rb @@ -1,33 +1,21 @@ # frozen_string_literal: true + require "bundler/rubygems_integration" require "bundler/source/git/git_proxy" module Bundler class Env - def write(io) + def self.write(io) io.write report end - def report(options = {}) + def self.report(options = {}) print_gemfile = options.delete(:print_gemfile) { true } print_gemspecs = options.delete(:print_gemspecs) { true } - out = String.new("## Environment\n\n```\n") - out << "Bundler #{Bundler::VERSION}\n" - out << "Rubygems #{Gem::VERSION}\n" - out << "Ruby #{ruby_version}" - out << "GEM_HOME #{ENV["GEM_HOME"]}\n" unless ENV["GEM_HOME"].nil? || ENV["GEM_HOME"].empty? - out << "GEM_PATH #{ENV["GEM_PATH"]}\n" unless ENV["GEM_PATH"] == ENV["GEM_HOME"] - out << "RVM #{ENV["rvm_version"]}\n" if ENV["rvm_version"] - out << "Git #{git_version}\n" - out << "Platform #{Gem::Platform.local}\n" - out << "OpenSSL #{OpenSSL::OPENSSL_VERSION}\n" if defined?(OpenSSL::OPENSSL_VERSION) - %w(rubygems-bundler open_gem).each do |name| - specs = Bundler.rubygems.find_name(name) - out << "#{name} (#{specs.map(&:version).join(",")})\n" unless specs.empty? - end - - out << "```\n" + out = String.new + append_formatted_table("Environment", environment, out) + append_formatted_table("Bundler Build Metadata", BuildMetadata.to_h, out) unless Bundler.settings.all.empty? out << "\n## Bundler settings\n\n```\n" @@ -43,9 +31,18 @@ module Bundler return out unless SharedHelpers.in_bundle? if print_gemfile + gemfiles = [Bundler.default_gemfile] + begin + gemfiles = Bundler.definition.gemfiles + rescue GemfileNotFound + nil + end + out << "\n## Gemfile\n" - out << "\n### #{Bundler.default_gemfile.relative_path_from(SharedHelpers.pwd)}\n\n" - out << "```ruby\n" << read_file(Bundler.default_gemfile).chomp << "\n```\n" + gemfiles.each do |gemfile| + out << "\n### #{Pathname.new(gemfile).relative_path_from(SharedHelpers.pwd)}\n\n" + out << "```ruby\n" << read_file(gemfile).chomp << "\n```\n" + end out << "\n### #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}\n\n" out << "```\n" << read_file(Bundler.default_lockfile).chomp << "\n```\n" @@ -63,9 +60,7 @@ module Bundler out end - private - - def read_file(filename) + def self.read_file(filename) File.read(filename.to_s).strip rescue Errno::ENOENT "<No #{filename} found>" @@ -73,22 +68,86 @@ module Bundler "#{e.class}: #{e.message}" end - def ruby_version + def self.ruby_version str = String.new("#{RUBY_VERSION}") if RUBY_VERSION < "1.9" str << " (#{RUBY_RELEASE_DATE}" str << " patchlevel #{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL - str << ") [#{RUBY_PLATFORM}]\n" + str << ") [#{RUBY_PLATFORM}]" else str << "p#{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL - str << " (#{RUBY_RELEASE_DATE} revision #{RUBY_REVISION}) [#{RUBY_PLATFORM}]\n" + str << " (#{RUBY_RELEASE_DATE} revision #{RUBY_REVISION}) [#{RUBY_PLATFORM}]" end end - def git_version + def self.git_version Bundler::Source::Git::GitProxy.new(nil, nil, nil).full_version rescue Bundler::Source::Git::GitNotInstalledError "not installed" end + + def self.version_of(script) + return "not installed" unless Bundler.which(script) + `#{script} --version` + end + + def self.chruby_version + return "not installed" unless Bundler.which("chruby-exec") + `chruby-exec -- chruby --version`. + sub(/.*^chruby: (#{Gem::Version::VERSION_PATTERN}).*/m, '\1') + end + + def self.environment + out = [] + + out << ["Bundler", Bundler::VERSION] + out << [" Platforms", Gem.platforms.join(", ")] + out << ["Ruby", ruby_version] + out << [" Full Path", Gem.ruby] + out << [" Config Dir", Pathname.new(Gem::ConfigFile::SYSTEM_WIDE_CONFIG_FILE).dirname] + out << ["RubyGems", Gem::VERSION] + out << [" Gem Home", ENV.fetch("GEM_HOME") { Gem.dir }] + out << [" Gem Path", ENV.fetch("GEM_PATH") { Gem.path.join(File::PATH_SEPARATOR) }] + out << [" User Path", Gem.user_dir] + out << [" Bin Dir", Gem.bindir] + out << ["OpenSSL"] if defined?(OpenSSL) + out << [" Compiled", OpenSSL::OPENSSL_VERSION] if defined?(OpenSSL::OPENSSL_VERSION) + out << [" Loaded", OpenSSL::OPENSSL_LIBRARY_VERSION] if defined?(OpenSSL::OPENSSL_LIBRARY_VERSION) + out << [" Cert File", OpenSSL::X509::DEFAULT_CERT_FILE] if defined?(OpenSSL::X509::DEFAULT_CERT_FILE) + out << [" Cert Dir", OpenSSL::X509::DEFAULT_CERT_DIR] if defined?(OpenSSL::X509::DEFAULT_CERT_DIR) + out << ["Tools"] + out << [" Git", git_version] + out << [" RVM", ENV.fetch("rvm_version") { version_of("rvm") }] + out << [" rbenv", version_of("rbenv")] + out << [" chruby", chruby_version] + + %w[rubygems-bundler open_gem].each do |name| + specs = Bundler.rubygems.find_name(name) + out << [" #{name}", "(#{specs.map(&:version).join(",")})"] unless specs.empty? + end + if (exe = caller.last.split(":").first) && exe =~ %r{(exe|bin)/bundler?\z} + shebang = File.read(exe).lines.first + shebang.sub!(/^#!\s*/, "") + unless shebang.start_with?(Gem.ruby, "/usr/bin/env ruby") + out << ["Gem.ruby", Gem.ruby] + out << ["bundle #!", shebang] + end + end + + out + end + + def self.append_formatted_table(title, pairs, out) + return if pairs.empty? + out << "\n" unless out.empty? + out << "## #{title}\n\n```\n" + ljust = pairs.map {|k, _v| k.to_s.length }.max + pairs.each do |k, v| + out << "#{k.to_s.ljust(ljust)} #{v}\n" + end + out << "```\n" + end + + private_class_method :read_file, :ruby_version, :git_version, :append_formatted_table, :version_of, :chruby_version end end diff --git a/lib/bundler/environment_preserver.rb b/lib/bundler/environment_preserver.rb index a891f4854d..af7c1ef0a4 100644 --- a/lib/bundler/environment_preserver.rb +++ b/lib/bundler/environment_preserver.rb @@ -1,12 +1,29 @@ # frozen_string_literal: true + module Bundler class EnvironmentPreserver + INTENTIONALLY_NIL = "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL".freeze + BUNDLER_KEYS = %w[ + BUNDLE_BIN_PATH + BUNDLE_GEMFILE + BUNDLER_ORIG_MANPATH + BUNDLER_VERSION + GEM_HOME + GEM_PATH + MANPATH + PATH + RB_USER_INSTALL + RUBYLIB + RUBYOPT + ].map(&:freeze).freeze + BUNDLER_PREFIX = "BUNDLER_ORIG_".freeze + # @param env [ENV] # @param keys [Array<String>] def initialize(env, keys) @original = env.to_hash @keys = keys - @prefix = "BUNDLER_ORIG_" + @prefix = BUNDLER_PREFIX end # @return [Hash] @@ -14,9 +31,10 @@ module Bundler env = @original.clone @keys.each do |key| value = env[key] - original_value = env[@prefix + key] - if !value.nil? && !value.empty? && original_value.nil? - env[@prefix + key] = value + if !value.nil? && !value.empty? + env[@prefix + key] ||= value + elsif value.nil? + env[@prefix + key] ||= INTENTIONALLY_NIL end end env @@ -27,10 +45,13 @@ module Bundler env = @original.clone @keys.each do |key| value_original = env[@prefix + key] - unless value_original.nil? || value_original.empty? + next if value_original.nil? || value_original.empty? + if value_original == INTENTIONALLY_NIL + env.delete(key) + else env[key] = value_original - env.delete(@prefix + key) end + env.delete(@prefix + key) end env end diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb index 6ce8493ea7..e471bce0b6 100644 --- a/lib/bundler/errors.rb +++ b/lib/bundler/errors.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class BundlerError < StandardError def self.status_code(code) diff --git a/lib/bundler/feature_flag.rb b/lib/bundler/feature_flag.rb index 150cac1e67..6a1809cd40 100644 --- a/lib/bundler/feature_flag.rb +++ b/lib/bundler/feature_flag.rb @@ -1,22 +1,59 @@ # frozen_string_literal: true + module Bundler class FeatureFlag def self.settings_flag(flag, &default) unless Bundler::Settings::BOOL_KEYS.include?(flag.to_s) raise "Cannot use `#{flag}` as a settings feature flag since it isn't a bool key" end - define_method("#{flag}?") do - value = Bundler.settings[flag] + + settings_method("#{flag}?", flag, &default) + end + private_class_method :settings_flag + + def self.settings_option(key, &default) + settings_method(key, key, &default) + end + private_class_method :settings_option + + def self.settings_method(name, key, &default) + define_method(name) do + value = Bundler.settings[key] value = instance_eval(&default) if value.nil? && !default.nil? value end end + private_class_method :settings_method (1..10).each {|v| define_method("bundler_#{v}_mode?") { major_version >= v } } + settings_flag(:allow_bundler_dependency_conflicts) { bundler_2_mode? } settings_flag(:allow_offline_install) { bundler_2_mode? } + settings_flag(:auto_clean_without_path) { bundler_2_mode? } + settings_flag(:cache_all) { bundler_2_mode? } + settings_flag(:cache_command_is_package) { bundler_2_mode? } + settings_flag(:console_command) { !bundler_2_mode? } + settings_flag(:default_install_uses_path) { bundler_2_mode? } + settings_flag(:deployment_means_frozen) { bundler_2_mode? } + settings_flag(:disable_multisource) { bundler_2_mode? } + settings_flag(:error_on_stderr) { bundler_2_mode? } + settings_flag(:forget_cli_options) { bundler_2_mode? } + settings_flag(:global_gem_cache) { bundler_2_mode? } + settings_flag(:init_gems_rb) { bundler_2_mode? } + settings_flag(:list_command) { bundler_2_mode? } + settings_flag(:lockfile_uses_separate_rubygems_sources) { bundler_2_mode? } settings_flag(:only_update_to_newer_versions) { bundler_2_mode? } settings_flag(:plugins) { @bundler_version >= Gem::Version.new("1.14") } + settings_flag(:prefer_gems_rb) { bundler_2_mode? } + settings_flag(:print_only_version_number) { bundler_2_mode? } + settings_flag(:setup_makes_kernel_gem_public) { !bundler_2_mode? } + settings_flag(:skip_default_git_sources) { bundler_2_mode? } + settings_flag(:specific_platform) { bundler_2_mode? } + settings_flag(:suppress_install_using_messages) { bundler_2_mode? } + settings_flag(:unlock_source_unlocks_spec) { !bundler_2_mode? } + settings_flag(:update_requires_all_flag) { bundler_2_mode? } + + settings_option(:default_cli_command) { bundler_2_mode? ? :cli_help : :install } def initialize(bundler_version) @bundler_version = Gem::Version.create(bundler_version) @@ -26,7 +63,5 @@ module Bundler @bundler_version.segments.first end private :major_version - - class << self; private :settings_flag; end end end diff --git a/lib/bundler/fetcher.rb b/lib/bundler/fetcher.rb index 9e208e4957..03ff528826 100644 --- a/lib/bundler/fetcher.rb +++ b/lib/bundler/fetcher.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "bundler/vendored_persistent" require "cgi" require "securerandom" @@ -237,7 +238,7 @@ module Bundler Bundler.settings[:ssl_client_cert] raise SSLError if needs_ssl && !defined?(OpenSSL::SSL) - con = Bundler::Persistent::Net::HTTP::Persistent.new "bundler", :ENV + con = PersistentHTTP.new "bundler", :ENV if gem_proxy = Bundler.rubygems.configuration[:http_proxy] con.proxy = URI.parse(gem_proxy) if gem_proxy != :no_proxy end @@ -248,8 +249,11 @@ module Bundler con.cert_store = bundler_cert_store end - if Bundler.settings[:ssl_client_cert] - pem = File.read(Bundler.settings[:ssl_client_cert]) + ssl_client_cert = Bundler.settings[:ssl_client_cert] || + (Bundler.rubygems.configuration.ssl_client_cert if + Bundler.rubygems.configuration.respond_to?(:ssl_client_cert)) + if ssl_client_cert + pem = File.read(ssl_client_cert) con.cert = OpenSSL::X509::Certificate.new(pem) con.key = OpenSSL::PKey::RSA.new(pem) end @@ -273,16 +277,19 @@ module Bundler Timeout::Error, EOFError, SocketError, Errno::ENETDOWN, Errno::ENETUNREACH, Errno::EINVAL, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EAGAIN, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, - Bundler::Persistent::Net::HTTP::Persistent::Error, Zlib::BufError, Errno::EHOSTUNREACH + PersistentHTTP::Error, Zlib::BufError, Errno::EHOSTUNREACH ].freeze def bundler_cert_store store = OpenSSL::X509::Store.new - if Bundler.settings[:ssl_ca_cert] - if File.directory? Bundler.settings[:ssl_ca_cert] - store.add_path Bundler.settings[:ssl_ca_cert] + ssl_ca_cert = Bundler.settings[:ssl_ca_cert] || + (Bundler.rubygems.configuration.ssl_ca_cert if + Bundler.rubygems.configuration.respond_to?(:ssl_ca_cert)) + if ssl_ca_cert + if File.directory? ssl_ca_cert + store.add_path ssl_ca_cert else - store.add_file Bundler.settings[:ssl_ca_cert] + store.add_file ssl_ca_cert end else store.set_default_paths diff --git a/lib/bundler/fetcher/base.rb b/lib/bundler/fetcher/base.rb index 271729a534..27987f670a 100644 --- a/lib/bundler/fetcher/base.rb +++ b/lib/bundler/fetcher/base.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class Fetcher class Base diff --git a/lib/bundler/fetcher/compact_index.rb b/lib/bundler/fetcher/compact_index.rb index 97de88101b..cfc74d642c 100644 --- a/lib/bundler/fetcher/compact_index.rb +++ b/lib/bundler/fetcher/compact_index.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "bundler/fetcher/base" require "bundler/worker" @@ -61,7 +62,7 @@ module Bundler compact_index_request :fetch_spec def available? - return nil unless md5_available? + return nil unless SharedHelpers.md5_available? user_home = Bundler.user_home return nil unless user_home.directory? && user_home.writable? # Read info file checksums out of /versions, so we can know if gems are up to date @@ -120,16 +121,6 @@ module Bundler Net::HTTPNotModified.new(nil, nil, nil) end end - - def md5_available? - require "openssl" - OpenSSL::Digest::MD5.digest("") - true - rescue LoadError - true - rescue OpenSSL::Digest::DigestError - false - end end end end diff --git a/lib/bundler/fetcher/dependency.rb b/lib/bundler/fetcher/dependency.rb index 445b0f2332..1430d1ebeb 100644 --- a/lib/bundler/fetcher/dependency.rb +++ b/lib/bundler/fetcher/dependency.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "bundler/fetcher/base" require "cgi" @@ -6,7 +7,7 @@ module Bundler class Fetcher class Dependency < Base def available? - fetch_uri.scheme != "file" && downloader.fetch(dependency_api_uri) + @available ||= fetch_uri.scheme != "file" && downloader.fetch(dependency_api_uri) rescue NetworkDownError => e raise HTTPError, e.message rescue AuthenticationRequiredError diff --git a/lib/bundler/fetcher/downloader.rb b/lib/bundler/fetcher/downloader.rb index 453e4645eb..cbc5e220bd 100644 --- a/lib/bundler/fetcher/downloader.rb +++ b/lib/bundler/fetcher/downloader.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class Fetcher class Downloader diff --git a/lib/bundler/fetcher/index.rb b/lib/bundler/fetcher/index.rb index d8e212989e..9529944391 100644 --- a/lib/bundler/fetcher/index.rb +++ b/lib/bundler/fetcher/index.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "bundler/fetcher/base" require "rubygems/remote_fetcher" diff --git a/lib/bundler/friendly_errors.rb b/lib/bundler/friendly_errors.rb index 3ba3dcdd91..f624185773 100644 --- a/lib/bundler/friendly_errors.rb +++ b/lib/bundler/friendly_errors.rb @@ -1,5 +1,6 @@ # encoding: utf-8 # frozen_string_literal: true + require "cgi" require "bundler/vendored_thor" @@ -92,7 +93,7 @@ module Bundler #{e.backtrace && e.backtrace.join("\n ").chomp} ``` - #{Bundler::Env.new.report} + #{Bundler::Env.report} --- TEMPLATE END ---------------------------------------------------------------- EOS @@ -119,6 +120,8 @@ module Bundler def self.with_friendly_errors yield + rescue SignalException + raise rescue Exception => e FriendlyErrors.log_error(e) exit FriendlyErrors.exit_status(e) diff --git a/lib/bundler/gem_helper.rb b/lib/bundler/gem_helper.rb index 936d1361fa..1d7fc508d5 100644 --- a/lib/bundler/gem_helper.rb +++ b/lib/bundler/gem_helper.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "bundler/vendored_thor" unless defined?(Thor) require "bundler" @@ -50,8 +51,8 @@ module Bundler install_gem(built_gem_path, :local) end - desc "Create tag #{version_tag} and build and push #{name}-#{version}.gem to Rubygems\n" \ - "To prevent publishing in Rubygems use `gem_push=no rake release`" + desc "Create tag #{version_tag} and build and push #{name}-#{version}.gem to #{gem_push_host}\n" \ + "To prevent publishing in RubyGems use `gem_push=no rake release`" task "release", [:remote] => ["build", "release:guard_clean", "release:source_control_push", "release:rubygem_push"] do end @@ -92,18 +93,14 @@ module Bundler protected def rubygem_push(path) - allowed_push_host = nil gem_command = "gem push '#{path}'" gem_command += " --key #{gem_key}" if gem_key - if @gemspec.respond_to?(:metadata) - allowed_push_host = @gemspec.metadata["allowed_push_host"] - gem_command += " --host #{allowed_push_host}" if allowed_push_host - end + gem_command += " --host #{allowed_push_host}" if allowed_push_host unless allowed_push_host || Bundler.user_home.join(".gem/credentials").file? raise "Your rubygems.org credentials aren't set. Run `gem push` to set them." end sh(gem_command) - Bundler.ui.confirm "Pushed #{name} #{version} to #{allowed_push_host ? allowed_push_host : "rubygems.org."}" + Bundler.ui.confirm "Pushed #{name} #{version} to #{gem_push_host}" end def built_gem_path @@ -116,6 +113,18 @@ module Bundler Bundler.ui.confirm "Pushed git commits and tags." end + def allowed_push_host + @gemspec.metadata["allowed_push_host"] if @gemspec.respond_to?(:metadata) + end + + def gem_push_host + env_rubygems_host = ENV["RUBYGEMS_HOST"] + env_rubygems_host = nil if + env_rubygems_host && env_rubygems_host.empty? + + allowed_push_host || env_rubygems_host || "rubygems.org" + end + def perform_git_push(options = "") cmd = "git push #{options}" out, code = sh_with_code(cmd) @@ -187,7 +196,7 @@ module Bundler end def gem_push? - !%w(n no nil false off 0).include?(ENV["gem_push"].to_s.downcase) + !%w[n no nil false off 0].include?(ENV["gem_push"].to_s.downcase) end end end diff --git a/lib/bundler/gem_helpers.rb b/lib/bundler/gem_helpers.rb index 955834ff01..019ae10c66 100644 --- a/lib/bundler/gem_helpers.rb +++ b/lib/bundler/gem_helpers.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler module GemHelpers GENERIC_CACHE = {} # rubocop:disable MutableConstant diff --git a/lib/bundler/gem_remote_fetcher.rb b/lib/bundler/gem_remote_fetcher.rb index 481838a5e2..9577535d63 100644 --- a/lib/bundler/gem_remote_fetcher.rb +++ b/lib/bundler/gem_remote_fetcher.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "rubygems/remote_fetcher" module Bundler diff --git a/lib/bundler/gem_tasks.rb b/lib/bundler/gem_tasks.rb index 230e7f28f2..f736517bd7 100644 --- a/lib/bundler/gem_tasks.rb +++ b/lib/bundler/gem_tasks.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "rake/clean" CLOBBER.include "pkg" diff --git a/lib/bundler/gem_version_promoter.rb b/lib/bundler/gem_version_promoter.rb index d60d823d9c..52b5386045 100644 --- a/lib/bundler/gem_version_promoter.rb +++ b/lib/bundler/gem_version_promoter.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler # This class contains all of the logic for determining the next version of a # Gem to update to based on the requested level (patch, minor, major). diff --git a/lib/bundler/gemdeps.rb b/lib/bundler/gemdeps.rb index 8595b8c7ea..cd4b25d0e6 100644 --- a/lib/bundler/gemdeps.rb +++ b/lib/bundler/gemdeps.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class Gemdeps def initialize(runtime) diff --git a/lib/bundler/graph.rb b/lib/bundler/graph.rb index e145590430..de6bba0214 100644 --- a/lib/bundler/graph.rb +++ b/lib/bundler/graph.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "set" module Bundler class Graph diff --git a/lib/bundler/index.rb b/lib/bundler/index.rb index 5f54796fa2..9166a92738 100644 --- a/lib/bundler/index.rb +++ b/lib/bundler/index.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "set" module Bundler @@ -111,6 +112,13 @@ module Bundler spec_sets.values.each(&blk) end sources.each {|s| s.each(&blk) } + self + end + + def spec_names + names = specs.keys + sources.map(&:spec_names) + names.uniq! + names end # returns a list of the dependencies @@ -191,14 +199,6 @@ module Bundler end end - wants_prerelease = dependency.requirement.prerelease? - wants_prerelease ||= base && base.any? {|base_spec| base_spec.version.prerelease? } - only_prerelease = specs.all? {|spec| spec.version.prerelease? } - - unless wants_prerelease || only_prerelease - found.reject! {|spec| spec.version.prerelease? } - end - found end end diff --git a/lib/bundler/injector.rb b/lib/bundler/injector.rb index cba1b3d5e5..7fe6a91ddd 100644 --- a/lib/bundler/injector.rb +++ b/lib/bundler/injector.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class Injector def self.inject(new_deps, options = {}) @@ -12,38 +13,40 @@ module Bundler end def inject(gemfile_path, lockfile_path) - if Bundler.settings[:frozen] + if Bundler.frozen? # ensure the lock and Gemfile are synced Bundler.definition.ensure_equivalent_gemfile_and_lockfile(true) - # temporarily remove frozen while we inject - frozen = Bundler.settings.delete(:frozen) end - # evaluate the Gemfile we have now - builder = Dsl.new - builder.eval_gemfile(gemfile_path) + # temporarily unfreeze + Bundler.settings.temporary(:deployment => false, :frozen => false) do + # evaluate the Gemfile we have now + builder = Dsl.new + builder.eval_gemfile(gemfile_path) + + # don't inject any gems that are already in the Gemfile + @new_deps -= builder.dependencies - # don't inject any gems that are already in the Gemfile - @new_deps -= builder.dependencies + # add new deps to the end of the in-memory Gemfile + # Set conservative versioning to false because we want to let the resolver resolve the version first + builder.eval_gemfile("injected gems", build_gem_lines(false)) if @new_deps.any? - # add new deps to the end of the in-memory Gemfile - # Set conservative versioining to false because we want to let the resolver resolve the version first - builder.eval_gemfile("injected gems", build_gem_lines(false)) if @new_deps.any? + # resolve to see if the new deps broke anything + @definition = builder.to_definition(lockfile_path, {}) + @definition.resolve_remotely! - # resolve to see if the new deps broke anything - @definition = builder.to_definition(lockfile_path, {}) - @definition.resolve_remotely! + # since nothing broke, we can add those gems to the gemfile + append_to(gemfile_path, build_gem_lines(@options[:conservative_versioning])) if @new_deps.any? - # since nothing broke, we can add those gems to the gemfile - append_to(gemfile_path, build_gem_lines(@options[:conservative_versioning])) if @new_deps.any? + # since we resolved successfully, write out the lockfile + @definition.lock(Bundler.default_lockfile) - # since we resolved successfully, write out the lockfile - @definition.lock(Bundler.default_lockfile) + # invalidate the cached Bundler.definition + Bundler.reset_paths! - # return an array of the deps that we added - return @new_deps - ensure - Bundler.settings[:frozen] = "1" if frozen + # return an array of the deps that we added + @new_deps + end end private diff --git a/lib/bundler/inline.rb b/lib/bundler/inline.rb index 38dcda6b5b..9d25f3261a 100644 --- a/lib/bundler/inline.rb +++ b/lib/bundler/inline.rb @@ -1,4 +1,7 @@ # frozen_string_literal: true + +require "bundler/compatibility_guard" + # Allows for declaring a Gemfile inline in a ruby script, optionally installing # any gems that aren't already installed on the user's system. # @@ -39,7 +42,7 @@ def gemfile(install = false, options = {}, &gemfile) def Bundler.root Bundler::SharedHelpers.pwd.expand_path end - ENV["BUNDLE_GEMFILE"] = "Gemfile" + Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", "Gemfile" Bundler::Plugin.gemfile_install(&gemfile) if Bundler.feature_flag.plugins? builder = Bundler::Dsl.new @@ -50,12 +53,7 @@ def gemfile(install = false, options = {}, &gemfile) definition.validate_runtime! missing_specs = proc do - begin - !definition.missing_specs.empty? - rescue Bundler::GemNotFound, Bundler::GitError - definition.instance_variable_set(:@index, nil) - true - end + definition.missing_specs? end Bundler.ui = ui if install diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb index bce0e46393..d1066c9c19 100644 --- a/lib/bundler/installer.rb +++ b/lib/bundler/installer.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "erb" require "rubygems/dependency_installer" require "bundler/worker" @@ -33,25 +34,26 @@ module Bundler # Runs the install procedures for a specific Gemfile. # - # Firstly, this method will check to see if Bundler.bundle_path exists - # and if not then will create it. This is usually the location of gems - # on the system, be it RVM or at a system path. + # Firstly, this method will check to see if `Bundler.bundle_path` exists + # and if not then Bundler will create the directory. This is usually the same + # location as RubyGems which typically is the `~/.gem` directory + # unless other specified. # - # Secondly, it checks if Bundler has been configured to be "frozen" + # Secondly, it checks if Bundler has been configured to be "frozen". # Frozen ensures that the Gemfile and the Gemfile.lock file are matching. # This stops a situation where a developer may update the Gemfile but may not run # `bundle install`, which leads to the Gemfile.lock file not being correctly updated. # If this file is not correctly updated then any other developer running # `bundle install` will potentially not install the correct gems. # - # Thirdly, Bundler checks if there are any dependencies specified in the Gemfile using - # Bundler::Environment#dependencies. If there are no dependencies specified then - # Bundler returns a warning message stating so and this method returns. + # Thirdly, Bundler checks if there are any dependencies specified in the Gemfile. + # If there are no dependencies specified then Bundler returns a warning message stating + # so and this method returns. # - # Fourthly, Bundler checks if the default lockfile (Gemfile.lock) exists, and if so - # then proceeds to set up a definition based on the default gemfile (Gemfile) and the - # default lock file (Gemfile.lock). However, this is not the case if the platform is different - # to that which is specified in Gemfile.lock, or if there are any missing specs for the gems. + # Fourthly, Bundler checks if the Gemfile.lock exists, and if so + # then proceeds to set up a definition based on the Gemfile and the Gemfile.lock. + # During this step Bundler will also download information about any new gems + # that are not in the Gemfile.lock and resolve any dependencies if needed. # # Fifthly, Bundler resolves the dependencies either through a cache of gems or by remote. # This then leads into the gems being installed, along with stubs for their executables, @@ -61,26 +63,36 @@ module Bundler # Sixthly, a new Gemfile.lock is created from the installed gems to ensure that the next time # that a user runs `bundle install` they will receive any updates from this process. # - # Finally: TODO add documentation for how the standalone process works. + # Finally, if the user has specified the standalone flag, Bundler will generate the needed + # require paths and save them in a `setup.rb` file. See `bundle standalone --help` for more + # information. def run(options) create_bundle_path - if Bundler.settings[:frozen] - @definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment]) - end + ProcessLock.lock do + if Bundler.frozen? + @definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment]) + end - if @definition.dependencies.empty? - Bundler.ui.warn "The Gemfile specifies no dependencies" - lock - return - end + if @definition.dependencies.empty? + Bundler.ui.warn "The Gemfile specifies no dependencies" + lock + return + end - resolve_if_need(options) - ensure_specs_are_compatible! - install(options) + if resolve_if_needed(options) + ensure_specs_are_compatible! + warn_on_incompatible_bundler_deps + load_plugins + options.delete(:jobs) + else + options[:jobs] = 1 # to avoid the overhead of Bundler::Worker + end + install(options) - lock unless Bundler.settings[:frozen] - Standalone.new(options[:standalone], @definition).generate if options[:standalone] + lock unless Bundler.frozen? + Standalone.new(options[:standalone], @definition).generate if options[:standalone] + end end def generate_bundler_executable_stubs(spec, options = {}) @@ -101,15 +113,21 @@ module Bundler end # double-assignment to avoid warnings about variables that will be used by ERB - bin_path = bin_path = Bundler.bin_path - template = template = File.read(File.expand_path("../templates/Executable", __FILE__)) - relative_gemfile_path = relative_gemfile_path = Bundler.default_gemfile.relative_path_from(bin_path) - ruby_command = ruby_command = Thor::Util.ruby_command + bin_path = Bundler.bin_path + bin_path = bin_path + relative_gemfile_path = Bundler.default_gemfile.relative_path_from(bin_path) + relative_gemfile_path = relative_gemfile_path + ruby_command = Thor::Util.ruby_command + ruby_command = ruby_command + template_path = File.expand_path("../templates/Executable", __FILE__) + if spec.name == "bundler" + template_path += ".bundler" + spec.executables = %(bundle) + end + template = File.read(template_path) exists = [] spec.executables.each do |executable| - next if executable == "bundle" - binstub_path = "#{bin_path}/#{executable}" if File.exist?(binstub_path) && !options[:force] exists << executable @@ -139,13 +157,19 @@ module Bundler def generate_standalone_bundler_executable_stubs(spec) # double-assignment to avoid warnings about variables that will be used by ERB bin_path = Bundler.bin_path - standalone_path = standalone_path = Bundler.root.join(Bundler.settings[:path]).relative_path_from(bin_path) + unless path = Bundler.settings[:path] + raise "Can't standalone without an explicit path set" + end + standalone_path = Bundler.root.join(path).relative_path_from(bin_path) + standalone_path = standalone_path template = File.read(File.expand_path("../templates/Executable.standalone", __FILE__)) - ruby_command = ruby_command = Thor::Util.ruby_command + ruby_command = Thor::Util.ruby_command + ruby_command = ruby_command spec.executables.each do |executable| next if executable == "bundle" - executable_path = executable_path = Pathname(spec.full_gem_path).join(spec.bindir, executable).relative_path_from(bin_path) + executable_path = Pathname(spec.full_gem_path).join(spec.bindir, executable).relative_path_from(bin_path) + executable_path = executable_path File.open "#{bin_path}/#{executable}", "w", 0o755 do |f| f.puts ERB.new(template, nil, "-").result(binding) end @@ -159,13 +183,32 @@ module Bundler # that said, it's a rare situation (other than rake), and parallel # installation is SO MUCH FASTER. so we let people opt in. def install(options) - Bundler.rubygems.load_plugins force = options["force"] - jobs = 1 - jobs = [Bundler.settings[:jobs].to_i - 1, 1].max if can_install_in_parallel? + jobs = options.delete(:jobs) do + if can_install_in_parallel? + [Bundler.settings[:jobs].to_i - 1, 1].max + else + 1 + end + end install_in_parallel jobs, options[:standalone], force end + def load_plugins + Bundler.rubygems.load_plugins + + requested_path_gems = @definition.requested_specs.select {|s| s.source.is_a?(Source::Path) } + path_plugin_files = requested_path_gems.map do |spec| + begin + Bundler.rubygems.spec_matches_for_glob(spec, "rubygems_plugin#{Bundler.rubygems.suffix_pattern}") + rescue TypeError + error_message = "#{spec.name} #{spec.version} has an invalid gemspec" + raise Gem::InvalidSpecificationException, error_message + end + end.flatten + Bundler.rubygems.load_plugin_files(path_plugin_files) + end + def ensure_specs_are_compatible! system_ruby = Bundler::RubyVersion.system rubygems_version = Gem::Version.create(Gem::VERSION) @@ -184,12 +227,28 @@ module Bundler end end + def warn_on_incompatible_bundler_deps + bundler_version = Gem::Version.create(Bundler::VERSION) + @definition.specs.each do |spec| + spec.dependencies.each do |dep| + next if dep.type == :development + next unless dep.name == "bundler".freeze + next if dep.requirement.satisfied_by?(bundler_version) + + Bundler.ui.warn "#{spec.name} (#{spec.version}) has dependency" \ + " #{SharedHelpers.pretty_dependency(dep)}" \ + ", which is unsatisfied by the current bundler version #{VERSION}" \ + ", so the dependency is being ignored" + end + end + end + def can_install_in_parallel? if Bundler.rubygems.provides?(">= 2.1.0") true else - Bundler.ui.warn "Rubygems #{Gem::VERSION} is not threadsafe, so your "\ - "gems will be installed one at a time. Upgrade to Rubygems 2.1.0 " \ + Bundler.ui.warn "RubyGems #{Gem::VERSION} is not threadsafe, so your "\ + "gems will be installed one at a time. Upgrade to RubyGems 2.1.0 " \ "or higher to enable parallel gem installation." false end @@ -207,23 +266,18 @@ module Bundler Bundler.mkdir_p(p) end unless Bundler.bundle_path.exist? rescue Errno::EEXIST - raise PathError, "Could not install to path `#{Bundler.settings[:path]}` " \ + raise PathError, "Could not install to path `#{Bundler.bundle_path}` " \ "because a file already exists at that path. Either remove or rename the file so the directory can be created." end - def resolve_if_need(options) - if !options["update"] && !options["force"] && !Bundler.settings[:inline] && Bundler.default_lockfile.file? - local = Bundler.ui.silence do - begin - tmpdef = Definition.build(Bundler.default_gemfile, Bundler.default_lockfile, nil) - true unless tmpdef.new_platform? || tmpdef.missing_dependencies.any? - rescue BundlerError - end - end + # returns whether or not a re-resolve was needed + def resolve_if_needed(options) + if !@definition.unlocking? && !options["force"] && !Bundler.settings[:inline] && Bundler.default_lockfile.file? + return false if @definition.nothing_changed? && !@definition.missing_specs? end - return if local options["local"] ? @definition.resolve_with_cache! : @definition.resolve_remotely! + true end def lock(opts = {}) diff --git a/lib/bundler/installer/gem_installer.rb b/lib/bundler/installer/gem_installer.rb index a4d9bcaa07..086b763d20 100644 --- a/lib/bundler/installer/gem_installer.rb +++ b/lib/bundler/installer/gem_installer.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class GemInstaller attr_reader :spec, :standalone, :worker, :force, :installer @@ -65,6 +66,7 @@ module Bundler end def generate_executable_stubs + return if Bundler.feature_flag.forget_cli_options? return if Bundler.settings[:inline] if Bundler.settings[:bin] && standalone installer.generate_standalone_bundler_executable_stubs(spec) diff --git a/lib/bundler/installer/parallel_installer.rb b/lib/bundler/installer/parallel_installer.rb index 97c124e015..95d9575c44 100644 --- a/lib/bundler/installer/parallel_installer.rb +++ b/lib/bundler/installer/parallel_installer.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "bundler/worker" require "bundler/installer/gem_installer" @@ -77,11 +78,6 @@ module Bundler new(*args).call end - # Returns max number of threads machine can handle with a min of 1 - def self.max_threads - [Bundler.settings[:jobs].to_i - 1, 1].max - end - attr_reader :size def initialize(installer, all_specs, size, standalone, force) @@ -99,49 +95,19 @@ module Bundler require "bundler/gem_remote_fetcher" if RUBY_VERSION < "1.9" check_for_corrupt_lockfile - enqueue_specs - process_specs until @specs.all?(&:installed?) || @specs.any?(&:failed?) + + if @size > 1 + install_with_worker + else + install_serially + end + handle_error if @specs.any?(&:failed?) @specs ensure worker_pool && worker_pool.stop end - def worker_pool - @worker_pool ||= Bundler::Worker.new @size, "Parallel Installer", lambda { |spec_install, worker_num| - gem_installer = Bundler::GemInstaller.new( - spec_install.spec, @installer, @standalone, worker_num, @force - ) - success, message = gem_installer.install_from_spec - if success && !message.nil? - spec_install.post_install_message = message - elsif !success - spec_install.state = :failed - spec_install.error = "#{message}\n\n#{require_tree_for_spec(spec_install.spec)}" - end - spec_install - } - end - - # Dequeue a spec and save its post-install message and then enqueue the - # remaining specs. - # Some specs might've had to wait til this spec was installed to be - # processed so the call to `enqueue_specs` is important after every - # dequeue. - def process_specs - spec = worker_pool.deq - spec.state = :installed unless spec.failed? - enqueue_specs - end - - def handle_error - errors = @specs.select(&:failed?).map(&:error) - if exception = errors.find {|e| e.is_a?(Bundler::BundlerError) } - raise exception - end - raise Bundler::InstallError, errors.map(&:to_s).join("\n\n") - end - def check_for_corrupt_lockfile missing_dependencies = @specs.map do |s| [ @@ -167,6 +133,71 @@ module Bundler Bundler.ui.warn(warning.join("\n")) end + private + + def install_with_worker + enqueue_specs + process_specs until finished_installing? + end + + def install_serially + until finished_installing? + raise "failed to find a spec to enqueue while installing serially" unless spec_install = @specs.find(&:ready_to_enqueue?) + spec_install.state = :enqueued + do_install(spec_install, 0) + end + end + + def worker_pool + @worker_pool ||= Bundler::Worker.new @size, "Parallel Installer", lambda { |spec_install, worker_num| + do_install(spec_install, worker_num) + } + end + + def do_install(spec_install, worker_num) + gem_installer = Bundler::GemInstaller.new( + spec_install.spec, @installer, @standalone, worker_num, @force + ) + success, message = begin + gem_installer.install_from_spec + rescue => e + raise e, "#{e}\n\n#{require_tree_for_spec(spec_install.spec)}" + end + if success + spec_install.state = :installed + spec_install.post_install_message = message unless message.nil? + else + spec_install.state = :failed + spec_install.error = "#{message}\n\n#{require_tree_for_spec(spec_install.spec)}" + end + spec_install + end + + # Dequeue a spec and save its post-install message and then enqueue the + # remaining specs. + # Some specs might've had to wait til this spec was installed to be + # processed so the call to `enqueue_specs` is important after every + # dequeue. + def process_specs + worker_pool.deq + enqueue_specs + end + + def finished_installing? + @specs.all? do |spec| + return true if spec.failed? + spec.installed? + end + end + + def handle_error + errors = @specs.select(&:failed?).map(&:error) + if exception = errors.find {|e| e.is_a?(Bundler::BundlerError) } + raise exception + end + raise Bundler::InstallError, errors.map(&:to_s).join("\n\n") + end + def require_tree_for_spec(spec) tree = @spec_set.what_required(spec) t = String.new("In #{File.basename(SharedHelpers.default_gemfile)}:\n") diff --git a/lib/bundler/installer/standalone.rb b/lib/bundler/installer/standalone.rb index 03411d85e2..ce0c9df1eb 100644 --- a/lib/bundler/installer/standalone.rb +++ b/lib/bundler/installer/standalone.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class Standalone def initialize(groups, definition) diff --git a/lib/bundler/lazy_specification.rb b/lib/bundler/lazy_specification.rb index 8d9a02c2b8..993952c23b 100644 --- a/lib/bundler/lazy_specification.rb +++ b/lib/bundler/lazy_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "uri" require "bundler/match_platform" @@ -68,7 +69,7 @@ module Bundler end def __materialize__ - search_object = Bundler.settings[:specific_platform] || Bundler.settings[:force_ruby_platform] ? self : Dependency.new(name, version) + search_object = Bundler.feature_flag.specific_platform? || Bundler.settings[:force_ruby_platform] ? self : Dependency.new(name, version) @specification = if source.is_a?(Source::Gemspec) && source.gemspec.name == name source.gemspec.tap {|s| s.source = source } else diff --git a/lib/bundler/lockfile_generator.rb b/lib/bundler/lockfile_generator.rb new file mode 100644 index 0000000000..585077d18d --- /dev/null +++ b/lib/bundler/lockfile_generator.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Bundler + class LockfileGenerator + attr_reader :definition + attr_reader :out + + # @private + def initialize(definition) + @definition = definition + @out = String.new + end + + def self.generate(definition) + new(definition).generate! + end + + def generate! + add_sources + add_platforms + add_dependencies + add_locked_ruby_version + add_bundled_with + + out + end + + private + + def add_sources + definition.send(:sources).lock_sources.each_with_index do |source, idx| + out << "\n" unless idx.zero? + + # Add the source header + out << source.to_lock + + # Find all specs for this source + specs = definition.resolve.select {|s| source.can_lock?(s) } + add_specs(specs) + end + end + + def add_specs(specs) + # This needs to be sorted by full name so that + # gems with the same name, but different platform + # are ordered consistently + specs.sort_by(&:full_name).each do |spec| + next if spec.name == "bundler".freeze + out << spec.to_lock + end + end + + def add_platforms + add_section("PLATFORMS", definition.platforms) + end + + def add_dependencies + out << "\nDEPENDENCIES\n" + + handled = [] + definition.dependencies.sort_by(&:to_s).each do |dep| + next if handled.include?(dep.name) + out << dep.to_lock + handled << dep.name + end + end + + def add_locked_ruby_version + return unless locked_ruby_version = definition.locked_ruby_version + add_section("RUBY VERSION", locked_ruby_version.to_s) + end + + def add_bundled_with + add_section("BUNDLED WITH", definition.locked_bundler_version.to_s) + end + + def add_section(name, value) + out << "\n#{name}\n" + case value + when Array + value.map(&:to_s).sort.each do |val| + out << " #{val}\n" + end + when Hash + value.to_a.sort_by {|k, _| k.to_s }.each do |key, val| + out << " #{key}: #{val}\n" + end + when String + out << " #{value}\n" + else + raise ArgumentError, "#{value.inspect} can't be serialized in a lockfile" + end + end + end +end diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb index dbf8926690..ff706fca1d 100644 --- a/lib/bundler/lockfile_parser.rb +++ b/lib/bundler/lockfile_parser.rb @@ -90,7 +90,7 @@ module Bundler send("parse_#{@state}", line) end end - @sources << @rubygems_aggregate + @sources << @rubygems_aggregate unless Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? @specs = @specs.values.sort_by(&:identifier) warn_for_outdated_bundler_version rescue ArgumentError => e @@ -141,10 +141,16 @@ module Bundler @sources << @current_source end when GEM - Array(@opts["remote"]).each do |url| - @rubygems_aggregate.add_remote(url) + if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + @opts["remotes"] = @opts.delete("remote") + @current_source = TYPES[@type].from_lock(@opts) + @sources << @current_source + else + Array(@opts["remote"]).each do |url| + @rubygems_aggregate.add_remote(url) + end + @current_source = @rubygems_aggregate end - @current_source = @rubygems_aggregate when PLUGIN @current_source = Plugin.source_from_lock(@opts) @sources << @current_source diff --git a/lib/bundler/match_platform.rb b/lib/bundler/match_platform.rb index 050cd0efd3..56cbbfb95d 100644 --- a/lib/bundler/match_platform.rb +++ b/lib/bundler/match_platform.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "bundler/gem_helpers" module Bundler diff --git a/lib/bundler/mirror.rb b/lib/bundler/mirror.rb index 97a6776adb..a6fa070eb8 100644 --- a/lib/bundler/mirror.rb +++ b/lib/bundler/mirror.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "socket" module Bundler @@ -37,7 +38,7 @@ module Bundler mirror = if config.all? @all else - (@mirrors[config.uri] = @mirrors[config.uri] || Mirror.new) + @mirrors[config.uri] ||= Mirror.new end config.update_mirror(mirror) end @@ -45,7 +46,9 @@ module Bundler private def fetch_valid_mirror_for(uri) - mirror = (@mirrors[URI(uri.to_s.downcase)] || @mirrors[URI(uri.to_s).host] || Mirror.new(uri)).validate!(@prober) + downcased = uri.to_s.downcase + mirror = @mirrors[downcased] || @mirrors[URI(downcased).host] || Mirror.new(uri) + mirror.validate!(@prober) mirror = Mirror.new(uri) unless mirror.valid? mirror end @@ -117,7 +120,7 @@ module Bundler def initialize(config_line, value) uri, fallback = - config_line.match(%r{^mirror\.(all|.+?)(\.fallback_timeout)?\/?$}).captures + config_line.match(%r{\Amirror\.(all|.+?)(\.fallback_timeout)?\/?\z}).captures @fallback = !fallback.nil? @all = false if uri == "all" diff --git a/lib/bundler/plugin.rb b/lib/bundler/plugin.rb index 66f485ef8e..99c9a867b0 100644 --- a/lib/bundler/plugin.rb +++ b/lib/bundler/plugin.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "bundler/plugin/api" module Bundler diff --git a/lib/bundler/plugin/api/source.rb b/lib/bundler/plugin/api/source.rb index 5d3f58df92..586477efb5 100644 --- a/lib/bundler/plugin/api/source.rb +++ b/lib/bundler/plugin/api/source.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true + require "uri" -require "digest/sha1" module Bundler module Plugin @@ -271,7 +271,7 @@ module Bundler end def uri_hash - Digest::SHA1.hexdigest(uri) + SharedHelpers.digest(:SHA1).hexdigest(uri) end # Note: Do not override if you don't know what you are doing. @@ -293,6 +293,13 @@ module Bundler def bundler_plugin_api_source? true end + + # @private + # This API on source might not be stable, and for now we expect plugins + # to download all specs in `#specs`, so we implement the method for + # compatibility purposes and leave it undocumented (and don't support) + # overriding it) + def double_check_for(*); end end end end diff --git a/lib/bundler/plugin/installer.rb b/lib/bundler/plugin/installer.rb index a50d0ceedd..5379c38979 100644 --- a/lib/bundler/plugin/installer.rb +++ b/lib/bundler/plugin/installer.rb @@ -13,12 +13,13 @@ module Bundler def install(names, options) version = options[:version] || [">= 0"] - - if options[:git] - install_git(names, version, options) - else - sources = options[:source] || Bundler.rubygems.sources - install_rubygems(names, version, sources) + Bundler.settings.temporary(:lockfile_uses_separate_rubygems_sources => false, :disable_multisource => false) do + if options[:git] + install_git(names, version, options) + else + sources = options[:source] || Bundler.rubygems.sources + install_rubygems(names, version, sources) + end end end diff --git a/lib/bundler/plugin/source_list.rb b/lib/bundler/plugin/source_list.rb index 33f5e5afbd..f0e212205f 100644 --- a/lib/bundler/plugin/source_list.rb +++ b/lib/bundler/plugin/source_list.rb @@ -5,13 +5,6 @@ module Bundler # approptiate options to be used with Source classes for plugin installation module Plugin class SourceList < Bundler::SourceList - def initialize - @path_sources = [] - @git_sources = [] - @rubygems_aggregate = Plugin::Installer::Rubygems.new - @rubygems_sources = [] - end - def add_git_source(options = {}) add_source_to_list Plugin::Installer::Git.new(options), git_sources end @@ -21,7 +14,13 @@ module Bundler end def all_sources - path_sources + git_sources + rubygems_sources + path_sources + git_sources + rubygems_sources + [metadata_source] + end + + private + + def rubygems_aggregate_class + Plugin::Installer::Rubygems end end end diff --git a/lib/bundler/process_lock.rb b/lib/bundler/process_lock.rb new file mode 100644 index 0000000000..4bd6931577 --- /dev/null +++ b/lib/bundler/process_lock.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Bundler + class ProcessLock + def self.lock(bundle_path = Bundler.bundle_path) + lock_file_path = File.join(bundle_path, "bundler.lock") + has_lock = false + + File.open(lock_file_path, "w") do |f| + f.flock(File::LOCK_EX) + has_lock = true + yield + f.flock(File::LOCK_UN) + end + rescue Errno::EACCES, Errno::ENOLCK + # In the case the user does not have access to + # create the lock file or is using NFS where + # locks are not available we skip locking. + yield + ensure + FileUtils.rm_f(lock_file_path) if has_lock + end + end +end diff --git a/lib/bundler/psyched_yaml.rb b/lib/bundler/psyched_yaml.rb index 69d2ae78c5..e654416a5a 100644 --- a/lib/bundler/psyched_yaml.rb +++ b/lib/bundler/psyched_yaml.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # Psych could be a gem, so try to ask for it begin gem "psych" @@ -25,3 +26,12 @@ module Bundler YamlLibrarySyntaxError = ::ArgumentError end end + +require "bundler/deprecate" +begin + Bundler::Deprecate.skip_during do + require "rubygems/safe_yaml" + end +rescue LoadError + # it's OK if the file isn't there +end diff --git a/lib/bundler/remote_specification.rb b/lib/bundler/remote_specification.rb index 208ee1d4b7..23e1234330 100644 --- a/lib/bundler/remote_specification.rb +++ b/lib/bundler/remote_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "uri" module Bundler diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index db2ae496a4..ddc1d702e0 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -1,178 +1,9 @@ # frozen_string_literal: true + module Bundler class Resolver require "bundler/vendored_molinillo" - - class Molinillo::VersionConflict - def printable_dep(dep) - if dep.is_a?(Bundler::Dependency) - DepProxy.new(dep, dep.platforms.join(", ")).to_s.strip - else - dep.to_s - end - end - - def message - conflicts.sort.reduce(String.new) do |o, (name, conflict)| - o << %(\nBundler could not find compatible versions for gem "#{name}":\n) - if conflict.locked_requirement - o << %( In snapshot (#{Bundler.default_lockfile.basename}):\n) - o << %( #{printable_dep(conflict.locked_requirement)}\n) - o << %(\n) - end - o << %( In Gemfile:\n) - trees = conflict.requirement_trees - - maximal = 1.upto(trees.size).map do |size| - trees.map(&:last).flatten(1).combination(size).to_a - end.flatten(1).select do |deps| - Bundler::VersionRanges.empty?(*Bundler::VersionRanges.for_many(deps.map(&:requirement))) - end.min_by(&:size) - trees.reject! {|t| !maximal.include?(t.last) } if maximal - - o << trees.sort_by {|t| t.reverse.map(&:name) }.map do |tree| - t = String.new - depth = 2 - tree.each do |req| - t << " " * depth << req.to_s - unless tree.last == req - if spec = conflict.activated_by_name[req.name] - t << %( was resolved to #{spec.version}, which) - end - t << %( depends on) - end - t << %(\n) - depth += 1 - end - t - end.join("\n") - - if name == "bundler" - o << %(\n Current Bundler version:\n bundler (#{Bundler::VERSION})) - other_bundler_required = !conflict.requirement.requirement.satisfied_by?(Gem::Version.new Bundler::VERSION) - end - - if name == "bundler" && other_bundler_required - o << "\n" - o << "This Gemfile requires a different version of Bundler.\n" - o << "Perhaps you need to update Bundler by running `gem install bundler`?\n" - end - if conflict.locked_requirement - o << "\n" - o << %(Running `bundle update` will rebuild your snapshot from scratch, using only\n) - o << %(the gems in your Gemfile, which may resolve the conflict.\n) - elsif !conflict.existing - o << "\n" - if conflict.requirement_trees.first.size > 1 - o << "Could not find gem '#{conflict.requirement}', which is required by " - o << "gem '#{conflict.requirement_trees.first[-2]}', in any of the sources." - else - o << "Could not find gem '#{conflict.requirement}' in any of the sources\n" - end - end - o - end.strip - end - end - - class SpecGroup < Array - include GemHelpers - - attr_reader :activated - - def initialize(a) - super - @required_by = [] - @activated_platforms = [] - @dependencies = nil - @specs = Hash.new do |specs, platform| - specs[platform] = select_best_platform_match(self, platform) - end - end - - def initialize_copy(o) - super - @activated_platforms = o.activated.dup - end - - def to_specs - @activated_platforms.map do |p| - next unless s = @specs[p] - lazy_spec = LazySpecification.new(name, version, s.platform, source) - lazy_spec.dependencies.replace s.dependencies - lazy_spec - end.compact - end - - def activate_platform!(platform) - return unless for?(platform) - return if @activated_platforms.include?(platform) - @activated_platforms << platform - end - - def name - @name ||= first.name - end - - def version - @version ||= first.version - end - - def source - @source ||= first.source - end - - def for?(platform) - spec = @specs[platform] - !spec.nil? - end - - def to_s - "#{name} (#{version})" - end - - def dependencies_for_activated_platforms - dependencies = @activated_platforms.map {|p| __dependencies[p] } - metadata_dependencies = @activated_platforms.map do |platform| - metadata_dependencies(@specs[platform], platform) - end - dependencies.concat(metadata_dependencies).flatten - end - - def platforms_for_dependency_named(dependency) - __dependencies.select {|_, deps| deps.map(&:name).include? dependency }.keys - end - - private - - def __dependencies - @dependencies = Hash.new do |dependencies, platform| - dependencies[platform] = [] - if spec = @specs[platform] - spec.dependencies.each do |dep| - next if dep.type == :development - dependencies[platform] << DepProxy.new(dep, platform) - end - end - dependencies[platform] - end - end - - def metadata_dependencies(spec, platform) - return [] unless spec - # Only allow endpoint specifications since they won't hit the network to - # fetch the full gemspec when calling required_ruby_version - return [] if !spec.is_a?(EndpointSpecification) && !spec.is_a?(Gem::Specification) - dependencies = [] - if !spec.required_ruby_version.nil? && !spec.required_ruby_version.none? - dependencies << DepProxy.new(Gem::Dependency.new("ruby\0", spec.required_ruby_version), platform) - end - if !spec.required_rubygems_version.nil? && !spec.required_rubygems_version.none? - dependencies << DepProxy.new(Gem::Dependency.new("rubygems\0", spec.required_rubygems_version), platform) - end - dependencies - end - end + require "bundler/resolver/spec_group" # Figures out the best possible configuration of gems that satisfies # the list of passed dependencies and any child dependencies without @@ -206,16 +37,22 @@ module Bundler additional_base_requirements.each {|d| @base_dg.add_vertex(d.name, d) } @platforms = platforms @gem_version_promoter = gem_version_promoter + @allow_bundler_dependency_conflicts = Bundler.feature_flag.allow_bundler_dependency_conflicts? + @lockfile_uses_separate_rubygems_sources = Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? end def start(requirements) + @prerelease_specified = {} + requirements.each {|dep| @prerelease_specified[dep.name] ||= dep.prerelease? } + verify_gemfile_dependencies_are_found!(requirements) dg = @resolver.resolve(requirements, @base_dg) dg.map(&:payload). reject {|sg| sg.name.end_with?("\0") }. map(&:to_specs).flatten rescue Molinillo::VersionConflict => e - raise VersionConflict.new(e.conflicts.keys.uniq, e.message) + message = version_conflict_message(e) + raise VersionConflict.new(e.conflicts.keys.uniq, message) rescue Molinillo::CircularDependencyError => e names = e.dependencies.sort_by(&:name).map {|d| "gem '#{d.name}'" } raise CyclicDependencyError, "Your bundle requires gems that depend" \ @@ -266,6 +103,14 @@ module Bundler search = @search_for[dependency] ||= begin index = index_for(dependency) results = index.search(dependency, @base[dependency.name]) + + unless @prerelease_specified[dependency.name] + # Move prereleases to the beginning of the list, so they're considered + # last during resolution. + pre, results = results.partition {|spec| spec.version.prerelease? } + results = pre + results + end + if vertex = @base_dg.vertex_named(dependency.name) locked_requirement = vertex.payload.requirement end @@ -281,7 +126,9 @@ module Bundler end nested.reduce([]) do |groups, (version, specs)| next groups if locked_requirement && !locked_requirement.satisfied_by?(version) - groups << SpecGroup.new(specs) + spec_group = SpecGroup.new(specs) + spec_group.ignores_bundler_dependencies = @allow_bundler_dependency_conflicts + groups << spec_group end else [] @@ -298,7 +145,20 @@ module Bundler end def index_for(dependency) - @source_requirements[dependency.name] || @index + source = @source_requirements[dependency.name] + if source + source.specs + elsif @lockfile_uses_separate_rubygems_sources + Index.build do |idx| + if dependency.all_sources + dependency.all_sources.each {|s| idx.add_source(s.specs) if s } + else + idx.add_source @source_requirements[:default].specs + end + end + else + @index + end end def name_for(dependency) @@ -319,23 +179,53 @@ module Bundler def requirement_satisfied_by?(requirement, activated, spec) return false unless requirement.matches_spec?(spec) || spec.source.is_a?(Source::Gemspec) + if spec.version.prerelease? && !requirement.prerelease? && search_for(requirement).any? {|sg| !sg.version.prerelease? } + vertex = activated.vertex_named(spec.name) + return false if vertex.requirements.none?(&:prerelease?) + end spec.activate_platform!(requirement.__platform) if !@platforms || @platforms.include?(requirement.__platform) true end + def relevant_sources_for_vertex(vertex) + if vertex.root? + [@source_requirements[vertex.name]] + elsif @lockfile_uses_separate_rubygems_sources + vertex.recursive_predecessors.map do |v| + @source_requirements[v.name] + end << @source_requirements[:default] + end + end + def sort_dependencies(dependencies, activated, conflicts) dependencies.sort_by do |dependency| + dependency.all_sources = relevant_sources_for_vertex(activated.vertex_named(dependency.name)) name = name_for(dependency) + vertex = activated.vertex_named(name) [ @base_dg.vertex_named(name) ? 0 : 1, - activated.vertex_named(name).payload ? 0 : 1, + vertex.payload ? 0 : 1, + vertex.root? ? 0 : 1, amount_constrained(dependency), conflicts[name] ? 0 : 1, - activated.vertex_named(name).payload ? 0 : search_for(dependency).count, + vertex.payload ? 0 : search_for(dependency).count, + self.class.platform_sort_key(dependency.__platform), ] end end + # Sort platforms from most general to most specific + def self.sort_platforms(platforms) + platforms.sort_by do |platform| + platform_sort_key(platform) + end + end + + def self.platform_sort_key(platform) + return ["", "", ""] if Gem::Platform::RUBY == platform + platform.to_a.map {|part| part || "" } + end + private # returns an integer \in (-\infty, 0] @@ -364,32 +254,34 @@ module Bundler def verify_gemfile_dependencies_are_found!(requirements) requirements.each do |requirement| - next if requirement.name == "bundler" + name = requirement.name + next if name == "bundler" next unless search_for(requirement).empty? - if (base = @base[requirement.name]) && !base.empty? + + cache_message = begin + " or in gems cached in #{Bundler.settings.app_cache_path}" if Bundler.app_cache.exist? + rescue GemfileNotFound + nil + end + + if (base = @base[name]) && !base.empty? version = base.first.version message = "You have requested:\n" \ - " #{requirement.name} #{requirement.requirement}\n\n" \ - "The bundle currently has #{requirement.name} locked at #{version}.\n" \ - "Try running `bundle update #{requirement.name}`\n\n" \ + " #{name} #{requirement.requirement}\n\n" \ + "The bundle currently has #{name} locked at #{version}.\n" \ + "Try running `bundle update #{name}`\n\n" \ "If you are updating multiple gems in your Gemfile at once,\n" \ "try passing them all to `bundle update`" - elsif requirement.source - name = requirement.name - specs = @source_requirements[name][name] + elsif source = @source_requirements[name] + specs = source.specs[name] versions_with_platforms = specs.map {|s| [s.version, s.platform] } - message = String.new("Could not find gem '#{requirement}' in #{requirement.source}.\n") + message = String.new("Could not find gem '#{SharedHelpers.pretty_dependency(requirement)}' in #{source}#{cache_message}.\n") message << if versions_with_platforms.any? - "Source contains '#{name}' at: #{formatted_versions_with_platforms(versions_with_platforms)}" + "The source contains '#{name}' at: #{formatted_versions_with_platforms(versions_with_platforms)}" else - "Source does not contain any versions of '#{requirement}'" + "The source does not contain any versions of '#{name}'" end else - cache_message = begin - " or in gems cached in #{Bundler.settings.app_cache_path}" if Bundler.app_cache.exist? - rescue GemfileNotFound - nil - end message = "Could not find gem '#{requirement}' in any of the gem sources " \ "listed in your Gemfile#{cache_message}." end @@ -402,9 +294,76 @@ module Bundler version = vwp.first platform = vwp.last version_platform_str = String.new(version.to_s) - version_platform_str << " #{platform}" unless platform.nil? + version_platform_str << " #{platform}" unless platform.nil? || platform == Gem::Platform::RUBY + version_platform_str end version_platform_strs.join(", ") end + + def version_conflict_message(e) + e.message_with_trees( + :solver_name => "Bundler", + :possibility_type => "gem", + :reduce_trees => lambda do |trees| + maximal = 1.upto(trees.size).map do |size| + trees.map(&:last).flatten(1).combination(size).to_a + end.flatten(1).select do |deps| + Bundler::VersionRanges.empty?(*Bundler::VersionRanges.for_many(deps.map(&:requirement))) + end.min_by(&:size) + trees.reject! {|t| !maximal.include?(t.last) } if maximal + + trees = trees.sort_by {|t| t.flatten.map(&:to_s) } + trees.uniq! {|t| t.flatten.map {|dep| [dep.name, dep.requirement] } } + + trees.sort_by {|t| t.reverse.map(&:name) } + end, + :printable_requirement => lambda {|req| SharedHelpers.pretty_dependency(req) }, + :additional_message_for_conflict => lambda do |o, name, conflict| + if name == "bundler" + o << %(\n Current Bundler version:\n bundler (#{Bundler::VERSION})) + other_bundler_required = !conflict.requirement.requirement.satisfied_by?(Gem::Version.new Bundler::VERSION) + end + + if name == "bundler" && other_bundler_required + o << "\n" + o << "This Gemfile requires a different version of Bundler.\n" + o << "Perhaps you need to update Bundler by running `gem install bundler`?\n" + end + if conflict.locked_requirement + o << "\n" + o << %(Running `bundle update` will rebuild your snapshot from scratch, using only\n) + o << %(the gems in your Gemfile, which may resolve the conflict.\n) + elsif !conflict.existing + o << "\n" + + relevant_sources = if conflict.requirement.source + [conflict.requirement.source] + elsif conflict.requirement.all_sources + conflict.requirement.all_sources + elsif @lockfile_uses_separate_rubygems_sources + # every conflict should have an explicit group of sources when we + # enforce strict pinning + raise "no source set for #{conflict}" + else + [] + end.compact.map(&:to_s).uniq.sort + + o << "Could not find gem '#{SharedHelpers.pretty_dependency(conflict.requirement)}'" + if conflict.requirement_trees.first.size > 1 + o << ", which is required by " + o << "gem '#{SharedHelpers.pretty_dependency(conflict.requirement_trees.first[-2])}'," + end + o << " " + + o << if relevant_sources.empty? + "in any of the sources.\n" + else + "in any of the relevant sources:\n #{relevant_sources * "\n "}\n" + end + end + end, + :version_for_spec => lambda {|spec| spec.version } + ) + end end end diff --git a/lib/bundler/resolver/spec_group.rb b/lib/bundler/resolver/spec_group.rb new file mode 100644 index 0000000000..9c10a4b733 --- /dev/null +++ b/lib/bundler/resolver/spec_group.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module Bundler + class Resolver + class SpecGroup + include GemHelpers + + attr_accessor :name, :version, :source + attr_accessor :ignores_bundler_dependencies + + def initialize(all_specs) + raise ArgumentError, "cannot initialize with an empty value" unless exemplary_spec = all_specs.first + @name = exemplary_spec.name + @version = exemplary_spec.version + @source = exemplary_spec.source + + @required_by = [] + @activated_platforms = [] + @dependencies = nil + @specs = Hash.new do |specs, platform| + specs[platform] = select_best_platform_match(all_specs, platform) + end + @ignores_bundler_dependencies = true + end + + def to_specs + @activated_platforms.map do |p| + next unless s = @specs[p] + lazy_spec = LazySpecification.new(name, version, s.platform, source) + lazy_spec.dependencies.replace s.dependencies + lazy_spec + end.compact + end + + def activate_platform!(platform) + return unless for?(platform) + return if @activated_platforms.include?(platform) + @activated_platforms << platform + end + + def for?(platform) + spec = @specs[platform] + !spec.nil? + end + + def to_s + @to_s ||= "#{name} (#{version})" + end + + def dependencies_for_activated_platforms + dependencies = @activated_platforms.map {|p| __dependencies[p] } + metadata_dependencies = @activated_platforms.map do |platform| + metadata_dependencies(@specs[platform], platform) + end + dependencies.concat(metadata_dependencies).flatten + end + + def platforms_for_dependency_named(dependency) + __dependencies.select {|_, deps| deps.map(&:name).include? dependency }.keys + end + + def ==(other) + return unless other.is_a?(SpecGroup) + name == other.name && + version == other.version && + source == other.source + end + + def eql?(other) + name.eql?(other.name) && + version.eql?(other.version) && + source.eql?(other.source) + end + + def hash + to_s.hash ^ source.hash + end + + private + + def __dependencies + @dependencies = Hash.new do |dependencies, platform| + dependencies[platform] = [] + if spec = @specs[platform] + spec.dependencies.each do |dep| + next if dep.type == :development + next if @ignores_bundler_dependencies && dep.name == "bundler".freeze + dependencies[platform] << DepProxy.new(dep, platform) + end + end + dependencies[platform] + end + end + + def metadata_dependencies(spec, platform) + return [] unless spec + # Only allow endpoint specifications since they won't hit the network to + # fetch the full gemspec when calling required_ruby_version + return [] if !spec.is_a?(EndpointSpecification) && !spec.is_a?(Gem::Specification) + dependencies = [] + if !spec.required_ruby_version.nil? && !spec.required_ruby_version.none? + dependencies << DepProxy.new(Gem::Dependency.new("ruby\0", spec.required_ruby_version), platform) + end + if !spec.required_rubygems_version.nil? && !spec.required_rubygems_version.none? + dependencies << DepProxy.new(Gem::Dependency.new("rubygems\0", spec.required_rubygems_version), platform) + end + dependencies + end + end + end +end diff --git a/lib/bundler/retry.rb b/lib/bundler/retry.rb index 092fb866b3..244606dcc9 100644 --- a/lib/bundler/retry.rb +++ b/lib/bundler/retry.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler # General purpose class for retrying code that may fail class Retry diff --git a/lib/bundler/ruby_dsl.rb b/lib/bundler/ruby_dsl.rb index a410b7f3d7..f6ba220cd5 100644 --- a/lib/bundler/ruby_dsl.rb +++ b/lib/bundler/ruby_dsl.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler module RubyDsl def ruby(*ruby_version) diff --git a/lib/bundler/ruby_version.rb b/lib/bundler/ruby_version.rb index f0a001d296..d4e1bdbfd5 100644 --- a/lib/bundler/ruby_version.rb +++ b/lib/bundler/ruby_version.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class RubyVersion attr_reader :versions, diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb index a0f8fa848b..e9f0eac355 100644 --- a/lib/bundler/rubygems_ext.rb +++ b/lib/bundler/rubygems_ext.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "pathname" if defined?(Gem::QuickLoader) @@ -14,7 +15,7 @@ begin # shouldn't be deferred. require "rubygems/source" rescue LoadError - # Not available before Rubygems 2.0.0, ignore + # Not available before RubyGems 2.0.0, ignore nil end @@ -135,7 +136,7 @@ module Gem end class Dependency - attr_accessor :source, :groups + attr_accessor :source, :groups, :all_sources alias_method :eql?, :== @@ -146,7 +147,7 @@ module Gem end def to_yaml_properties - instance_variables.reject {|p| ["@source", "@groups"].include?(p.to_s) } + instance_variables.reject {|p| ["@source", "@groups", "@all_sources"].include?(p.to_s) } end def to_lock @@ -158,7 +159,7 @@ module Gem out end - # Backport of performance enhancement added to Rubygems 1.4 + # Backport of performance enhancement added to RubyGems 1.4 def matches_spec?(spec) # name can be a Regexp, so use === return false unless name === spec.name diff --git a/lib/bundler/rubygems_gem_installer.rb b/lib/bundler/rubygems_gem_installer.rb index 977e13d948..2b7fa8e0f6 100644 --- a/lib/bundler/rubygems_gem_installer.rb +++ b/lib/bundler/rubygems_gem_installer.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "rubygems/installer" module Bundler @@ -17,6 +18,28 @@ module Bundler super && validate_bundler_checksum(options[:bundler_expected_checksum]) end + def build_extensions + extension_cache_path = options[:bundler_extension_cache_path] + return super unless extension_cache_path && extension_dir = Bundler.rubygems.spec_extension_dir(spec) + + extension_dir = Pathname.new(extension_dir) + build_complete = SharedHelpers.filesystem_access(extension_cache_path.join("gem.build_complete"), :read, &:file?) + if build_complete && !options[:force] + SharedHelpers.filesystem_access(extension_dir.parent, &:mkpath) + SharedHelpers.filesystem_access(extension_cache_path) do + FileUtils.cp_r extension_cache_path, spec.extension_dir + end + else + super + if extension_dir.directory? # not made for gems without extensions + SharedHelpers.filesystem_access(extension_cache_path.parent, &:mkpath) + SharedHelpers.filesystem_access(extension_cache_path) do + FileUtils.cp_r extension_dir, extension_cache_path + end + end + end + end + private def validate_bundler_checksum(checksum) @@ -25,7 +48,7 @@ module Bundler return true unless source = @package.instance_variable_get(:@gem) return true unless source.respond_to?(:with_read_io) digest = source.with_read_io do |io| - digest = Digest::SHA256.new + digest = SharedHelpers.digest(:SHA256).new digest << io.read(16_384) until io.eof? io.rewind send(checksum_type(checksum), digest) diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb index c3e16e086c..0f16b6231d 100644 --- a/lib/bundler/rubygems_integration.rb +++ b/lib/bundler/rubygems_integration.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "monitor" require "rubygems" require "rubygems/config_file" @@ -84,6 +85,19 @@ module Bundler spec.respond_to?(:default_gem?) && spec.default_gem? end + def spec_matches_for_glob(spec, glob) + return spec.matches_for_glob(glob) if spec.respond_to?(:matches_for_glob) + + spec.load_paths.map do |lp| + Dir["#{lp}/#{glob}#{suffix_pattern}"] + end.flatten(1) + end + + def spec_extension_dir(spec) + return unless spec.respond_to?(:extension_dir) + spec.extension_dir + end + def stub_set_spec(stub, spec) stub.instance_variable_set(:@spec, spec) end @@ -158,6 +172,10 @@ module Bundler Gem.post_reset_hooks end + def suffix_pattern + Gem.suffix_pattern + end + def gem_cache gem_path.map {|p| File.expand_path("cache", p) } end @@ -165,7 +183,7 @@ module Bundler def spec_cache_dirs @spec_cache_dirs ||= begin dirs = gem_path.map {|dir| File.join(dir, "specifications") } - dirs << Gem.spec_cache_dir if Gem.respond_to?(:spec_cache_dir) # Not in Rubygems 2.0.3 or earlier + dirs << Gem.spec_cache_dir if Gem.respond_to?(:spec_cache_dir) # Not in RubyGems 2.0.3 or earlier dirs.uniq.select {|dir| File.directory? dir } end end @@ -179,7 +197,7 @@ module Bundler end def repository_subdirectories - %w(cache doc gems specifications) + %w[cache doc gems specifications] end def clear_paths @@ -190,8 +208,12 @@ module Bundler Gem.bin_path(gem, bin, ver) end + def path_separator + File::PATH_SEPARATOR + end + def preserve_paths - # this is a no-op outside of Rubygems 1.8 + # this is a no-op outside of RubyGems 1.8 yield end @@ -212,6 +234,10 @@ module Bundler Gem.load_plugins if Gem.respond_to?(:load_plugins) end + def load_plugin_files(files) + Gem.load_plugin_files(files) if Gem.respond_to?(:load_plugin_files) + end + def ui=(obj) Gem::DefaultUserInteraction.ui = obj end @@ -233,9 +259,9 @@ module Bundler {} # if we can't download them, there aren't any end - # TODO: This is for older versions of Rubygems... should we support the + # TODO: This is for older versions of RubyGems... should we support the # X-Gemfile-Source header on these old versions? - # Maybe the newer implementation will work on older Rubygems? + # Maybe the newer implementation will work on older RubyGems? # It seems difficult to keep this implementation and still send the header. def fetch_all_remote_specs(remote) old_sources = Bundler.rubygems.sources @@ -273,6 +299,7 @@ module Bundler def spec_from_gem(path, policy = nil) require "rubygems/security" + require "bundler/psyched_yaml" gem_from_path(path, security_policies[policy]).spec rescue Gem::Package::FormatError raise GemspecError, "Could not read gem at #{path}. It may be corrupted." @@ -306,7 +333,7 @@ module Bundler end def security_policy_keys - %w(High Medium Low AlmostNo No).map {|level| "#{level}Security" } + %w[High Medium Low AlmostNo No].map {|level| "#{level}Security" } end def security_policies @@ -377,9 +404,8 @@ module Bundler raise e end - # TODO: delete this in 2.0, it's a backwards compatibility shim - # see https://github.com/bundler/bundler/issues/5102 - kernel_class.send(:public, :gem) + # backwards compatibility shim, see https://github.com/bundler/bundler/issues/5102 + kernel_class.send(:public, :gem) if Bundler.feature_flag.setup_makes_kernel_gem_public? end end @@ -434,9 +460,9 @@ module Bundler raise Gem::Exception, "no default executable for #{spec.full_name}" unless exec_name ||= spec.default_executable - unless spec.name == name - Bundler::SharedHelpers.major_deprecation \ - "Bundler is using a binstub that was created for a different gem.\n" \ + unless spec.name == gem_name + Bundler::SharedHelpers.major_deprecation 2, + "Bundler is using a binstub that was created for a different gem (#{spec.name}).\n" \ "You should run `bundle binstub #{gem_name}` " \ "to work around a system/bundle conflict." end @@ -476,7 +502,7 @@ module Bundler redefine_method(gem_class, :refresh) {} end - # Replace or hook into Rubygems to provide a bundlerized view + # Replace or hook into RubyGems to provide a bundlerized view # of the world. def replace_entrypoints(specs) specs_by_name = specs.reduce({}) do |h, s| @@ -492,8 +518,8 @@ module Bundler Gem.clear_paths end - # This backports the correct segment generation code from Rubygems 1.4+ - # by monkeypatching it into the method in Rubygems 1.3.6 and 1.3.7. + # This backports the correct segment generation code from RubyGems 1.4+ + # by monkeypatching it into the method in RubyGems 1.3.6 and 1.3.7. def backport_segment_generation redefine_method(Gem::Version, :segments) do @segments ||= @version.scan(/[0-9]+|[a-z]+/i).map do |s| @@ -512,7 +538,7 @@ module Bundler end # This backports base_dir which replaces installation path - # Rubygems 1.8+ + # RubyGems 1.8+ def backport_base_dir redefine_method(Gem::Specification, :base_dir) do return Gem.dir unless loaded_from @@ -581,7 +607,7 @@ module Bundler end end - # Rubygems 1.4 through 1.6 + # RubyGems 1.4 through 1.6 class Legacy < RubygemsIntegration def initialize super @@ -592,7 +618,7 @@ module Bundler end def stub_rubygems(specs) - # Rubygems versions lower than 1.7 use SourceIndex#from_gems_in + # RubyGems versions lower than 1.7 use SourceIndex#from_gems_in source_index_class = (class << Gem::SourceIndex; self; end) redefine_method(source_index_class, :from_gems_in) do |*args| Gem::SourceIndex.new.tap do |source_index| @@ -624,7 +650,7 @@ module Bundler end end - # Rubygems versions 1.3.6 and 1.3.7 + # RubyGems versions 1.3.6 and 1.3.7 class Ancient < Legacy def initialize super @@ -632,7 +658,7 @@ module Bundler end end - # Rubygems 1.7 + # RubyGems 1.7 class Transitional < Legacy def stub_rubygems(specs) stub_source_index(specs) @@ -646,7 +672,7 @@ module Bundler end end - # Rubygems 1.8.5-1.8.19 + # RubyGems 1.8.5-1.8.19 class Modern < RubygemsIntegration def stub_rubygems(specs) Gem::Specification.all = specs @@ -667,9 +693,9 @@ module Bundler end end - # Rubygems 1.8.0 to 1.8.4 + # RubyGems 1.8.0 to 1.8.4 class AlmostModern < Modern - # Rubygems [>= 1.8.0, < 1.8.5] has a bug that changes Gem.dir whenever + # RubyGems [>= 1.8.0, < 1.8.5] has a bug that changes Gem.dir whenever # you call Gem::Installer#install with an :install_dir set. We have to # change it back for our sudo mode to work. def preserve_paths @@ -680,9 +706,9 @@ module Bundler end end - # Rubygems 1.8.20+ + # RubyGems 1.8.20+ class MoreModern < Modern - # Rubygems 1.8.20 and adds the skip_validation parameter, so that's + # RubyGems 1.8.20 and adds the skip_validation parameter, so that's # when we start passing it through. def build(spec, skip_validation = false) require "rubygems/builder" @@ -690,7 +716,7 @@ module Bundler end end - # Rubygems 2.0 + # RubyGems 2.0 class Future < RubygemsIntegration def stub_rubygems(specs) Gem::Specification.all = specs @@ -767,6 +793,10 @@ module Bundler def install_with_build_args(args) yield end + + def path_separator + Gem.path_separator + end end # RubyGems 2.1.0 @@ -855,7 +885,7 @@ module Bundler RubygemsIntegration::Transitional.new elsif RubygemsIntegration.provides?(">= 1.4.0") RubygemsIntegration::Legacy.new - else # Rubygems 1.3.6 and 1.3.7 + else # RubyGems 1.3.6 and 1.3.7 RubygemsIntegration::Ancient.new end end diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb index 5540509d74..f27597b854 100644 --- a/lib/bundler/runtime.rb +++ b/lib/bundler/runtime.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require "digest/sha1" module Bundler class Runtime @@ -11,7 +10,7 @@ module Bundler end def setup(*groups) - @definition.ensure_equivalent_gemfile_and_lockfile if Bundler.settings[:frozen] + @definition.ensure_equivalent_gemfile_and_lockfile if Bundler.frozen? groups.map!(&:to_sym) @@ -262,9 +261,6 @@ module Bundler end def setup_manpath - # Store original MANPATH for restoration later in with_clean_env() - ENV["BUNDLER_ORIG_MANPATH"] = ENV["MANPATH"] - # Add man/ subdirectories from activated bundles to MANPATH for man(1) manuals = $LOAD_PATH.map do |path| man_subdir = path.sub(/lib$/, "man") @@ -272,7 +268,7 @@ module Bundler end.compact return if manuals.empty? - ENV["MANPATH"] = manuals.concat( + Bundler::SharedHelpers.set_env "MANPATH", manuals.concat( ENV["MANPATH"].to_s.split(File::PATH_SEPARATOR) ).uniq.join(File::PATH_SEPARATOR) end diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index 1898738b7c..f33e9453be 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -1,60 +1,89 @@ # frozen_string_literal: true + require "uri" module Bundler class Settings autoload :Mirror, "bundler/mirror" autoload :Mirrors, "bundler/mirror" + autoload :Validator, "bundler/settings/validator" - BOOL_KEYS = %w( + BOOL_KEYS = %w[ + allow_bundler_dependency_conflicts + allow_deployment_source_credential_changes allow_offline_install + auto_clean_without_path auto_install cache_all cache_all_platforms + cache_command_is_package + console_command + default_install_uses_path + deployment + deployment_means_frozen disable_checksum_validation disable_exec_load disable_local_branch_check + disable_multisource disable_shared_gems disable_version_check + error_on_stderr force_ruby_platform + forget_cli_options frozen gem.coc gem.mit + global_gem_cache ignore_messages + init_gems_rb + list_command + lockfile_uses_separate_rubygems_sources major_deprecations no_install no_prune only_update_to_newer_versions + path.system plugins + prefer_gems_rb + print_only_version_number + setup_makes_kernel_gem_public silence_root_warning - ).freeze - - NUMBER_KEYS = %w( + skip_default_git_sources + specific_platform + suppress_install_using_messages + unlock_source_unlocks_spec + update_requires_all_flag + ].freeze + + NUMBER_KEYS = %w[ redirect retry ssl_verify_mode timeout - ).freeze + ].freeze + + ARRAY_KEYS = %w[ + with + without + ].freeze DEFAULT_CONFIG = { + :disable_version_check => true, :redirect => 5, :retry => 3, :timeout => 10, }.freeze - attr_accessor :cli_flags_given - def initialize(root = nil) @root = root @local_config = load_config(local_config_file) @global_config = load_config(global_config_file) - @cli_flags_given = false @temporary = {} end def [](name) key = key_for(name) - value = @temporary.fetch(name) do + value = @temporary.fetch(key) do @local_config.fetch(key) do ENV.fetch(key) do @global_config.fetch(key) do @@ -65,48 +94,59 @@ module Bundler converted_value(value, name) end - def []=(key, value) - if cli_flags_given + def set_command_option(key, value) + if Bundler.feature_flag.forget_cli_options? + temporary(key => value) + value + else command = if value.nil? "bundle config --delete #{key}" else "bundle config #{key} #{Array(value).join(":")}" end - Bundler::SharedHelpers.major_deprecation \ + Bundler::SharedHelpers.major_deprecation 2,\ "flags passed to commands " \ "will no longer be automatically remembered. Instead please set flags " \ "you want remembered between commands using `bundle config " \ "<setting name> <setting value>`, i.e. `#{command}`" + + set_local(key, value) end + end + + def set_command_option_if_given(key, value) + return if value.nil? + set_command_option(key, value) + end + + def set_local(key, value) local_config_file || raise(GemfileNotFound, "Could not locate Gemfile") + set_key(key, value, @local_config, local_config_file) end - alias_method :set_local, :[]= def temporary(update) - existing = Hash[update.map {|k, _| [k, @temporary[k]] }] - @temporary.update(update) + existing = Hash[update.map {|k, _| [k, @temporary[key_for(k)]] }] + update.each do |k, v| + set_key(k, v, @temporary, nil) + end return unless block_given? begin yield ensure - existing.each {|k, v| v.nil? ? @temporary.delete(k) : @temporary[k] = v } + existing.each {|k, v| set_key(k, v, @temporary, nil) } end end - def delete(key) - @local_config.delete(key_for(key)) - end - def set_global(key, value) set_key(key, value, @global_config, global_config_file) end def all - env_keys = ENV.keys.select {|k| k =~ /BUNDLE_.*/ } + env_keys = ENV.keys.grep(/\ABUNDLE_.+/) - keys = @global_config.keys | @local_config.keys | env_keys + keys = @temporary.keys | @global_config.keys | @local_config.keys | env_keys keys.map do |key| key.sub(/^BUNDLE_/, "").gsub(/__/, ".").downcase @@ -132,7 +172,7 @@ module Bundler def gem_mirrors all.inject(Mirrors.new) do |mirrors, k| - mirrors.parse(k, self[k]) if k =~ /^mirror\./ + mirrors.parse(k, self[k]) if k.start_with?("mirror.") mirrors end end @@ -140,6 +180,7 @@ module Bundler def locations(key) key = key_for(key) locations = {} + locations[:temporary] = @temporary[key] if @temporary.key?(key) locations[:local] = @local_config[key] if @local_config.key?(key) locations[:env] = ENV[key] if ENV[key] locations[:global] = @global_config[key] if @global_config.key?(key) @@ -151,6 +192,11 @@ module Bundler key = key_for(exposed_key) locations = [] + + if @temporary.key?(key) + locations << "Set for the current command: #{converted_value(@temporary[key], exposed_key).inspect}" + end + if @local_config.key?(key) locations << "Set for your local app (#{local_config_file}): #{converted_value(@local_config[key], exposed_key).inspect}" end @@ -167,37 +213,56 @@ module Bundler locations end - def without=(array) - set_array(:without, array) - end + # for legacy reasons, the ruby scope isnt appended when the setting comes from ENV or the global config, + # nor do we respect :disable_shared_gems + def path + key = key_for(:path) + path = ENV[key] || @global_config[key] + if path && !@temporary.key?(key) && !@local_config.key?(key) + return Path.new(path, false, false, false) + end - def with=(array) - set_array(:with, array) + system_path = self["path.system"] || (self[:disable_shared_gems] == false) + Path.new(self[:path], true, system_path, Bundler.feature_flag.default_install_uses_path?) end - def without - get_array(:without) - end + Path = Struct.new(:explicit_path, :append_ruby_scope, :system_path, :default_install_uses_path) do + def path + path = base_path + path = File.join(path, Bundler.ruby_scope) if append_ruby_scope && !use_system_gems? + path + end - def with - get_array(:with) - end + def use_system_gems? + return true if system_path + return false if explicit_path + !default_install_uses_path + end - # @local_config["BUNDLE_PATH"] should be prioritized over ENV["BUNDLE_PATH"] - def path - key = key_for(:path) - path = ENV[key] || @global_config[key] - return path if path && !@local_config.key?(key) + def base_path + path = explicit_path + path ||= ".bundle" unless use_system_gems? + path ||= Bundler.rubygems.gem_dir + path + end - if path = self[:path] - "#{path}/#{Bundler.ruby_scope}" - else - Bundler.rubygems.gem_dir + def validate! + return unless explicit_path && system_path + path = Bundler.settings.pretty_values_for(:path) + path.unshift(nil, "path:") unless path.empty? + system_path = Bundler.settings.pretty_values_for("path.system") + system_path.unshift(nil, "path.system:") unless system_path.empty? + disable_shared_gems = Bundler.settings.pretty_values_for(:disable_shared_gems) + disable_shared_gems.unshift(nil, "disable_shared_gems:") unless disable_shared_gems.empty? + raise InvalidOption, + "Using a custom path while using system gems is unsupported.\n#{path.join("\n")}\n#{system_path.join("\n")}\n#{disable_shared_gems.join("\n")}" end end def allow_sudo? - !@local_config.key?(key_for(:path)) + key = key_for(:path) + path_configured = @temporary.key?(key) || @local_config.key?(key) + !path_configured end def ignore_config? @@ -205,14 +270,17 @@ module Bundler end def app_cache_path - @app_cache_path ||= begin - path = self[:cache_path] || "vendor/cache" - raise InvalidOption, "Cache path must be relative to the bundle path" if path.start_with?("/") - path - end + @app_cache_path ||= self[:cache_path] || "vendor/cache" end - private + def validate! + all.each do |raw_key| + [@local_config, ENV, @global_config].each do |settings| + value = converted_value(settings[key_for(raw_key)], raw_key) + Validator.validate!(raw_key, value, settings.to_hash.dup) + end + end + end def key_for(key) key = Settings.normalize_uri(key).to_s if key.is_a?(String) && /https?:/ =~ key @@ -220,15 +288,17 @@ module Bundler "BUNDLE_#{key}" end + private + def parent_setting_for(name) - split_specfic_setting_for(name)[0] + split_specific_setting_for(name)[0] end - def specfic_gem_for(name) - split_specfic_setting_for(name)[1] + def specific_gem_for(name) + split_specific_setting_for(name)[1] end - def split_specfic_setting_for(name) + def split_specific_setting_for(name) name.split(".") end @@ -245,43 +315,57 @@ module Bundler end end - def is_num(value) - NUMBER_KEYS.include?(value.to_s) + def is_num(key) + NUMBER_KEYS.include?(key.to_s) end - def get_array(key) - self[key] ? self[key].split(":").map(&:to_sym) : [] + def is_array(key) + ARRAY_KEYS.include?(key.to_s) end - def set_array(key, array) - self[key] = (array.empty? ? nil : array.join(":")) if array + def to_array(value) + return [] unless value + value.split(":").map(&:to_sym) end - def set_key(key, value, hash, file) - key = key_for(key) + def array_to_s(array) + array = Array(array) + return nil if array.empty? + array.join(":").tr(" ", ":") + end - unless hash[key] == value - hash[key] = value - hash.delete(key) if value.nil? - SharedHelpers.filesystem_access(file) do |p| - FileUtils.mkdir_p(p.dirname) - require "bundler/yaml_serializer" - p.open("w") {|f| f.write(YAMLSerializer.dump(hash)) } - end - end + def set_key(raw_key, value, hash, file) + raw_key = raw_key.to_s + value = array_to_s(value) if is_array(raw_key) + + key = key_for(raw_key) + + return if hash[key] == value + + hash[key] = value + hash.delete(key) if value.nil? - value + Validator.validate!(raw_key, converted_value(value, raw_key), hash) + + return unless file + SharedHelpers.filesystem_access(file) do |p| + FileUtils.mkdir_p(p.dirname) + require "bundler/yaml_serializer" + p.open("w") {|f| f.write(YAMLSerializer.dump(hash)) } + end end def converted_value(value, key) - if value.nil? + if is_array(key) + to_array(value) + elsif value.nil? nil elsif is_bool(key) || value == "false" to_bool(value) elsif is_num(key) value.to_i else - value + value.to_s end end @@ -325,16 +409,34 @@ module Bundler end end + PER_URI_OPTIONS = %w[ + fallback_timeout + ].freeze + + NORMALIZE_URI_OPTIONS_PATTERN = + / + \A + (\w+\.)? # optional prefix key + (https?.*?) # URI + (\.#{Regexp.union(PER_URI_OPTIONS)})? # optional suffix key + \z + /ix + # TODO: duplicates Rubygems#normalize_uri # TODO: is this the correct place to validate mirror URIs? def self.normalize_uri(uri) uri = uri.to_s - uri = "#{uri}/" unless uri =~ %r{/\Z} + if uri =~ NORMALIZE_URI_OPTIONS_PATTERN + prefix = $1 + uri = $2 + suffix = $3 + end + uri = "#{uri}/" unless uri.end_with?("/") uri = URI(uri) unless uri.absolute? raise ArgumentError, format("Gem sources must be absolute. You provided '%s'.", uri) end - uri + "#{prefix}#{uri}#{suffix}" end end end diff --git a/lib/bundler/settings/validator.rb b/lib/bundler/settings/validator.rb new file mode 100644 index 0000000000..9aa1627fb2 --- /dev/null +++ b/lib/bundler/settings/validator.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module Bundler + class Settings + class Validator + class Rule + attr_reader :description + + def initialize(keys, description, &validate) + @keys = keys + @description = description + @validate = validate + end + + def validate!(key, value, settings) + instance_exec(key, value, settings, &@validate) + end + + def fail!(key, value, *reasons) + reasons.unshift @description + raise InvalidOption, "Setting `#{key}` to #{value.inspect} failed:\n#{reasons.map {|r| " - #{r}" }.join("\n")}" + end + + def set(settings, key, value, *reasons) + hash_key = k(key) + return if settings[hash_key] == value + reasons.unshift @description + Bundler.ui.info "Setting `#{key}` to #{value.inspect}, since #{reasons.join(", ")}" + if value.nil? + settings.delete(hash_key) + else + settings[hash_key] = value + end + end + + def k(key) + Bundler.settings.key_for(key) + end + end + + def self.rules + @rules ||= Hash.new {|h, k| h[k] = [] } + end + private_class_method :rules + + def self.rule(keys, description, &blk) + rule = Rule.new(keys, description, &blk) + keys.each {|k| rules[k] << rule } + end + private_class_method :rule + + def self.validate!(key, value, settings) + rules_to_validate = rules[key] + rules_to_validate.each {|rule| rule.validate!(key, value, settings) } + end + + rule %w[path path.system], "path and path.system are mutually exclusive" do |key, value, settings| + if key == "path" && value + set(settings, "path.system", nil) + elsif key == "path.system" && value + set(settings, :path, nil) + end + end + + rule %w[with without], "a group cannot be in both `with` & `without` simultaneously" do |key, value, settings| + with = settings.fetch(k(:with), "").split(":").map(&:to_sym) + without = settings.fetch(k(:without), "").split(":").map(&:to_sym) + + other_key = key == "with" ? :without : :with + other_setting = key == "with" ? without : with + + conflicting = with & without + if conflicting.any? + fail!(key, value, "`#{other_key}` is current set to #{other_setting.inspect}", "the `#{conflicting.join("`, `")}` groups conflict") + end + end + end + end +end diff --git a/lib/bundler/setup.rb b/lib/bundler/setup.rb index 9aae6478cd..ac6a5bf861 100644 --- a/lib/bundler/setup.rb +++ b/lib/bundler/setup.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "bundler/shared_helpers" if Bundler::SharedHelpers.in_bundle? diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb index a9141a1346..5566bc5832 100644 --- a/lib/bundler/shared_helpers.rb +++ b/lib/bundler/shared_helpers.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true + +require "bundler/compatibility_guard" + require "pathname" require "rubygems" +require "bundler/version" require "bundler/constants" require "bundler/rubygems_integration" require "bundler/current_ruby" @@ -19,10 +23,16 @@ end module Bundler module SharedHelpers - def default_gemfile + def root gemfile = find_gemfile raise GemfileNotFound, "Could not locate Gemfile" unless gemfile - Pathname.new(gemfile).untaint + Pathname.new(gemfile).untaint.expand_path.parent + end + + def default_gemfile + gemfile = find_gemfile(:order_matters) + raise GemfileNotFound, "Could not locate Gemfile" unless gemfile + Pathname.new(gemfile).untaint.expand_path end def default_lockfile @@ -63,7 +73,7 @@ module Bundler end def with_clean_git_env(&block) - keys = %w(GIT_DIR GIT_WORK_TREE) + keys = %w[GIT_DIR GIT_WORK_TREE] old_env = keys.inject({}) do |h, k| h.update(k => ENV[k]) end @@ -129,20 +139,34 @@ module Bundler namespace.const_get(constant_name) end - def major_deprecation(message) + def major_deprecation(major_version, message) + if Bundler.bundler_major_version >= major_version + require "bundler/errors" + raise DeprecatedError, "[REMOVED FROM #{major_version}.0] #{message}" + end + return unless prints_major_deprecations? @major_deprecation_ui ||= Bundler::UI::Shell.new("no-color" => true) ui = Bundler.ui.is_a?(@major_deprecation_ui.class) ? Bundler.ui : @major_deprecation_ui - ui.warn("[DEPRECATED FOR #{Bundler::VERSION.split(".").first.to_i + 1}.0] #{message}") + ui.warn("[DEPRECATED FOR #{major_version}.0] #{message}") end def print_major_deprecations! - deprecate_gemfile(find_gemfile) if find_gemfile == find_file("Gemfile") + multiple_gemfiles = search_up(".") do |dir| + gemfiles = gemfile_names.select {|gf| File.file? File.expand_path(gf, dir) } + next if gemfiles.empty? + break false if gemfiles.size == 1 + end + if multiple_gemfiles && Bundler.bundler_major_version == 1 + Bundler::SharedHelpers.major_deprecation 2, \ + "gems.rb and gems.locked will be preferred to Gemfile and Gemfile.lock." + end + if RUBY_VERSION < "2" - major_deprecation("Bundler will only support ruby >= 2.0, you are running #{RUBY_VERSION}") + major_deprecation(2, "Bundler will only support ruby >= 2.0, you are running #{RUBY_VERSION}") end return if Bundler.rubygems.provides?(">= 2") - major_deprecation("Bundler will only support rubygems >= 2.0, you are running #{Bundler.rubygems.version}") + major_deprecation(2, "Bundler will only support rubygems >= 2.0, you are running #{Bundler.rubygems.version}") end def trap(signal, override = false, &block) @@ -170,23 +194,59 @@ module Bundler "\nEither installing with `--full-index` or running `bundle update #{spec.name}` should fix the problem." end + def pretty_dependency(dep, print_source = false) + msg = String.new(dep.name) + msg << " (#{dep.requirement})" unless dep.requirement == Gem::Requirement.default + if dep.is_a?(Bundler::Dependency) + platform_string = dep.platforms.join(", ") + msg << " " << platform_string if !platform_string.empty? && platform_string != Gem::Platform::RUBY + end + msg << " from the `#{dep.source}` source" if print_source && dep.source + msg + end + + def md5_available? + return @md5_available if defined?(@md5_available) + @md5_available = begin + require "openssl" + OpenSSL::Digest::MD5.digest("") + true + rescue LoadError + true + rescue OpenSSL::Digest::DigestError + false + end + end + + def digest(name) + require "digest" + Digest(name) + end + private def validate_bundle_path - return unless Bundler.bundle_path.to_s.include?(File::PATH_SEPARATOR) - message = "Your bundle path contains a '#{File::PATH_SEPARATOR}', " \ + path_separator = Bundler.rubygems.path_separator + return unless Bundler.bundle_path.to_s.split(path_separator).size > 1 + message = "Your bundle path contains text matching #{path_separator.inspect}, " \ "which is the path separator for your system. Bundler cannot " \ "function correctly when the Bundle path contains the " \ "system's PATH separator. Please change your " \ - "bundle path to not include '#{File::PATH_SEPARATOR}'." \ + "bundle path to not match #{path_separator.inspect}." \ "\nYour current bundle path is '#{Bundler.bundle_path}'." raise Bundler::PathError, message end - def find_gemfile + def find_gemfile(order_matters = false) given = ENV["BUNDLE_GEMFILE"] return given if given && !given.empty? - find_file("Gemfile", "gems.rb") + names = gemfile_names + names.reverse! if order_matters && Bundler.feature_flag.prefer_gems_rb? + find_file(*names) + end + + def gemfile_names + ["Gemfile", "gems.rb"] end def find_file(*names) @@ -226,40 +286,51 @@ module Bundler end end + def set_env(key, value) + raise ArgumentError, "new key #{key}" unless EnvironmentPreserver::BUNDLER_KEYS.include?(key) + orig_key = "#{EnvironmentPreserver::BUNDLER_PREFIX}#{key}" + orig = ENV[key] + orig ||= EnvironmentPreserver::INTENTIONALLY_NIL + ENV[orig_key] ||= orig + + ENV[key] = value + end + public :set_env + def set_bundle_variables begin - ENV["BUNDLE_BIN_PATH"] = Bundler.rubygems.bin_path("bundler", "bundle", VERSION) + Bundler::SharedHelpers.set_env "BUNDLE_BIN_PATH", Bundler.rubygems.bin_path("bundler", "bundle", VERSION) rescue Gem::GemNotFoundException if File.exist?(File.expand_path("../../../exe/bundle", __FILE__)) - ENV["BUNDLE_BIN_PATH"] = File.expand_path("../../../exe/bundle", __FILE__) + Bundler::SharedHelpers.set_env "BUNDLE_BIN_PATH", File.expand_path("../../../exe/bundle", __FILE__) else - ENV["BUNDLE_BIN_PATH"] = File.expand_path("../../../../bin/bundle", __FILE__) + Bundler::SharedHelpers.set_env "BUNDLE_BIN_PATH", File.expand_path("../../../../bin/bundle", __FILE__) end end # Set BUNDLE_GEMFILE - ENV["BUNDLE_GEMFILE"] = find_gemfile.to_s - ENV["BUNDLER_VERSION"] = Bundler::VERSION + Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", find_gemfile(:order_matters).to_s + Bundler::SharedHelpers.set_env "BUNDLER_VERSION", Bundler::VERSION end def set_path validate_bundle_path paths = (ENV["PATH"] || "").split(File::PATH_SEPARATOR) paths.unshift "#{Bundler.bundle_path}/bin" - ENV["PATH"] = paths.uniq.join(File::PATH_SEPARATOR) + Bundler::SharedHelpers.set_env "PATH", paths.uniq.join(File::PATH_SEPARATOR) end def set_rubyopt rubyopt = [ENV["RUBYOPT"]].compact return if !rubyopt.empty? && rubyopt.first =~ %r{-rbundler/setup} rubyopt.unshift %(-rbundler/setup) - ENV["RUBYOPT"] = rubyopt.join(" ") + Bundler::SharedHelpers.set_env "RUBYOPT", rubyopt.join(" ") end def set_rubylib rubylib = (ENV["RUBYLIB"] || "").split(File::PATH_SEPARATOR) rubylib.unshift bundler_ruby_lib - ENV["RUBYLIB"] = rubylib.uniq.join(File::PATH_SEPARATOR) + Bundler::SharedHelpers.set_env "RUBYLIB", rubylib.uniq.join(File::PATH_SEPARATOR) end def bundler_ruby_lib @@ -290,12 +361,6 @@ module Bundler true end - def deprecate_gemfile(gemfile) - return unless gemfile && File.basename(gemfile) == "Gemfile" - Bundler::SharedHelpers.major_deprecation \ - "gems.rb and gems.locked will be preferred to Gemfile and Gemfile.lock." - end - extend self end end diff --git a/lib/bundler/similarity_detector.rb b/lib/bundler/similarity_detector.rb index e9c1413ea3..b7f3ee7afa 100644 --- a/lib/bundler/similarity_detector.rb +++ b/lib/bundler/similarity_detector.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class SimilarityDetector SimilarityScore = Struct.new(:string, :distance) diff --git a/lib/bundler/source.rb b/lib/bundler/source.rb index cf56ed1cc1..5a1f05098b 100644 --- a/lib/bundler/source.rb +++ b/lib/bundler/source.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true + module Bundler class Source autoload :Gemspec, "bundler/source/gemspec" autoload :Git, "bundler/source/git" + autoload :Metadata, "bundler/source/metadata" autoload :Path, "bundler/source/path" autoload :Rubygems, "bundler/source/rubygems" @@ -31,6 +33,15 @@ module Bundler spec.source == self end + # it's possible that gems from one source depend on gems from some + # other source, so now we download gemspecs and iterate over those + # dependencies, looking for gems we don't have info on yet. + def double_check_for(*); end + + def dependency_names_to_double_check + specs.dependency_names + end + def include?(other) other == self end @@ -39,6 +50,10 @@ module Bundler "#<#{self.class}:0x#{object_id} #{self}>" end + def path? + instance_of?(Bundler::Source::Path) + end + private def version_color(spec_version, locked_spec_version) @@ -54,5 +69,26 @@ module Bundler def earlier_version?(spec_version, locked_spec_version) Gem::Version.new(spec_version) < Gem::Version.new(locked_spec_version) end + + def print_using_message(message) + if !message.include?("(was ") && Bundler.feature_flag.suppress_install_using_messages? + Bundler.ui.debug message + else + Bundler.ui.info message + end + end + + def extension_cache_path(spec) + return unless Bundler.feature_flag.global_gem_cache? + return unless source_slug = extension_cache_slug(spec) + Bundler.user_cache.join( + "extensions", Gem::Platform.local.to_s, Bundler.ruby_scope, + source_slug, spec.full_name + ) + end + + def extension_cache_slug(_) + nil + end end end diff --git a/lib/bundler/source/gemspec.rb b/lib/bundler/source/gemspec.rb index 05e613277f..7e3447e776 100644 --- a/lib/bundler/source/gemspec.rb +++ b/lib/bundler/source/gemspec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class Source class Gemspec < Path diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb index b3e218e390..a1a59ddce5 100644 --- a/lib/bundler/source/git.rb +++ b/lib/bundler/source/git.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require "fileutils" + +require "bundler/vendored_fileutils" require "uri" -require "digest/sha1" module Bundler class Source @@ -18,7 +18,7 @@ module Bundler @allow_remote = false # Stringify options that could be set as symbols - %w(ref branch tag revision).each {|k| options[k] = options[k].to_s if options[k] } + %w[ref branch tag revision].each {|k| options[k] = options[k].to_s if options[k] } @uri = options["uri"] || "" @branch = options["branch"] @@ -39,7 +39,7 @@ module Bundler out = String.new("GIT\n") out << " remote: #{@uri}\n" out << " revision: #{revision}\n" - %w(ref branch tag submodules).each do |opt| + %w[ref branch tag submodules].each do |opt| out << " #{opt}: #{options[opt]}\n" if options[opt] end out << " glob: #{@glob}\n" unless @glob == DEFAULT_GLOB @@ -169,15 +169,13 @@ module Bundler def install(spec, options = {}) force = options[:force] - Bundler.ui.info "Using #{version_message(spec)} from #{self}" + print_using_message "Using #{version_message(spec)} from #{self}" - if requires_checkout? && !@copied && !force + if (requires_checkout? && !@copied) || force Bundler.ui.debug " * Checking out revision: #{ref}" git_proxy.copy_to(install_path, submodules) serialize_gemspecs_in(install_path) @copied = true - elsif force - git_proxy.copy_to(install_path, submodules) end generate_bin_options = { :disable_extensions => !Bundler.rubygems.spec_missing_extensions?(spec), :build_args => options[:build_args] } @@ -188,7 +186,7 @@ module Bundler def cache(spec, custom_path = nil) app_cache_path = app_cache_path(custom_path) - return unless Bundler.settings[:cache_all] + return unless Bundler.feature_flag.cache_all? return if path == app_cache_path cached! FileUtils.rm_rf(app_cache_path) @@ -210,13 +208,11 @@ module Bundler # When using local git repos, this is set to the local repo. def cache_path @cache_path ||= begin - git_scope = "#{base_name}-#{uri_hash}" - - if Bundler.requires_sudo? - Bundler.user_bundle_path.join("cache/git", git_scope) + if Bundler.requires_sudo? || Bundler.feature_flag.global_gem_cache? + Bundler.user_cache else - Bundler.cache.join("git", git_scope) - end + Bundler.bundle_path.join("cache", "bundler") + end.join("git", git_scope) end end @@ -287,7 +283,7 @@ module Bundler # If there is no URI scheme, assume it is an ssh/git URI input = uri end - Digest::SHA1.hexdigest(input) + SharedHelpers.digest(:SHA1).hexdigest(input) end def cached_revision @@ -304,9 +300,9 @@ module Bundler def fetch git_proxy.checkout - rescue GitError + rescue GitError => e raise unless Bundler.feature_flag.allow_offline_install? - Bundler.ui.warn "Using cached git data because of network errors" + Bundler.ui.warn "Using cached git data because of network errors:\n#{e}" end # no-op, since we validate when re-serializing the gemspec @@ -319,6 +315,14 @@ module Bundler StubSpecification.from_stub(stub) end end + + def git_scope + "#{base_name}-#{uri_hash}" + end + + def extension_cache_slug(_) + extension_dir_name + end end end end diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb index c05d7a5afa..c56dda66ea 100644 --- a/lib/bundler/source/git/git_proxy.rb +++ b/lib/bundler/source/git/git_proxy.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "shellwords" require "tempfile" module Bundler @@ -62,7 +63,7 @@ module Bundler begin @revision ||= find_local_revision rescue GitCommandError - raise MissingGitRevisionError.new(ref, uri) + raise MissingGitRevisionError.new(ref, URICredentialsFilter.credential_filtered_uri(uri)) end @revision @@ -90,18 +91,21 @@ module Bundler end def checkout - if path.exist? - return if has_revision_cached? - Bundler.ui.info "Fetching #{URICredentialsFilter.credential_filtered_uri(uri)}" - in_path do - git_retry %(fetch --force --quiet --tags #{uri_escaped_with_configured_credentials} "refs/heads/*:refs/heads/*") - end - else - Bundler.ui.info "Fetching #{URICredentialsFilter.credential_filtered_uri(uri)}" + return if path.exist? && has_revision_cached? + extra_ref = "#{Shellwords.shellescape(ref)}:#{Shellwords.shellescape(ref)}" if ref && ref.start_with?("refs/") + + Bundler.ui.info "Fetching #{URICredentialsFilter.credential_filtered_uri(uri)}" + + unless path.exist? SharedHelpers.filesystem_access(path.dirname) do |p| FileUtils.mkdir_p(p) end git_retry %(clone #{uri_escaped_with_configured_credentials} "#{path}" --bare --no-hardlinks --quiet) + return unless extra_ref + end + + in_path do + git_retry %(fetch --force --quiet --tags #{uri_escaped_with_configured_credentials} "refs/heads/*:refs/heads/*" #{extra_ref}) end end @@ -149,7 +153,7 @@ module Bundler end def git_retry(command) - Bundler::Retry.new("`git #{command}`", GitNotAllowedError).attempts do + Bundler::Retry.new("`git #{URICredentialsFilter.credential_filtered_string(command, uri)}`", GitNotAllowedError).attempts do git(command) end end @@ -217,6 +221,7 @@ module Bundler def in_path(&blk) checkout unless path.exist? + _ = URICredentialsFilter # load it before we chdir SharedHelpers.chdir(path, &blk) end diff --git a/lib/bundler/source/metadata.rb b/lib/bundler/source/metadata.rb new file mode 100644 index 0000000000..93909002c7 --- /dev/null +++ b/lib/bundler/source/metadata.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Bundler + class Source + class Metadata < Source + def specs + @specs ||= Index.build do |idx| + idx << Gem::Specification.new("ruby\0", RubyVersion.system.to_gem_version_with_patchlevel) + idx << Gem::Specification.new("rubygems\0", Gem::VERSION) + + idx << Gem::Specification.new do |s| + s.name = "bundler" + s.version = VERSION + s.platform = Gem::Platform::RUBY + s.source = self + s.authors = ["bundler team"] + s.bindir = "exe" + s.executables = %w[bundle] + # can't point to the actual gemspec or else the require paths will be wrong + s.loaded_from = File.expand_path("..", __FILE__) + end + if loaded_spec = nil && Bundler.rubygems.loaded_specs("bundler") + idx << loaded_spec # this has to come after the fake gemspec, to override it + elsif local_spec = Bundler.rubygems.find_name("bundler").find {|s| s.version.to_s == VERSION } + idx << local_spec + end + + idx.each {|s| s.source = self } + end + end + + def cached!; end + + def remote!; end + + def options + {} + end + + def install(spec, _opts = {}) + print_using_message "Using #{version_message(spec)}" + nil + end + + def to_s + "the local ruby installation" + end + + def ==(other) + self.class == other.class + end + alias_method :eql?, :== + + def hash + self.class.hash + end + + def version_message(spec) + "#{spec.name} #{spec.version}" + end + end + end +end diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb index 8dd0763cc1..ed734bf549 100644 --- a/lib/bundler/source/path.rb +++ b/lib/bundler/source/path.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class Source class Path < Source @@ -35,10 +36,12 @@ module Bundler end def remote! + @local_specs = nil @allow_remote = true end def cached! + @local_specs = nil @allow_cached = true end @@ -74,14 +77,14 @@ module Bundler end def install(spec, options = {}) - Bundler.ui.info "Using #{version_message(spec)} from #{self}" + print_using_message "Using #{version_message(spec)} from #{self}" generate_bin(spec, :disable_extensions => true) nil # no post-install message end def cache(spec, custom_path = nil) app_cache_path = app_cache_path(custom_path) - return unless Bundler.settings[:cache_all] + return unless Bundler.feature_flag.cache_all? return if expand(@original_path).to_s.index(root_path.to_s + "/") == 0 unless @original_path.exist? @@ -113,10 +116,6 @@ module Bundler Bundler.root end - def is_a_path? - instance_of?(Path) - end - def expanded_original_path @expanded_original_path ||= expand(original_path) end @@ -228,7 +227,8 @@ module Bundler spec, :env_shebang => false, :disable_extensions => options[:disable_extensions], - :build_args => options[:build_args] + :build_args => options[:build_args], + :bundler_extension_cache_path => extension_cache_path(spec) ) installer.post_install rescue Gem::InvalidSpecificationException => e @@ -242,7 +242,7 @@ module Bundler "to modify their .gemspec so it can work with `gem build`." end - Bundler.ui.warn "The validation message from Rubygems was:\n #{e.message}" + Bundler.ui.warn "The validation message from RubyGems was:\n #{e.message}" end end end diff --git a/lib/bundler/source/path/installer.rb b/lib/bundler/source/path/installer.rb index 9c2f74a31b..a0357ffa39 100644 --- a/lib/bundler/source/path/installer.rb +++ b/lib/bundler/source/path/installer.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class Source class Path @@ -6,6 +7,7 @@ module Bundler attr_reader :spec def initialize(spec, options = {}) + @options = options @spec = spec @gem_dir = Bundler.rubygems.path(spec.full_gem_path) @wrappers = true diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index 353194f53f..6f4157364f 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "uri" require "rubygems/user_interaction" @@ -31,6 +32,7 @@ module Bundler end def cached! + @specs = nil @allow_cached = true end @@ -49,6 +51,7 @@ module Bundler end def can_lock?(spec) + return super if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? spec.source.is_a?(Rubygems) end @@ -69,8 +72,12 @@ module Bundler end def to_s - remote_names = remotes.map(&:to_s).join(", ") - "rubygems repository #{remote_names}" + if remotes.empty? + "locally installed gems" + else + remote_names = remotes.map(&:to_s).join(", ") + "rubygems repository #{remote_names} or installed locally" + end end alias_method :name, :to_s @@ -99,8 +106,8 @@ module Bundler end end - if installed?(spec) && (!force || spec.name.eql?("bundler")) - Bundler.ui.info "Using #{version_message(spec)}" + if installed?(spec) && !force + print_using_message "Using #{version_message(spec)}" return nil # no post-install message end @@ -141,7 +148,8 @@ module Bundler :wrappers => true, :env_shebang => true, :build_args => opts[:build_args], - :bundler_expected_checksum => spec.respond_to?(:checksum) && spec.checksum + :bundler_expected_checksum => spec.respond_to?(:checksum) && spec.checksum, + :bundler_extension_cache_path => extension_cache_path(spec) ).install end spec.full_gem_path = installed_spec.full_gem_path @@ -212,13 +220,21 @@ module Bundler @remotes.unshift(uri) unless @remotes.include?(uri) end - def replace_remotes(other_remotes) + def equivalent_remotes?(other_remotes) + other_remotes.map(&method(:remove_auth)) == @remotes.map(&method(:remove_auth)) + end + + def replace_remotes(other_remotes, allow_equivalent = false) return false if other_remotes == @remotes + equivalent = allow_equivalent && equivalent_remotes?(other_remotes) + @remotes = [] other_remotes.reverse_each do |r| add_remote r.to_s end + + !equivalent end def unmet_deps @@ -236,6 +252,43 @@ module Bundler end end + def double_check_for(unmet_dependency_names, override_dupes = false, index = specs) + return unless @allow_remote + raise ArgumentError, "missing index" unless index + + return unless api_fetchers.any? + + unmet_dependency_names = unmet_dependency_names.call + unless unmet_dependency_names.nil? + if api_fetchers.size <= 1 + # can't do this when there are multiple fetchers because then we might not fetch from _all_ + # of them + unmet_dependency_names -= remote_specs.spec_names # avoid re-fetching things we've already gotten + end + return if unmet_dependency_names.empty? + end + + Bundler.ui.debug "Double checking for #{unmet_dependency_names || "all specs (due to the size of the request)"} in #{self}" + + fetch_names(api_fetchers, unmet_dependency_names, index, override_dupes) + end + + def dependency_names_to_double_check + names = [] + remote_specs.each do |spec| + case spec + when EndpointSpecification, Gem::Specification, StubSpecification, LazySpecification + names.concat(spec.runtime_dependencies) + when RemoteSpecification # from the full index + return nil + else + raise "unhandled spec type (#{spec.inspect})" + end + end + names.map!(&:name) if names + names + end + protected def credless_remotes @@ -276,7 +329,7 @@ module Bundler end def suppress_configured_credentials(remote) - remote_nouser = remote.dup.tap {|uri| uri.user = uri.password = nil }.to_s + remote_nouser = remove_auth(remote) if remote.userinfo && remote.userinfo == Bundler.settings[remote_nouser] remote_nouser else @@ -284,15 +337,14 @@ module Bundler end end + def remove_auth(remote) + remote.dup.tap {|uri| uri.user = uri.password = nil }.to_s + end + def installed_specs - @installed_specs ||= begin - idx = Index.new - have_bundler = false + @installed_specs ||= Index.build do |idx| Bundler.rubygems.all_specs.reverse_each do |spec| - if spec.name == "bundler" - next unless spec.version.to_s == VERSION - have_bundler = true - end + next if spec.name == "bundler" spec.source = self if Bundler.rubygems.spec_missing_extensions?(spec, false) Bundler.ui.debug "Source #{self} is ignoring #{spec} because it is missing extensions" @@ -300,23 +352,6 @@ module Bundler end idx << spec end - - # Always have bundler locally - unless have_bundler - # We're running bundler directly from the source - # so, let's create a fake gemspec for it (it's a path) - # gemspec - bundler = Gem::Specification.new do |s| - s.name = "bundler" - s.version = VERSION - s.platform = Gem::Platform::RUBY - s.source = self - s.authors = ["bundler team"] - s.loaded_from = File.expand_path("..", __FILE__) - end - idx << bundler - end - idx end end @@ -334,9 +369,9 @@ module Bundler end idx << s end - end - idx + idx + end end def api_fetchers @@ -348,71 +383,36 @@ module Bundler index_fetchers = fetchers - api_fetchers # gather lists from non-api sites - index_fetchers.each do |f| - Bundler.ui.info "Fetching source index from #{f.uri}" - idx.use f.specs_with_retry(nil, self) - end + fetch_names(index_fetchers, nil, idx, false) # because ensuring we have all the gems we need involves downloading # the gemspecs of those gems, if the non-api sites contain more than - # about 100 gems, we treat all sites as non-api for speed. + # about 500 gems, we treat all sites as non-api for speed. allow_api = idx.size < API_REQUEST_LIMIT && dependency_names.size < API_REQUEST_LIMIT Bundler.ui.debug "Need to query more than #{API_REQUEST_LIMIT} gems." \ " Downloading full index instead..." unless allow_api - if allow_api - api_fetchers.each do |f| - Bundler.ui.info "Fetching gem metadata from #{f.uri}", Bundler.ui.debug? - idx.use f.specs_with_retry(dependency_names, self) - Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over - end - - # Suppose the gem Foo depends on the gem Bar. Foo exists in Source A. Bar has some versions that exist in both - # sources A and B. At this point, the API request will have found all the versions of Bar in source A, - # but will not have found any versions of Bar from source B, which is a problem if the requested version - # of Foo specifically depends on a version of Bar that is only found in source B. This ensures that for - # each spec we found, we add all possible versions from all sources to the index. - loop do - idxcount = idx.size - api_fetchers.each do |f| - Bundler.ui.info "Fetching version metadata from #{f.uri}", Bundler.ui.debug? - idx.use f.specs_with_retry(idx.dependency_names, self), true - Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over - end - break if idxcount == idx.size - end - - if api_fetchers.any? - # it's possible that gems from one source depend on gems from some - # other source, so now we download gemspecs and iterate over those - # dependencies, looking for gems we don't have info on yet. - unmet = idx.unmet_dependency_names - - # if there are any cross-site gems we missed, get them now - api_fetchers.each do |f| - Bundler.ui.info "Fetching dependency metadata from #{f.uri}", Bundler.ui.debug? - idx.use f.specs_with_retry(unmet, self) - Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over - end if unmet.any? - else - allow_api = false - end - end + fetch_names(api_fetchers, allow_api && dependency_names, idx, false) + end + end - unless allow_api - api_fetchers.each do |f| - Bundler.ui.info "Fetching source index from #{f.uri}" - idx.use f.specs_with_retry(nil, self) - end + def fetch_names(fetchers, dependency_names, index, override_dupes) + fetchers.each do |f| + if dependency_names + Bundler.ui.info "Fetching gem metadata from #{f.uri}", Bundler.ui.debug? + index.use f.specs_with_retry(dependency_names, self), override_dupes + Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over + else + Bundler.ui.info "Fetching source index from #{f.uri}" + index.use f.specs_with_retry(nil, self), override_dupes end end end def fetch_gem(spec) return false unless spec.remote - uri = spec.remote.uri + spec.fetch_platform - Bundler.ui.confirm("Fetching #{version_message(spec)}") download_path = requires_sudo? ? Bundler.tmp(spec.full_name) : rubygems_dir gem_path = "#{rubygems_dir}/cache/#{spec.full_name}.gem" @@ -420,7 +420,7 @@ module Bundler SharedHelpers.filesystem_access("#{download_path}/cache") do |p| FileUtils.mkdir_p(p) end - Bundler.rubygems.download_gem(spec, uri, download_path) + download_gem(spec, download_path) if requires_sudo? SharedHelpers.filesystem_access("#{rubygems_dir}/cache") do |p| @@ -457,6 +457,76 @@ module Bundler def cache_path Bundler.app_cache end + + private + + # Checks if the requested spec exists in the global cache. If it does, + # we copy it to the download path, and if it does not, we download it. + # + # @param [Specification] spec + # the spec we want to download or retrieve from the cache. + # + # @param [String] download_path + # the local directory the .gem will end up in. + # + def download_gem(spec, download_path) + local_path = File.join(download_path, "cache/#{spec.full_name}.gem") + + if (cache_path = download_cache_path(spec)) && cache_path.file? + SharedHelpers.filesystem_access(local_path) do + FileUtils.cp(cache_path, local_path) + end + else + uri = spec.remote.uri + Bundler.ui.confirm("Fetching #{version_message(spec)}") + Bundler.rubygems.download_gem(spec, uri, download_path) + cache_globally(spec, local_path) + end + end + + # Checks if the requested spec exists in the global cache. If it does + # not, we create the relevant global cache subdirectory if it does not + # exist and copy the spec from the local cache to the global cache. + # + # @param [Specification] spec + # the spec we want to copy to the global cache. + # + # @param [String] local_cache_path + # the local directory from which we want to copy the .gem. + # + def cache_globally(spec, local_cache_path) + return unless cache_path = download_cache_path(spec) + return if cache_path.exist? + + SharedHelpers.filesystem_access(cache_path.dirname, &:mkpath) + SharedHelpers.filesystem_access(cache_path) do + FileUtils.cp(local_cache_path, cache_path) + end + end + + # Returns the global cache path of the calling Rubygems::Source object. + # + # Note that the Source determines the path's subdirectory. We use this + # subdirectory in the global cache path so that gems with the same name + # -- and possibly different versions -- from different sources are saved + # to their respective subdirectories and do not override one another. + # + # @param [Gem::Specification] specification + # + # @return [Pathname] The global cache path. + # + def download_cache_path(spec) + return unless Bundler.feature_flag.global_gem_cache? + return unless remote = spec.remote + return unless cache_slug = remote.cache_slug + + Bundler.user_cache.join("gems", cache_slug, spec.file_name) + end + + def extension_cache_slug(spec) + return unless remote = spec.remote + remote.cache_slug + end end end end diff --git a/lib/bundler/source/rubygems/remote.rb b/lib/bundler/source/rubygems/remote.rb index b49e645506..e73baaa992 100644 --- a/lib/bundler/source/rubygems/remote.rb +++ b/lib/bundler/source/rubygems/remote.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler class Source class Rubygems @@ -20,10 +21,12 @@ module Bundler # def cache_slug @cache_slug ||= begin + return nil unless SharedHelpers.md5_available? + cache_uri = original_uri || uri uri_parts = [cache_uri.host, cache_uri.user, cache_uri.port, cache_uri.path] - uri_digest = Digest::MD5.hexdigest(uri_parts.compact.join(".")) + uri_digest = SharedHelpers.digest(:MD5).hexdigest(uri_parts.compact.join(".")) uri_parts[-1] = uri_digest uri_parts.compact.join(".") diff --git a/lib/bundler/source_list.rb b/lib/bundler/source_list.rb index b6ce6029c8..ac2adacb3d 100644 --- a/lib/bundler/source_list.rb +++ b/lib/bundler/source_list.rb @@ -1,16 +1,21 @@ # frozen_string_literal: true + module Bundler class SourceList attr_reader :path_sources, :git_sources, - :plugin_sources + :plugin_sources, + :global_rubygems_source, + :metadata_source def initialize - @path_sources = [] - @git_sources = [] - @plugin_sources = [] - @rubygems_aggregate = Source::Rubygems.new - @rubygems_sources = [] + @path_sources = [] + @git_sources = [] + @plugin_sources = [] + @global_rubygems_source = nil + @rubygems_aggregate = rubygems_aggregate_class.new + @rubygems_sources = [] + @metadata_source = Source::Metadata.new end def add_path_source(options = {}) @@ -35,13 +40,28 @@ module Bundler add_source_to_list Plugin.source(source).new(options), @plugin_sources end + def global_rubygems_source=(uri) + if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + @global_rubygems_source ||= rubygems_aggregate_class.new("remotes" => uri) + end + add_rubygems_remote(uri) + end + def add_rubygems_remote(uri) + if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + return if Bundler.feature_flag.disable_multisource? + raise InvalidOption, "`lockfile_uses_separate_rubygems_sources` cannot be set without `disable_multisource` being set" + end @rubygems_aggregate.add_remote(uri) @rubygems_aggregate end + def default_source + global_rubygems_source || @rubygems_aggregate + end + def rubygems_sources - @rubygems_sources + [@rubygems_aggregate] + @rubygems_sources + [default_source] end def rubygems_remotes @@ -49,18 +69,25 @@ module Bundler end def all_sources - path_sources + git_sources + plugin_sources + rubygems_sources + path_sources + git_sources + plugin_sources + rubygems_sources + [metadata_source] end def get(source) - source_list_for(source).find {|s| source == s } + source_list_for(source).find {|s| equal_source?(source, s) || equivalent_source?(source, s) } end def lock_sources - lock_sources = (path_sources + git_sources + plugin_sources).sort_by(&:to_s) - lock_sources << combine_rubygems_sources + if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + [[default_source], @rubygems_sources, git_sources, path_sources, plugin_sources].map do |sources| + sources.sort_by(&:to_s) + end.flatten(1) + else + lock_sources = (path_sources + git_sources + plugin_sources).sort_by(&:to_s) + lock_sources << combine_rubygems_sources + end end + # Returns true if there are changes def replace_sources!(replacement_sources) return true if replacement_sources.empty? @@ -70,13 +97,14 @@ module Bundler end end - replacement_rubygems = + replacement_rubygems = !Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? && replacement_sources.detect {|s| s.is_a?(Source::Rubygems) } @rubygems_aggregate = replacement_rubygems if replacement_rubygems - # Return true if there were changes - lock_sources.to_set != replacement_sources.to_set || - rubygems_remotes.to_set != replacement_rubygems.remotes.to_set + return true if !equal_sources?(lock_sources, replacement_sources) && !equivalent_sources?(lock_sources, replacement_sources) + return true if replacement_rubygems && rubygems_remotes.to_set != replacement_rubygems.remotes.to_set + + false end def cached! @@ -93,6 +121,10 @@ module Bundler private + def rubygems_aggregate_class + Source::Rubygems + end + def add_source_to_list(source, list) list.unshift(source).uniq! source @@ -122,5 +154,33 @@ module Bundler "protocol to keep your data secure." end end + + def equal_sources?(lock_sources, replacement_sources) + lock_sources.to_set == replacement_sources.to_set + end + + def equal_source?(source, other_source) + source == other_source + end + + def equivalent_source?(source, other_source) + return false unless Bundler.settings[:allow_deployment_source_credential_changes] && source.is_a?(Source::Rubygems) + + equivalent_rubygems_sources?([source], [other_source]) + end + + def equivalent_sources?(lock_sources, replacement_sources) + return false unless Bundler.settings[:allow_deployment_source_credential_changes] + + lock_rubygems_sources, lock_other_sources = lock_sources.partition {|s| s.is_a?(Source::Rubygems) } + replacement_rubygems_sources, replacement_other_sources = replacement_sources.partition {|s| s.is_a?(Source::Rubygems) } + + equivalent_rubygems_sources?(lock_rubygems_sources, replacement_rubygems_sources) && equal_sources?(lock_other_sources, replacement_other_sources) + end + + def equivalent_rubygems_sources?(lock_sources, replacement_sources) + actual_remotes = replacement_sources.map(&:remotes).flatten.uniq + lock_sources.all? {|s| s.equivalent_remotes?(actual_remotes) } + end end end diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb index 9642633578..7cd3021997 100644 --- a/lib/bundler/spec_set.rb +++ b/lib/bundler/spec_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "tsort" require "forwardable" require "set" @@ -76,7 +77,7 @@ module Bundler end def materialize(deps, missing_specs = nil) - materialized = self.for(deps, [], false, true, missing_specs).to_a + materialized = self.for(deps, [], false, true, !missing_specs).to_a deps = materialized.map(&:name).uniq materialized.map! do |s| next s unless s.is_a?(LazySpecification) @@ -109,9 +110,10 @@ module Bundler def merge(set) arr = sorted.dup - set.each do |s| - next if arr.any? {|s2| s2.name == s.name && s2.version == s.version && s2.platform == s.platform } - arr << s + set.each do |set_spec| + full_name = set_spec.full_name + next if arr.any? {|spec| spec.full_name == full_name } + arr << set_spec end SpecSet.new(arr) end diff --git a/lib/bundler/ssl_certs/certificate_manager.rb b/lib/bundler/ssl_certs/certificate_manager.rb index a5e5d84b64..26fc38ec18 100644 --- a/lib/bundler/ssl_certs/certificate_manager.rb +++ b/lib/bundler/ssl_certs/certificate_manager.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true -require "fileutils" + +require "bundler/vendored_fileutils" require "net/https" require "openssl" diff --git a/lib/bundler/stub_specification.rb b/lib/bundler/stub_specification.rb index aeacf245a3..0dd024024a 100644 --- a/lib/bundler/stub_specification.rb +++ b/lib/bundler/stub_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "bundler/remote_specification" module Bundler diff --git a/lib/bundler/templates/Executable b/lib/bundler/templates/Executable index fe22de0a6d..9289debc26 100755 --- a/lib/bundler/templates/Executable +++ b/lib/bundler/templates/Executable @@ -1,5 +1,6 @@ #!/usr/bin/env <%= Bundler.settings[:shebang] || RbConfig::CONFIG["ruby_install_name"] %> # frozen_string_literal: true + # # This file was generated by Bundler. # @@ -7,6 +8,9 @@ # this file is here to facilitate running it. # +bundle_binstub = File.expand_path("../bundle", __FILE__) +load(bundle_binstub) if File.file?(bundle_binstub) + require "pathname" ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../<%= relative_gemfile_path %>", Pathname.new(__FILE__).realpath) diff --git a/lib/bundler/templates/Executable.bundler b/lib/bundler/templates/Executable.bundler new file mode 100644 index 0000000000..eeda90b584 --- /dev/null +++ b/lib/bundler/templates/Executable.bundler @@ -0,0 +1,105 @@ +#!/usr/bin/env <%= Bundler.settings[:shebang] || RbConfig::CONFIG["ruby_install_name"] %> +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application '<%= executable %>' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 || ">= 0.a" + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../<%= relative_gemfile_path %>", __FILE__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_version + @bundler_version ||= begin + env_var_version || cli_arg_version || + lockfile_version || "#{Gem::Requirement.default}.a" + end + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + # must dup string for RG < 1.8 compatibility + activate_bundler(bundler_version.dup) + end + + def activate_bundler(bundler_version) + if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0") + bundler_version = "< 2" + end + gem_error = activation_error_handling do + gem "bundler", bundler_version + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("<%= spec.name %>", "<%= executable %>") +end diff --git a/lib/bundler/templates/Gemfile b/lib/bundler/templates/Gemfile index 21c6283123..1afd2cce67 100644 --- a/lib/bundler/templates/Gemfile +++ b/lib/bundler/templates/Gemfile @@ -1,4 +1,5 @@ # frozen_string_literal: true + source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } diff --git a/lib/bundler/templates/gems.rb b/lib/bundler/templates/gems.rb new file mode 100644 index 0000000000..547cd6e8d9 --- /dev/null +++ b/lib/bundler/templates/gems.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# A sample gems.rb +source "https://rubygems.org" + +git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } + +# gem "rails" diff --git a/lib/bundler/templates/newgem/README.md.tt b/lib/bundler/templates/newgem/README.md.tt index edbe55dabe..868a0afe67 100644 --- a/lib/bundler/templates/newgem/README.md.tt +++ b/lib/bundler/templates/newgem/README.md.tt @@ -37,7 +37,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/<%= co ## License -The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). +The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). <% end -%> <% if config[:coc] -%> diff --git a/lib/bundler/templates/newgem/gitignore.tt b/lib/bundler/templates/newgem/gitignore.tt index 573d76b4c2..b1c9f9986c 100644 --- a/lib/bundler/templates/newgem/gitignore.tt +++ b/lib/bundler/templates/newgem/gitignore.tt @@ -1,6 +1,5 @@ /.bundle/ /.yardoc -/Gemfile.lock /_yardoc/ /coverage/ /doc/ diff --git a/lib/bundler/templates/newgem/newgem.gemspec.tt b/lib/bundler/templates/newgem/newgem.gemspec.tt index caea7fe7be..9a87a1374a 100644 --- a/lib/bundler/templates/newgem/newgem.gemspec.tt +++ b/lib/bundler/templates/newgem/newgem.gemspec.tt @@ -1,4 +1,7 @@ +<%- if RUBY_VERSION < "2.0.0" -%> # coding: utf-8 +<%- end -%> + lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "<%= config[:namespaced_path] %>/version" @@ -9,7 +12,7 @@ Gem::Specification.new do |spec| spec.authors = [<%= config[:author].inspect %>] spec.email = [<%= config[:email].inspect %>] - spec.summary = %q{TODO: Write a short summary, because Rubygems requires one.} + spec.summary = %q{TODO: Write a short summary, because RubyGems requires one.} spec.description = %q{TODO: Write a longer description or delete this line.} spec.homepage = "TODO: Put your gem's website or public repo URL here." <%- if config[:mit] -%> diff --git a/lib/bundler/templates/newgem/rspec.tt b/lib/bundler/templates/newgem/rspec.tt index 8c18f1abdd..34c5164d9b 100644 --- a/lib/bundler/templates/newgem/rspec.tt +++ b/lib/bundler/templates/newgem/rspec.tt @@ -1,2 +1,3 @@ --format documentation --color +--require spec_helper diff --git a/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt b/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt index b7ef7f9e4a..c63b487830 100644 --- a/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt +++ b/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt @@ -1,5 +1,3 @@ -require "spec_helper" - RSpec.describe <%= config[:constant_name] %> do it "has a version number" do expect(<%= config[:constant_name] %>::VERSION).not_to be nil diff --git a/lib/bundler/ui.rb b/lib/bundler/ui.rb index 794c000dc4..8138b30d38 100644 --- a/lib/bundler/ui.rb +++ b/lib/bundler/ui.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler module UI autoload :RGProxy, "bundler/ui/rg_proxy" diff --git a/lib/bundler/ui/rg_proxy.rb b/lib/bundler/ui/rg_proxy.rb index 95a1ecdf0c..e2f98481db 100644 --- a/lib/bundler/ui/rg_proxy.rb +++ b/lib/bundler/ui/rg_proxy.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "bundler/ui" require "rubygems/user_interaction" diff --git a/lib/bundler/ui/shell.rb b/lib/bundler/ui/shell.rb index 87a92471fb..3b3b6bfb53 100644 --- a/lib/bundler/ui/shell.rb +++ b/lib/bundler/ui/shell.rb @@ -1,15 +1,16 @@ # frozen_string_literal: true + require "bundler/vendored_thor" module Bundler module UI class Shell - LEVELS = %w(silent error warn confirm info debug).freeze + LEVELS = %w[silent error warn confirm info debug].freeze attr_writer :shell def initialize(options = {}) - if options["no-color"] || !STDOUT.tty? + if options["no-color"] || !$stdout.tty? Thor::Base.shell = Thor::Shell::Basic end @shell = Thor::Base.shell.new @@ -30,13 +31,18 @@ module Bundler end def warn(msg, newline = nil) + return unless level("warn") return if @warning_history.include? msg @warning_history << msg - tell_me(msg, :yellow, newline) if level("warn") + + return tell_err(msg, :yellow, newline) if Bundler.feature_flag.error_on_stderr? + tell_me(msg, :yellow, newline) end def error(msg, newline = nil) - tell_me(msg, :red, newline) if level("error") + return unless level("error") + return tell_err(msg, :red, newline) if Bundler.feature_flag.error_on_stderr? + tell_me(msg, :red, newline) end def debug(msg, newline = nil) @@ -103,6 +109,11 @@ module Bundler end def tell_err(message, color = nil, newline = nil) + newline = message.to_s !~ /( |\t)\Z/ unless newline + message = word_wrap(message) if newline.is_a?(Hash) && newline[:wrap] + + color = nil if color && !$stderr.tty? + buffer = @shell.send(:prepare_message, message, *color) buffer << "\n" if newline && !message.to_s.end_with?("\n") diff --git a/lib/bundler/ui/silent.rb b/lib/bundler/ui/silent.rb index 48390b7198..dca1b2ac86 100644 --- a/lib/bundler/ui/silent.rb +++ b/lib/bundler/ui/silent.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler module UI class Silent diff --git a/lib/bundler/uri_credentials_filter.rb b/lib/bundler/uri_credentials_filter.rb index 997a307533..ee3692268c 100644 --- a/lib/bundler/uri_credentials_filter.rb +++ b/lib/bundler/uri_credentials_filter.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler module URICredentialsFilter module_function diff --git a/lib/bundler/vendor/fileutils/lib/fileutils.rb b/lib/bundler/vendor/fileutils/lib/fileutils.rb new file mode 100644 index 0000000000..cc69740845 --- /dev/null +++ b/lib/bundler/vendor/fileutils/lib/fileutils.rb @@ -0,0 +1,1638 @@ +# frozen_string_literal: true +# +# = fileutils.rb +# +# Copyright (c) 2000-2007 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the same terms of ruby. +# +# == module Bundler::FileUtils +# +# Namespace for several file utility methods for copying, moving, removing, etc. +# +# === Module Functions +# +# require 'bundler/vendor/fileutils/lib/fileutils' +# +# Bundler::FileUtils.cd(dir, options) +# Bundler::FileUtils.cd(dir, options) {|dir| block } +# Bundler::FileUtils.pwd() +# Bundler::FileUtils.mkdir(dir, options) +# Bundler::FileUtils.mkdir(list, options) +# Bundler::FileUtils.mkdir_p(dir, options) +# Bundler::FileUtils.mkdir_p(list, options) +# Bundler::FileUtils.rmdir(dir, options) +# Bundler::FileUtils.rmdir(list, options) +# Bundler::FileUtils.ln(target, link, options) +# Bundler::FileUtils.ln(targets, dir, options) +# Bundler::FileUtils.ln_s(target, link, options) +# Bundler::FileUtils.ln_s(targets, dir, options) +# Bundler::FileUtils.ln_sf(target, link, options) +# Bundler::FileUtils.cp(src, dest, options) +# Bundler::FileUtils.cp(list, dir, options) +# Bundler::FileUtils.cp_r(src, dest, options) +# Bundler::FileUtils.cp_r(list, dir, options) +# Bundler::FileUtils.mv(src, dest, options) +# Bundler::FileUtils.mv(list, dir, options) +# Bundler::FileUtils.rm(list, options) +# Bundler::FileUtils.rm_r(list, options) +# Bundler::FileUtils.rm_rf(list, options) +# Bundler::FileUtils.install(src, dest, options) +# Bundler::FileUtils.chmod(mode, list, options) +# Bundler::FileUtils.chmod_R(mode, list, options) +# Bundler::FileUtils.chown(user, group, list, options) +# Bundler::FileUtils.chown_R(user, group, list, options) +# Bundler::FileUtils.touch(list, options) +# +# The <tt>options</tt> parameter is a hash of options, taken from the list +# <tt>:force</tt>, <tt>:noop</tt>, <tt>:preserve</tt>, and <tt>:verbose</tt>. +# <tt>:noop</tt> means that no changes are made. The other three are obvious. +# Each method documents the options that it honours. +# +# All methods that have the concept of a "source" file or directory can take +# either one file or a list of files in that argument. See the method +# documentation for examples. +# +# There are some `low level' methods, which do not accept any option: +# +# Bundler::FileUtils.copy_entry(src, dest, preserve = false, dereference = false) +# Bundler::FileUtils.copy_file(src, dest, preserve = false, dereference = true) +# Bundler::FileUtils.copy_stream(srcstream, deststream) +# Bundler::FileUtils.remove_entry(path, force = false) +# Bundler::FileUtils.remove_entry_secure(path, force = false) +# Bundler::FileUtils.remove_file(path, force = false) +# Bundler::FileUtils.compare_file(path_a, path_b) +# Bundler::FileUtils.compare_stream(stream_a, stream_b) +# Bundler::FileUtils.uptodate?(file, cmp_list) +# +# == module Bundler::FileUtils::Verbose +# +# This module has all methods of Bundler::FileUtils module, but it outputs messages +# before acting. This equates to passing the <tt>:verbose</tt> flag to methods +# in Bundler::FileUtils. +# +# == module Bundler::FileUtils::NoWrite +# +# This module has all methods of Bundler::FileUtils module, but never changes +# files/directories. This equates to passing the <tt>:noop</tt> flag to methods +# in Bundler::FileUtils. +# +# == module Bundler::FileUtils::DryRun +# +# This module has all methods of Bundler::FileUtils module, but never changes +# files/directories. This equates to passing the <tt>:noop</tt> and +# <tt>:verbose</tt> flags to methods in Bundler::FileUtils. +# + +module Bundler::FileUtils + + def self.private_module_function(name) #:nodoc: + module_function name + private_class_method name + end + + # + # Returns the name of the current directory. + # + def pwd + Dir.pwd + end + module_function :pwd + + alias getwd pwd + module_function :getwd + + # + # Changes the current directory to the directory +dir+. + # + # If this method is called with block, resumes to the old + # working directory after the block execution finished. + # + # Bundler::FileUtils.cd('/', :verbose => true) # chdir and report it + # + # Bundler::FileUtils.cd('/') do # chdir + # # ... # do something + # end # return to original directory + # + def cd(dir, verbose: nil, &block) # :yield: dir + fu_output_message "cd #{dir}" if verbose + Dir.chdir(dir, &block) + fu_output_message 'cd -' if verbose and block + end + module_function :cd + + alias chdir cd + module_function :chdir + + # + # Returns true if +new+ is newer than all +old_list+. + # Non-existent files are older than any file. + # + # Bundler::FileUtils.uptodate?('hello.o', %w(hello.c hello.h)) or \ + # system 'make hello.o' + # + def uptodate?(new, old_list) + return false unless File.exist?(new) + new_time = File.mtime(new) + old_list.each do |old| + if File.exist?(old) + return false unless new_time > File.mtime(old) + end + end + true + end + module_function :uptodate? + + def remove_trailing_slash(dir) #:nodoc: + dir == '/' ? dir : dir.chomp(?/) + end + private_module_function :remove_trailing_slash + + # + # Creates one or more directories. + # + # Bundler::FileUtils.mkdir 'test' + # Bundler::FileUtils.mkdir %w( tmp data ) + # Bundler::FileUtils.mkdir 'notexist', :noop => true # Does not really create. + # Bundler::FileUtils.mkdir 'tmp', :mode => 0700 + # + def mkdir(list, mode: nil, noop: nil, verbose: nil) + list = fu_list(list) + fu_output_message "mkdir #{mode ? ('-m %03o ' % mode) : ''}#{list.join ' '}" if verbose + return if noop + + list.each do |dir| + fu_mkdir dir, mode + end + end + module_function :mkdir + + # + # Creates a directory and all its parent directories. + # For example, + # + # Bundler::FileUtils.mkdir_p '/usr/local/lib/ruby' + # + # causes to make following directories, if it does not exist. + # + # * /usr + # * /usr/local + # * /usr/local/lib + # * /usr/local/lib/ruby + # + # You can pass several directories at a time in a list. + # + def mkdir_p(list, mode: nil, noop: nil, verbose: nil) + list = fu_list(list) + fu_output_message "mkdir -p #{mode ? ('-m %03o ' % mode) : ''}#{list.join ' '}" if verbose + return *list if noop + + list.map {|path| remove_trailing_slash(path)}.each do |path| + # optimize for the most common case + begin + fu_mkdir path, mode + next + rescue SystemCallError + next if File.directory?(path) + end + + stack = [] + until path == stack.last # dirname("/")=="/", dirname("C:/")=="C:/" + stack.push path + path = File.dirname(path) + end + stack.pop # root directory should exist + stack.reverse_each do |dir| + begin + fu_mkdir dir, mode + rescue SystemCallError + raise unless File.directory?(dir) + end + end + end + + return *list + end + module_function :mkdir_p + + alias mkpath mkdir_p + alias makedirs mkdir_p + module_function :mkpath + module_function :makedirs + + def fu_mkdir(path, mode) #:nodoc: + path = remove_trailing_slash(path) + if mode + Dir.mkdir path, mode + File.chmod mode, path + else + Dir.mkdir path + end + end + private_module_function :fu_mkdir + + # + # Removes one or more directories. + # + # Bundler::FileUtils.rmdir 'somedir' + # Bundler::FileUtils.rmdir %w(somedir anydir otherdir) + # # Does not really remove directory; outputs message. + # Bundler::FileUtils.rmdir 'somedir', :verbose => true, :noop => true + # + def rmdir(list, parents: nil, noop: nil, verbose: nil) + list = fu_list(list) + fu_output_message "rmdir #{parents ? '-p ' : ''}#{list.join ' '}" if verbose + return if noop + list.each do |dir| + begin + Dir.rmdir(dir = remove_trailing_slash(dir)) + if parents + until (parent = File.dirname(dir)) == '.' or parent == dir + dir = parent + Dir.rmdir(dir) + end + end + rescue Errno::ENOTEMPTY, Errno::EEXIST, Errno::ENOENT + end + end + end + module_function :rmdir + + # + # :call-seq: + # Bundler::FileUtils.ln(target, link, force: nil, noop: nil, verbose: nil) + # Bundler::FileUtils.ln(target, dir, force: nil, noop: nil, verbose: nil) + # Bundler::FileUtils.ln(targets, dir, force: nil, noop: nil, verbose: nil) + # + # In the first form, creates a hard link +link+ which points to +target+. + # If +link+ already exists, raises Errno::EEXIST. + # But if the :force option is set, overwrites +link+. + # + # Bundler::FileUtils.ln 'gcc', 'cc', verbose: true + # Bundler::FileUtils.ln '/usr/bin/emacs21', '/usr/bin/emacs' + # + # In the second form, creates a link +dir/target+ pointing to +target+. + # In the third form, creates several hard links in the directory +dir+, + # pointing to each item in +targets+. + # If +dir+ is not a directory, raises Errno::ENOTDIR. + # + # Bundler::FileUtils.cd '/sbin' + # Bundler::FileUtils.ln %w(cp mv mkdir), '/bin' # Now /sbin/cp and /bin/cp are linked. + # + def ln(src, dest, force: nil, noop: nil, verbose: nil) + fu_output_message "ln#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose + return if noop + fu_each_src_dest0(src, dest) do |s,d| + remove_file d, true if force + File.link s, d + end + end + module_function :ln + + alias link ln + module_function :link + + # + # :call-seq: + # Bundler::FileUtils.ln_s(target, link, force: nil, noop: nil, verbose: nil) + # Bundler::FileUtils.ln_s(target, dir, force: nil, noop: nil, verbose: nil) + # Bundler::FileUtils.ln_s(targets, dir, force: nil, noop: nil, verbose: nil) + # + # In the first form, creates a symbolic link +link+ which points to +target+. + # If +link+ already exists, raises Errno::EEXIST. + # But if the :force option is set, overwrites +link+. + # + # Bundler::FileUtils.ln_s '/usr/bin/ruby', '/usr/local/bin/ruby' + # Bundler::FileUtils.ln_s 'verylongsourcefilename.c', 'c', force: true + # + # In the second form, creates a link +dir/target+ pointing to +target+. + # In the third form, creates several symbolic links in the directory +dir+, + # pointing to each item in +targets+. + # If +dir+ is not a directory, raises Errno::ENOTDIR. + # + # Bundler::FileUtils.ln_s Dir.glob('/bin/*.rb'), '/home/foo/bin' + # + def ln_s(src, dest, force: nil, noop: nil, verbose: nil) + fu_output_message "ln -s#{force ? 'f' : ''} #{[src,dest].flatten.join ' '}" if verbose + return if noop + fu_each_src_dest0(src, dest) do |s,d| + remove_file d, true if force + File.symlink s, d + end + end + module_function :ln_s + + alias symlink ln_s + module_function :symlink + + # + # :call-seq: + # Bundler::FileUtils.ln_sf(*args) + # + # Same as + # + # Bundler::FileUtils.ln_s(*args, force: true) + # + def ln_sf(src, dest, noop: nil, verbose: nil) + ln_s src, dest, force: true, noop: noop, verbose: verbose + end + module_function :ln_sf + + # + # Copies a file content +src+ to +dest+. If +dest+ is a directory, + # copies +src+ to +dest/src+. + # + # If +src+ is a list of files, then +dest+ must be a directory. + # + # Bundler::FileUtils.cp 'eval.c', 'eval.c.org' + # Bundler::FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6' + # Bundler::FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6', :verbose => true + # Bundler::FileUtils.cp 'symlink', 'dest' # copy content, "dest" is not a symlink + # + def cp(src, dest, preserve: nil, noop: nil, verbose: nil) + fu_output_message "cp#{preserve ? ' -p' : ''} #{[src,dest].flatten.join ' '}" if verbose + return if noop + fu_each_src_dest(src, dest) do |s, d| + copy_file s, d, preserve + end + end + module_function :cp + + alias copy cp + module_function :copy + + # + # Copies +src+ to +dest+. If +src+ is a directory, this method copies + # all its contents recursively. If +dest+ is a directory, copies + # +src+ to +dest/src+. + # + # +src+ can be a list of files. + # + # # Installing Ruby library "mylib" under the site_ruby + # Bundler::FileUtils.rm_r site_ruby + '/mylib', :force + # Bundler::FileUtils.cp_r 'lib/', site_ruby + '/mylib' + # + # # Examples of copying several files to target directory. + # Bundler::FileUtils.cp_r %w(mail.rb field.rb debug/), site_ruby + '/tmail' + # Bundler::FileUtils.cp_r Dir.glob('*.rb'), '/home/foo/lib/ruby', :noop => true, :verbose => true + # + # # If you want to copy all contents of a directory instead of the + # # directory itself, c.f. src/x -> dest/x, src/y -> dest/y, + # # use following code. + # Bundler::FileUtils.cp_r 'src/.', 'dest' # cp_r('src', 'dest') makes dest/src, + # # but this doesn't. + # + def cp_r(src, dest, preserve: nil, noop: nil, verbose: nil, + dereference_root: true, remove_destination: nil) + fu_output_message "cp -r#{preserve ? 'p' : ''}#{remove_destination ? ' --remove-destination' : ''} #{[src,dest].flatten.join ' '}" if verbose + return if noop + fu_each_src_dest(src, dest) do |s, d| + copy_entry s, d, preserve, dereference_root, remove_destination + end + end + module_function :cp_r + + # + # Copies a file system entry +src+ to +dest+. + # If +src+ is a directory, this method copies its contents recursively. + # This method preserves file types, c.f. symlink, directory... + # (FIFO, device files and etc. are not supported yet) + # + # Both of +src+ and +dest+ must be a path name. + # +src+ must exist, +dest+ must not exist. + # + # If +preserve+ is true, this method preserves owner, group, and + # modified time. Permissions are copied regardless +preserve+. + # + # If +dereference_root+ is true, this method dereference tree root. + # + # If +remove_destination+ is true, this method removes each destination file before copy. + # + def copy_entry(src, dest, preserve = false, dereference_root = false, remove_destination = false) + Entry_.new(src, nil, dereference_root).wrap_traverse(proc do |ent| + destent = Entry_.new(dest, ent.rel, false) + File.unlink destent.path if remove_destination && File.file?(destent.path) + ent.copy destent.path + end, proc do |ent| + destent = Entry_.new(dest, ent.rel, false) + ent.copy_metadata destent.path if preserve + end) + end + module_function :copy_entry + + # + # Copies file contents of +src+ to +dest+. + # Both of +src+ and +dest+ must be a path name. + # + def copy_file(src, dest, preserve = false, dereference = true) + ent = Entry_.new(src, nil, dereference) + ent.copy_file dest + ent.copy_metadata dest if preserve + end + module_function :copy_file + + # + # Copies stream +src+ to +dest+. + # +src+ must respond to #read(n) and + # +dest+ must respond to #write(str). + # + def copy_stream(src, dest) + IO.copy_stream(src, dest) + end + module_function :copy_stream + + # + # Moves file(s) +src+ to +dest+. If +file+ and +dest+ exist on the different + # disk partition, the file is copied then the original file is removed. + # + # Bundler::FileUtils.mv 'badname.rb', 'goodname.rb' + # Bundler::FileUtils.mv 'stuff.rb', '/notexist/lib/ruby', :force => true # no error + # + # Bundler::FileUtils.mv %w(junk.txt dust.txt), '/home/foo/.trash/' + # Bundler::FileUtils.mv Dir.glob('test*.rb'), 'test', :noop => true, :verbose => true + # + def mv(src, dest, force: nil, noop: nil, verbose: nil, secure: nil) + fu_output_message "mv#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose + return if noop + fu_each_src_dest(src, dest) do |s, d| + destent = Entry_.new(d, nil, true) + begin + if destent.exist? + if destent.directory? + raise Errno::EEXIST, d + else + destent.remove_file if rename_cannot_overwrite_file? + end + end + begin + File.rename s, d + rescue Errno::EXDEV + copy_entry s, d, true + if secure + remove_entry_secure s, force + else + remove_entry s, force + end + end + rescue SystemCallError + raise unless force + end + end + end + module_function :mv + + alias move mv + module_function :move + + def rename_cannot_overwrite_file? #:nodoc: + /emx/ =~ RUBY_PLATFORM + end + private_module_function :rename_cannot_overwrite_file? + + # + # Remove file(s) specified in +list+. This method cannot remove directories. + # All StandardErrors are ignored when the :force option is set. + # + # Bundler::FileUtils.rm %w( junk.txt dust.txt ) + # Bundler::FileUtils.rm Dir.glob('*.so') + # Bundler::FileUtils.rm 'NotExistFile', :force => true # never raises exception + # + def rm(list, force: nil, noop: nil, verbose: nil) + list = fu_list(list) + fu_output_message "rm#{force ? ' -f' : ''} #{list.join ' '}" if verbose + return if noop + + list.each do |path| + remove_file path, force + end + end + module_function :rm + + alias remove rm + module_function :remove + + # + # Equivalent to + # + # Bundler::FileUtils.rm(list, :force => true) + # + def rm_f(list, noop: nil, verbose: nil) + rm list, force: true, noop: noop, verbose: verbose + end + module_function :rm_f + + alias safe_unlink rm_f + module_function :safe_unlink + + # + # remove files +list+[0] +list+[1]... If +list+[n] is a directory, + # removes its all contents recursively. This method ignores + # StandardError when :force option is set. + # + # Bundler::FileUtils.rm_r Dir.glob('/tmp/*') + # Bundler::FileUtils.rm_r 'some_dir', :force => true + # + # WARNING: This method causes local vulnerability + # if one of parent directories or removing directory tree are world + # writable (including /tmp, whose permission is 1777), and the current + # process has strong privilege such as Unix super user (root), and the + # system has symbolic link. For secure removing, read the documentation + # of #remove_entry_secure carefully, and set :secure option to true. + # Default is :secure=>false. + # + # NOTE: This method calls #remove_entry_secure if :secure option is set. + # See also #remove_entry_secure. + # + def rm_r(list, force: nil, noop: nil, verbose: nil, secure: nil) + list = fu_list(list) + fu_output_message "rm -r#{force ? 'f' : ''} #{list.join ' '}" if verbose + return if noop + list.each do |path| + if secure + remove_entry_secure path, force + else + remove_entry path, force + end + end + end + module_function :rm_r + + # + # Equivalent to + # + # Bundler::FileUtils.rm_r(list, :force => true) + # + # WARNING: This method causes local vulnerability. + # Read the documentation of #rm_r first. + # + def rm_rf(list, noop: nil, verbose: nil, secure: nil) + rm_r list, force: true, noop: noop, verbose: verbose, secure: secure + end + module_function :rm_rf + + alias rmtree rm_rf + module_function :rmtree + + # + # This method removes a file system entry +path+. +path+ shall be a + # regular file, a directory, or something. If +path+ is a directory, + # remove it recursively. This method is required to avoid TOCTTOU + # (time-of-check-to-time-of-use) local security vulnerability of #rm_r. + # #rm_r causes security hole when: + # + # * Parent directory is world writable (including /tmp). + # * Removing directory tree includes world writable directory. + # * The system has symbolic link. + # + # To avoid this security hole, this method applies special preprocess. + # If +path+ is a directory, this method chown(2) and chmod(2) all + # removing directories. This requires the current process is the + # owner of the removing whole directory tree, or is the super user (root). + # + # WARNING: You must ensure that *ALL* parent directories cannot be + # moved by other untrusted users. For example, parent directories + # should not be owned by untrusted users, and should not be world + # writable except when the sticky bit set. + # + # WARNING: Only the owner of the removing directory tree, or Unix super + # user (root) should invoke this method. Otherwise this method does not + # work. + # + # For details of this security vulnerability, see Perl's case: + # + # * http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-0448 + # * http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2004-0452 + # + # For fileutils.rb, this vulnerability is reported in [ruby-dev:26100]. + # + def remove_entry_secure(path, force = false) + unless fu_have_symlink? + remove_entry path, force + return + end + fullpath = File.expand_path(path) + st = File.lstat(fullpath) + unless st.directory? + File.unlink fullpath + return + end + # is a directory. + parent_st = File.stat(File.dirname(fullpath)) + unless parent_st.world_writable? + remove_entry path, force + return + end + unless parent_st.sticky? + raise ArgumentError, "parent directory is world writable, Bundler::FileUtils#remove_entry_secure does not work; abort: #{path.inspect} (parent directory mode #{'%o' % parent_st.mode})" + end + # freeze tree root + euid = Process.euid + File.open(fullpath + '/.') {|f| + unless fu_stat_identical_entry?(st, f.stat) + # symlink (TOC-to-TOU attack?) + File.unlink fullpath + return + end + f.chown euid, -1 + f.chmod 0700 + unless fu_stat_identical_entry?(st, File.lstat(fullpath)) + # TOC-to-TOU attack? + File.unlink fullpath + return + end + } + # ---- tree root is frozen ---- + root = Entry_.new(path) + root.preorder_traverse do |ent| + if ent.directory? + ent.chown euid, -1 + ent.chmod 0700 + end + end + root.postorder_traverse do |ent| + begin + ent.remove + rescue + raise unless force + end + end + rescue + raise unless force + end + module_function :remove_entry_secure + + def fu_have_symlink? #:nodoc: + File.symlink nil, nil + rescue NotImplementedError + return false + rescue TypeError + return true + end + private_module_function :fu_have_symlink? + + def fu_stat_identical_entry?(a, b) #:nodoc: + a.dev == b.dev and a.ino == b.ino + end + private_module_function :fu_stat_identical_entry? + + # + # This method removes a file system entry +path+. + # +path+ might be a regular file, a directory, or something. + # If +path+ is a directory, remove it recursively. + # + # See also #remove_entry_secure. + # + def remove_entry(path, force = false) + Entry_.new(path).postorder_traverse do |ent| + begin + ent.remove + rescue + raise unless force + end + end + rescue + raise unless force + end + module_function :remove_entry + + # + # Removes a file +path+. + # This method ignores StandardError if +force+ is true. + # + def remove_file(path, force = false) + Entry_.new(path).remove_file + rescue + raise unless force + end + module_function :remove_file + + # + # Removes a directory +dir+ and its contents recursively. + # This method ignores StandardError if +force+ is true. + # + def remove_dir(path, force = false) + remove_entry path, force # FIXME?? check if it is a directory + end + module_function :remove_dir + + # + # Returns true if the contents of a file +a+ and a file +b+ are identical. + # + # Bundler::FileUtils.compare_file('somefile', 'somefile') #=> true + # Bundler::FileUtils.compare_file('/dev/null', '/dev/urandom') #=> false + # + def compare_file(a, b) + return false unless File.size(a) == File.size(b) + File.open(a, 'rb') {|fa| + File.open(b, 'rb') {|fb| + return compare_stream(fa, fb) + } + } + end + module_function :compare_file + + alias identical? compare_file + alias cmp compare_file + module_function :identical? + module_function :cmp + + # + # Returns true if the contents of a stream +a+ and +b+ are identical. + # + def compare_stream(a, b) + bsize = fu_stream_blksize(a, b) + sa = String.new(capacity: bsize) + sb = String.new(capacity: bsize) + begin + a.read(bsize, sa) + b.read(bsize, sb) + return true if sa.empty? && sb.empty? + end while sa == sb + false + end + module_function :compare_stream + + # + # If +src+ is not same as +dest+, copies it and changes the permission + # mode to +mode+. If +dest+ is a directory, destination is +dest+/+src+. + # This method removes destination before copy. + # + # Bundler::FileUtils.install 'ruby', '/usr/local/bin/ruby', :mode => 0755, :verbose => true + # Bundler::FileUtils.install 'lib.rb', '/usr/local/lib/ruby/site_ruby', :verbose => true + # + def install(src, dest, mode: nil, owner: nil, group: nil, preserve: nil, + noop: nil, verbose: nil) + if verbose + msg = +"install -c" + msg << ' -p' if preserve + msg << ' -m ' << mode_to_s(mode) if mode + msg << " -o #{owner}" if owner + msg << " -g #{group}" if group + msg << ' ' << [src,dest].flatten.join(' ') + fu_output_message msg + end + return if noop + uid = fu_get_uid(owner) + gid = fu_get_gid(group) + fu_each_src_dest(src, dest) do |s, d| + st = File.stat(s) + unless File.exist?(d) and compare_file(s, d) + remove_file d, true + copy_file s, d + File.utime st.atime, st.mtime, d if preserve + File.chmod fu_mode(mode, st), d if mode + File.chown uid, gid, d if uid or gid + end + end + end + module_function :install + + def user_mask(target) #:nodoc: + target.each_char.inject(0) do |mask, chr| + case chr + when "u" + mask | 04700 + when "g" + mask | 02070 + when "o" + mask | 01007 + when "a" + mask | 07777 + else + raise ArgumentError, "invalid `who' symbol in file mode: #{chr}" + end + end + end + private_module_function :user_mask + + def apply_mask(mode, user_mask, op, mode_mask) #:nodoc: + case op + when '=' + (mode & ~user_mask) | (user_mask & mode_mask) + when '+' + mode | (user_mask & mode_mask) + when '-' + mode & ~(user_mask & mode_mask) + end + end + private_module_function :apply_mask + + def symbolic_modes_to_i(mode_sym, path) #:nodoc: + mode = if File::Stat === path + path.mode + else + File.stat(path).mode + end + mode_sym.split(/,/).inject(mode & 07777) do |current_mode, clause| + target, *actions = clause.split(/([=+-])/) + raise ArgumentError, "invalid file mode: #{mode_sym}" if actions.empty? + target = 'a' if target.empty? + user_mask = user_mask(target) + actions.each_slice(2) do |op, perm| + need_apply = op == '=' + mode_mask = (perm || '').each_char.inject(0) do |mask, chr| + case chr + when "r" + mask | 0444 + when "w" + mask | 0222 + when "x" + mask | 0111 + when "X" + if FileTest.directory? path + mask | 0111 + else + mask + end + when "s" + mask | 06000 + when "t" + mask | 01000 + when "u", "g", "o" + if mask.nonzero? + current_mode = apply_mask(current_mode, user_mask, op, mask) + end + need_apply = false + copy_mask = user_mask(chr) + (current_mode & copy_mask) / (copy_mask & 0111) * (user_mask & 0111) + else + raise ArgumentError, "invalid `perm' symbol in file mode: #{chr}" + end + end + + if mode_mask.nonzero? || need_apply + current_mode = apply_mask(current_mode, user_mask, op, mode_mask) + end + end + current_mode + end + end + private_module_function :symbolic_modes_to_i + + def fu_mode(mode, path) #:nodoc: + mode.is_a?(String) ? symbolic_modes_to_i(mode, path) : mode + end + private_module_function :fu_mode + + def mode_to_s(mode) #:nodoc: + mode.is_a?(String) ? mode : "%o" % mode + end + private_module_function :mode_to_s + + # + # Changes permission bits on the named files (in +list+) to the bit pattern + # represented by +mode+. + # + # +mode+ is the symbolic and absolute mode can be used. + # + # Absolute mode is + # Bundler::FileUtils.chmod 0755, 'somecommand' + # Bundler::FileUtils.chmod 0644, %w(my.rb your.rb his.rb her.rb) + # Bundler::FileUtils.chmod 0755, '/usr/bin/ruby', :verbose => true + # + # Symbolic mode is + # Bundler::FileUtils.chmod "u=wrx,go=rx", 'somecommand' + # Bundler::FileUtils.chmod "u=wr,go=rr", %w(my.rb your.rb his.rb her.rb) + # Bundler::FileUtils.chmod "u=wrx,go=rx", '/usr/bin/ruby', :verbose => true + # + # "a" :: is user, group, other mask. + # "u" :: is user's mask. + # "g" :: is group's mask. + # "o" :: is other's mask. + # "w" :: is write permission. + # "r" :: is read permission. + # "x" :: is execute permission. + # "X" :: + # is execute permission for directories only, must be used in conjunction with "+" + # "s" :: is uid, gid. + # "t" :: is sticky bit. + # "+" :: is added to a class given the specified mode. + # "-" :: Is removed from a given class given mode. + # "=" :: Is the exact nature of the class will be given a specified mode. + + def chmod(mode, list, noop: nil, verbose: nil) + list = fu_list(list) + fu_output_message sprintf('chmod %s %s', mode_to_s(mode), list.join(' ')) if verbose + return if noop + list.each do |path| + Entry_.new(path).chmod(fu_mode(mode, path)) + end + end + module_function :chmod + + # + # Changes permission bits on the named files (in +list+) + # to the bit pattern represented by +mode+. + # + # Bundler::FileUtils.chmod_R 0700, "/tmp/app.#{$$}" + # Bundler::FileUtils.chmod_R "u=wrx", "/tmp/app.#{$$}" + # + def chmod_R(mode, list, noop: nil, verbose: nil, force: nil) + list = fu_list(list) + fu_output_message sprintf('chmod -R%s %s %s', + (force ? 'f' : ''), + mode_to_s(mode), list.join(' ')) if verbose + return if noop + list.each do |root| + Entry_.new(root).traverse do |ent| + begin + ent.chmod(fu_mode(mode, ent.path)) + rescue + raise unless force + end + end + end + end + module_function :chmod_R + + # + # Changes owner and group on the named files (in +list+) + # to the user +user+ and the group +group+. +user+ and +group+ + # may be an ID (Integer/String) or a name (String). + # If +user+ or +group+ is nil, this method does not change + # the attribute. + # + # Bundler::FileUtils.chown 'root', 'staff', '/usr/local/bin/ruby' + # Bundler::FileUtils.chown nil, 'bin', Dir.glob('/usr/bin/*'), :verbose => true + # + def chown(user, group, list, noop: nil, verbose: nil) + list = fu_list(list) + fu_output_message sprintf('chown %s %s', + (group ? "#{user}:#{group}" : user || ':'), + list.join(' ')) if verbose + return if noop + uid = fu_get_uid(user) + gid = fu_get_gid(group) + list.each do |path| + Entry_.new(path).chown uid, gid + end + end + module_function :chown + + # + # Changes owner and group on the named files (in +list+) + # to the user +user+ and the group +group+ recursively. + # +user+ and +group+ may be an ID (Integer/String) or + # a name (String). If +user+ or +group+ is nil, this + # method does not change the attribute. + # + # Bundler::FileUtils.chown_R 'www', 'www', '/var/www/htdocs' + # Bundler::FileUtils.chown_R 'cvs', 'cvs', '/var/cvs', :verbose => true + # + def chown_R(user, group, list, noop: nil, verbose: nil, force: nil) + list = fu_list(list) + fu_output_message sprintf('chown -R%s %s %s', + (force ? 'f' : ''), + (group ? "#{user}:#{group}" : user || ':'), + list.join(' ')) if verbose + return if noop + uid = fu_get_uid(user) + gid = fu_get_gid(group) + list.each do |root| + Entry_.new(root).traverse do |ent| + begin + ent.chown uid, gid + rescue + raise unless force + end + end + end + end + module_function :chown_R + + begin + require 'etc' + rescue LoadError # rescue LoadError for miniruby + end + + def fu_get_uid(user) #:nodoc: + return nil unless user + case user + when Integer + user + when /\A\d+\z/ + user.to_i + else + Etc.getpwnam(user) ? Etc.getpwnam(user).uid : nil + end + end + private_module_function :fu_get_uid + + def fu_get_gid(group) #:nodoc: + return nil unless group + case group + when Integer + group + when /\A\d+\z/ + group.to_i + else + Etc.getgrnam(group) ? Etc.getgrnam(group).gid : nil + end + end + private_module_function :fu_get_gid + + # + # Updates modification time (mtime) and access time (atime) of file(s) in + # +list+. Files are created if they don't exist. + # + # Bundler::FileUtils.touch 'timestamp' + # Bundler::FileUtils.touch Dir.glob('*.c'); system 'make' + # + def touch(list, noop: nil, verbose: nil, mtime: nil, nocreate: nil) + list = fu_list(list) + t = mtime + if verbose + fu_output_message "touch #{nocreate ? '-c ' : ''}#{t ? t.strftime('-t %Y%m%d%H%M.%S ') : ''}#{list.join ' '}" + end + return if noop + list.each do |path| + created = nocreate + begin + File.utime(t, t, path) + rescue Errno::ENOENT + raise if created + File.open(path, 'a') { + ; + } + created = true + retry if t + end + end + end + module_function :touch + + private + + module StreamUtils_ + private + + def fu_windows? + /mswin|mingw|bccwin|emx/ =~ RUBY_PLATFORM + end + + def fu_copy_stream0(src, dest, blksize = nil) #:nodoc: + IO.copy_stream(src, dest) + end + + def fu_stream_blksize(*streams) + streams.each do |s| + next unless s.respond_to?(:stat) + size = fu_blksize(s.stat) + return size if size + end + fu_default_blksize() + end + + def fu_blksize(st) + s = st.blksize + return nil unless s + return nil if s == 0 + s + end + + def fu_default_blksize + 1024 + end + end + + include StreamUtils_ + extend StreamUtils_ + + class Entry_ #:nodoc: internal use only + include StreamUtils_ + + def initialize(a, b = nil, deref = false) + @prefix = @rel = @path = nil + if b + @prefix = a + @rel = b + else + @path = a + end + @deref = deref + @stat = nil + @lstat = nil + end + + def inspect + "\#<#{self.class} #{path()}>" + end + + def path + if @path + File.path(@path) + else + join(@prefix, @rel) + end + end + + def prefix + @prefix || @path + end + + def rel + @rel + end + + def dereference? + @deref + end + + def exist? + begin + lstat + true + rescue Errno::ENOENT + false + end + end + + def file? + s = lstat! + s and s.file? + end + + def directory? + s = lstat! + s and s.directory? + end + + def symlink? + s = lstat! + s and s.symlink? + end + + def chardev? + s = lstat! + s and s.chardev? + end + + def blockdev? + s = lstat! + s and s.blockdev? + end + + def socket? + s = lstat! + s and s.socket? + end + + def pipe? + s = lstat! + s and s.pipe? + end + + S_IF_DOOR = 0xD000 + + def door? + s = lstat! + s and (s.mode & 0xF000 == S_IF_DOOR) + end + + def entries + opts = {} + opts[:encoding] = ::Encoding::UTF_8 if fu_windows? + Dir.entries(path(), opts)\ + .reject {|n| n == '.' or n == '..' }\ + .map {|n| Entry_.new(prefix(), join(rel(), n.untaint)) } + end + + def stat + return @stat if @stat + if lstat() and lstat().symlink? + @stat = File.stat(path()) + else + @stat = lstat() + end + @stat + end + + def stat! + return @stat if @stat + if lstat! and lstat!.symlink? + @stat = File.stat(path()) + else + @stat = lstat! + end + @stat + rescue SystemCallError + nil + end + + def lstat + if dereference? + @lstat ||= File.stat(path()) + else + @lstat ||= File.lstat(path()) + end + end + + def lstat! + lstat() + rescue SystemCallError + nil + end + + def chmod(mode) + if symlink? + File.lchmod mode, path() if have_lchmod? + else + File.chmod mode, path() + end + end + + def chown(uid, gid) + if symlink? + File.lchown uid, gid, path() if have_lchown? + else + File.chown uid, gid, path() + end + end + + def copy(dest) + lstat + case + when file? + copy_file dest + when directory? + if !File.exist?(dest) and descendant_directory?(dest, path) + raise ArgumentError, "cannot copy directory %s to itself %s" % [path, dest] + end + begin + Dir.mkdir dest + rescue + raise unless File.directory?(dest) + end + when symlink? + File.symlink File.readlink(path()), dest + when chardev? + raise "cannot handle device file" unless File.respond_to?(:mknod) + mknod dest, ?c, 0666, lstat().rdev + when blockdev? + raise "cannot handle device file" unless File.respond_to?(:mknod) + mknod dest, ?b, 0666, lstat().rdev + when socket? + raise "cannot handle socket" unless File.respond_to?(:mknod) + mknod dest, nil, lstat().mode, 0 + when pipe? + raise "cannot handle FIFO" unless File.respond_to?(:mkfifo) + mkfifo dest, 0666 + when door? + raise "cannot handle door: #{path()}" + else + raise "unknown file type: #{path()}" + end + end + + def copy_file(dest) + File.open(path()) do |s| + File.open(dest, 'wb', s.stat.mode) do |f| + IO.copy_stream(s, f) + end + end + end + + def copy_metadata(path) + st = lstat() + if !st.symlink? + File.utime st.atime, st.mtime, path + end + mode = st.mode + begin + if st.symlink? + begin + File.lchown st.uid, st.gid, path + rescue NotImplementedError + end + else + File.chown st.uid, st.gid, path + end + rescue Errno::EPERM, Errno::EACCES + # clear setuid/setgid + mode &= 01777 + end + if st.symlink? + begin + File.lchmod mode, path + rescue NotImplementedError + end + else + File.chmod mode, path + end + end + + def remove + if directory? + remove_dir1 + else + remove_file + end + end + + def remove_dir1 + platform_support { + Dir.rmdir path().chomp(?/) + } + end + + def remove_file + platform_support { + File.unlink path + } + end + + def platform_support + return yield unless fu_windows? + first_time_p = true + begin + yield + rescue Errno::ENOENT + raise + rescue => err + if first_time_p + first_time_p = false + begin + File.chmod 0700, path() # Windows does not have symlink + retry + rescue SystemCallError + end + end + raise err + end + end + + def preorder_traverse + stack = [self] + while ent = stack.pop + yield ent + stack.concat ent.entries.reverse if ent.directory? + end + end + + alias traverse preorder_traverse + + def postorder_traverse + if directory? + entries().each do |ent| + ent.postorder_traverse do |e| + yield e + end + end + end + ensure + yield self + end + + def wrap_traverse(pre, post) + pre.call self + if directory? + entries.each do |ent| + ent.wrap_traverse pre, post + end + end + post.call self + end + + private + + $fileutils_rb_have_lchmod = nil + + def have_lchmod? + # This is not MT-safe, but it does not matter. + if $fileutils_rb_have_lchmod == nil + $fileutils_rb_have_lchmod = check_have_lchmod? + end + $fileutils_rb_have_lchmod + end + + def check_have_lchmod? + return false unless File.respond_to?(:lchmod) + File.lchmod 0 + return true + rescue NotImplementedError + return false + end + + $fileutils_rb_have_lchown = nil + + def have_lchown? + # This is not MT-safe, but it does not matter. + if $fileutils_rb_have_lchown == nil + $fileutils_rb_have_lchown = check_have_lchown? + end + $fileutils_rb_have_lchown + end + + def check_have_lchown? + return false unless File.respond_to?(:lchown) + File.lchown nil, nil + return true + rescue NotImplementedError + return false + end + + def join(dir, base) + return File.path(dir) if not base or base == '.' + return File.path(base) if not dir or dir == '.' + File.join(dir, base) + end + + if File::ALT_SEPARATOR + DIRECTORY_TERM = "(?=[/#{Regexp.quote(File::ALT_SEPARATOR)}]|\\z)" + else + DIRECTORY_TERM = "(?=/|\\z)" + end + SYSCASE = File::FNM_SYSCASE.nonzero? ? "-i" : "" + + def descendant_directory?(descendant, ascendant) + /\A(?#{SYSCASE}:#{Regexp.quote(ascendant)})#{DIRECTORY_TERM}/ =~ File.dirname(descendant) + end + end # class Entry_ + + def fu_list(arg) #:nodoc: + [arg].flatten.map {|path| File.path(path) } + end + private_module_function :fu_list + + def fu_each_src_dest(src, dest) #:nodoc: + fu_each_src_dest0(src, dest) do |s, d| + raise ArgumentError, "same file: #{s} and #{d}" if fu_same?(s, d) + yield s, d + end + end + private_module_function :fu_each_src_dest + + def fu_each_src_dest0(src, dest) #:nodoc: + if tmp = Array.try_convert(src) + tmp.each do |s| + s = File.path(s) + yield s, File.join(dest, File.basename(s)) + end + else + src = File.path(src) + if File.directory?(dest) + yield src, File.join(dest, File.basename(src)) + else + yield src, File.path(dest) + end + end + end + private_module_function :fu_each_src_dest0 + + def fu_same?(a, b) #:nodoc: + File.identical?(a, b) + end + private_module_function :fu_same? + + @fileutils_output = $stderr + @fileutils_label = '' + + def fu_output_message(msg) #:nodoc: + @fileutils_output ||= $stderr + @fileutils_label ||= '' + @fileutils_output.puts @fileutils_label + msg + end + private_module_function :fu_output_message + + # This hash table holds command options. + OPT_TABLE = {} #:nodoc: internal use only + (private_instance_methods & methods(false)).inject(OPT_TABLE) {|tbl, name| + (tbl[name.to_s] = instance_method(name).parameters).map! {|t, n| n if t == :key}.compact! + tbl + } + + # + # Returns an Array of method names which have any options. + # + # p Bundler::FileUtils.commands #=> ["chmod", "cp", "cp_r", "install", ...] + # + def self.commands + OPT_TABLE.keys + end + + # + # Returns an Array of option names. + # + # p Bundler::FileUtils.options #=> ["noop", "force", "verbose", "preserve", "mode"] + # + def self.options + OPT_TABLE.values.flatten.uniq.map {|sym| sym.to_s } + end + + # + # Returns true if the method +mid+ have an option +opt+. + # + # p Bundler::FileUtils.have_option?(:cp, :noop) #=> true + # p Bundler::FileUtils.have_option?(:rm, :force) #=> true + # p Bundler::FileUtils.have_option?(:rm, :preserve) #=> false + # + def self.have_option?(mid, opt) + li = OPT_TABLE[mid.to_s] or raise ArgumentError, "no such method: #{mid}" + li.include?(opt) + end + + # + # Returns an Array of option names of the method +mid+. + # + # p Bundler::FileUtils.options_of(:rm) #=> ["noop", "verbose", "force"] + # + def self.options_of(mid) + OPT_TABLE[mid.to_s].map {|sym| sym.to_s } + end + + # + # Returns an Array of method names which have the option +opt+. + # + # p Bundler::FileUtils.collect_method(:preserve) #=> ["cp", "cp_r", "copy", "install"] + # + def self.collect_method(opt) + OPT_TABLE.keys.select {|m| OPT_TABLE[m].include?(opt) } + end + + LOW_METHODS = singleton_methods(false) - collect_method(:noop).map(&:intern) + module LowMethods + private + def _do_nothing(*)end + ::Bundler::FileUtils::LOW_METHODS.map {|name| alias_method name, :_do_nothing} + end + + METHODS = singleton_methods() - [:private_module_function, + :commands, :options, :have_option?, :options_of, :collect_method] + + # + # This module has all methods of Bundler::FileUtils module, but it outputs messages + # before acting. This equates to passing the <tt>:verbose</tt> flag to + # methods in Bundler::FileUtils. + # + module Verbose + include Bundler::FileUtils + @fileutils_output = $stderr + @fileutils_label = '' + names = ::Bundler::FileUtils.collect_method(:verbose) + names.each do |name| + module_eval(<<-EOS, __FILE__, __LINE__ + 1) + def #{name}(*args, **options) + super(*args, **options, verbose: true) + end + EOS + end + private(*names) + extend self + class << self + public(*::Bundler::FileUtils::METHODS) + end + end + + # + # This module has all methods of Bundler::FileUtils module, but never changes + # files/directories. This equates to passing the <tt>:noop</tt> flag + # to methods in Bundler::FileUtils. + # + module NoWrite + include Bundler::FileUtils + include LowMethods + @fileutils_output = $stderr + @fileutils_label = '' + names = ::Bundler::FileUtils.collect_method(:noop) + names.each do |name| + module_eval(<<-EOS, __FILE__, __LINE__ + 1) + def #{name}(*args, **options) + super(*args, **options, noop: true) + end + EOS + end + private(*names) + extend self + class << self + public(*::Bundler::FileUtils::METHODS) + end + end + + # + # This module has all methods of Bundler::FileUtils module, but never changes + # files/directories, with printing message before acting. + # This equates to passing the <tt>:noop</tt> and <tt>:verbose</tt> flag + # to methods in Bundler::FileUtils. + # + module DryRun + include Bundler::FileUtils + include LowMethods + @fileutils_output = $stderr + @fileutils_label = '' + names = ::Bundler::FileUtils.collect_method(:noop) + names.each do |name| + module_eval(<<-EOS, __FILE__, __LINE__ + 1) + def #{name}(*args, **options) + super(*args, **options, noop: true, verbose: true) + end + EOS + end + private(*names) + extend self + class << self + public(*::Bundler::FileUtils::METHODS) + end + end + +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo.rb b/lib/bundler/vendor/molinillo/lib/molinillo.rb index 134bf1d720..9e2867144f 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo.rb @@ -1,4 +1,6 @@ # frozen_string_literal: true + +require 'bundler/vendor/molinillo/lib/molinillo/compatibility' require 'bundler/vendor/molinillo/lib/molinillo/gem_metadata' require 'bundler/vendor/molinillo/lib/molinillo/errors' require 'bundler/vendor/molinillo/lib/molinillo/resolver' diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/compatibility.rb b/lib/bundler/vendor/molinillo/lib/molinillo/compatibility.rb new file mode 100644 index 0000000000..3eba8e4083 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/compatibility.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Bundler::Molinillo + # Hacks needed for old Ruby versions. + module Compatibility + module_function + + if [].respond_to?(:flat_map) + # Flat map + # @param [Enumerable] enum an enumerable object + # @block the block to flat-map with + # @return The enum, flat-mapped + def flat_map(enum, &blk) + enum.flat_map(&blk) + end + else + # Flat map + # @param [Enumerable] enum an enumerable object + # @block the block to flat-map with + # @return The enum, flat-mapped + def flat_map(enum, &blk) + enum.map(&blk).flatten(1) + end + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb index 253c18764f..bcacf35243 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo # @!visibility private module Delegates @@ -45,6 +46,12 @@ module Bundler::Molinillo current_state = state || Bundler::Molinillo::ResolutionState.empty current_state.conflicts end + + # (see Bundler::Molinillo::ResolutionState#unused_unwind_options) + def unused_unwind_options + current_state = state || Bundler::Molinillo::ResolutionState.empty + current_state.unused_unwind_options + end end end end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb index 29f48d5b3c..ec9c770a28 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo module Delegates # Delegates all {Bundler::Molinillo::SpecificationProvider} methods to a diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb index 76e84ab7e6..677a8bd916 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'set' require 'tsort' @@ -147,8 +148,8 @@ module Bundler::Molinillo vertex = add_vertex(name, payload, root) vertex.explicit_requirements << requirement if root parent_names.each do |parent_name| - parent_node = vertex_named(parent_name) - add_edge(parent_node, vertex, requirement) + parent_vertex = vertex_named(parent_name) + add_edge(parent_vertex, vertex, requirement) end vertex end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb index e0dfe6cbbd..c04c7eec9c 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo class DependencyGraph # An action that modifies a {DependencyGraph} that is reversible. diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb index 9092e4d546..9849aea2fe 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' module Bundler::Molinillo class DependencyGraph diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb index eda4251801..0a1e08255b 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' module Bundler::Molinillo class DependencyGraph diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb index e9125a59c6..1d9f4b327d 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' module Bundler::Molinillo class DependencyGraph diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb index d20b2cb0e0..385dcbdd06 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' module Bundler::Molinillo class DependencyGraph diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb index 72a705e023..8582dd19c1 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular' require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex' require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge' diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb index 8d8e10fedf..37286d104a 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' module Bundler::Molinillo class DependencyGraph diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb index 53524d36ad..d6ad16e07a 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' module Bundler::Molinillo class DependencyGraph diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb index eab989e7bc..e4d016de24 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo class DependencyGraph # A vertex in a {DependencyGraph} that encapsulates a {#name} and a @@ -32,7 +33,7 @@ module Bundler::Molinillo # @return [Array<Object>] all of the requirements that required # this vertex def requirements - incoming_edges.map(&:requirement) + explicit_requirements + (incoming_edges.map(&:requirement) + explicit_requirements).uniq end # @return [Array<Edge>] the edges of {#graph} that have `self` as their @@ -53,7 +54,7 @@ module Bundler::Molinillo # {#descendent?} def recursive_predecessors vertices = predecessors - vertices += vertices.map(&:recursive_predecessors).flatten(1) + vertices += Compatibility.flat_map(vertices, &:recursive_predecessors) vertices.uniq! vertices end @@ -68,7 +69,7 @@ module Bundler::Molinillo # {#ancestor?} def recursive_successors vertices = successors - vertices += vertices.map(&:recursive_successors).flatten(1) + vertices += Compatibility.flat_map(vertices, &:recursive_successors) vertices.uniq! vertices end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb b/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb index f904bd0814..fb343250b1 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo # An error that occurred during the resolution process class ResolverError < StandardError; end @@ -41,11 +42,11 @@ module Bundler::Molinillo attr_reader :dependencies # Initializes a new error with the given circular vertices. - # @param [Array<DependencyGraph::Vertex>] nodes the nodes in the dependency + # @param [Array<DependencyGraph::Vertex>] vertices the vertices in the dependency # that caused the error - def initialize(nodes) - super "There is a circular dependency between #{nodes.map(&:name).join(' and ')}" - @dependencies = nodes.map(&:payload).to_set + def initialize(vertices) + super "There is a circular dependency between #{vertices.map(&:name).join(' and ')}" + @dependencies = vertices.map { |vertex| vertex.payload.possibilities.last }.to_set end end @@ -55,11 +56,16 @@ module Bundler::Molinillo # resolution to fail attr_reader :conflicts + # @return [SpecificationProvider] the specification provider used during + # resolution + attr_reader :specification_provider + # Initializes a new error with the given version conflicts. # @param [{String => Resolution::Conflict}] conflicts see {#conflicts} - def initialize(conflicts) + # @param [SpecificationProvider] specification_provider see {#specification_provider} + def initialize(conflicts, specification_provider) pairs = [] - conflicts.values.flatten.map(&:requirements).flatten.each do |conflicting| + Compatibility.flat_map(conflicts.values.flatten, &:requirements).each do |conflicting| conflicting.each do |source, conflict_requirements| conflict_requirements.each do |c| pairs << [c, source] @@ -69,7 +75,64 @@ module Bundler::Molinillo super "Unable to satisfy the following requirements:\n\n" \ "#{pairs.map { |r, d| "- `#{r}` required by `#{d}`" }.join("\n")}" + @conflicts = conflicts + @specification_provider = specification_provider + end + + require 'bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider' + include Delegates::SpecificationProvider + + # @return [String] An error message that includes requirement trees, + # which is much more detailed & customizable than the default message + # @param [Hash] opts the options to create a message with. + # @option opts [String] :solver_name The user-facing name of the solver + # @option opts [String] :possibility_type The generic name of a possibility + # @option opts [Proc] :reduce_trees A proc that reduced the list of requirement trees + # @option opts [Proc] :printable_requirement A proc that pretty-prints requirements + # @option opts [Proc] :additional_message_for_conflict A proc that appends additional + # messages for each conflict + # @option opts [Proc] :version_for_spec A proc that returns the version number for a + # possibility + def message_with_trees(opts = {}) + solver_name = opts.delete(:solver_name) { self.class.name.split('::').first } + possibility_type = opts.delete(:possibility_type) { 'possibility named' } + reduce_trees = opts.delete(:reduce_trees) { proc { |trees| trees.uniq.sort_by(&:to_s) } } + printable_requirement = opts.delete(:printable_requirement) { proc { |req| req.to_s } } + additional_message_for_conflict = opts.delete(:additional_message_for_conflict) { proc {} } + version_for_spec = opts.delete(:version_for_spec) { proc(&:to_s) } + + conflicts.sort.reduce(''.dup) do |o, (name, conflict)| + o << %(\n#{solver_name} could not find compatible versions for #{possibility_type} "#{name}":\n) + if conflict.locked_requirement + o << %( In snapshot (#{name_for_locking_dependency_source}):\n) + o << %( #{printable_requirement.call(conflict.locked_requirement)}\n) + o << %(\n) + end + o << %( In #{name_for_explicit_dependency_source}:\n) + trees = reduce_trees.call(conflict.requirement_trees) + + o << trees.map do |tree| + t = ''.dup + depth = 2 + tree.each do |req| + t << ' ' * depth << req.to_s + unless tree.last == req + if spec = conflict.activated_by_name[name_for(req)] + t << %( was resolved to #{version_for_spec.call(spec)}, which) + end + t << %( depends on) + end + t << %(\n) + depth += 1 + end + t + end.join("\n") + + additional_message_for_conflict.call(o, name, conflict) + + o + end.strip end end end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb b/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb index a4fb6dd68e..3feb7be9b5 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true + module Bundler::Molinillo # The version of Bundler::Molinillo. - VERSION = '0.5.7'.freeze + VERSION = '0.6.4'.freeze end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb b/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb index 0f1ad195f2..fa094c1981 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo # Provides information about specifcations and dependencies to the resolver, # allowing the {Resolver} class to remain generic while still providing power diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb b/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb index d47cfa2928..a166bc6991 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo # Conveys information about the resolution process to a user. module UI @@ -48,7 +49,8 @@ module Bundler::Molinillo if debug? debug_info = yield debug_info = debug_info.inspect unless debug_info.is_a?(String) - output.puts debug_info.split("\n").map { |s| ' ' * depth + s } + debug_info = debug_info.split("\n").map { |s| ":#{depth.to_s.rjust 4}: #{s}" } + output.puts debug_info end end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb b/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb index 1845966a75..0eb665d17a 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo class Resolver # A specific resolution from a given {Resolver} @@ -8,22 +9,125 @@ module Bundler::Molinillo # @attr [{String,Nil=>[Object]}] requirements the requirements that caused the conflict # @attr [Object, nil] existing the existing spec that was in conflict with # the {#possibility} - # @attr [Object] possibility the spec that was unable to be activated due - # to a conflict + # @attr [Object] possibility_set the set of specs that was unable to be + # activated due to a conflict. # @attr [Object] locked_requirement the relevant locking requirement. # @attr [Array<Array<Object>>] requirement_trees the different requirement # trees that led to every requirement for the conflicting name. # @attr [{String=>Object}] activated_by_name the already-activated specs. + # @attr [Object] underlying_error an error that has occurred during resolution, and + # will be raised at the end of it if no resolution is found. Conflict = Struct.new( :requirement, :requirements, :existing, - :possibility, + :possibility_set, :locked_requirement, :requirement_trees, - :activated_by_name + :activated_by_name, + :underlying_error ) + class Conflict + # @return [Object] a spec that was unable to be activated due to a conflict + def possibility + possibility_set && possibility_set.latest_version + end + end + + # A collection of possibility states that share the same dependencies + # @attr [Array] dependencies the dependencies for this set of possibilities + # @attr [Array] possibilities the possibilities + PossibilitySet = Struct.new(:dependencies, :possibilities) + + class PossibilitySet + # String representation of the possibility set, for debugging + def to_s + "[#{possibilities.join(', ')}]" + end + + # @return [Object] most up-to-date dependency in the possibility set + def latest_version + possibilities.last + end + end + + # Details of the state to unwind to when a conflict occurs, and the cause of the unwind + # @attr [Integer] state_index the index of the state to unwind to + # @attr [Object] state_requirement the requirement of the state we're unwinding to + # @attr [Array] requirement_tree for the requirement we're relaxing + # @attr [Array] conflicting_requirements the requirements that combined to cause the conflict + # @attr [Array] requirement_trees for the conflict + # @attr [Array] requirements_unwound_to_instead array of unwind requirements that were chosen over this unwind + UnwindDetails = Struct.new( + :state_index, + :state_requirement, + :requirement_tree, + :conflicting_requirements, + :requirement_trees, + :requirements_unwound_to_instead + ) + + class UnwindDetails + include Comparable + + # We compare UnwindDetails when choosing which state to unwind to. If + # two options have the same state_index we prefer the one most + # removed from a requirement that caused the conflict. Both options + # would unwind to the same state, but a `grandparent` option will + # filter out fewer of its possibilities after doing so - where a state + # is both a `parent` and a `grandparent` to requirements that have + # caused a conflict this is the correct behaviour. + # @param [UnwindDetail] other UnwindDetail to be compared + # @return [Integer] integer specifying ordering + def <=>(other) + if state_index > other.state_index + 1 + elsif state_index == other.state_index + reversed_requirement_tree_index <=> other.reversed_requirement_tree_index + else + -1 + end + end + + # @return [Integer] index of state requirement in reversed requirement tree + # (the conflicting requirement itself will be at position 0) + def reversed_requirement_tree_index + @reversed_requirement_tree_index ||= + if state_requirement + requirement_tree.reverse.index(state_requirement) + else + 999_999 + end + end + + # @return [Boolean] where the requirement of the state we're unwinding + # to directly caused the conflict. Note: in this case, it is + # impossible for the state we're unwinding to to be a parent of + # any of the other conflicting requirements (or we would have + # circularity) + def unwinding_to_primary_requirement? + requirement_tree.last == state_requirement + end + + # @return [Array] array of sub-dependencies to avoid when choosing a + # new possibility for the state we've unwound to. Only relevant for + # non-primary unwinds + def sub_dependencies_to_avoid + @requirements_to_avoid ||= + requirement_trees.map do |tree| + index = tree.index(state_requirement) + tree[index + 1] if index + end.compact + end + + # @return [Array] array of all the requirements that led to the need for + # this unwind + def all_requirements + @all_requirements ||= requirement_trees.flatten(1) + end + end + # @return [SpecificationProvider] the provider that knows about # dependencies, requirements, specifications, versions, etc. attr_reader :specification_provider @@ -64,7 +168,7 @@ module Bundler::Molinillo start_resolution while state - break unless state.requirements.any? || state.requirement + break if !state.requirement && state.requirements.empty? indicate_progress if state.respond_to?(:pop_possibility_state) # DependencyState debug(depth) { "Creating possibility state for #{requirement} (#{possibilities.count} remaining)" } @@ -78,7 +182,7 @@ module Bundler::Molinillo process_topmost_state end - activated.freeze + resolve_activated_specs ensure end_resolution end @@ -109,6 +213,19 @@ module Bundler::Molinillo resolver_ui.before_resolution end + def resolve_activated_specs + activated.vertices.each do |_, vertex| + next unless vertex.payload + + latest_version = vertex.payload.possibilities.reverse_each.find do |possibility| + vertex.requirements.all? { |req| requirement_satisfied_by?(req, activated, possibility) } + end + + activated.set_payload(vertex.name, latest_version) + end + activated.freeze + end + # Ends the resolution process # @return [void] def end_resolution @@ -136,9 +253,12 @@ module Bundler::Molinillo if possibility attempt_to_activate else - create_conflict if state.is_a? PossibilityState - unwind_for_conflict until possibility && state.is_a?(DependencyState) + create_conflict + unwind_for_conflict end + rescue CircularDependencyError => underlying_error + create_conflict(underlying_error) + unwind_for_conflict end # @return [Object] the current possibility that the resolution is trying @@ -158,7 +278,10 @@ module Bundler::Molinillo # @return [DependencyState] the initial state for the resolution def initial_state graph = DependencyGraph.new.tap do |dg| - original_requested.each { |r| dg.add_vertex(name_for(r), nil, true).tap { |v| v.explicit_requirements << r } } + original_requested.each do |requested| + vertex = dg.add_vertex(name_for(requested), nil, true) + vertex.explicit_requirements << requested + end dg.tag(:initial_state) end @@ -169,45 +292,280 @@ module Bundler::Molinillo requirements, graph, initial_requirement, - initial_requirement && search_for(initial_requirement), + possibilities_for_requirement(initial_requirement, graph), 0, - {} + {}, + [] ) end # Unwinds the states stack because a conflict has been encountered # @return [void] def unwind_for_conflict - debug(depth) { "Unwinding for conflict: #{requirement} to #{state_index_for_unwind / 2}" } + details_for_unwind = build_details_for_unwind + unwind_options = unused_unwind_options + debug(depth) { "Unwinding for conflict: #{requirement} to #{details_for_unwind.state_index / 2}" } conflicts.tap do |c| - sliced_states = states.slice!((state_index_for_unwind + 1)..-1) - raise VersionConflict.new(c) unless state + sliced_states = states.slice!((details_for_unwind.state_index + 1)..-1) + raise_error_unless_state(c) activated.rewind_to(sliced_states.first || :initial_state) if sliced_states state.conflicts = c + state.unused_unwind_options = unwind_options + filter_possibilities_after_unwind(details_for_unwind) index = states.size - 1 @parents_of.each { |_, a| a.reject! { |i| i >= index } } + state.unused_unwind_options.reject! { |uw| uw.state_index >= index } + end + end + + # Raises a VersionConflict error, or any underlying error, if there is no + # current state + # @return [void] + def raise_error_unless_state(conflicts) + return if state + + error = conflicts.values.map(&:underlying_error).compact.first + raise error || VersionConflict.new(conflicts, specification_provider) + end + + # @return [UnwindDetails] Details of the nearest index to which we could unwind + def build_details_for_unwind + # Get the possible unwinds for the current conflict + current_conflict = conflicts[name] + binding_requirements = binding_requirements_for_conflict(current_conflict) + unwind_details = unwind_options_for_requirements(binding_requirements) + + last_detail_for_current_unwind = unwind_details.sort.last + current_detail = last_detail_for_current_unwind + + # Look for past conflicts that could be unwound to affect the + # requirement tree for the current conflict + relevant_unused_unwinds = unused_unwind_options.select do |alternative| + intersecting_requirements = + last_detail_for_current_unwind.all_requirements & + alternative.requirements_unwound_to_instead + next if intersecting_requirements.empty? + # Find the highest index unwind whilst looping through + current_detail = alternative if alternative > current_detail + alternative + end + + # Add the current unwind options to the `unused_unwind_options` array. + # The "used" option will be filtered out during `unwind_for_conflict`. + state.unused_unwind_options += unwind_details.reject { |detail| detail.state_index == -1 } + + # Update the requirements_unwound_to_instead on any relevant unused unwinds + relevant_unused_unwinds.each { |d| d.requirements_unwound_to_instead << current_detail.state_requirement } + unwind_details.each { |d| d.requirements_unwound_to_instead << current_detail.state_requirement } + + current_detail + end + + # @param [Array<Object>] array of requirements that combine to create a conflict + # @return [Array<UnwindDetails>] array of UnwindDetails that have a chance + # of resolving the passed requirements + def unwind_options_for_requirements(binding_requirements) + unwind_details = [] + + trees = [] + binding_requirements.reverse_each do |r| + partial_tree = [r] + trees << partial_tree + unwind_details << UnwindDetails.new(-1, nil, partial_tree, binding_requirements, trees, []) + + # If this requirement has alternative possibilities, check if any would + # satisfy the other requirements that created this conflict + requirement_state = find_state_for(r) + if conflict_fixing_possibilities?(requirement_state, binding_requirements) + unwind_details << UnwindDetails.new( + states.index(requirement_state), + r, + partial_tree, + binding_requirements, + trees, + [] + ) + end + + # Next, look at the parent of this requirement, and check if the requirement + # could have been avoided if an alternative PossibilitySet had been chosen + parent_r = parent_of(r) + next if parent_r.nil? + partial_tree.unshift(parent_r) + requirement_state = find_state_for(parent_r) + if requirement_state.possibilities.any? { |set| !set.dependencies.include?(r) } + unwind_details << UnwindDetails.new( + states.index(requirement_state), + parent_r, + partial_tree, + binding_requirements, + trees, + [] + ) + end + + # Finally, look at the grandparent and up of this requirement, looking + # for any possibilities that wouldn't create their parent requirement + grandparent_r = parent_of(parent_r) + until grandparent_r.nil? + partial_tree.unshift(grandparent_r) + requirement_state = find_state_for(grandparent_r) + if requirement_state.possibilities.any? { |set| !set.dependencies.include?(parent_r) } + unwind_details << UnwindDetails.new( + states.index(requirement_state), + grandparent_r, + partial_tree, + binding_requirements, + trees, + [] + ) + end + parent_r = grandparent_r + grandparent_r = parent_of(parent_r) + end + end + + unwind_details + end + + # @param [DependencyState] state + # @param [Array] array of requirements + # @return [Boolean] whether or not the given state has any possibilities + # that could satisfy the given requirements + def conflict_fixing_possibilities?(state, binding_requirements) + return false unless state + + state.possibilities.any? do |possibility_set| + possibility_set.possibilities.any? do |poss| + possibility_satisfies_requirements?(poss, binding_requirements) + end + end + end + + # Filter's a state's possibilities to remove any that would not fix the + # conflict we've just rewound from + # @param [UnwindDetails] details of the conflict just unwound from + # @return [void] + def filter_possibilities_after_unwind(unwind_details) + return unless state && !state.possibilities.empty? + + if unwind_details.unwinding_to_primary_requirement? + filter_possibilities_for_primary_unwind(unwind_details) + else + filter_possibilities_for_parent_unwind(unwind_details) + end + end + + # Filter's a state's possibilities to remove any that would not satisfy + # the requirements in the conflict we've just rewound from + # @param [UnwindDetails] details of the conflict just unwound from + # @return [void] + def filter_possibilities_for_primary_unwind(unwind_details) + unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } + unwinds_to_state << unwind_details + unwind_requirement_sets = unwinds_to_state.map(&:conflicting_requirements) + + state.possibilities.reject! do |possibility_set| + possibility_set.possibilities.none? do |poss| + unwind_requirement_sets.any? do |requirements| + possibility_satisfies_requirements?(poss, requirements) + end + end end end - # @return [Integer] The index to which the resolution should unwind in the - # case of conflict. - def state_index_for_unwind - current_requirement = requirement - existing_requirement = requirement_for_existing_name(name) - index = -1 - [current_requirement, existing_requirement].each do |r| - until r.nil? - current_state = find_state_for(r) - if state_any?(current_state) - current_index = states.index(current_state) - index = current_index if current_index > index - break + # @param [Object] possibility a single possibility + # @param [Array] requirements an array of requirements + # @return [Boolean] whether the possibility satisfies all of the + # given requirements + def possibility_satisfies_requirements?(possibility, requirements) + name = name_for(possibility) + + activated.tag(:swap) + activated.set_payload(name, possibility) if activated.vertex_named(name) + satisfied = requirements.all? { |r| requirement_satisfied_by?(r, activated, possibility) } + activated.rewind_to(:swap) + + satisfied + end + + # Filter's a state's possibilities to remove any that would (eventually) + # create a requirement in the conflict we've just rewound from + # @param [UnwindDetails] details of the conflict just unwound from + # @return [void] + def filter_possibilities_for_parent_unwind(unwind_details) + unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } + unwinds_to_state << unwind_details + + primary_unwinds = unwinds_to_state.select(&:unwinding_to_primary_requirement?).uniq + parent_unwinds = unwinds_to_state.uniq - primary_unwinds + + allowed_possibility_sets = Compatibility.flat_map(primary_unwinds) do |unwind| + states[unwind.state_index].possibilities.select do |possibility_set| + possibility_set.possibilities.any? do |poss| + possibility_satisfies_requirements?(poss, unwind.conflicting_requirements) end - r = parent_of(r) end end - index + requirements_to_avoid = Compatibility.flat_map(parent_unwinds, &:sub_dependencies_to_avoid) + + state.possibilities.reject! do |possibility_set| + !allowed_possibility_sets.include?(possibility_set) && + (requirements_to_avoid - possibility_set.dependencies).empty? + end + end + + # @param [Conflict] conflict + # @return [Array] minimal array of requirements that would cause the passed + # conflict to occur. + def binding_requirements_for_conflict(conflict) + return [conflict.requirement] if conflict.possibility.nil? + + possible_binding_requirements = conflict.requirements.values.flatten(1).uniq + + # When there’s a `CircularDependency` error the conflicting requirement + # (the one causing the circular) won’t be `conflict.requirement` + # (which won’t be for the right state, because we won’t have created it, + # because it’s circular). + # We need to make sure we have that requirement in the conflict’s list, + # otherwise we won’t be able to unwind properly, so we just return all + # the requirements for the conflict. + return possible_binding_requirements if conflict.underlying_error + + possibilities = search_for(conflict.requirement) + + # If all the requirements together don't filter out all possibilities, + # then the only two requirements we need to consider are the initial one + # (where the dependency's version was first chosen) and the last + if binding_requirement_in_set?(nil, possible_binding_requirements, possibilities) + return [conflict.requirement, requirement_for_existing_name(name_for(conflict.requirement))].compact + end + + # Loop through the possible binding requirements, removing each one + # that doesn't bind. Use a `reverse_each` as we want the earliest set of + # binding requirements, and don't use `reject!` as we wish to refine the + # array *on each iteration*. + binding_requirements = possible_binding_requirements.dup + possible_binding_requirements.reverse_each do |req| + next if req == conflict.requirement + unless binding_requirement_in_set?(req, binding_requirements, possibilities) + binding_requirements -= [req] + end + end + + binding_requirements + end + + # @param [Object] requirement we wish to check + # @param [Array] array of requirements + # @param [Array] array of possibilities the requirements will be used to filter + # @return [Boolean] whether or not the given requirement is required to filter + # out all elements of the array of possibilities. + def binding_requirement_in_set?(requirement, possible_binding_requirements, possibilities) + possibilities.any? do |poss| + possibility_satisfies_requirements?(poss, possible_binding_requirements - [requirement]) + end end # @return [Object] the requirement that led to `requirement` being added @@ -222,7 +580,8 @@ module Bundler::Molinillo # @return [Object] the requirement that led to a version of a possibility # with the given name being activated. def requirement_for_existing_name(name) - return nil unless activated.vertex_named(name).payload + return nil unless vertex = activated.vertex_named(name) + return nil unless vertex.payload states.find { |s| s.name == name }.requirement end @@ -230,18 +589,12 @@ module Bundler::Molinillo # `requirement`. def find_state_for(requirement) return nil unless requirement - states.reverse_each.find { |i| requirement == i.requirement && i.is_a?(DependencyState) } - end - - # @return [Boolean] whether or not the given state has any possibilities - # left. - def state_any?(state) - state && state.possibilities.any? + states.find { |i| requirement == i.requirement } end # @return [Conflict] a {Conflict} that reflects the failure to activate # the {#possibility} in conjunction with the current {#state} - def create_conflict + def create_conflict(underlying_error = nil) vertex = activated.vertex_named(name) locked_requirement = locked_requirement_named(name) @@ -250,18 +603,21 @@ module Bundler::Molinillo requirements[name_for_explicit_dependency_source] = vertex.explicit_requirements end requirements[name_for_locking_dependency_source] = [locked_requirement] if locked_requirement - vertex.incoming_edges.each { |edge| (requirements[edge.origin.payload] ||= []).unshift(edge.requirement) } + vertex.incoming_edges.each do |edge| + (requirements[edge.origin.payload.latest_version] ||= []).unshift(edge.requirement) + end activated_by_name = {} - activated.each { |v| activated_by_name[v.name] = v.payload if v.payload } + activated.each { |v| activated_by_name[v.name] = v.payload.latest_version if v.payload } conflicts[name] = Conflict.new( requirement, requirements, - vertex.payload, + vertex.payload && vertex.payload.latest_version, possibility, locked_requirement, requirement_trees, - activated_by_name + activated_by_name, + underlying_error ) end @@ -311,116 +667,48 @@ module Bundler::Molinillo # @return [void] def attempt_to_activate debug(depth) { 'Attempting to activate ' + possibility.to_s } - existing_node = activated.vertex_named(name) - if existing_node.payload - debug(depth) { "Found existing spec (#{existing_node.payload})" } - attempt_to_activate_existing_spec(existing_node) - else - attempt_to_activate_new_spec - end - end - - # Attempts to activate the current {#possibility} (given that it has - # already been activated) - # @return [void] - def attempt_to_activate_existing_spec(existing_node) - existing_spec = existing_node.payload - if requirement_satisfied_by?(requirement, activated, existing_spec) - new_requirements = requirements.dup - push_state_for_requirements(new_requirements, false) + existing_vertex = activated.vertex_named(name) + if existing_vertex.payload + debug(depth) { "Found existing spec (#{existing_vertex.payload})" } + attempt_to_filter_existing_spec(existing_vertex) else - return if attempt_to_swap_possibility - create_conflict - debug(depth) { "Unsatisfied by existing spec (#{existing_node.payload})" } - unwind_for_conflict - end - end - - # Attempts to swp the current {#possibility} with the already-activated - # spec with the given name - # @return [Boolean] Whether the possibility was swapped into {#activated} - def attempt_to_swap_possibility - activated.tag(:swap) - vertex = activated.vertex_named(name) - activated.set_payload(name, possibility) - if !vertex.requirements. - all? { |r| requirement_satisfied_by?(r, activated, possibility) } || - !new_spec_satisfied? - activated.rewind_to(:swap) - return - end - fixup_swapped_children(vertex) - activate_spec - end - - # Ensures there are no orphaned successors to the given {vertex}. - # @param [DependencyGraph::Vertex] vertex the vertex to fix up. - # @return [void] - def fixup_swapped_children(vertex) # rubocop:disable Metrics/CyclomaticComplexity - payload = vertex.payload - deps = dependencies_for(payload).group_by(&method(:name_for)) - vertex.outgoing_edges.each do |outgoing_edge| - requirement = outgoing_edge.requirement - parent_index = @parents_of[requirement].last - succ = outgoing_edge.destination - matching_deps = Array(deps[succ.name]) - dep_matched = matching_deps.include?(requirement) - - # only push the current index when it was originally required by the - # same named spec - if parent_index && states[parent_index].name == name - @parents_of[requirement].push(states.size - 1) + latest = possibility.latest_version + # use reject!(!satisfied) for 1.8.7 compatibility + possibility.possibilities.reject! do |possibility| + !requirement_satisfied_by?(requirement, activated, possibility) end - - if matching_deps.empty? && !succ.root? && succ.predecessors.to_a == [vertex] - debug(depth) { "Removing orphaned spec #{succ.name} after swapping #{name}" } - succ.requirements.each { |r| @parents_of.delete(r) } - - removed_names = activated.detach_vertex_named(succ.name).map(&:name) - requirements.delete_if do |r| - # the only removed vertices are those with no other requirements, - # so it's safe to delete only based upon name here - removed_names.include?(name_for(r)) - end - elsif !dep_matched - debug(depth) { "Removing orphaned dependency #{requirement} after swapping #{name}" } - # also reset if we're removing the edge, but only if its parent has - # already been fixed up - @parents_of[requirement].push(states.size - 1) if @parents_of[requirement].empty? - - activated.delete_edge(outgoing_edge) - requirements.delete(requirement) + if possibility.latest_version.nil? + # ensure there's a possibility for better error messages + possibility.possibilities << latest if latest + create_conflict + unwind_for_conflict + else + activate_new_spec end end end - # Attempts to activate the current {#possibility} (given that it hasn't - # already been activated) + # Attempts to update the existing vertex's `PossibilitySet` with a filtered version # @return [void] - def attempt_to_activate_new_spec - if new_spec_satisfied? - activate_spec + def attempt_to_filter_existing_spec(vertex) + filtered_set = filtered_possibility_set(vertex) + if !filtered_set.possibilities.empty? + activated.set_payload(name, filtered_set) + new_requirements = requirements.dup + push_state_for_requirements(new_requirements, false) else create_conflict + debug(depth) { "Unsatisfied by existing spec (#{vertex.payload})" } unwind_for_conflict end end - # @return [Boolean] whether the current spec is satisfied as a new - # possibility. - def new_spec_satisfied? - unless requirement_satisfied_by?(requirement, activated, possibility) - debug(depth) { 'Unsatisfied by requested spec' } - return false - end - - locked_requirement = locked_requirement_named(name) - - locked_spec_satisfied = !locked_requirement || - requirement_satisfied_by?(locked_requirement, activated, possibility) - debug(depth) { 'Unsatisfied by locked spec' } unless locked_spec_satisfied - - locked_spec_satisfied + # Generates a filtered version of the existing vertex's `PossibilitySet` using the + # current state's `requirement` + # @param [Object] existing vertex + # @return [PossibilitySet] filtered possibility set + def filtered_possibility_set(vertex) + PossibilitySet.new(vertex.payload.dependencies, vertex.payload.possibilities & possibility.possibilities) end # @param [String] requirement_name the spec name to search for @@ -434,7 +722,7 @@ module Bundler::Molinillo # Add the current {#possibility} to the dependency graph of the current # {#state} # @return [void] - def activate_spec + def activate_new_spec conflicts.delete(name) debug(depth) { "Activated #{name} at #{possibility}" } activated.set_payload(name, possibility) @@ -442,14 +730,14 @@ module Bundler::Molinillo end # Requires the dependencies that the recently activated spec has - # @param [Object] activated_spec the specification that has just been + # @param [Object] activated_possibility the PossibilitySet that has just been # activated # @return [void] - def require_nested_dependencies_for(activated_spec) - nested_dependencies = dependencies_for(activated_spec) + def require_nested_dependencies_for(possibility_set) + nested_dependencies = dependencies_for(possibility_set.latest_version) debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" } nested_dependencies.each do |d| - activated.add_child_vertex(name_for(d), nil, [name_for(activated_spec)], d) + activated.add_child_vertex(name_for(d), nil, [name_for(possibility_set.latest_version)], d) parent_index = states.size - 1 parents = @parents_of[d] parents << parent_index if parents.empty? @@ -464,20 +752,75 @@ module Bundler::Molinillo # @return [void] def push_state_for_requirements(new_requirements, requires_sort = true, new_activated = activated) new_requirements = sort_dependencies(new_requirements.uniq, new_activated, conflicts) if requires_sort - new_requirement = new_requirements.shift + new_requirement = nil + loop do + new_requirement = new_requirements.shift + break if new_requirement.nil? || states.none? { |s| s.requirement == new_requirement } + end new_name = new_requirement ? name_for(new_requirement) : ''.freeze - possibilities = new_requirement ? search_for(new_requirement) : [] + possibilities = possibilities_for_requirement(new_requirement) handle_missing_or_push_dependency_state DependencyState.new( new_name, new_requirements, new_activated, - new_requirement, possibilities, depth, conflicts.dup + new_requirement, possibilities, depth, conflicts.dup, unused_unwind_options.dup ) end + # Checks a proposed requirement with any existing locked requirement + # before generating an array of possibilities for it. + # @param [Object] the proposed requirement + # @return [Array] possibilities + def possibilities_for_requirement(requirement, activated = self.activated) + return [] unless requirement + if locked_requirement_named(name_for(requirement)) + return locked_requirement_possibility_set(requirement, activated) + end + + group_possibilities(search_for(requirement)) + end + + # @param [Object] the proposed requirement + # @return [Array] possibility set containing only the locked requirement, if any + def locked_requirement_possibility_set(requirement, activated = self.activated) + all_possibilities = search_for(requirement) + locked_requirement = locked_requirement_named(name_for(requirement)) + + # Longwinded way to build a possibilities array with either the locked + # requirement or nothing in it. Required, since the API for + # locked_requirement isn't guaranteed. + locked_possibilities = all_possibilities.select do |possibility| + requirement_satisfied_by?(locked_requirement, activated, possibility) + end + + group_possibilities(locked_possibilities) + end + + # Build an array of PossibilitySets, with each element representing a group of + # dependency versions that all have the same sub-dependency version constraints + # and are contiguous. + # @param [Array] an array of possibilities + # @return [Array] an array of possibility sets + def group_possibilities(possibilities) + possibility_sets = [] + current_possibility_set = nil + + possibilities.reverse_each do |possibility| + dependencies = dependencies_for(possibility) + if current_possibility_set && current_possibility_set.dependencies == dependencies + current_possibility_set.possibilities.unshift(possibility) + else + possibility_sets.unshift(PossibilitySet.new(dependencies, [possibility])) + current_possibility_set = possibility_sets.first + end + end + + possibility_sets + end + # Pushes a new {DependencyState}. # If the {#specification_provider} says to # {SpecificationProvider#allow_missing?} that particular requirement, and # there are no possibilities for that requirement, then `state` is not - # pushed, and the node in {#activated} is removed, and we continue + # pushed, and the vertex in {#activated} is removed, and we continue # resolving the remaining requirements. # @param [DependencyState] state # @return [void] diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb b/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb index 50d853b146..7d36858778 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph' module Bundler::Molinillo diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/state.rb b/lib/bundler/vendor/molinillo/lib/molinillo/state.rb index 3a8107cf1a..68fa1f54e3 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/state.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/state.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo # A state that a {Resolution} can be in # @attr [String] name the name of the current requirement @@ -7,7 +8,8 @@ module Bundler::Molinillo # @attr [Object] requirement the current requirement # @attr [Object] possibilities the possibilities to satisfy the current requirement # @attr [Integer] depth the depth of the resolution - # @attr [Set<Object>] conflicts unresolved conflicts + # @attr [Hash] conflicts unresolved conflicts, indexed by dependency name + # @attr [Array<UnwindDetails>] unused_unwind_options unwinds for previous conflicts that weren't explored ResolutionState = Struct.new( :name, :requirements, @@ -15,14 +17,15 @@ module Bundler::Molinillo :requirement, :possibilities, :depth, - :conflicts + :conflicts, + :unused_unwind_options ) class ResolutionState # Returns an empty resolution state # @return [ResolutionState] an empty state def self.empty - new(nil, [], DependencyGraph.new, nil, nil, 0, Set.new) + new(nil, [], DependencyGraph.new, nil, nil, 0, {}, []) end end @@ -40,7 +43,8 @@ module Bundler::Molinillo requirement, [possibilities.pop], depth + 1, - conflicts.dup + conflicts.dup, + unused_unwind_options.dup ).tap do |state| state.activated.tag(state) end diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb index c872a79c13..7cbca5bc06 100644 --- a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb +++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb @@ -814,7 +814,7 @@ class Bundler::Persistent::Net::HTTP::Persistent ## # Pipelines +requests+ to the HTTP server at +uri+ yielding responses if a - # block is given. Returns all responses recieved. + # block is given. Returns all responses received. # # See # Net::HTTP::Pipeline[http://docs.seattlerb.org/net-http-pipeline/Net/HTTP/Pipeline.html] diff --git a/lib/bundler/vendor/thor/lib/thor/runner.rb b/lib/bundler/vendor/thor/lib/thor/runner.rb index 65ae422d7f..b110b8d478 100644 --- a/lib/bundler/vendor/thor/lib/thor/runner.rb +++ b/lib/bundler/vendor/thor/lib/thor/runner.rb @@ -3,7 +3,7 @@ require "bundler/vendor/thor/lib/thor/group" require "bundler/vendor/thor/lib/thor/core_ext/io_binary_read" require "yaml" -require "digest/md5" +require "digest" require "pathname" class Bundler::Thor::Runner < Bundler::Thor #:nodoc: # rubocop:disable ClassLength @@ -90,7 +90,7 @@ class Bundler::Thor::Runner < Bundler::Thor #:nodoc: # rubocop:disable ClassLeng end thor_yaml[as] = { - :filename => Digest::MD5.hexdigest(name + as), + :filename => Digest(:MD5).hexdigest(name + as), :location => location, :namespaces => Bundler::Thor::Util.namespaces_in_content(contents, base) } diff --git a/lib/bundler/vendored_fileutils.rb b/lib/bundler/vendored_fileutils.rb new file mode 100644 index 0000000000..d14e98baf7 --- /dev/null +++ b/lib/bundler/vendored_fileutils.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Bundler; end +if RUBY_VERSION >= "2.4" + require "bundler/vendor/fileutils/lib/fileutils" +else + # the version we vendor is 2.4+ + require "fileutils" +end diff --git a/lib/bundler/vendored_molinillo.rb b/lib/bundler/vendored_molinillo.rb index 7b231263cb..061b634f72 100644 --- a/lib/bundler/vendored_molinillo.rb +++ b/lib/bundler/vendored_molinillo.rb @@ -1,3 +1,4 @@ # frozen_string_literal: true + module Bundler; end require "bundler/vendor/molinillo/lib/molinillo" diff --git a/lib/bundler/vendored_persistent.rb b/lib/bundler/vendored_persistent.rb index 729ac6b6f5..de9c42fcc1 100644 --- a/lib/bundler/vendored_persistent.rb +++ b/lib/bundler/vendored_persistent.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # We forcibly require OpenSSL, because net/http/persistent will only autoload # it. On some Rubies, autoload fails but explicit require succeeds. begin @@ -15,3 +16,37 @@ module Bundler end end require "bundler/vendor/net-http-persistent/lib/net/http/persistent" + +module Bundler + class PersistentHTTP < Persistent::Net::HTTP::Persistent + def connection_for(uri) + connection = super + warn_old_tls_version_rubygems_connection(uri, connection) + connection + end + + def warn_old_tls_version_rubygems_connection(uri, connection) + return unless connection.use_ssl? + return unless (uri.host || "").end_with?("rubygems.org") + + socket = connection.instance_variable_get(:@socket) + return unless socket + socket_io = socket.io + return unless socket_io.respond_to?(:ssl_version) + ssl_version = socket_io.ssl_version + + case ssl_version + when /TLSv([\d\.]+)/ + version = Gem::Version.new($1) + if version < Gem::Version.new("1.2") + Bundler.ui.warn \ + "Warning: Your Ruby version is compiled against a copy of OpenSSL that is very old. " \ + "Starting in January 2018, RubyGems.org will refuse connection requests from these " \ + "very old versions of OpenSSL. If you will need to continue installing gems after " \ + "January 2018, please follow this guide to upgrade: http://ruby.to/tls-outdated.", + :wrap => true + end + end + end + end +end diff --git a/lib/bundler/vendored_thor.rb b/lib/bundler/vendored_thor.rb index 4a5d0cf6bb..8cca090f55 100644 --- a/lib/bundler/vendored_thor.rb +++ b/lib/bundler/vendored_thor.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler def self.require_thor_actions Kernel.send(:require, "bundler/vendor/thor/lib/thor/actions") diff --git a/lib/bundler/version.rb b/lib/bundler/version.rb index b2dad6dfb6..c2355ff2c8 100644 --- a/lib/bundler/version.rb +++ b/lib/bundler/version.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: true +# frozen_string_literal: false # Ruby 1.9.3 and old RubyGems don't play nice with frozen version strings # rubocop:disable MutableConstant @@ -7,7 +7,7 @@ module Bundler # We're doing this because we might write tests that deal # with other versions of bundler and we are unsure how to # handle this better. - VERSION = "1.15.4" unless defined?(::Bundler::VERSION) + VERSION = "1.16.0" unless defined?(::Bundler::VERSION) def self.overwrite_loaded_gem_version begin @@ -21,4 +21,8 @@ module Bundler end private_class_method :overwrite_loaded_gem_version overwrite_loaded_gem_version + + def self.bundler_major_version + @bundler_major_version ||= VERSION.split(".").first.to_i + end end diff --git a/lib/bundler/version_ranges.rb b/lib/bundler/version_ranges.rb index 1ee8440edd..ec25716cde 100644 --- a/lib/bundler/version_ranges.rb +++ b/lib/bundler/version_ranges.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler module VersionRanges NEq = Struct.new(:version) diff --git a/lib/bundler/vlad.rb b/lib/bundler/vlad.rb index db78f84baa..68181e7db8 100644 --- a/lib/bundler/vlad.rb +++ b/lib/bundler/vlad.rb @@ -1,4 +1,9 @@ # frozen_string_literal: true + +require "bundler/shared_helpers" +Bundler::SharedHelpers.major_deprecation 2, + "The Bundler task for Vlad" + # Vlad task for Bundler. # # Add "require 'bundler/vlad'" in your Vlad deploy.rb, and diff --git a/lib/bundler/worker.rb b/lib/bundler/worker.rb index b73a7ed04a..e91cfa7805 100644 --- a/lib/bundler/worker.rb +++ b/lib/bundler/worker.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "thread" module Bundler diff --git a/lib/bundler/yaml_serializer.rb b/lib/bundler/yaml_serializer.rb index 3c9eccafc2..0fd81c40ef 100644 --- a/lib/bundler/yaml_serializer.rb +++ b/lib/bundler/yaml_serializer.rb @@ -37,7 +37,7 @@ module Bundler HASH_REGEX = / ^ ([ ]*) # indentations - (.*) # key + (.+) # key (?::(?=(?:\s|$))) # : (without the lookahead the #key includes this when : is present in value) [ ]? (?: !\s)? # optional exclamation mark found with ruby 1.9.3 @@ -54,10 +54,10 @@ module Bundler last_empty_key = nil str.split(/\r?\n/).each do |line| if match = HASH_REGEX.match(line) - indent, key, _, val = match.captures + indent, key, quote, val = match.captures key = convert_to_backward_compatible_key(key) depth = indent.scan(/ /).length - if val.empty? + if quote.empty? && val.empty? new_hash = {} stack[depth][key] = new_hash stack[depth + 1] = new_hash |