diff options
108 files changed, 1832 insertions, 557 deletions
diff --git a/.travis.yml b/.travis.yml index f8dd2dd7ca..ba3035a4d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,11 +13,6 @@ branches: - /.+-stable$/ notifications: - email: - # andre - - secure: "bCcvqJT7YrBawtkXXwHhT+jOFth7r2Qv/30PkkbhQxk6Jb3xambjCOJ3U6vJ\ngYmiL50exi5lUp3oc3SEbHN5t2CrZqOZDQ6o7P8EAmB5c0oH2RrYaFOkI5Gt\nul/jGH/96A9sj0aMwG7JfdMSfhqj1DUKAm2PnnbXPL853VfmT24=" - # terence - - secure: "MQ8eA5Jb8YzEpAo58DRGfVJklAPcEbAulpBZnTxp0am6ldneDtJHbQk21w6R\nj5GsDHlzr/lMp/GHIimtUZ7rLohfND8fj/W7fs1Dkd4eN02/ERt98x3pHlqv\nvZgSnZ39uVYv+OcphraE24QaRaGWLhWZAMYQTVe/Yz50NyG8g1U=" slack: on_success: change on_failure: always @@ -43,12 +38,16 @@ env: # We need to know if changes to rubygems will break bundler on release - RGV=master # Test the latest rubygems release with all of our supported rubies - - RGV=v2.6.4 - - RGV=v2.4.8 + - RGV=v2.6.6 matrix: include: - # Ruby 2.2, Rubygems 2.4.5 and up (included by RGV above) + # Ruby 2.3, Rubygems 2.5.1 and up + - rvm: 2.2 + env: RGV=v2.5.2 + # Ruby 2.2, Rubygems 2.4.5 and up + - rvm: 2.2 + env: RGV=v2.4.8 # Ruby 2.1, Rubygems 2.2.2 and up - rvm: 2.1 env: RGV=v2.2.5 @@ -74,9 +73,12 @@ matrix: env: RGV=v1.6.2 - rvm: 1.9.3 env: RGV=v1.5.3 + # Ruby 1.8.7, Rubygems 1.3.6 and up - rvm: 1.8.7 env: RGV=v2.2.5 + # ALLOWED FAILURES + # since the great Travis image outage, frequent random segfaults :'( - rvm: 1.8.7 env: RGV=v2.0.14 - rvm: 1.8.7 @@ -93,16 +95,26 @@ matrix: env: RGV=v1.3.7 - rvm: 1.8.7 env: RGV=v1.3.6 - - # ALLOWED FAILURES - # For no apparent reason, this often goes over the Travis limit - - rvm: 1.8.7 - env: RGV=v2.1.11 # Ruby-head (we want to know how we're doing, but not fail the build) - rvm: ruby-head env: RGV=master allow_failures: - rvm: 1.8.7 - env: RGV=v2.1.11 + env: RGV=v2.0.14 + - rvm: 1.8.7 + env: RGV=v1.8.29 + - rvm: 1.8.7 + env: RGV=v1.7.2 + - rvm: 1.8.7 + env: RGV=v1.6.2 + - rvm: 1.8.7 + env: RGV=v1.5.3 + - rvm: 1.8.7 + env: RGV=v1.4.2 + - rvm: 1.8.7 + env: RGV=v1.3.7 + - rvm: 1.8.7 + env: RGV=v1.3.6 - rvm: ruby-head + env: RGV=master diff --git a/CHANGELOG.md b/CHANGELOG.md index d3f6ae5e23..5499bc828e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ Bugfixes: - ensure redefined methods have the same visibility as the one they're replacing, fixing `Kernel.require` failing on JRuby (#4975, @segiddins) - ensure that Bundler won't complain about a corrupt lockfile when no lockfile exists when using `gemspec` in the Gemfile (#5006, @segiddins) - fail gracefully when parsing the metadata for a gemspec from the compact index fails (@segiddins) - - fix invoking bundler with binstubs generated by RubyGems 2.6.2+ (#4974, @chrismo) + - fix system gems not being copied to --path on bundle install (e.g. --deployment) (#4974, @chrismo) Performance: @@ -53,7 +53,7 @@ Features: - print gem installation errors after other install output (#4834, @segiddins) - add `lock --remove-platform` flag to remove platforms from the lock (#4877, @segiddins) - add `only_update_to_newer_versions` setting to prevent downgrades during `update` (@segiddins) - - expanded expirimental plugin support to include hooks and sources (@asutoshpalai) + - expanded experimental plugin support to include hooks and sources (@asutoshpalai) Bugfixes: @@ -131,6 +131,11 @@ Bugfixes: - allow running `bundle install --deployment` after `bundle package --all` with path gems (#2175, @allenzhao) - add support for patchlevels in ruby versions in the gemfile and gemspecs (#4593, @chalkos) +## 1.12.6 (2016-10-10) + +Bugfixes: + - add support for weak etags to the new index (@segiddins) + ## 1.12.5 (2016-05-25) Bugfixes: diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 8964b75c3d..67e1527677 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -12,7 +12,7 @@ If you have any questions after reading this page, please feel free to contact e ## How you can help -We track [small bugs and features](https://github.com/bundler/bundler/issues?labels=small) so that anyone who wants to help can start with something that's not too overwhelming. We also keep a [list of things anyone can help with, any time](https://github.com/bundler/bundler/blob/master/CONTRIBUTING.md#contributing). If nothing on those lists looks good, talk to us, and we'll figure out what you can help with. We can absolutely use your help, no matter what level of programming skill you have at the moment. +We track [small bugs and features](https://github.com/bundler/bundler/labels/contribution%3A%20small) so that anyone who wants to help can start with something that's not too overwhelming. We also keep a [list of things anyone can help with, any time](https://github.com/bundler/bundler/blob/master/CONTRIBUTING.md#contributing). If nothing on those lists looks good, talk to us, and we'll figure out what you can help with. We can absolutely use your help, no matter what level of programming skill you have at the moment. # Development setup @@ -40,6 +40,8 @@ Bundler doesn't use a Gemfile to list development dependencies, because when we The `BUNDLE_DISABLE_POSTIT` environment variable ensures that the version of Bundler in `/path/to/bundler/lib` will be used. Without that environment setting, Bundler will automatically download, install, and run the version of Bundler listed in `Gemfile.lock`. With that set up, you can test changes you've made to Bundler by running `dbundle`, without interfering with the regular `bundle` command. +To dive into the code with Pry: `RUBYOPT=-rpry dbundle` to require pry and then run commands. + # Submitting Pull Requests Before you submit a pull request, please remember to do the following: @@ -10,6 +10,23 @@ Detailed information about each Bundler command, including help with common prob ## Troubleshooting +### Permission denied when installing bundler + +Certain operating systems such as MacOS and Ubuntu have versions of Ruby that require evelated privileges to install gems. + + ERROR: While executing gem ... (Gem::FilePermissionError) + You don't have write permissions for the /Library/Ruby/Gems/2.0.0 directory. + +There are multiple ways to solve this issue. You can install bundler with elevated privilges using `sudo` or `su`. + + sudo gem install bundler + +If you cannot elevated your privileges or do not want to globally install Bundler, you can use the `--user-install` option. + + gem install bundler --user-install + +This will install Bundler into your home directory. Note that you will need to append `~/.gem/ruby/<ruby version>/bin` to your `$PATH` variable to use `bundle`. + ### Heroku errors Please open a ticket with [Heroku](https://www.heroku.com) if you're having trouble deploying. They have a professional support team who can help you resolve Heroku issues far better than the Bundler team can. If the problem that you are having turns out to be a bug in Bundler itself, [Heroku support](https://www.heroku.com/support) can get the exact details to us. @@ -19,6 +19,8 @@ bundle install bundle exec rspec ``` +For help with installation issues, see [ISSUES](https://github.com/bundler/bundler/blob/master/ISSUES.md) + See [bundler.io](http://bundler.io) for the full documentation. ### Troubleshooting @@ -127,7 +127,7 @@ begin rubyopt = ENV["RUBYOPT"] # When editing this list, also edit .travis.yml! branches = %w(master) - releases = %w(v1.3.6 v1.3.7 v1.4.2 v1.5.3 v1.6.2 v1.7.2 v1.8.29 v2.0.14 v2.1.11 v2.2.5 v2.4.8 v2.6.4) + releases = %w(v1.3.6 v1.3.7 v1.4.2 v1.5.3 v1.6.2 v1.7.2 v1.8.29 v2.0.14 v2.1.11 v2.2.5 v2.4.8 v2.5.2 v2.6.6) (branches + releases).each do |rg| desc "Run specs with Rubygems #{rg}" RSpec::Core::RakeTask.new(rg) do |t| @@ -285,13 +285,6 @@ end begin require "automatiek" - Automatiek::RakeTask.new("compact_index_client") do |lib| - lib.download = { :github => "https://github.com/bundler/compact_index_client" } - lib.namespace = "CompactIndexClient" - lib.prefix = "Bundler" - lib.vendor_lib = "lib/bundler/vendor/compact_index_client" - end - Automatiek::RakeTask.new("molinillo") do |lib| lib.download = { :github => "https://github.com/CocoaPods/Molinillo" } lib.namespace = "Molinillo" diff --git a/exe/bundle b/exe/bundle index 51a9035d97..ec88ea7552 100755 --- a/exe/bundle +++ b/exe/bundle @@ -4,12 +4,10 @@ # Exit cleanly from an early interrupt Signal.trap("INT") { exit 1 } -unless ENV["BUNDLE_DISABLE_POSTIT"] - update = "update".start_with?(ARGV.first || " ") && ARGV.find {|a| a.start_with?("--bundler") } - update &&= update =~ /--bundler(?:=(.+))?/ && $1 || "> 0.a" - ENV["BUNDLER_VERSION"] = update if update - require "bundler/postit_trampoline" -end +update = "update".start_with?(ARGV.first || " ") && ARGV.find {|a| a.start_with?("--bundler") } +update &&= update =~ /--bundler(?:=(.+))?/ && $1 || "> 0.a" +ENV["BUNDLER_VERSION"] = update if update +require "bundler/postit_trampoline" require "bundler" # Check if an older version of bundler is installed diff --git a/lib/bundler.rb b/lib/bundler.rb index a152db323c..0dcaa03965 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -3,6 +3,8 @@ require "fileutils" require "pathname" require "rbconfig" require "thread" +require "tmpdir" + require "bundler/errors" require "bundler/environment_preserver" require "bundler/gem_remote_fetcher" @@ -27,6 +29,7 @@ module Bundler autoload :EndpointSpecification, "bundler/endpoint_specification" autoload :Env, "bundler/env" autoload :Fetcher, "bundler/fetcher" + autoload :FeatureFlag, "bundler/feature_flag" autoload :GemHelper, "bundler/gem_helper" autoload :GemHelpers, "bundler/gem_helpers" autoload :GemVersionPromoter, "bundler/gem_version_promoter" @@ -90,7 +93,7 @@ module Bundler # Return if all groups are already loaded return @setup if defined?(@setup) && @setup - definition.validate_ruby! + definition.validate_runtime! SharedHelpers.print_major_deprecations! @@ -142,8 +145,41 @@ module Bundler "#{Bundler.rubygems.ruby_engine}/#{Bundler.rubygems.config_map[:ruby_version]}" end + def user_home + @user_home ||= begin + home = Bundler.rubygems.user_home + warning = "Your home directory is not set properly:" + if home.nil? + warning += "\n * It is not set at all" + elsif !File.directory?(home) + warning += "\n * `#{home}` is not a directory" + elsif !File.writable?(home) + warning += "\n * `#{home}` is not writable" + else + return @user_home = Pathname.new(home) + end + + login = Etc.getlogin || "unknown" + + tmp_home = Pathname.new(Dir.tmpdir).join("bundler", "home", login) + begin + SharedHelpers.filesystem_access(tmp_home, :write) do |p| + FileUtils.mkdir_p(p) + end + rescue => e + warning += "\n\nBundler also failed to create a temporary home directory at `#{tmp_home}`:\n#{e}" + raise warning + end + + warning += "\n\nBundler will use `#{tmp_home}` as your home directory temporarily" + + Bundler.ui.warn(warning) + tmp_home + end + end + def user_bundle_path - Pathname.new(Bundler.rubygems.user_home).join(".bundle") + Pathname.new(user_home).join(".bundle") end def home @@ -257,6 +293,11 @@ EOF with_clean_env { Kernel.exec(*args) } end + def local_platform + return Gem::Platform::RUBY if settings[:force_ruby_platform] + Gem::Platform.local + end + def default_gemfile SharedHelpers.default_gemfile end @@ -328,17 +369,23 @@ EOF def sudo(str) SUDO_MUTEX.synchronize do prompt = "\n\n" + <<-PROMPT.gsub(/^ {6}/, "").strip + " " - Your user account isn't allowed to install to the system Rubygems. + Your user account isn't allowed to install to the system RubyGems. You can cancel this installation and run: bundle install --path vendor/bundle to install the gems into ./vendor/bundle/, or you can enter your password - and install the bundled gems to Rubygems using sudo. + and install the bundled gems to RubyGems using sudo. Password: PROMPT + unless @prompted_for_sudo ||= system(%(sudo -k -p "#{prompt}" true)) + raise SudoNotPermittedError, + "Bundler requires sudo access to install at the moment. " \ + "Try installing again, granting Bundler sudo access when prompted, or installing into a different path." + end + `sudo -p "#{prompt}" #{str}` end end @@ -389,6 +436,10 @@ EOF @git_present = Bundler.which("git") || Bundler.which("git.exe") end + def feature_flag + @feature_flag ||= FeatureFlag.new(VERSION) + end + def reset! @root = nil @settings = nil @@ -398,6 +449,7 @@ EOF @locked_gems = nil @bundle_path = nil @bin_path = nil + @user_home = nil Plugin.reset! diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 0453480401..f92ecb1c33 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -36,8 +36,10 @@ module Bundler 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"] + unprinted_warnings.each {|w| Bundler.ui.warn(w) } if ENV["RUBYGEMS_GEMDEPS"] && !ENV["RUBYGEMS_GEMDEPS"].empty? Bundler.ui.warn( @@ -92,7 +94,7 @@ module Bundler end def self.handle_no_command_error(command, has_namespace = $thor_runner) - if Bundler.settings[:plugins] && Bundler::Plugin.command?(command) + if Bundler.feature_flag.plugins? && Bundler::Plugin.command?(command) return Bundler::Plugin.exec_command(command, ARGV[1..-1]) end @@ -184,11 +186,9 @@ module Bundler map "i" => "install" def install require "bundler/cli/install" - no_install = Bundler.settings[:no_install] - Bundler.settings[:no_install] = false if no_install == true - Install.new(options.dup).run - ensure - Bundler.settings[:no_install] = no_install unless no_install.nil? + Bundler.settings.temporary(:no_install => false) do + Install.new(options.dup).run + end end desc "update [OPTIONS]", "update the current environment" @@ -255,7 +255,7 @@ module Bundler "Overwrite existing binstubs if they exist" method_option "path", :type => :string, :lazy_default => "bin", :banner => "Binstub destination directory (default bin)" - method_option "standalone", :type => :array, :lazy_default => [], :banner => + method_option "standalone", :type => :boolean, :banner => "Make binstubs that can work without the Bundler runtime" def binstubs(*gems) require "bundler/cli/binstubs" @@ -269,6 +269,8 @@ module Bundler versions of the given gems. Prerelease gems are ignored by default. If your gems are up to date, Bundler will exit with a status of 0. Otherwise, it will exit 1. 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 "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" @@ -443,7 +445,7 @@ module Bundler end desc "lock", "Creates a lockfile without installing" - method_option "update", :type => :array, :lazy_default => [], :banner => + method_option "update", :type => :array, :lazy_default => true, :banner => "ignore the existing lockfile, update all gems by default, or update list of given gems" method_option "local", :type => :boolean, :default => false, :banner => "do not attempt to fetch remote gemspecs and use the local gem cache only" @@ -457,6 +459,14 @@ module Bundler "add a new platform to the lockfile" method_option "remove-platform", :type => :array, :default => [], :banner => "remove a platform from the lockfile" + method_option "patch", :type => :boolean, :hide => true, :banner => + "Prefer updating only to next patch version" + method_option "minor", :type => :boolean, :hide => true, :banner => + "Prefer updating only to next minor version" + method_option "major", :type => :boolean, :hide => true, :banner => + "Prefer updating to next major version (default)" + method_option "strict", :type => :boolean, :hide => true, :banner => + "Do not allow any gem to be updated past latest --patch/--minor/--major" def lock require "bundler/cli/lock" Lock.new(options).run @@ -482,7 +492,7 @@ module Bundler Doctor.new(options).run end - if Bundler.settings[:plugins] + if Bundler.feature_flag.plugins? require "bundler/cli/plugin" desc "plugin SUBCOMMAND ...ARGS", "manage the bundler plugins" subcommand "plugin", Plugin diff --git a/lib/bundler/cli/binstubs.rb b/lib/bundler/cli/binstubs.rb index f7a27b01bb..95103b7dd8 100644 --- a/lib/bundler/cli/binstubs.rb +++ b/lib/bundler/cli/binstubs.rb @@ -10,7 +10,7 @@ module Bundler end def run - Bundler.definition.validate_ruby! + Bundler.definition.validate_runtime! Bundler.settings[:bin] = options["path"] if options["path"] Bundler.settings[:bin] = nil if options["path"] && options["path"].empty? installer = Installer.new(Bundler.root, Bundler.definition) diff --git a/lib/bundler/cli/cache.rb b/lib/bundler/cli/cache.rb index c8c63e926c..5ba105a31d 100644 --- a/lib/bundler/cli/cache.rb +++ b/lib/bundler/cli/cache.rb @@ -7,7 +7,7 @@ module Bundler end def run - Bundler.definition.validate_ruby! + 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") diff --git a/lib/bundler/cli/check.rb b/lib/bundler/cli/check.rb index 738d40b622..3f504ff621 100644 --- a/lib/bundler/cli/check.rb +++ b/lib/bundler/cli/check.rb @@ -15,7 +15,7 @@ module Bundler begin definition = Bundler.definition - definition.validate_ruby! + definition.validate_runtime! not_installed = definition.missing_specs rescue GemNotFound, VersionConflict Bundler.ui.error "Bundler can't satisfy your Gemfile's dependencies." diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb index 491ea04a11..6f45322db8 100644 --- a/lib/bundler/cli/common.rb +++ b/lib/bundler/cli/common.rb @@ -52,5 +52,14 @@ module Bundler message += "\nDid you mean #{suggestions}?" if suggestions message end + + def self.config_gem_version_promoter(definition, opts) + patch_level = [:major, :minor, :patch].select {|v| opts.keys.include?(v.to_s) } + raise InvalidOption, "Provide only one of the following options: #{patch_level.join(", ")}" unless patch_level.length <= 1 + definition.gem_version_promoter.tap do |gvp| + gvp.level = patch_level.first || :major + gvp.strict = opts[:strict] + end + end end end diff --git a/lib/bundler/cli/doctor.rb b/lib/bundler/cli/doctor.rb index 8fd862a1c2..728662024b 100644 --- a/lib/bundler/cli/doctor.rb +++ b/lib/bundler/cli/doctor.rb @@ -14,11 +14,11 @@ module Bundler end def otool_available? - system("otool --version 2>&1 >#{Bundler::NULL}") + system("otool --version 2>#{Bundler::NULL} >#{Bundler::NULL}") end def ldd_available? - !system("ldd --help 2>&1 >#{Bundler::NULL}").nil? + !system("ldd --help 2>#{Bundler::NULL} >#{Bundler::NULL}").nil? end def dylibs_darwin(path) @@ -55,22 +55,18 @@ module Bundler Dir.glob("#{spec.full_gem_path}/**/*.bundle") end + def check! + require "bundler/cli/check" + Bundler::CLI::Check.new({}).run + end + def run Bundler.ui.level = "error" if options[:quiet] + check! + definition = Bundler.definition broken_links = {} - begin - definition = Bundler.definition - definition.validate_ruby! - not_installed = definition.missing_specs - raise GemNotFound if not_installed.any? - rescue GemNotFound - Bundler.ui.warn "This bundle's gems must be installed to run this command." - Bundler.ui.warn "Install missing gems with `bundle install`." - exit 0 - end - definition.specs.each do |spec| bundles_for_gem(spec).each do |bundle| bad_paths = dylibs(bundle).select {|f| !File.exist?(f) } @@ -82,13 +78,15 @@ module Bundler end if broken_links.any? - Bundler.ui.error "The following gems are missing OS dependencies" - broken_links.each do |spec, paths| - paths.uniq.each do |path| - Bundler.ui.error " * #{spec.name}: #{path}" + message = "The following gems are missing OS dependencies:" + broken_links.map do |spec, paths| + paths.uniq.map do |path| + "\n * #{spec.name}: #{path}" end - end - exit 1 + end.flatten.sort.each {|m| message += m } + raise ProductionError, message + else + Bundler.ui.info "No issues found with the installed bundle" end end end diff --git a/lib/bundler/cli/gem.rb b/lib/bundler/cli/gem.rb index 27f4262e30..4dc0dbdb6b 100644 --- a/lib/bundler/cli/gem.rb +++ b/lib/bundler/cli/gem.rb @@ -44,7 +44,8 @@ module Bundler :test => options[:test], :ext => options[:ext], :exe => options[:exe], - :bundler_version => bundler_dependency_version + :bundler_version => bundler_dependency_version, + :git_user_name => git_user_name.empty? ? "[USERNAME]" : git_user_name } ensure_safe_gem_name(name, constant_array) diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb index 5c7b8c5b0b..f66716b39d 100644 --- a/lib/bundler/cli/install.rb +++ b/lib/bundler/cli/install.rb @@ -60,13 +60,10 @@ module Bundler "the --binstubs option will be removed in favor of `bundle binstubs`" end - # rubygems plugins sometimes hook into the gem install process - Gem.load_env_plugins if Gem.respond_to?(:load_env_plugins) - - Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins] + Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.feature_flag.plugins? definition = Bundler.definition - definition.validate_ruby! + definition.validate_runtime! installer = Installer.install(Bundler.root, definition, options) Bundler.load.cache if Bundler.app_cache.exist? && !options["no-cache"] && !Bundler.settings[:frozen] diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb index a6a95f895c..eb47c9efb0 100644 --- a/lib/bundler/cli/lock.rb +++ b/lib/bundler/cli/lock.rb @@ -1,4 +1,6 @@ # frozen_string_literal: true +require "bundler/cli/common" + module Bundler class CLI::Lock attr_reader :options @@ -17,14 +19,13 @@ module Bundler ui = Bundler.ui Bundler.ui = UI::Silent.new if print - gems = options[:update] Bundler::Fetcher.disable_endpoint = options["full-index"] - if gems && !gems.empty? - definition = Bundler.definition(:gems => gems) - else - definition = Bundler.definition(true) - end + update = options[:update] + update = { :gems => update } if update.is_a?(Array) + definition = Bundler.definition(update) + + Bundler::CLI::Common.config_gem_version_promoter(Bundler.definition, options) if options[:update] options["remove-platform"].each do |platform| definition.remove_platform(platform) diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb index 09b2d71453..5729c07ffe 100644 --- a/lib/bundler/cli/outdated.rb +++ b/lib/bundler/cli/outdated.rb @@ -4,6 +4,7 @@ require "bundler/cli/common" module Bundler class CLI::Outdated attr_reader :options, :gems + def initialize(options, gems) @options = options @gems = gems @@ -18,7 +19,7 @@ module Bundler Bundler::CLI::Common.select_spec(gem_name) end - Bundler.definition.validate_ruby! + Bundler.definition.validate_runtime! current_specs = Bundler.ui.silence { Bundler.load.specs } current_dependencies = {} Bundler.ui.silence { Bundler.load.dependencies.each {|dep| current_dependencies[dep.name] = dep } } @@ -30,7 +31,7 @@ module Bundler Bundler.definition(:gems => gems, :sources => sources) end - definition_resolution = proc { options["local"] ? definition.resolve_with_cache! : definition.resolve_remotely! } + definition_resolution = proc { options[:local] ? definition.resolve_with_cache! : definition.resolve_remotely! } if options[:parseable] Bundler.ui.silence(&definition_resolution) else @@ -38,8 +39,9 @@ module Bundler end Bundler.ui.info "" + outdated_gems_by_groups = {} + outdated_gems_list = [] - out_count = 0 # Loop through the current specs gemfile_specs, dependency_specs = current_specs.partition {|spec| current_dependencies.key? spec.name } [gemfile_specs.sort_by(&:name), dependency_specs.sort_by(&:name)].flatten.each do |current_spec| @@ -47,7 +49,7 @@ module Bundler dependency = current_dependencies[current_spec.name] - if options["strict"] + if options[:strict] active_spec = definition.specs.detect {|spec| spec.name == current_spec.name && spec.platform == current_spec.platform } else active_specs = definition.index[current_spec.name].select {|spec| spec.platform == current_spec.platform }.sort_by(&:version) @@ -67,47 +69,89 @@ module Bundler gem_outdated = Gem::Version.new(active_spec.version) > Gem::Version.new(current_spec.version) git_outdated = current_spec.git_version != active_spec.git_version if gem_outdated || git_outdated - unless options[:parseable] - if out_count == 0 - if options["pre"] - Bundler.ui.info "Outdated gems included in the bundle (including pre-releases):" - else - Bundler.ui.info "Outdated gems included in the bundle:" - end - end - end - - spec_version = "#{active_spec.version}#{active_spec.git_version}" - current_version = "#{current_spec.version}#{current_spec.git_version}" - dependency_version = %(, requested #{dependency.requirement}) if dependency && dependency.specific? - + groups = nil if dependency && !options[:parseable] groups = dependency.groups.join(", ") - pl = (dependency.groups.length > 1) ? "s" : "" - groups = " in group#{pl} \"#{groups}\"" end - spec_outdated_info = "#{active_spec.name} (newest #{spec_version}, installed #{current_version}#{dependency_version})" - if options[:parseable] - Bundler.ui.info spec_outdated_info.to_s.rstrip - else - Bundler.ui.info " * #{spec_outdated_info}#{groups}".rstrip - end + outdated_gems_list << { :active_spec => active_spec, + :current_spec => current_spec, + :dependency => dependency, + :groups => groups } - out_count += 1 + outdated_gems_by_groups[groups] ||= [] + outdated_gems_by_groups[groups] << { :active_spec => active_spec, + :current_spec => current_spec, + :dependency => dependency, + :groups => groups } end + Bundler.ui.debug "from #{active_spec.loaded_from}" end - if out_count.zero? + if outdated_gems_list.empty? Bundler.ui.info "Bundle up to date!\n" unless options[:parseable] else + unless options[:parseable] + if options[:pre] + Bundler.ui.info "Outdated gems included in the bundle (including pre-releases):" + else + Bundler.ui.info "Outdated gems included in the bundle:" + end + end + + options_include_groups = [:group, :groups].select {|v| options.keys.include?(v.to_s) } + if options_include_groups.any? + ordered_groups = outdated_gems_by_groups.keys.compact.sort + [nil, ordered_groups].flatten.each do |groups| + gems = outdated_gems_by_groups[groups] + contains_group = if groups + groups.split(",").include?(options[:group]) + else + options[:group] == "group" + end + + next if (!options[:groups] && !contains_group) || gems.nil? + + unless options[:parseable] + if groups + Bundler.ui.info "===== Group #{groups} =====" + else + Bundler.ui.info "===== Without group =====" + end + end + + gems.each do |gem| + print_gem(gem[:current_spec], gem[:active_spec], gem[:dependency], groups, options_include_groups.any?) + end + end + else + outdated_gems_list.each do |gem| + print_gem(gem[:current_spec], gem[:active_spec], gem[:dependency], gem[:groups], options_include_groups.any?) + end + end + exit 1 end end private + def print_gem(current_spec, active_spec, dependency, groups, options_include_groups) + spec_version = "#{active_spec.version}#{active_spec.git_version}" + current_version = "#{current_spec.version}#{current_spec.git_version}" + dependency_version = %(, requested #{dependency.requirement}) if dependency && dependency.specific? + + spec_outdated_info = "#{active_spec.name} (newest #{spec_version}, installed #{current_version}#{dependency_version})" + if options[:parseable] + Bundler.ui.info spec_outdated_info.to_s.rstrip + elsif options_include_groups || !groups + Bundler.ui.info " * #{spec_outdated_info}".rstrip + else + Bundler.ui.info " * #{spec_outdated_info} in groups \"#{groups}\"".rstrip + end + end + def check_for_deployment_mode if Bundler.settings[:frozen] error_message = "You are trying to check outdated gems in deployment mode. " \ @@ -123,7 +167,6 @@ module Bundler active_major = active_spec.version.segments.first update_present = false - update_present = active_major > current_major if options[:major] if !update_present && (options[:minor] || options[:patch]) && current_major == active_major diff --git a/lib/bundler/cli/platform.rb b/lib/bundler/cli/platform.rb index b5f906bfd9..9fdab0a53c 100644 --- a/lib/bundler/cli/platform.rb +++ b/lib/bundler/cli/platform.rb @@ -29,7 +29,7 @@ module Bundler output << "Your Gemfile specifies a Ruby version requirement:\n* #{ruby_version}" begin - Bundler.definition.validate_ruby! + Bundler.definition.validate_runtime! output << "Your current platform satisfies the Ruby version requirement." rescue RubyVersionMismatch => e output << e.message diff --git a/lib/bundler/cli/show.rb b/lib/bundler/cli/show.rb index d67b086dd1..77e845a603 100644 --- a/lib/bundler/cli/show.rb +++ b/lib/bundler/cli/show.rb @@ -13,7 +13,7 @@ module Bundler def run Bundler.ui.silence do - Bundler.definition.validate_ruby! + Bundler.definition.validate_runtime! Bundler.load.lock end diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb index 1c35659e0b..51de98bf34 100644 --- a/lib/bundler/cli/update.rb +++ b/lib/bundler/cli/update.rb @@ -1,4 +1,6 @@ # frozen_string_literal: true +require "bundler/cli/common" + module Bundler class CLI::Update attr_reader :options, :gems @@ -10,7 +12,7 @@ module Bundler def run Bundler.ui.level = "error" if options[:quiet] - Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins] + Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.feature_flag.plugins? sources = Array(options[:source]) groups = Array(options[:group]).map(&:to_sym) @@ -27,7 +29,6 @@ module Bundler names = Bundler.locked_gems.specs.map(&:name) gems.each do |g| next if names.include?(g) - require "bundler/cli/common" raise GemNotFound, Bundler::CLI::Common.gem_not_found_message(g, names) end @@ -39,12 +40,7 @@ module Bundler Bundler.definition(:gems => gems, :sources => sources, :ruby => options[:ruby]) end - patch_level = [:major, :minor, :patch].select {|v| options.keys.include?(v.to_s) } - raise ProductionError, "Provide only one of the following options: #{patch_level.join(", ")}" unless patch_level.length <= 1 - Bundler.definition.gem_version_promoter.tap do |gvp| - gvp.level = patch_level.first || :major - gvp.strict = options[:strict] - end + Bundler::CLI::Common.config_gem_version_promoter(Bundler.definition, options) Bundler::Fetcher.disable_endpoint = options["full-index"] @@ -54,10 +50,7 @@ module Bundler Bundler.settings[:jobs] = opts["jobs"] if opts["jobs"] - # rubygems plugins sometimes hook into the gem install process - Gem.load_env_plugins if Gem.respond_to?(:load_env_plugins) - - Bundler.definition.validate_ruby! + Bundler.definition.validate_runtime! Installer.install Bundler.root, Bundler.definition, opts Bundler.load.cache if Bundler.app_cache.exist? diff --git a/lib/bundler/compact_index_client.rb b/lib/bundler/compact_index_client.rb new file mode 100644 index 0000000000..1ff3deda01 --- /dev/null +++ b/lib/bundler/compact_index_client.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true +require "pathname" +require "set" + +module Bundler + class CompactIndexClient + DEBUG_MUTEX = Mutex.new + def self.debug + return unless ENV["DEBUG_COMPACT_INDEX"] + DEBUG_MUTEX.synchronize { warn("[#{self}] #{yield}") } + end + + class Error < StandardError; end + + require "bundler/compact_index_client/cache" + require "bundler/compact_index_client/updater" + + attr_reader :directory + + # @return [Lambda] A lambda that takes an array of inputs and a block, and + # maps the inputs with the block in parallel. + # + attr_accessor :in_parallel + + def initialize(directory, fetcher) + @directory = Pathname.new(directory) + @updater = Updater.new(fetcher) + @cache = Cache.new(@directory) + @endpoints = Set.new + @info_checksums_by_name = {} + @in_parallel = lambda do |inputs, &blk| + inputs.map(&blk) + end + end + + def names + Bundler::CompactIndexClient.debug { "/names" } + update(@cache.names_path, "names") + @cache.names + end + + def versions + Bundler::CompactIndexClient.debug { "/versions" } + update(@cache.versions_path, "versions") + versions, @info_checksums_by_name = @cache.versions + versions + end + + def dependencies(names) + Bundler::CompactIndexClient.debug { "dependencies(#{names})" } + in_parallel.call(names) do |name| + update_info(name) + @cache.dependencies(name).map {|d| d.unshift(name) } + end.flatten(1) + end + + def spec(name, version, platform = nil) + Bundler::CompactIndexClient.debug { "spec(name = #{name}, version = #{version}, platform = #{platform})" } + update_info(name) + @cache.specific_dependency(name, version, platform) + end + + def update_and_parse_checksums! + Bundler::CompactIndexClient.debug { "update_and_parse_checksums!" } + return @info_checksums_by_name if @parsed_checksums + update(@cache.versions_path, "versions") + @info_checksums_by_name = @cache.checksums + @parsed_checksums = true + end + + private + + def update(local_path, remote_path) + Bundler::CompactIndexClient.debug { "update(#{local_path}, #{remote_path})" } + unless @endpoints.add?(remote_path) + Bundler::CompactIndexClient.debug { "already fetched #{remote_path}" } + return + end + @updater.update(local_path, url(remote_path)) + end + + def update_info(name) + Bundler::CompactIndexClient.debug { "update_info(#{name})" } + path = @cache.info_path(name) + checksum = @updater.checksum_for_file(path) + unless existing = @info_checksums_by_name[name] + Bundler::CompactIndexClient.debug { "skipping updating info for #{name} since it is missing from versions" } + return + end + if checksum == existing + Bundler::CompactIndexClient.debug { "skipping updating info for #{name} since the versions checksum matches the local checksum" } + return + end + Bundler::CompactIndexClient.debug { "updating info for #{name} since the versions checksum #{existing} != the local checksum #{checksum}" } + update(path, "info/#{name}") + end + + def url(path) + path + end + end +end diff --git a/lib/bundler/compact_index_client/cache.rb b/lib/bundler/compact_index_client/cache.rb new file mode 100644 index 0000000000..e44f05dc7e --- /dev/null +++ b/lib/bundler/compact_index_client/cache.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true +require "digest/md5" + +module Bundler + class CompactIndexClient + class Cache + attr_reader :directory + + def initialize(directory) + @directory = Pathname.new(directory).expand_path + info_roots.each do |dir| + SharedHelpers.filesystem_access(dir) do + FileUtils.mkdir_p(dir) + end + end + end + + def names + lines(names_path) + end + + def names_path + directory.join("names") + end + + def versions + versions_by_name = Hash.new {|hash, key| hash[key] = [] } + info_checksums_by_name = {} + + lines(versions_path).each do |line| + name, versions_string, info_checksum = line.split(" ", 3) + info_checksums_by_name[name] = info_checksum || "" + versions_string.split(",").each do |version| + if version.start_with?("-") + version = version[1..-1].split("-", 2).unshift(name) + versions_by_name[name].delete(version) + else + version = version.split("-", 2).unshift(name) + versions_by_name[name] << version + end + end + end + + [versions_by_name, info_checksums_by_name] + end + + def versions_path + directory.join("versions") + end + + def checksums + checksums = {} + + lines(versions_path).each do |line| + name, _, checksum = line.split(" ", 3) + checksums[name] = checksum + end + + checksums + end + + def dependencies(name) + lines(info_path(name)).map do |line| + parse_gem(line) + end + end + + def info_path(name) + name = name.to_s + if name =~ /[^a-z0-9_-]/ + name += "-#{Digest::MD5.hexdigest(name).downcase}" + info_roots.last.join(name) + else + info_roots.first.join(name) + end + end + + def specific_dependency(name, version, platform) + pattern = [version, platform].compact.join("-") + return nil if pattern.empty? + + gem_lines = info_path(name).read + gem_line = gem_lines[/^#{Regexp.escape(pattern)}\b.*/, 0] + gem_line ? parse_gem(gem_line) : nil + end + + private + + def lines(path) + return [] unless path.file? + lines = SharedHelpers.filesystem_access(path, :read, &:read).split("\n") + header = lines.index("---") + header ? lines[header + 1..-1] : lines + end + + def parse_gem(string) + version_and_platform, rest = string.split(" ", 2) + version, platform = version_and_platform.split("-", 2) + dependencies, requirements = rest.split("|", 2).map {|s| s.split(",") } if rest + dependencies = dependencies ? dependencies.map {|d| parse_dependency(d) } : [] + requirements = requirements ? requirements.map {|r| parse_dependency(r) } : [] + [version, platform, dependencies, requirements] + end + + def parse_dependency(string) + dependency = string.split(":") + dependency[-1] = dependency[-1].split("&") if dependency.size > 1 + dependency + end + + def info_roots + [ + directory.join("info"), + directory.join("info-special-characters"), + ] + end + end + end +end diff --git a/lib/bundler/compact_index_client/updater.rb b/lib/bundler/compact_index_client/updater.rb new file mode 100644 index 0000000000..b407c64039 --- /dev/null +++ b/lib/bundler/compact_index_client/updater.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true +require "fileutils" +require "stringio" +require "tmpdir" +require "zlib" + +module Bundler + class CompactIndexClient + class Updater + class MisMatchedChecksumError < Error + def initialize(path, server_checksum, local_checksum) + @path = path + @server_checksum = server_checksum + @local_checksum = local_checksum + end + + def message + "The checksum of /#{@path} does not match the checksum provided by the server! Something is wrong " \ + "(local checksum is #{@local_checksum.inspect}, was expecting #{@server_checksum.inspect})." + end + end + + def initialize(fetcher) + @fetcher = fetcher + end + + def update(local_path, remote_path, retrying = nil) + headers = {} + + Dir.mktmpdir("bundler-compact-index-") do |local_temp_dir| + local_temp_path = Pathname.new(local_temp_dir).join(local_path.basename) + + # first try to fetch any new bytes on the existing file + if retrying.nil? && local_path.file? + FileUtils.cp local_path, local_temp_path + headers["If-None-Match"] = etag_for(local_temp_path) + headers["Range"] = "bytes=#{local_temp_path.size}-" + else + # Fastly ignores Range when Accept-Encoding: gzip is set + headers["Accept-Encoding"] = "gzip" + end + + response = @fetcher.call(remote_path, headers) + return nil if response.is_a?(Net::HTTPNotModified) + + content = response.body + if response["Content-Encoding"] == "gzip" + content = Zlib::GzipReader.new(StringIO.new(content)).read + end + + mode = response.is_a?(Net::HTTPPartialContent) ? "a" : "w" + SharedHelpers.filesystem_access(local_temp_path) do + local_temp_path.open(mode) {|f| f << content } + end + + response_etag = response["ETag"].gsub(%r{\AW/}, "") + if etag_for(local_temp_path) == response_etag + SharedHelpers.filesystem_access(local_path) do + FileUtils.mv(local_temp_path, local_path) + end + return nil + end + + if retrying + raise MisMatchedChecksumError.new(remote_path, response_etag, etag_for(local_temp_path)) + end + + update(local_path, remote_path, :retrying) + end + end + + def etag_for(path) + sum = checksum_for_file(path) + sum ? %("#{sum}") : nil + end + + def checksum_for_file(path) + return nil unless path.file? + # This must use IO.read instead of Digest.file().hexdigest + # 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)) + end + end + end + end +end diff --git a/lib/bundler/current_ruby.rb b/lib/bundler/current_ruby.rb index 6180285942..7b3d87e320 100644 --- a/lib/bundler/current_ruby.rb +++ b/lib/bundler/current_ruby.rb @@ -57,15 +57,15 @@ module Bundler end def mswin64? - Bundler::WINDOWS && Gem::Platform.local.os == "mswin64" && Gem::Platform.local.cpu == "x64" + Bundler::WINDOWS && Bundler.local_platform.os == "mswin64" && Bundler.local_platform.cpu == "x64" end def mingw? - Bundler::WINDOWS && Gem::Platform.local.os == "mingw32" && Gem::Platform.local.cpu != "x64" + Bundler::WINDOWS && Bundler.local_platform.os == "mingw32" && Bundler.local_platform.cpu != "x64" end def x64_mingw? - Bundler::WINDOWS && Gem::Platform.local.os == "mingw32" && Gem::Platform.local.cpu == "x64" + Bundler::WINDOWS && Bundler.local_platform.os == "mingw32" && Bundler.local_platform.cpu == "x64" end (KNOWN_MINOR_VERSIONS + KNOWN_MAJOR_VERSIONS).each do |version| diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index f14085f60a..8a6bf0d17c 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -62,6 +62,7 @@ module Bundler @specs = nil @ruby_version = ruby_version + @lockfile = lockfile @lockfile_contents = String.new @locked_bundler_version = nil @locked_ruby_version = nil @@ -69,7 +70,8 @@ module Bundler if lockfile && File.exist?(lockfile) @lockfile_contents = Bundler.read_file(lockfile) @locked_gems = LockfileParser.new(@lockfile_contents) - @platforms = @locked_gems.platforms + @locked_platforms = @locked_gems.platforms + @platforms = @locked_platforms.dup @locked_bundler_version = @locked_gems.bundler_version @locked_ruby_version = @locked_gems.ruby_version @@ -90,21 +92,17 @@ module Bundler @locked_deps = [] @locked_specs = SpecSet.new([]) @locked_sources = [] + @locked_platforms = [] end @unlock[:gems] ||= [] @unlock[:sources] ||= [] - @unlock[:ruby] ||= if @ruby_version && @locked_ruby_version - unless locked_ruby_version_object = RubyVersion.from_string(@locked_ruby_version) - raise LockfileError, "Failed to create a `RubyVersion` object from " \ - "`#{@locked_ruby_version}` found in #{lockfile} -- try running `bundle update --ruby`." - end + @unlock[:ruby] ||= if @ruby_version && locked_ruby_version_object @ruby_version.diff(locked_ruby_version_object) end @unlocking ||= @unlock[:ruby] ||= (!@locked_ruby_version ^ !@ruby_version) - current_platform = Bundler.rubygems.platforms.map {|p| generic(p) }.compact.last - add_platform(current_platform) + add_current_platform unless Bundler.settings[:frozen] @path_changes = converge_paths eager_unlock = expand_dependencies(@unlock[:gems]) @@ -247,7 +245,7 @@ module Bundler else # Run a resolve against the locally available gems Bundler.ui.debug("Found changes from the lockfile, re-resolving dependencies because #{change_reason}") - last_resolve.merge Resolver.resolve(expanded_dependencies, index, source_requirements, last_resolve, ruby_version, gem_version_promoter, additional_base_requirements_for_resolve) + last_resolve.merge Resolver.resolve(expanded_dependencies, index, source_requirements, last_resolve, gem_version_promoter, additional_base_requirements_for_resolve) end end end @@ -262,6 +260,8 @@ module Bundler 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 @@ -338,6 +338,18 @@ module Bundler end end + def locked_ruby_version_object + return unless @locked_ruby_version + @locked_ruby_version_object ||= begin + unless version = RubyVersion.from_string(@locked_ruby_version) + raise LockfileError, "The Ruby version #{@locked_ruby_version} from " \ + "#{@lockfile} could not be parsed. " \ + "Try running bundle update --ruby to resolve this." + end + version + end + end + def to_lock out = String.new @@ -401,6 +413,11 @@ module Bundler deleted = [] changed = [] + new_platforms = @platforms - @locked_platforms + deleted_platforms = @locked_platforms - @platforms + added.concat new_platforms.map {|p| "* platform: #{p}" } + deleted.concat deleted_platforms.map {|p| "* platform: #{p}" } + gemfile_sources = sources.lock_sources new_sources = gemfile_sources - @locked_sources @@ -449,6 +466,11 @@ module Bundler raise ProductionError, msg if added.any? || deleted.any? || changed.any? end + def validate_runtime! + validate_ruby! + validate_platforms! + end + def validate_ruby! return unless ruby_version @@ -474,6 +496,22 @@ module Bundler end end + # TODO: refactor this so that `match_platform` can be called with two platforms + DummyPlatform = Struct.new(:platform) + class DummyPlatform; include MatchPlatform; end + def validate_platforms! + return if @platforms.any? do |bundle_platform| + bundle_platform = DummyPlatform.new(bundle_platform) + Bundler.rubygems.platforms.any? do |local_platform| + bundle_platform.match_platform(local_platform) + end + end + + raise ProductionError, "Your bundle only supports platforms #{@platforms.map(&:to_s)} " \ + "but your local platforms are #{Bundler.rubygems.platforms.map(&:to_s)}, and " \ + "there's no compatible match between those two lists." + end + def add_platform(platform) @new_platform ||= !@platforms.include?(platform) @platforms |= [platform] @@ -484,6 +522,12 @@ module Bundler raise InvalidOption, "Unable to remove the platform `#{platform}` since the only platforms are #{@platforms.join ", "}" end + def add_current_platform + current_platform = Bundler.local_platform + add_platform(current_platform) if Bundler.settings[:specific_platform] + add_platform(generic(current_platform)) + end + attr_reader :sources private :sources @@ -726,16 +770,54 @@ module Bundler @locked_specs.any? {|s| s.satisfies?(dep) && (!dep.source || s.source.include?(dep.source)) } end + # This list of dependencies is only used in #resolve, so it's OK to add + # the metadata dependencies here def expanded_dependencies - @expanded_dependencies ||= expand_dependencies(dependencies, @remote) + @expanded_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 + + def concat_ruby_version_requirements(ruby_version, ruby_versions = []) + return ruby_versions unless ruby_version + if ruby_version.patchlevel + ruby_versions << ruby_version.to_gem_version_with_patchlevel + else + ruby_versions.concat(ruby_version.versions.map do |version| + requirement = Gem::Requirement.new(version) + if requirement.exact? + "~> #{version}.0" + else + requirement + end + end) + end end def expand_dependencies(dependencies, remote = false) deps = [] dependencies.each do |dep| dep = Dependency.new(dep, ">= 0") unless dep.respond_to?(:name) - next unless remote || dep.current_platform? - dep.gem_platforms(@platforms).each do |p| + next if !remote && !dep.current_platform? + platforms = dep.gem_platforms(@platforms) + if platforms.empty? + Bundler.ui.warn \ + "The dependency #{dep} will be unused by any of the platforms Bundler is installing for. " \ + "Bundler is installing for #{@platforms.join ", "} but the dependency " \ + "is only for #{dep.platforms.map {|p| Dependency::PLATFORM_MAP[p] }.join ", "}. " \ + "To add those platforms to the bundle, run `bundle lock --add-platform #{dep.platforms.join ", "}`." + end + platforms.each do |p| deps << DepProxy.new(dep, p) if remote || p == generic_local_platform end end @@ -812,9 +894,10 @@ module Bundler end def additional_base_requirements_for_resolve - return [] unless @locked_gems && Bundler.settings[:only_update_to_newer_versions] + return [] unless @locked_gems && Bundler.feature_flag.only_update_to_newer_versions? @locked_gems.specs.reduce({}) do |requirements, locked_spec| - requirements[locked_spec.name] = Gem::Dependency.new(locked_spec.name, ">= #{locked_spec.version}") + dep = Gem::Dependency.new(locked_spec.name, ">= #{locked_spec.version}") + requirements[locked_spec.name] = DepProxy.new(dep, locked_spec.platform) requirements end.values end diff --git a/lib/bundler/dependency.rb b/lib/bundler/dependency.rb index 1b1808b40a..66162d741a 100644 --- a/lib/bundler/dependency.rb +++ b/lib/bundler/dependency.rb @@ -52,7 +52,7 @@ module Bundler :x64_mingw_20 => Gem::Platform::X64_MINGW, :x64_mingw_21 => Gem::Platform::X64_MINGW, :x64_mingw_22 => Gem::Platform::X64_MINGW, - :x64_mingw_23 => Gem::Platform::X64_MINGW + :x64_mingw_23 => Gem::Platform::X64_MINGW, }.freeze REVERSE_PLATFORM_MAP = {}.tap do |reverse_platform_map| diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index b064c80d4c..cdbae076f0 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -65,7 +65,7 @@ module Bundler case specs_by_name_and_version.size when 1 specs = specs_by_name_and_version.values.first - spec = specs.find {|s| s.match_platform(Gem::Platform.local) } || specs.first + spec = specs.find {|s| s.match_platform(Bundler.local_platform) } || specs.first @gemspecs << spec @@ -393,7 +393,8 @@ module Bundler "as an option for #{command}, but it is invalid." end - message << " Valid options are: #{valid_keys.join(", ")}" + 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 end diff --git a/lib/bundler/env.rb b/lib/bundler/env.rb index 75edbf2b88..f7c6f7e83d 100644 --- a/lib/bundler/env.rb +++ b/lib/bundler/env.rb @@ -20,6 +20,8 @@ module Bundler 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? @@ -33,6 +35,8 @@ module Bundler end end + return out unless SharedHelpers.in_bundle? + if print_gemfile out << "\n#{Bundler.default_gemfile.relative_path_from(SharedHelpers.pwd)}\n\n" out << " " << read_file(Bundler.default_gemfile).gsub(/\n/, "\n ") << "\n" diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb index 7681ea73ae..dd5782fb3d 100644 --- a/lib/bundler/errors.rb +++ b/lib/bundler/errors.rb @@ -52,6 +52,7 @@ module Bundler class CyclicDependencyError < BundlerError; status_code(21); end class GemfileLockNotFound < BundlerError; status_code(22); end class PluginError < BundlerError; status_code(29); end + class SudoNotPermittedError < BundlerError; status_code(30); end class GemfileEvalError < GemfileError; end class MarshalError < StandardError; end diff --git a/lib/bundler/feature_flag.rb b/lib/bundler/feature_flag.rb new file mode 100644 index 0000000000..150cac1e67 --- /dev/null +++ b/lib/bundler/feature_flag.rb @@ -0,0 +1,32 @@ +# 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] + value = instance_eval(&default) if value.nil? && !default.nil? + value + end + end + + (1..10).each {|v| define_method("bundler_#{v}_mode?") { major_version >= v } } + + settings_flag(:allow_offline_install) { bundler_2_mode? } + settings_flag(:only_update_to_newer_versions) { bundler_2_mode? } + settings_flag(:plugins) { @bundler_version >= Gem::Version.new("1.14") } + + def initialize(bundler_version) + @bundler_version = Gem::Version.create(bundler_version) + end + + def major_version + @bundler_version.segments.first + end + private :major_version + + class << self; private :settings_flag; end + end +end diff --git a/lib/bundler/fetcher/compact_index.rb b/lib/bundler/fetcher/compact_index.rb index 34391bc260..38105a3f8f 100644 --- a/lib/bundler/fetcher/compact_index.rb +++ b/lib/bundler/fetcher/compact_index.rb @@ -5,7 +5,7 @@ require "bundler/worker" module Bundler class Fetcher class CompactIndex < Base - require "bundler/vendor/compact_index_client/lib/compact_index_client" + require "bundler/compact_index_client" def self.compact_index_request(method_name) method = instance_method(method_name) @@ -61,7 +61,7 @@ module Bundler compact_index_request :fetch_spec def available? - user_home = Pathname.new(Bundler.rubygems.user_home) + 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 fetch_uri.scheme != "file" && compact_index_client.update_and_parse_checksums! @@ -114,7 +114,7 @@ module Bundler def call(path, headers) fetcher.downloader.fetch(fetcher.fetch_uri + path, headers) rescue NetworkDownError => e - raise unless Bundler.settings[:allow_offline_install] && headers["If-None-Match"] + raise unless Bundler.feature_flag.allow_offline_install? && headers["If-None-Match"] ui.warn "Using the cached data for the new index because of a network error: #{e}" Net::HTTPNotModified.new(nil, nil, nil) end diff --git a/lib/bundler/fetcher/dependency.rb b/lib/bundler/fetcher/dependency.rb index 1cd5f9a213..445b0f2332 100644 --- a/lib/bundler/fetcher/dependency.rb +++ b/lib/bundler/fetcher/dependency.rb @@ -73,7 +73,7 @@ module Bundler def dependency_api_uri(gem_names = []) uri = fetch_uri + "api/v1/dependencies" - uri.query = "gems=#{CGI.escape(gem_names.join(","))}" if gem_names.any? + uri.query = "gems=#{CGI.escape(gem_names.sort.join(","))}" if gem_names.any? uri end end diff --git a/lib/bundler/fetcher/downloader.rb b/lib/bundler/fetcher/downloader.rb index c8d714c05a..ee1aa1a972 100644 --- a/lib/bundler/fetcher/downloader.rb +++ b/lib/bundler/fetcher/downloader.rb @@ -38,6 +38,8 @@ module Bundler end def request(uri, options) + validate_uri_scheme!(uri) + Bundler.ui.debug "HTTP GET #{uri}" req = Net::HTTP::Get.new uri.request_uri, options if uri.user @@ -61,6 +63,15 @@ module Bundler raise HTTPError, "Network error while fetching #{URICredentialsFilter.credential_filtered_uri(uri)}" end end + + private + + def validate_uri_scheme!(uri) + return if uri.scheme =~ /\Ahttps?\z/ + raise InvalidOption, + "The request uri `#{uri}` has an invalid scheme (`#{uri.scheme}`). " \ + "Did you mean `http` or `https`?" + end end end end diff --git a/lib/bundler/friendly_errors.rb b/lib/bundler/friendly_errors.rb index df45dd6946..c2853294a7 100644 --- a/lib/bundler/friendly_errors.rb +++ b/lib/bundler/friendly_errors.rb @@ -37,6 +37,11 @@ module Bundler when Gem::InvalidSpecificationException Bundler.ui.error error.message, :wrap => true when SystemExit + when *[defined?(Java::JavaLang::OutOfMemoryError) && Java::JavaLang::OutOfMemoryError].compact + Bundler.ui.error "\nYour JVM has run out of memory, and Bundler cannot continue. " \ + "You can decrease the amount of memory Bundler needs by removing gems from your Gemfile, " \ + "especially large gems. (Gems can be as large as hundreds of megabytes, and Bundler has to read those files!). " \ + "Alternatively, you can increase the amount of memory the JVM is able to use by running Bundler with jruby -J-Xmx1024m -S bundle (JRuby defaults to 500MB)." else request_issue_report_for(error) end end diff --git a/lib/bundler/gem_helper.rb b/lib/bundler/gem_helper.rb index fdb2db7dbf..73cbf9e0d1 100644 --- a/lib/bundler/gem_helper.rb +++ b/lib/bundler/gem_helper.rb @@ -98,7 +98,7 @@ module Bundler allowed_push_host = @gemspec.metadata["allowed_push_host"] gem_command += " --host #{allowed_push_host}" if allowed_push_host end - unless allowed_push_host || Pathname.new("~/.gem/credentials").expand_path.file? + 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) diff --git a/lib/bundler/gem_helpers.rb b/lib/bundler/gem_helpers.rb index 5c824ffefc..955834ff01 100644 --- a/lib/bundler/gem_helpers.rb +++ b/lib/bundler/gem_helpers.rb @@ -25,8 +25,76 @@ module Bundler module_function :generic def generic_local_platform - generic(Gem::Platform.local) + generic(Bundler.local_platform) end module_function :generic_local_platform + + def platform_specificity_match(spec_platform, user_platform) + spec_platform = Gem::Platform.new(spec_platform) + return PlatformMatch::EXACT_MATCH if spec_platform == user_platform + return PlatformMatch::WORST_MATCH if spec_platform.nil? || spec_platform == Gem::Platform::RUBY || user_platform == Gem::Platform::RUBY + + PlatformMatch.new( + PlatformMatch.os_match(spec_platform, user_platform), + PlatformMatch.cpu_match(spec_platform, user_platform), + PlatformMatch.platform_version_match(spec_platform, user_platform) + ) + end + module_function :platform_specificity_match + + def select_best_platform_match(specs, platform) + specs.select {|spec| spec.match_platform(platform) }. + min_by {|spec| platform_specificity_match(spec.platform, platform) } + end + module_function :select_best_platform_match + + PlatformMatch = Struct.new(:os_match, :cpu_match, :platform_version_match) + class PlatformMatch + def <=>(other) + return nil unless other.is_a?(PlatformMatch) + + m = os_match <=> other.os_match + return m unless m.zero? + + m = cpu_match <=> other.cpu_match + return m unless m.zero? + + m = platform_version_match <=> other.platform_version_match + m + end + + EXACT_MATCH = new(-1, -1, -1).freeze + WORST_MATCH = new(1_000_000, 1_000_000, 1_000_000).freeze + + def self.os_match(spec_platform, user_platform) + if spec_platform.os == user_platform.os + 0 + else + 1 + end + end + + def self.cpu_match(spec_platform, user_platform) + if spec_platform.cpu == user_platform.cpu + 0 + elsif spec_platform.cpu == "arm" && user_platform.cpu.to_s.start_with?("arm") + 0 + elsif spec_platform.cpu.nil? || spec_platform.cpu == "universal" + 1 + else + 2 + end + end + + def self.platform_version_match(spec_platform, user_platform) + if spec_platform.version == user_platform.version + 0 + elsif spec_platform.version.nil? + 1 + else + 2 + end + end + end end end diff --git a/lib/bundler/inline.rb b/lib/bundler/inline.rb index dcaf22944c..dec3be3e98 100644 --- a/lib/bundler/inline.rb +++ b/lib/bundler/inline.rb @@ -41,13 +41,13 @@ def gemfile(install = false, options = {}, &gemfile) end ENV["BUNDLE_GEMFILE"] ||= "Gemfile" - Bundler::Plugin.gemfile_install(&gemfile) if Bundler.settings[:plugins] + Bundler::Plugin.gemfile_install(&gemfile) if Bundler.feature_flag.plugins? builder = Bundler::Dsl.new builder.instance_eval(&gemfile) definition = builder.to_definition(nil, true) def definition.lock(*); end - definition.validate_ruby! + definition.validate_runtime! missing_specs = proc do begin diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb index 528dee177e..c4892ff186 100644 --- a/lib/bundler/installer.rb +++ b/lib/bundler/installer.rb @@ -159,6 +159,7 @@ 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? @@ -207,7 +208,7 @@ module Bundler end unless Bundler.bundle_path.exist? rescue Errno::EEXIST raise PathError, "Could not install to path `#{Bundler.settings[:path]}` " \ - "because of an invalid symlink. Remove the symlink so the directory can be created." + "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) diff --git a/lib/bundler/lazy_specification.rb b/lib/bundler/lazy_specification.rb index 83ab9ef967..7508347c32 100644 --- a/lib/bundler/lazy_specification.rb +++ b/lib/bundler/lazy_specification.rb @@ -6,6 +6,20 @@ require "bundler/match_platform" module Bundler class LazySpecification Identifier = Struct.new(:name, :version, :source, :platform, :dependencies) + class Identifier + include Comparable + def <=>(other) + return unless other.is_a?(Identifier) + [name, version, platform_string] <=> [other.name, other.version, other.platform_string] + end + + protected + + def platform_string + platform_string = platform.to_s + platform_string == Index::RUBY ? Index::NULL : platform_string + end + end include MatchPlatform @@ -55,10 +69,11 @@ module Bundler end def __materialize__ + search_object = Bundler.settings[: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 - source.specs.search(Gem::Dependency.new(name, version)).last + source.specs.search(search_object).last end end diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb index 063a1887fa..51148ab614 100644 --- a/lib/bundler/lockfile_parser.rb +++ b/lib/bundler/lockfile_parser.rb @@ -92,7 +92,7 @@ module Bundler end end @sources << @rubygems_aggregate - @specs = @specs.values + @specs = @specs.values.sort_by(&:identifier) warn_for_outdated_bundler_version rescue ArgumentError => e Bundler.ui.debug(e) diff --git a/lib/bundler/match_platform.rb b/lib/bundler/match_platform.rb index fed418b593..0a4e4c7e3a 100644 --- a/lib/bundler/match_platform.rb +++ b/lib/bundler/match_platform.rb @@ -8,7 +8,8 @@ module Bundler def match_platform(p) Gem::Platform::RUBY == platform || platform.nil? || p == platform || - generic(Gem::Platform.new(platform)) === p + generic(Gem::Platform.new(platform)) === p || + Gem::Platform.new(platform) === p end end end diff --git a/lib/bundler/plugin.rb b/lib/bundler/plugin.rb index 06f0317786..e700ae71f1 100644 --- a/lib/bundler/plugin.rb +++ b/lib/bundler/plugin.rb @@ -62,7 +62,9 @@ module Bundler save_plugins plugins, installed_specs, builder.inferred_plugins rescue => e - Bundler.ui.error "Failed to install plugin: #{e.message}\n #{e.backtrace[0]}" + unless e.is_a?(GemfileError) + Bundler.ui.error "Failed to install plugin: #{e.message}\n #{e.backtrace[0]}" + end raise end @@ -158,7 +160,7 @@ module Bundler # # @param [String] event def hook(event, *args, &arg_blk) - return unless Bundler.settings[:plugins] + return unless Bundler.feature_flag.plugins? plugins = index.hook_plugins(event) return unless plugins.any? diff --git a/lib/bundler/postit_trampoline.rb b/lib/bundler/postit_trampoline.rb index dbb23aa4d9..2a22489954 100644 --- a/lib/bundler/postit_trampoline.rb +++ b/lib/bundler/postit_trampoline.rb @@ -1,13 +1,18 @@ # frozen_string_literal: true -if ENV["BUNDLE_ENABLE_TRAMPOLINE"] - module BundlerVendoredPostIt; end - require "bundler/vendor/postit/lib/postit" - require "rubygems" +module BundlerVendoredPostIt; end +require "bundler/vendor/postit/lib/postit" +require "rubygems" - environment = BundlerVendoredPostIt::PostIt::Environment.new([]) - version = Gem::Requirement.new(environment.bundler_version) +environment = BundlerVendoredPostIt::PostIt::Environment.new([]) +version = Gem::Requirement.new(environment.bundler_version) +if version.requirements.size == 1 && version.requirements.first.first == "=" # version.exact? + if version.requirements.first.last.segments.first >= 2 + ENV["BUNDLE_ENABLE_TRAMPOLINE"] = "true" + end +end +if ENV["BUNDLE_ENABLE_TRAMPOLINE"] && !ENV["BUNDLE_DISABLE_POSTIT"] installed_version = if defined?(Bundler::VERSION) Bundler::VERSION @@ -65,4 +70,4 @@ You're running Bundler #{installed_version} but this project uses #{running_vers abort "The running bundler (#{running_version}) does not match the required `#{version}`" end -end # unless ENV["BUNDLE_ENABLE_TRAMPOLINE"] +end # if ENV["BUNDLE_ENABLE_TRAMPOLINE"] && !ENV["BUNDLE_DISABLE_POSTIT"] diff --git a/lib/bundler/remote_specification.rb b/lib/bundler/remote_specification.rb index 6a02897c63..112c7f97fe 100644 --- a/lib/bundler/remote_specification.rb +++ b/lib/bundler/remote_specification.rb @@ -81,5 +81,10 @@ module Bundler def method_missing(method, *args, &blk) _remote_specification.send(method, *args, &blk) end + + def respond_to?(method, include_all = false) + super || _remote_specification.respond_to?(method, include_all) + end + public :respond_to? end end diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 10d5404028..b8016b37a9 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -66,48 +66,39 @@ module Bundler end end - ALL = Bundler::Dependency::PLATFORM_MAP.values.uniq.freeze - class SpecGroup < Array include GemHelpers - attr_reader :activated, :required_by + attr_reader :activated def initialize(a) super - @required_by = [] - @activated = [] + @required_by = [] + @activated_platforms = [] @dependencies = nil - @specs = {} - - ALL.each do |p| - @specs[p] = reverse.find {|s| s.match_platform(p) } + @specs = Hash.new do |specs, platform| + specs[platform] = select_best_platform_match(self, platform) end end def initialize_copy(o) super - @required_by = o.required_by.dup - @activated = o.activated.dup + @activated_platforms = o.activated.dup end def to_specs - specs = {} - - @activated.each do |p| + @activated_platforms.map do |p| next unless s = @specs[p] - platform = generic(Gem::Platform.new(s.platform)) - next if specs[platform] - - lazy_spec = LazySpecification.new(name, version, platform, source) + lazy_spec = LazySpecification.new(name, version, s.platform, source) lazy_spec.dependencies.replace s.dependencies - specs[platform] = lazy_spec - end - specs.values + lazy_spec + end.compact end def activate_platform!(platform) - @activated << platform if !@activated.include?(platform) && for?(platform, nil) + return unless for?(platform) + return if @activated_platforms.include?(platform) + @activated_platforms << platform end def name @@ -122,17 +113,9 @@ module Bundler @source ||= first.source end - def for?(platform, ruby_version) + def for?(platform) spec = @specs[platform] - return false unless spec - - return true if ruby_version.nil? - # Only allow endpoint specifications since they won't hit the network to - # fetch the full gemspec when calling required_ruby_version - return true if !spec.is_a?(EndpointSpecification) && !spec.is_a?(Gem::Specification) - return true if spec.required_ruby_version.nil? - - spec.required_ruby_version.satisfied_by?(ruby_version.to_gem_version_with_patchlevel) + !spec.nil? end def to_s @@ -140,7 +123,11 @@ module Bundler end def dependencies_for_activated_platforms - @activated.map {|p| __dependencies[p] }.flatten + 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) @@ -150,18 +137,31 @@ module Bundler private def __dependencies - @dependencies ||= begin - dependencies = {} - ALL.each do |p| - next unless spec = @specs[p] - dependencies[p] = [] + @dependencies = Hash.new do |dependencies, platform| + dependencies[platform] = [] + if spec = @specs[platform] spec.dependencies.each do |dep| next if dep.type == :development - dependencies[p] << DepProxy.new(dep, p) + dependencies[platform] << DepProxy.new(dep, platform) end end - dependencies + 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 @@ -175,30 +175,34 @@ module Bundler # ==== Returns # <GemBundle>,nil:: If the list of dependencies can be resolved, a # collection of gemspecs is returned. Otherwise, nil is returned. - def self.resolve(requirements, index, source_requirements = {}, base = [], ruby_version = nil, gem_version_promoter = GemVersionPromoter.new, additional_base_requirements = []) + def self.resolve(requirements, index, source_requirements = {}, base = [], gem_version_promoter = GemVersionPromoter.new, additional_base_requirements = []) base = SpecSet.new(base) unless base.is_a?(SpecSet) - resolver = new(index, source_requirements, base, ruby_version, gem_version_promoter, additional_base_requirements) + resolver = new(index, source_requirements, base, gem_version_promoter, additional_base_requirements) result = resolver.start(requirements) SpecSet.new(result) end - def initialize(index, source_requirements, base, ruby_version, gem_version_promoter, additional_base_requirements) + def initialize(index, source_requirements, base, gem_version_promoter, additional_base_requirements) @index = index @source_requirements = source_requirements @base = base @resolver = Molinillo::Resolver.new(self, self) @search_for = {} @base_dg = Molinillo::DependencyGraph.new - @base.each {|ls| @base_dg.add_vertex(ls.name, Dependency.new(ls.name, ls.version), true) } + @base.each do |ls| + dep = Dependency.new(ls.name, ls.version) + @base_dg.add_vertex(ls.name, DepProxy.new(dep, ls.platform), true) + end additional_base_requirements.each {|d| @base_dg.add_vertex(d.name, d) } - @ruby_version = ruby_version @gem_version_promoter = gem_version_promoter end def start(requirements) verify_gemfile_dependencies_are_found!(requirements) dg = @resolver.resolve(requirements, @base_dg) - dg.map(&:payload).map(&:to_specs).flatten + 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) rescue Molinillo::CircularDependencyError => e @@ -279,7 +283,7 @@ module Bundler @gem_version_promoter.sort_versions(dependency, spec_groups) end end - search.select {|sg| sg.for?(platform, @ruby_version) }.each {|sg| sg.activate_platform!(platform) } + search.select {|sg| sg.for?(platform) }.each {|sg| sg.activate_platform!(platform) } end def index_for(dependency) @@ -303,7 +307,8 @@ module Bundler end def requirement_satisfied_by?(requirement, activated, spec) - requirement.matches_spec?(spec) || spec.source.is_a?(Source::Gemspec) + return false unless requirement.matches_spec?(spec) || spec.source.is_a?(Source::Gemspec) + spec.activate_platform!(requirement.__platform) || spec.for?(requirement.__platform) end def sort_dependencies(dependencies, activated, conflicts) diff --git a/lib/bundler/ruby_version.rb b/lib/bundler/ruby_version.rb index 4ffc572bf0..ebdefe63fc 100644 --- a/lib/bundler/ruby_version.rb +++ b/lib/bundler/ruby_version.rb @@ -128,6 +128,11 @@ module Bundler end end + def exact? + return @exact if defined?(@exact) + @exact = versions.all? {|v| Gem::Requirement.create(v).exact? } + end + private def matches?(requirements, version) diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb index e9a7267f86..53a153e560 100644 --- a/lib/bundler/rubygems_ext.rb +++ b/lib/bundler/rubygems_ext.rb @@ -169,6 +169,11 @@ module Gem def none? @none ||= (to_s == ">= 0") end unless allocate.respond_to?(:none?) + + def exact? + return false unless @requirements.size == 1 + @requirements[0][0] == "=" + end unless allocate.respond_to?(:exact?) end class Platform diff --git a/lib/bundler/rubygems_gem_installer.rb b/lib/bundler/rubygems_gem_installer.rb index e18f46268b..28ad988b94 100644 --- a/lib/bundler/rubygems_gem_installer.rb +++ b/lib/bundler/rubygems_gem_installer.rb @@ -12,5 +12,53 @@ module Bundler def check_executable_overwrite(filename) # Bundler needs to install gems regardless of binstub overwriting end + + def pre_install_checks + super && validate_bundler_checksum(options[:bundler_expected_checksum]) + end + + private + + def validate_bundler_checksum(checksum) + return true if Bundler.settings[:disable_checksum_validation] + return true unless checksum + 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 << io.read(16_384) until io.eof? + io.rewind + send(checksum_type(checksum), digest) + end + unless digest == checksum + raise SecurityError, + "The checksum for the downloaded `#{spec.full_name}.gem` did not match " \ + "the checksum given by the API. This means that the contents of the " \ + "gem appear to be different from what was uploaded, and could be an indicator of a security issue.\n" \ + "(The expected SHA256 checksum was #{checksum.inspect}, but the checksum for the downloaded gem was #{digest.inspect}.)\n" \ + "Bundler cannot continue installing #{spec.name} (#{spec.version})." + end + true + end + + def checksum_type(checksum) + case checksum.length + when 64 then :hexdigest! + when 44 then :base64digest! + else raise InstallError, "The given checksum for #{spec.full_name} (#{checksum.inspect}) is not a valid SHA256 hexdigest nor base64digest" + end + end + + def hexdigest!(digest) + digest.hexdigest! + end + + def base64digest!(digest) + if digest.respond_to?(:base64digest!) + digest.base64digest! + else + [digest.digest!].pack("m0") + end + end end end diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb index 1c44ef3ddd..90da0b4645 100644 --- a/lib/bundler/rubygems_integration.rb +++ b/lib/bundler/rubygems_integration.rb @@ -80,6 +80,7 @@ module Bundler end def platforms + return [Gem::Platform::RUBY] if Bundler.settings[:force_ruby_platform] Gem.platforms end @@ -194,6 +195,10 @@ module Bundler end end + def load_plugins + Gem.load_plugins + end + def ui=(obj) Gem::DefaultUserInteraction.ui = obj end diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb index fda499cf5a..45f445aec1 100644 --- a/lib/bundler/runtime.rb +++ b/lib/bundler/runtime.rb @@ -140,7 +140,8 @@ module Bundler Bundler.ui.info "Updating files in #{Bundler.settings.app_cache_path}" - specs.each do |spec| + specs_to_cache = Bundler.settings[:cache_all_platforms] ? @definition.resolve.materialized_for_all_platforms : specs + specs_to_cache.each do |spec| next if spec.name == "bundler" next if spec.source.is_a?(Source::Gemspec) spec.source.send(:fetch_gem, spec) if Bundler.settings[:cache_all_platforms] && spec.source.respond_to?(:fetch_gem, true) diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index 01594066c3..2f532c832f 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -7,6 +7,7 @@ module Bundler allow_offline_install auto_install cache_all + disable_checksum_validation disable_exec_load disable_local_branch_check disable_shared_gems @@ -17,6 +18,7 @@ module Bundler major_deprecations no_install no_prune + force_ruby_platform only_update_to_newer_versions plugins silence_root_warning @@ -42,11 +44,18 @@ module Bundler @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 = (@local_config[key] || ENV[key] || @global_config[key] || DEFAULT_CONFIG[name]) + value = @temporary.fetch(name) do + @local_config.fetch(key) do + ENV.fetch(key) do + @global_config.fetch(key) do + DEFAULT_CONFIG.fetch(name) do + nil + end end end end end if value.nil? nil @@ -76,9 +85,19 @@ module Bundler 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) + return unless block_given? + begin + yield + ensure + existing.each {|k, v| v.nil? ? @temporary.delete(k) : @temporary[k] = v } + end + end + def delete(key) @local_config.delete(key_for(key)) end diff --git a/lib/bundler/setup.rb b/lib/bundler/setup.rb index 8b4b479778..4e213beed3 100644 --- a/lib/bundler/setup.rb +++ b/lib/bundler/setup.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require "bundler/postit_trampoline" unless ENV["BUNDLE_DISABLE_POSTIT"] +require "bundler/postit_trampoline" require "bundler/shared_helpers" if Bundler::SharedHelpers.in_bundle? diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb index 69543356a2..0ddcea1ca5 100644 --- a/lib/bundler/shared_helpers.rb +++ b/lib/bundler/shared_helpers.rb @@ -39,10 +39,12 @@ module Bundler bundle_dir = find_directory(".bundle") return nil unless bundle_dir - global_bundle_dir = File.join(Bundler.rubygems.user_home, ".bundle") + bundle_dir = Pathname.new(bundle_dir) + + global_bundle_dir = Bundler.user_home.join(".bundle") return nil if bundle_dir == global_bundle_dir - Pathname.new(bundle_dir) + bundle_dir end def in_bundle? @@ -202,10 +204,15 @@ module Bundler def set_rubylib rubylib = (ENV["RUBYLIB"] || "").split(File::PATH_SEPARATOR) - rubylib.unshift File.expand_path("../..", __FILE__) + rubylib.unshift bundler_ruby_lib ENV["RUBYLIB"] = rubylib.uniq.join(File::PATH_SEPARATOR) end + def bundler_ruby_lib + File.expand_path("../..", __FILE__) + end + private :bundler_ruby_lib + def clean_load_path # handle 1.9 where system gems are always on the load path if defined?(::Gem) @@ -224,7 +231,8 @@ module Bundler def prints_major_deprecations? require "bundler" - return false unless Bundler.settings[:major_deprecations] + deprecation_release = Bundler::VERSION.split(".").drop(1).include?("99") + return false if !deprecation_release && !Bundler.settings[:major_deprecations] require "bundler/deprecate" return false if Bundler::Deprecate.skip true diff --git a/lib/bundler/source.rb b/lib/bundler/source.rb index 08c1e6f79d..9d65d4613b 100644 --- a/lib/bundler/source.rb +++ b/lib/bundler/source.rb @@ -14,6 +14,7 @@ module Bundler def version_message(spec) message = "#{spec.name} #{spec.version}" + message += " (#{spec.platform})" if spec.platform != Gem::Platform::RUBY && !spec.platform.nil? if Bundler.locked_gems locked_spec = Bundler.locked_gems.specs.find {|s| s.name == spec.name } diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb index 60fb555f0a..d1757a4a93 100644 --- a/lib/bundler/source/git.rb +++ b/lib/bundler/source/git.rb @@ -292,7 +292,7 @@ module Bundler def fetch git_proxy.checkout rescue GitError - raise unless Bundler.settings[:allow_offline_install] + raise unless Bundler.feature_flag.allow_offline_install? Bundler.ui.warn "Using cached git data because of network errors" end end diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb index 4b76d18735..c44f00d7b1 100644 --- a/lib/bundler/source/git/git_proxy.rb +++ b/lib/bundler/source/git/git_proxy.rb @@ -224,6 +224,11 @@ module Bundler raise GitError, "The git source #{uri} is not yet checked out. Please run `bundle install` before trying to start your application" end + # TODO: Replace this with Open3 when upgrading to bundler 2 + # Similar to #git_null, as Open3 is not cross-platform, + # a temporary way is to use Tempfile to capture the stderr. + # When replacing this using Open3, make sure git_null is + # also replaced by Open3, so stdout and stderr all got handled properly. def capture_and_filter_stderr(uri) return_value, captured_err = "" backup_stderr = STDERR.dup diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb index 69bb0c1af2..87a490446c 100644 --- a/lib/bundler/source/path.rb +++ b/lib/bundler/source/path.rb @@ -46,7 +46,7 @@ module Bundler def to_lock out = String.new("PATH\n") - out << " remote: #{relative_path}\n" + out << " remote: #{lockfile_path}\n" out << " glob: #{@glob}\n" unless @glob == DEFAULT_GLOB out << " specs:\n" end @@ -129,6 +129,11 @@ module Bundler "`#{somepath}`.\nThe error message was: #{e.message}." end + def lockfile_path + return relative_path if path.absolute? + expand(path).relative_path_from(Bundler.root) + end + def app_cache_path(custom_path = nil) @app_cache_path ||= Bundler.app_cache(custom_path).join(app_cache_dirname) end diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index aedad7086d..89f7673eb8 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -140,7 +140,8 @@ module Bundler :bin_dir => bin_path.to_s, :ignore_dependencies => true, :wrappers => true, - :env_shebang => true + :env_shebang => true, + :bundler_expected_checksum => spec.respond_to?(:checksum) && spec.checksum ).install end spec.full_gem_path = installed_spec.full_gem_path diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb index f2ccac4742..fe31b17f0e 100644 --- a/lib/bundler/spec_set.rb +++ b/lib/bundler/spec_set.rb @@ -24,17 +24,9 @@ module Bundler dep = deps.shift next if handled[dep] || skip.include?(dep.name) - spec = lookup[dep.name].find do |s| - if match_current_platform - Gem::Platform.match(s.platform) - else - s.match_platform(dep.__platform) - end - end - handled[dep] = true - if spec + if spec = spec_for_dependency(dep, match_current_platform) specs << spec spec.dependencies.each do |d| @@ -99,6 +91,20 @@ module Bundler SpecSet.new(materialized.compact) end + # Materialize for all the specs in the spec set, regardless of what platform they're for + # This is in contrast to how for does platform filtering (and specifically different from how `materialize` calls `for` only for the current platform) + # @return [Array<Gem::Specification>] + def materialized_for_all_platforms + names = @specs.map(&:name).uniq + @specs.map do |s| + next s unless s.is_a?(LazySpecification) + s.source.dependency_names = names if s.source.respond_to?(:dependency_names=) + spec = s.__materialize__ + raise GemNotFound, "Could not find #{s.full_name} in any of the sources" unless spec + spec + end + end + def merge(set) arr = sorted.dup set.each do |s| @@ -133,10 +139,7 @@ module Bundler def lookup @lookup ||= begin lookup = Hash.new {|h, k| h[k] = [] } - specs = @specs.sort_by do |s| - s.platform.to_s == "ruby" ? "\0" : s.platform.to_s - end - specs.reverse_each do |s| + Index.sort_specs(@specs).reverse_each do |s| lookup[s.name] << s end lookup @@ -147,6 +150,18 @@ module Bundler @specs.each {|s| yield s } end + def spec_for_dependency(dep, match_current_platform) + if match_current_platform + Bundler.rubygems.platforms.reverse_each do |pl| + match = GemHelpers.select_best_platform_match(lookup[dep.name], pl) + return match if match + end + nil + else + GemHelpers.select_best_platform_match(lookup[dep.name], dep.__platform) + end + end + def tsort_each_child(s) s.dependencies.sort_by(&:name).each do |d| next if d.type == :development diff --git a/lib/bundler/templates/newgem/README.md.tt b/lib/bundler/templates/newgem/README.md.tt index 30c7b93609..ad8d88b6e4 100644 --- a/lib/bundler/templates/newgem/README.md.tt +++ b/lib/bundler/templates/newgem/README.md.tt @@ -32,7 +32,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/<%= config[:name] %>.<% if config[:coc] %> This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.<% end %> +Bug reports and pull requests are welcome on GitHub at https://github.com/<%= config[:git_user_name] %>/<%= config[:name] %>.<% if config[:coc] %> This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.<% end %> <% if config[:mit] %> ## License diff --git a/lib/bundler/templates/newgem/bin/console.tt b/lib/bundler/templates/newgem/bin/console.tt index f402bd639e..a27f82430f 100644 --- a/lib/bundler/templates/newgem/bin/console.tt +++ b/lib/bundler/templates/newgem/bin/console.tt @@ -11,4 +11,4 @@ require "<%= config[:namespaced_path] %>" # Pry.start require "irb" -IRB.start +IRB.start(__FILE__) diff --git a/lib/bundler/ui/shell.rb b/lib/bundler/ui/shell.rb index 5c1fa61568..697290f795 100644 --- a/lib/bundler/ui/shell.rb +++ b/lib/bundler/ui/shell.rb @@ -83,6 +83,10 @@ module Bundler with_level("silent", &blk) end + def unprinted_warnings + [] + end + private # valimism diff --git a/lib/bundler/ui/silent.rb b/lib/bundler/ui/silent.rb index 367eaa58c2..5e0037f488 100644 --- a/lib/bundler/ui/silent.rb +++ b/lib/bundler/ui/silent.rb @@ -2,6 +2,10 @@ module Bundler module UI class Silent + def initialize + @warnings = [] + end + def add_color(string, color) string end @@ -13,6 +17,7 @@ module Bundler end def warn(message, newline = nil) + @warnings |= [message] end def error(message, newline = nil) @@ -44,6 +49,10 @@ module Bundler def silence yield end + + def unprinted_warnings + @warnings + end end end end diff --git a/lib/bundler/vendor/compact_index_client/lib/compact_index_client.rb b/lib/bundler/vendor/compact_index_client/lib/compact_index_client.rb deleted file mode 100644 index 257e4c109e..0000000000 --- a/lib/bundler/vendor/compact_index_client/lib/compact_index_client.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true -require "pathname" -require "set" - -class Bundler::CompactIndexClient - class Error < StandardError; end - - require "bundler/vendor/compact_index_client/lib/compact_index_client/cache" - require "bundler/vendor/compact_index_client/lib/compact_index_client/updater" - require "bundler/vendor/compact_index_client/lib/compact_index_client/version" - - attr_reader :directory - - # @return [Lambda] A lambda that takes an array of inputs and a block, and - # maps the inputs with the block in parallel. - # - attr_accessor :in_parallel - - def initialize(directory, fetcher) - @directory = Pathname.new(directory) - @updater = Updater.new(fetcher) - @cache = Cache.new(@directory) - @endpoints = Set.new - @info_checksums_by_name = {} - @parsed_checksums = false - @in_parallel = lambda do |inputs, &blk| - inputs.map(&blk) - end - end - - def names - update(@cache.names_path, "names") - @cache.names - end - - def versions - update(@cache.versions_path, "versions") - versions, @info_checksums_by_name = @cache.versions - versions - end - - def dependencies(names) - in_parallel.call(names) do |name| - update_info(name) - @cache.dependencies(name).map {|d| d.unshift(name) } - end.flatten(1) - end - - def spec(name, version, platform = nil) - update_info(name) - @cache.specific_dependency(name, version, platform) - end - - def update_and_parse_checksums! - return @info_checksums_by_name if @parsed_checksums - update(@cache.versions_path, "versions") - @info_checksums_by_name = @cache.checksums - @parsed_checksums = true - end - -private - - def update(local_path, remote_path) - return unless @endpoints.add?(remote_path) - @updater.update(local_path, url(remote_path)) - end - - def update_info(name) - path = @cache.info_path(name) - checksum = @updater.checksum_for_file(path) - return unless existing = @info_checksums_by_name[name] - return if checksum == existing - update(path, "info/#{name}") - end - - def url(path) - path - end -end diff --git a/lib/bundler/vendor/compact_index_client/lib/compact_index_client/cache.rb b/lib/bundler/vendor/compact_index_client/lib/compact_index_client/cache.rb deleted file mode 100644 index d2639ee717..0000000000 --- a/lib/bundler/vendor/compact_index_client/lib/compact_index_client/cache.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true -require "digest/md5" -class Bundler::CompactIndexClient - class Cache - attr_reader :directory - - def initialize(directory) - @directory = Pathname.new(directory).expand_path - info_roots.each {|dir| FileUtils.mkdir_p(dir) } - end - - def names - lines(names_path) - end - - def names_path - directory.join("names") - end - - def versions - versions_by_name = Hash.new {|hash, key| hash[key] = [] } - info_checksums_by_name = {} - - lines(versions_path).each do |line| - name, versions_string, info_checksum = line.split(" ", 3) - info_checksums_by_name[name] = info_checksum || "" - versions_string.split(",").each do |version| - if version.start_with?("-") - version = version[1..-1].split("-", 2).unshift(name) - versions_by_name[name].delete(version) - else - version = version.split("-", 2).unshift(name) - versions_by_name[name] << version - end - end - end - - [versions_by_name, info_checksums_by_name] - end - - def versions_path - directory.join("versions") - end - - def checksums - checksums = {} - - lines(versions_path).each do |line| - name, _, checksum = line.split(" ", 3) - checksums[name] = checksum - end - - checksums - end - - def dependencies(name) - lines(info_path(name)).map do |line| - parse_gem(line) - end - end - - def info_path(name) - name = name.to_s - if name =~ /[^a-z0-9_-]/ - name += "-#{Digest::MD5.hexdigest(name).downcase}" - info_roots.last.join(name) - else - info_roots.first.join(name) - end - end - - def specific_dependency(name, version, platform) - pattern = [version, platform].compact.join("-") - return nil if pattern.empty? - - gem_lines = info_path(name).read - gem_line = gem_lines[/^#{Regexp.escape(pattern)}\b.*/, 0] - gem_line ? parse_gem(gem_line) : nil - end - - private - - def lines(path) - return [] unless path.file? - lines = path.read.split("\n") - header = lines.index("---") - lines = header ? lines[header + 1..-1] : lines - end - - def parse_gem(string) - version_and_platform, rest = string.split(" ", 2) - version, platform = version_and_platform.split("-", 2) - dependencies, requirements = rest.split("|", 2).map {|s| s.split(",") } if rest - dependencies = dependencies ? dependencies.map {|d| parse_dependency(d) } : [] - requirements = requirements ? requirements.map {|r| parse_dependency(r) } : [] - [version, platform, dependencies, requirements] - end - - def parse_dependency(string) - dependency = string.split(":") - dependency[-1] = dependency[-1].split("&") if dependency.size > 1 - dependency - end - - def info_roots - [ - directory.join("info"), - directory.join("info-special-characters"), - ] - end - end -end diff --git a/lib/bundler/vendor/compact_index_client/lib/compact_index_client/updater.rb b/lib/bundler/vendor/compact_index_client/lib/compact_index_client/updater.rb deleted file mode 100644 index 40c61644e3..0000000000 --- a/lib/bundler/vendor/compact_index_client/lib/compact_index_client/updater.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true -require "fileutils" -require "stringio" -require "tmpdir" -require "zlib" - -class Bundler::CompactIndexClient - class Updater - class MisMatchedChecksumError < Error - def initialize(path, server_checksum, local_checksum) - @path = path - @server_checksum = server_checksum - @local_checksum = local_checksum - end - - def message - "The checksum of /#{@path} does not match the checksum provided by the server! Something is wrong " \ - "(local checksum is #{@local_checksum.inspect}, was expecting #{@server_checksum.inspect})." - end - end - - def initialize(fetcher) - @fetcher = fetcher - end - - def update(local_path, remote_path, retrying = nil) - headers = {} - - Dir.mktmpdir("bundler-compact-index-") do |local_temp_dir| - local_temp_path = Pathname.new(local_temp_dir).join(local_path.basename) - - # first try to fetch any new bytes on the existing file - if retrying.nil? && local_path.file? - FileUtils.cp local_path, local_temp_path - headers["If-None-Match"] = etag_for(local_temp_path) - headers["Range"] = "bytes=#{local_temp_path.size}-" - else - # Fastly ignores Range when Accept-Encoding: gzip is set - headers["Accept-Encoding"] = "gzip" - end - - response = @fetcher.call(remote_path, headers) - return if response.is_a?(Net::HTTPNotModified) - - content = response.body - if response["Content-Encoding"] == "gzip" - content = Zlib::GzipReader.new(StringIO.new(content)).read - end - - mode = response.is_a?(Net::HTTPPartialContent) ? "a" : "w" - local_temp_path.open(mode) {|f| f << content } - - response_etag = response["ETag"].gsub(%r{\AW/}, "") - if etag_for(local_temp_path) == response_etag - FileUtils.mv(local_temp_path, local_path) - return - end - - if retrying.nil? - update(local_path, remote_path, :retrying) - else - raise MisMatchedChecksumError.new(remote_path, response_etag, etag_for(local_temp_path)) - end - end - end - - def etag_for(path) - sum = checksum_for_file(path) - sum ? %("#{sum}") : nil - end - - def checksum_for_file(path) - return nil unless path.file? - # This must use IO.read instead of Digest.file().hexdigest - # because we need to preserve \n line endings on windows when calculating - # the checksum - Digest::MD5.hexdigest(IO.read(path)) - end - end -end diff --git a/lib/bundler/vendor/compact_index_client/lib/compact_index_client/version.rb b/lib/bundler/vendor/compact_index_client/lib/compact_index_client/version.rb deleted file mode 100644 index 64520daead..0000000000 --- a/lib/bundler/vendor/compact_index_client/lib/compact_index_client/version.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true -class Bundler::CompactIndexClient - VERSION = "0.1.0".freeze -end diff --git a/lib/bundler/yaml_serializer.rb b/lib/bundler/yaml_serializer.rb index dede8fd5fd..3c9eccafc2 100644 --- a/lib/bundler/yaml_serializer.rb +++ b/lib/bundler/yaml_serializer.rb @@ -52,7 +52,7 @@ module Bundler stack = [res] last_hash = nil last_empty_key = nil - str.split("\n").each do |line| + str.split(/\r?\n/).each do |line| if match = HASH_REGEX.match(line) indent, key, _, val = match.captures key = convert_to_backward_compatible_key(key) diff --git a/man/bundle-config.ronn b/man/bundle-config.ronn index 3ea6d10973..dc701c584f 100644 --- a/man/bundle-config.ronn +++ b/man/bundle-config.ronn @@ -142,7 +142,7 @@ learn more about their operation in [bundle install(1)][bundle-install]. and key in PEM format. * `cache_path` (`BUNDLE_CACHE_PATH`): The directory that bundler will place cached gems in when running <code>bundle package</code>, and that bundler - will look in when installing gems. + will look in when installing gems. Defaults to `vendor/bundle`. * `disable_multisource` (`BUNDLE_DISABLE_MULTISOURCE`): When set, Gemfiles containing multiple sources will produce errors instead of warnings. Use `bundle config --delete disable_multisource` to unset. diff --git a/spec/bundler/env_spec.rb b/spec/bundler/env_spec.rb index 73d1f1d7df..245073d9b8 100644 --- a/spec/bundler/env_spec.rb +++ b/spec/bundler/env_spec.rb @@ -15,6 +15,7 @@ describe Bundler::Env do expect(out).to include(Gem::VERSION) expect(out).to include(env.send(:ruby_version)) expect(out).to include(env.send(:git_version)) + expect(out).to include(OpenSSL::OPENSSL_VERSION) end context "when there is a Gemfile and a lockfile and print_gemfile is true" do @@ -48,6 +49,14 @@ describe Bundler::Env do end end + context "when there no Gemfile and print_gemfile is true" do + let(:output) { env.report(:print_gemfile => true) } + + it "prints the environment" do + expect(output).to start_with("Environment") + end + end + context "when Gemfile contains a gemspec and print_gemspecs is true" do let(:gemspec) do <<-GEMSPEC.gsub(/^\s+/, "") diff --git a/spec/bundler/fetcher/compact_index_spec.rb b/spec/bundler/fetcher/compact_index_spec.rb index f6c6ba2ee1..e3f36666cc 100644 --- a/spec/bundler/fetcher/compact_index_spec.rb +++ b/spec/bundler/fetcher/compact_index_spec.rb @@ -15,7 +15,7 @@ describe Bundler::Fetcher::CompactIndex do it "has only one thread open at the end of the run" do compact_index.specs_for_names(["lskdjf"]) - thread_count = Thread.list.select {|thread| thread.status == "run" }.count + thread_count = Thread.list.count {|thread| thread.status == "run" } expect(thread_count).to eq 1 end diff --git a/spec/bundler/fetcher/dependency_spec.rb b/spec/bundler/fetcher/dependency_spec.rb index bf7749d07a..d021a246f7 100644 --- a/spec/bundler/fetcher/dependency_spec.rb +++ b/spec/bundler/fetcher/dependency_spec.rb @@ -262,13 +262,13 @@ describe Bundler::Fetcher::Dependency do let(:uri) { URI("http://gem-api.com") } context "with gem names" do - let(:gem_names) { [%w(foo bar), %w(bundler rubocop)] } + let(:gem_names) { %w(foo bar bundler rubocop) } before { allow(subject).to receive(:fetch_uri).and_return(uri) } it "should return an api calling uri with the gems in the query" do expect(subject.dependency_api_uri(gem_names).to_s).to eq( - "http://gem-api.com/api/v1/dependencies?gems=foo%2Cbar%2Cbundler%2Crubocop" + "http://gem-api.com/api/v1/dependencies?gems=bar%2Cbundler%2Cfoo%2Crubocop" ) end end diff --git a/spec/bundler/mirror_spec.rb b/spec/bundler/mirror_spec.rb index eb0ccf0bdf..6a81ef2af4 100644 --- a/spec/bundler/mirror_spec.rb +++ b/spec/bundler/mirror_spec.rb @@ -131,6 +131,16 @@ describe Bundler::Settings::Mirror do end end end + + describe "#==" do + it "returns true if uri and fallback timeout are the same" do + uri = "https://ruby.taobao.org" + mirror = Bundler::Settings::Mirror.new(uri, 1) + another_mirror = Bundler::Settings::Mirror.new(uri, 1) + + expect(mirror == another_mirror).to be true + end + end end end diff --git a/spec/bundler/remote_specification_spec.rb b/spec/bundler/remote_specification_spec.rb index 6a8e9a6434..d958ca85eb 100644 --- a/spec/bundler/remote_specification_spec.rb +++ b/spec/bundler/remote_specification_spec.rb @@ -158,16 +158,30 @@ describe Bundler::RemoteSpecification do describe "method missing" do context "and is present in Gem::Specification" do - let(:remote_spec) { double(:remote_spec) } + let(:remote_spec) { double(:remote_spec, :authors => "abcd") } before do - allow_any_instance_of(Gem::Specification).to receive(:respond_to?).and_return(true) allow(subject).to receive(:_remote_specification).and_return(remote_spec) + expect(subject.methods.map(&:to_sym)).not_to include(:authors) end it "should send through to Gem::Specification" do - expect(remote_spec).to receive(:send).with(:missing_method_call).once - subject.missing_method_call + expect(subject.authors).to eq("abcd") + end + end + end + + describe "respond to missing?" do + context "and is present in Gem::Specification" do + let(:remote_spec) { double(:remote_spec, :authors => "abcd") } + + before do + allow(subject).to receive(:_remote_specification).and_return(remote_spec) + expect(subject.methods.map(&:to_sym)).not_to include(:authors) + end + + it "should send through to Gem::Specification" do + expect(subject.respond_to?(:authors)).to be_truthy end end end diff --git a/spec/bundler/settings_spec.rb b/spec/bundler/settings_spec.rb index 66189eae72..5a9d0cb08b 100644 --- a/spec/bundler/settings_spec.rb +++ b/spec/bundler/settings_spec.rb @@ -106,6 +106,18 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow end end + describe "#temporary" do + it "reset after used" do + Bundler.settings[:no_install] = true + + Bundler.settings.temporary(:no_install => false) do + expect(Bundler.settings[:no_install]).to eq false + end + + expect(Bundler.settings[:no_install]).to eq true + end + end + describe "#set_global" do context "when it's not possible to write to the file" do it "raises an PermissionError with explanation" do diff --git a/spec/bundler/shared_helpers_spec.rb b/spec/bundler/shared_helpers_spec.rb index 4c0d61cf0a..8826dcd4dd 100644 --- a/spec/bundler/shared_helpers_spec.rb +++ b/spec/bundler/shared_helpers_spec.rb @@ -234,7 +234,9 @@ describe Bundler::SharedHelpers do shared_examples_for "ENV['RUBYLIB'] gets set correctly" do let(:ruby_lib_path) { "stubbed_ruby_lib_dir" } - before { allow(File).to receive(:expand_path).and_return(ruby_lib_path) } + before do + allow(Bundler::SharedHelpers).to receive(:bundler_ruby_lib).and_return(ruby_lib_path) + end it "ensures bundler's ruby version lib path is in ENV['RUBYLIB']" do subject.set_bundle_environment @@ -324,7 +326,6 @@ describe Bundler::SharedHelpers do let(:ruby_lib_path) { "stubbed_ruby_lib_dir" } before do - allow(File).to receive(:expand_path).and_return(ruby_lib_path) ENV["RUBYLIB"] = ruby_lib_path end diff --git a/spec/bundler/source_spec.rb b/spec/bundler/source_spec.rb index 61c6aea05c..ea171d387c 100644 --- a/spec/bundler/source_spec.rb +++ b/spec/bundler/source_spec.rb @@ -22,7 +22,7 @@ describe Bundler::Source do end describe "#version_message" do - let(:spec) { double(:spec, :name => "nokogiri", :version => ">= 1.6") } + let(:spec) { double(:spec, :name => "nokogiri", :version => ">= 1.6", :platform => rb) } shared_examples_for "the lockfile specs are not relevant" do it "should return a string with the spec name and version" do diff --git a/spec/bundler/yaml_serializer_spec.rb b/spec/bundler/yaml_serializer_spec.rb index 0b3261336c..bf86d2a076 100644 --- a/spec/bundler/yaml_serializer_spec.rb +++ b/spec/bundler/yaml_serializer_spec.rb @@ -125,6 +125,29 @@ describe Bundler::YAMLSerializer do expect(serializer.load(yaml)).to eq(hash) end + + it "handles windows-style CRLF line endings" do + yaml = strip_whitespace(<<-YAML).gsub("\n", "\r\n") + --- + nested_hash: + contains_array: + - "Why shouldn't you write with a broken pencil?" + - "Because it's pointless!" + - oh so silly + YAML + + hash = { + "nested_hash" => { + "contains_array" => [ + "Why shouldn't you write with a broken pencil?", + "Because it's pointless!", + "oh so silly", + ], + }, + } + + expect(serializer.load(yaml)).to eq(hash) + end end describe "against yaml lib" do diff --git a/spec/cache/gems_spec.rb b/spec/cache/gems_spec.rb index 474233a83c..19dd16e4e9 100644 --- a/spec/cache/gems_spec.rb +++ b/spec/cache/gems_spec.rb @@ -238,7 +238,7 @@ describe "bundle cache" do gem "platform_specific" G - expect(cached_gem("platform_specific-1.0-#{Gem::Platform.local}")).to exist + expect(cached_gem("platform_specific-1.0-#{Bundler.local_platform}")).to exist expect(cached_gem("platform_specific-1.0-java")).to exist end diff --git a/spec/commands/doctor_spec.rb b/spec/commands/doctor_spec.rb index 236138a6c8..8debeb55e4 100644 --- a/spec/commands/doctor_spec.rb +++ b/spec/commands/doctor_spec.rb @@ -55,7 +55,10 @@ describe "bundle doctor" do expect(doctor).to receive(:dylibs).exactly(2).times.and_return ["/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib"] allow(File).to receive(:exist?).and_call_original allow(File).to receive(:exist?).with("/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib").and_return(false) - expect { doctor.run }.to raise_error SystemExit - expect(@stdout.string).to include("libicui18n.57.1.dylib") + expect { doctor.run }.to raise_error Bundler::ProductionError, strip_whitespace(<<-E).strip + The following gems are missing OS dependencies: + * bundler: /usr/local/opt/icu4c/lib/libicui18n.57.1.dylib + * rack: /usr/local/opt/icu4c/lib/libicui18n.57.1.dylib + E end end diff --git a/spec/commands/exec_spec.rb b/spec/commands/exec_spec.rb index 906abc0555..4dc47919de 100644 --- a/spec/commands/exec_spec.rb +++ b/spec/commands/exec_spec.rb @@ -565,6 +565,42 @@ describe "bundle exec" do it_behaves_like "it runs" end + context "regarding $0 and __FILE__" do + let(:executable) { super() + <<-'RUBY' } + + puts "$0: #{$0.inspect}" + puts "__FILE__: #{__FILE__.inspect}" + RUBY + + let(:expected) { super() + <<-EOS.chomp } + +$0: #{path.to_s.inspect} +__FILE__: #{path.to_s.inspect} + EOS + + it_behaves_like "it runs" + + context "when the path is relative" do + let(:path) { super().relative_path_from(bundled_app) } + + if LessThanProc.with(RUBY_VERSION).call("1.9") + pending "relative paths have ./ __FILE__" + else + it_behaves_like "it runs" + end + end + + context "when the path is relative with a leading ./" do + let(:path) { Pathname.new("./#{super().relative_path_from(Pathname.pwd)}") } + + if LessThanProc.with(RUBY_VERSION).call("< 1.9") + pending "relative paths with ./ have absolute __FILE__" + else + it_behaves_like "it runs" + end + end + end + context "signals being trapped by bundler" do let(:executable) { strip_whitespace <<-RUBY } #{shebang} diff --git a/spec/commands/install_spec.rb b/spec/commands/install_spec.rb index eb78ced86e..457fec26cb 100644 --- a/spec/commands/install_spec.rb +++ b/spec/commands/install_spec.rb @@ -216,7 +216,7 @@ describe "bundle install with gem sources" do G run "require 'platform_specific' ; puts PLATFORM_SPECIFIC" - expect(out).to eq("1.0.0 #{Gem::Platform.local}") + expect(out).to eq("1.0.0 #{Bundler.local_platform}") end it "falls back on plain ruby" do diff --git a/spec/commands/lock_spec.rb b/spec/commands/lock_spec.rb index 1e189a9659..1c00aa3c1f 100644 --- a/spec/commands/lock_spec.rb +++ b/spec/commands/lock_spec.rb @@ -10,9 +10,11 @@ describe "bundle lock" do strip_lockfile bundled_app(file).read end + let(:repo) { gem_repo1 } + before :each do gemfile <<-G - source "file://#{gem_repo1}" + source "file://#{repo}" gem "rails" gem "with_license" gem "foo" @@ -20,7 +22,7 @@ describe "bundle lock" do @lockfile = strip_lockfile <<-L GEM - remote: file:#{gem_repo1}/ + remote: file:#{repo}/ specs: actionmailer (2.3.2) activesupport (= 2.3.2) @@ -77,7 +79,7 @@ describe "bundle lock" do it "writes a lockfile when there is an outdated lockfile using --update" do lockfile @lockfile.gsub("2.3.2", "2.3.1") - bundle "lock --update" + bundle! "lock --update" expect(read_lockfile).to eq(@lockfile) end @@ -104,6 +106,54 @@ describe "bundle lock" do expect(read_lockfile).to eq(@lockfile) end + # see update_spec for more coverage on same options. logic is shared so it's not necessary + # to repeat coverage here. + context "conservative updates" do + before do + build_repo4 do + build_gem "foo", %w(1.4.3 1.4.4) do |s| + s.add_dependency "bar", "~> 2.0" + end + build_gem "foo", %w(1.4.5 1.5.0) do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "foo", %w(1.5.1) do |s| + s.add_dependency "bar", "~> 3.0" + end + build_gem "bar", %w(2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0) + build_gem "qux", %w(1.0.0 1.0.1 1.1.0 2.0.0) + end + + # establish a lockfile set to 1.4.3 + install_gemfile <<-G + source "file://#{gem_repo4}" + gem 'foo', '1.4.3' + gem 'bar', '2.0.3' + gem 'qux', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "file://#{gem_repo4}" + gem 'foo' + gem 'qux' + G + end + + it "single gem updates dependent gem to minor" do + bundle "lock --update foo --patch" + + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w(foo-1.4.5 bar-2.1.1 qux-1.0.0).sort) + end + + it "minor preferred with strict" do + bundle "lock --update --minor --strict" + + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w(foo-1.5.0 bar-2.1.1 qux-1.1.0).sort) + end + end + it "supports adding new platforms" do bundle! "lock --add-platform java x86-mingw32" @@ -132,4 +182,119 @@ describe "bundle lock" do bundle "lock --remove-platform #{local}" expect(out).to include("Removing all platforms from the bundle is not allowed") end + + # from https://github.com/bundler/bundler/issues/4896 + it "properly adds platforms when platform requirements come from different dependencies" do + build_repo4 do + build_gem "ffi", "1.9.14" + build_gem "ffi", "1.9.14" do |s| + s.platform = mingw + end + + build_gem "gssapi", "0.1" + build_gem "gssapi", "0.2" + build_gem "gssapi", "0.3" + build_gem "gssapi", "1.2.0" do |s| + s.add_dependency "ffi", ">= 1.0.1" + end + + build_gem "mixlib-shellout", "2.2.6" + build_gem "mixlib-shellout", "2.2.6" do |s| + s.platform = "universal-mingw32" + s.add_dependency "win32-process", "~> 0.8.2" + end + + # we need all these versions to get the sorting the same as it would be + # pulling from rubygems.org + %w(0.8.3 0.8.2 0.8.1 0.8.0).each do |v| + build_gem "win32-process", v do |s| + s.add_dependency "ffi", ">= 1.0.0" + end + end + end + + gemfile <<-G + source "file:#{gem_repo4}" + + gem "mixlib-shellout" + gem "gssapi" + G + + simulate_platform(mingw) { bundle! :lock } + + expect(the_bundle.lockfile).to read_as(strip_whitespace(<<-G)) + GEM + remote: file:#{gem_repo4}/ + specs: + ffi (1.9.14-x86-mingw32) + gssapi (1.2.0) + ffi (>= 1.0.1) + mixlib-shellout (2.2.6-universal-mingw32) + win32-process (~> 0.8.2) + win32-process (0.8.3) + ffi (>= 1.0.0) + + PLATFORMS + x86-mingw32 + + DEPENDENCIES + gssapi + mixlib-shellout + + BUNDLED WITH + #{Bundler::VERSION} + G + + simulate_platform(rb) { bundle! :lock } + + expect(the_bundle.lockfile).to read_as(strip_whitespace(<<-G)) + GEM + remote: file:#{gem_repo4}/ + specs: + ffi (1.9.14) + ffi (1.9.14-x86-mingw32) + gssapi (1.2.0) + ffi (>= 1.0.1) + mixlib-shellout (2.2.6) + mixlib-shellout (2.2.6-universal-mingw32) + win32-process (~> 0.8.2) + win32-process (0.8.3) + ffi (>= 1.0.0) + + PLATFORMS + ruby + x86-mingw32 + + DEPENDENCIES + gssapi + mixlib-shellout + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + context "when an update is available" do + let(:repo) { gem_repo2 } + + before do + lockfile(@lockfile) + build_repo2 do + build_gem "foo", "2.0" + end + end + + it "does not implicitly update" do + bundle! "lock" + + expect(read_lockfile).to eq(@lockfile) + end + + it "accounts for changes in the gemfile" do + gemfile gemfile.gsub('"foo"', '"foo", "2.0"') + bundle! "lock" + + expect(read_lockfile).to eq(@lockfile.sub("foo (1.0)", "foo (2.0)").sub(/foo$/, "foo (= 2.0)")) + end + end end diff --git a/spec/commands/newgem_spec.rb b/spec/commands/newgem_spec.rb index cb3c48132a..6e80aa7a60 100644 --- a/spec/commands/newgem_spec.rb +++ b/spec/commands/newgem_spec.rb @@ -112,6 +112,35 @@ describe "bundle gem" do end end + context "README.md" do + let(:gem_name) { "test_gem" } + let(:generated_gem) { Bundler::GemHelper.new(bundled_app(gem_name).to_s) } + + context "git config user.name present" do + before do + execute_bundle_gem(gem_name) + end + + it "contribute URL set to git username" do + expect(bundled_app("test_gem/README.md").read).not_to include("[USERNAME]") + end + end + + context "git config user.name is absent" do + before do + `git config --unset user.name` + reset! + in_app_root + bundle "gem #{gem_name}" + remove_push_guard(gem_name) + end + + it "contribute URL set to [USERNAME]" do + expect(bundled_app("test_gem/README.md").read).to include("[USERNAME]") + end + end + end + it "generates a valid gemspec" do system_gems ["rake-10.0.2"] diff --git a/spec/commands/outdated_spec.rb b/spec/commands/outdated_spec.rb index 6420c28ac7..ab00e64b9d 100644 --- a/spec/commands/outdated_spec.rb +++ b/spec/commands/outdated_spec.rb @@ -72,6 +72,120 @@ describe "bundle outdated" do end end + describe "with --group option" do + def test_group_option(group = nil, gems_list_size = 1) + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "weakling", "~> 0.0.1" + gem "terranova", '8' + group :development, :test do + gem "duradura", '7.0' + gem 'activesupport', '2.3.5' + end + G + + update_repo2 do + build_gem "activesupport", "3.0" + build_gem "terranova", "9" + build_gem "duradura", "8.0" + end + + bundle "outdated --group #{group}" + + # Gem names are one per-line, between "*" and their parenthesized version. + gem_list = out.split("\n").map {|g| g[/\* (.*) \(/, 1] }.compact + expect(gem_list).to eq(gem_list.sort) + expect(gem_list.size).to eq gems_list_size + end + + it "not outdated gems" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "weakling", "~> 0.0.1" + gem "terranova", '8' + group :development, :test do + gem 'activesupport', '2.3.5' + gem "duradura", '7.0' + end + G + + bundle "outdated --group" + expect(out).to include("Bundle up to date!") + end + + it "returns a sorted list of outdated gems from one group => 'default'" do + test_group_option("default") + + expect(out).to include("===== Group default =====") + expect(out).to include("terranova (") + + expect(out).not_to include("===== Group development, test =====") + expect(out).not_to include("activesupport") + expect(out).not_to include("duradura") + end + + it "returns a sorted list of outdated gems from one group => 'development'" do + test_group_option("development", 2) + + expect(out).not_to include("===== Group default =====") + expect(out).not_to include("terranova (") + + expect(out).to include("===== Group development, test =====") + expect(out).to include("activesupport") + expect(out).to include("duradura") + end + end + + describe "with --groups option" do + it "not outdated gems" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "weakling", "~> 0.0.1" + gem "terranova", '8' + group :development, :test do + gem 'activesupport', '2.3.5' + gem "duradura", '7.0' + end + G + + bundle "outdated --groups" + expect(out).to include("Bundle up to date!") + end + + it "returns a sorted list of outdated gems by groups" do + install_gemfile <<-G + source "file://#{gem_repo2}" + + gem "weakling", "~> 0.0.1" + gem "terranova", '8' + group :development, :test do + gem 'activesupport', '2.3.5' + gem "duradura", '7.0' + end + G + + update_repo2 do + build_gem "activesupport", "3.0" + build_gem "terranova", "9" + build_gem "duradura", "8.0" + end + + bundle "outdated --groups" + expect(out).to include("===== Group default =====") + expect(out).to include("terranova (newest 9, installed 8, requested = 8)") + expect(out).to include("===== Group development, test =====") + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)") + expect(out).to include("duradura (newest 8.0, installed 7.0, requested = 7.0)") + + expect(out).not_to include("weakling (") + + # TODO: check gems order inside the group + end + end + describe "with --local option" do it "doesn't hit repo2" do FileUtils.rm_rf(gem_repo2) diff --git a/spec/install/gemfile/eval_gemfile_spec.rb b/spec/install/gemfile/eval_gemfile_spec.rb index 2660ac98c2..29f27550e4 100644 --- a/spec/install/gemfile/eval_gemfile_spec.rb +++ b/spec/install/gemfile/eval_gemfile_spec.rb @@ -26,6 +26,24 @@ describe "bundle install with gemfile that uses eval_gemfile" do end end + context "eval-ed Gemfile has relative-path gems" do + before do + build_lib("a", :path => "gems/a") + create_file "nested/Gemfile-nested", <<-G + gem "a", :path => "../gems/a" + G + + gemfile <<-G + eval_gemfile "nested/Gemfile-nested" + G + end + + it "installs the path gem" do + bundle! :install + expect(the_bundle).to include_gem("a 1.0") + end + end + context "Gemfile uses gemspec paths after eval-ing a Gemfile" do before { create_file "other/Gemfile-other" } diff --git a/spec/install/gemfile/platform_spec.rb b/spec/install/gemfile/platform_spec.rb index 58129bb313..9caa0b5731 100644 --- a/spec/install/gemfile/platform_spec.rb +++ b/spec/install/gemfile/platform_spec.rb @@ -184,7 +184,7 @@ describe "bundle install with platform conditionals" do gemfile <<-G source "file://#{gem_repo1}" - gem "some_gem", platform: :rbx + gem "some_gem", :platform => :rbx G bundle "install --local" @@ -204,6 +204,23 @@ describe "bundle install with platform conditionals" do bundle "install --local" expect(out).not_to match(/Could not find gem 'some_gem/) end + + it "prints a helpful warning when a dependency is unused on any platform" do + simulate_platform "ruby" + simulate_ruby_engine "ruby" + + gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", :platform => :jruby + G + + bundle! "install" + + expect(out).to include <<-O.strip +The dependency #{Gem::Dependency.new("rack", ">= 0")} will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for java. To add those platforms to the bundle, run `bundle lock --add-platform jruby`. + O + end end describe "when a gem has no architecture" do diff --git a/spec/install/gemfile/specific_platform_spec.rb b/spec/install/gemfile/specific_platform_spec.rb new file mode 100644 index 0000000000..3e12f94c86 --- /dev/null +++ b/spec/install/gemfile/specific_platform_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true +require "spec_helper" + +describe "bundle install with specific_platform enabled" do + before do + bundle "config specific_platform true" + + build_repo2 do + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86_64-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x64-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "universal-darwin" } + + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86_64-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x64-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.5") + + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "universal-darwin" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x86_64-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x86-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x86-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "x64-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.4") + + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x86_64-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x86-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x86-linux" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "x64-mingw32" } + build_gem("google-protobuf", "3.0.0.alpha.5.0.3") {|s| s.platform = "universal-darwin" } + + build_gem("google-protobuf", "3.0.0.alpha.4.0") + build_gem("google-protobuf", "3.0.0.alpha.3.1.pre") + build_gem("google-protobuf", "3.0.0.alpha.3") + build_gem("google-protobuf", "3.0.0.alpha.2.0") + build_gem("google-protobuf", "3.0.0.alpha.1.1") + build_gem("google-protobuf", "3.0.0.alpha.1.0") + end + end + + let(:google_protobuf) { <<-G } + source "file:#{gem_repo2}" + gem "google-protobuf" + G + + context "when on a darwin machine" do + before { simulate_platform "x86_64-darwin-15" } + + it "locks to both the specific darwin platform and ruby" do + install_gemfile!(google_protobuf) + expect(the_bundle.locked_gems.platforms).to eq([pl("ruby"), pl("x86_64-darwin-15")]) + expect(the_bundle).to include_gem("google-protobuf 3.0.0.alpha.5.0.5.1 universal-darwin") + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w( + google-protobuf-3.0.0.alpha.5.0.5.1 + google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin + )) + end + + it "caches both the universal-darwin and ruby gems when --all-platforms is passed" do + gemfile(google_protobuf) + bundle! "package --all-platforms" + expect([cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1"), cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin")]). + to all(exist) + end + + context "when adding a platform via lock --add_platform" do + it "adds the foreign platform" do + install_gemfile!(google_protobuf) + bundle! "lock --add-platform=#{x64_mingw}" + + expect(the_bundle.locked_gems.platforms).to eq([rb, x64_mingw, pl("x86_64-darwin-15")]) + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w( + google-protobuf-3.0.0.alpha.5.0.5.1 + google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin + google-protobuf-3.0.0.alpha.5.0.5.1-x64-mingw32 + )) + end + + it "falls back on plain ruby when that version doesnt have a platform-specific gem" do + install_gemfile!(google_protobuf) + bundle! "lock --add-platform=#{java}" + + expect(the_bundle.locked_gems.platforms).to eq([java, rb, pl("x86_64-darwin-15")]) + expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w( + google-protobuf-3.0.0.alpha.5.0.5.1 + google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin + )) + end + end + end +end diff --git a/spec/install/gems/compact_index_spec.rb b/spec/install/gems/compact_index_spec.rb index b34d9e872d..001118b513 100644 --- a/spec/install/gems/compact_index_spec.rb +++ b/spec/install/gems/compact_index_spec.rb @@ -696,6 +696,49 @@ The checksum of /versions does not match the checksum provided by the server! So expect(the_bundle).to include_gems "rack 1.0.0" end + it "fails gracefully when the source URI has an invalid scheme" do + install_gemfile <<-G + source "htps://rubygems.org" + gem "rack" + G + expect(exitstatus).to eq(15) if exitstatus + expect(out).to end_with(<<-E.strip) + The request uri `htps://index.rubygems.org/versions` has an invalid scheme (`htps`). Did you mean `http` or `https`? + E + end + + describe "checksum validation", :rubygems => ">= 2.3.0" do + it "raises when the checksum does not match" do + install_gemfile <<-G, :artifice => "compact_index_wrong_gem_checksum" + source "#{source_uri}" + gem "rack" + G + expect(exitstatus).to eq(19) if exitstatus + expect(out). + to include("The checksum for the downloaded `rack-1.0.0.gem` did not match the checksum given by the API."). + and include("This means that the contents of the gem appear to be different from what was uploaded, and could be an indicator of a security issue."). + and match(/\(The expected SHA256 checksum was "#{"ab" * 22}", but the checksum for the downloaded gem was ".+?"\.\)/). + and include("Bundler cannot continue installing rack (1.0.0).") + end + + it "raises when the checksum is the wrong length" do + install_gemfile <<-G, :artifice => "compact_index_wrong_gem_checksum", :env => { "BUNDLER_SPEC_RACK_CHECKSUM" => "checksum!" } + source "#{source_uri}" + gem "rack" + G + expect(exitstatus).to eq(5) if exitstatus + expect(out).to include("The given checksum for rack-1.0.0 (\"checksum!\") is not a valid SHA256 hexdigest nor base64digest") + end + + it "does not raise when disable_checksum_validation is set" do + bundle! "config disable_checksum_validation true" + install_gemfile! <<-G, :artifice => "compact_index_wrong_gem_checksum" + source "#{source_uri}" + gem "rack" + G + end + end + it "works when cache dir is world-writable" do install_gemfile! <<-G, :artifice => "compact_index" File.umask(0000) diff --git a/spec/install/gems/resolving_spec.rb b/spec/install/gems/resolving_spec.rb index 816799c0f8..0204a222f9 100644 --- a/spec/install/gems/resolving_spec.rb +++ b/spec/install/gems/resolving_spec.rb @@ -119,20 +119,58 @@ describe "bundle install with install-time dependencies" do end context "allows no gems" do - it "does not try to install those gems" do + before do build_repo2 do build_gem "require_ruby" do |s| s.required_ruby_version = "> 9000" end end + end - install_gemfile <<-G - source "file://#{gem_repo2}" - gem 'require_ruby' - G + let(:ruby_requirement) { %("#{RUBY_VERSION}") } + let(:error_message_requirement) { "~> #{RUBY_VERSION}.0" } + + shared_examples_for "ruby version conflicts" do + it "raises an error during resolution" do + install_gemfile <<-G, :artifice => "compact_index", :env => { "BUNDLER_SPEC_GEM_REPO" => gem_repo2 } + source "http://localgemserver.test/" + ruby #{ruby_requirement} + gem 'require_ruby' + G + + expect(out).to_not include("Gem::InstallError: require_ruby requires Ruby version > 9000") + + nice_error = strip_whitespace(<<-E).strip + Fetching gem metadata from http://localgemserver.test/. + Fetching version metadata from http://localgemserver.test/ + Resolving dependencies... + Bundler could not find compatible versions for gem "ruby\0": + In Gemfile: + ruby\0 (#{error_message_requirement}) + + require_ruby was resolved to 1.0, which depends on + ruby\0 (> 9000) + + Could not find gem 'ruby\0 (> 9000)', which is required by gem 'require_ruby', in any of the sources. + E + expect(out).to eq(nice_error) + end + end + + it_behaves_like "ruby version conflicts" + + describe "with a < requirement" do + let(:ruby_requirement) { %("< 5000") } + let(:error_message_requirement) { "< 5000" } + + it_behaves_like "ruby version conflicts" + end + + describe "with a compound requirement" do + let(:ruby_requirement) { %("< 5000", "> 0.1") } + let(:error_message_requirement) { "< 5000, > 0.1" } - expect(out).to_not include("Gem::InstallError: require_ruby requires Ruby version > 9000") - expect(out).to include("require_ruby-1.0 requires ruby version > 9000, which is incompatible with the current version, #{Bundler::RubyVersion.system}") + it_behaves_like "ruby version conflicts" end end end diff --git a/spec/install/gemspecs_spec.rb b/spec/install/gemspecs_spec.rb index 3e6021b7e2..8f719bf601 100644 --- a/spec/install/gemspecs_spec.rb +++ b/spec/install/gemspecs_spec.rb @@ -50,7 +50,7 @@ describe "bundle install" do context "when ruby version is specified in gemspec and gemfile" do it "installs when patch level is not specified and the version matches" do build_lib("foo", :path => bundled_app) do |s| - s.required_ruby_version = RUBY_VERSION + s.required_ruby_version = "~> #{RUBY_VERSION}.0" end install_gemfile <<-G diff --git a/spec/install/path_spec.rb b/spec/install/path_spec.rb index 3d84fffd58..03c42f008c 100644 --- a/spec/install/path_spec.rb +++ b/spec/install/path_spec.rb @@ -130,21 +130,21 @@ describe "bundle install" do end end - describe "to a dead symlink" do + describe "to a file" do before do in_app_root do - `ln -s /tmp/idontexist bundle` + `touch /tmp/idontexist bundle` end end - it "reports the symlink is dead" do + it "reports the file exists" do gemfile <<-G source "file://#{gem_repo1}" gem "rack" G bundle "install --path bundle" - expect(out).to match(/invalid symlink/) + expect(out).to match(/file already exists/) end end end diff --git a/spec/other/major_deprecation_spec.rb b/spec/other/major_deprecation_spec.rb index c8a2633279..6505023d13 100644 --- a/spec/other/major_deprecation_spec.rb +++ b/spec/other/major_deprecation_spec.rb @@ -4,6 +4,22 @@ require "spec_helper" describe "major deprecations" do let(:warnings) { out } # change to err in 2.0 + context "in a .99 version" do + before do + simulate_bundler_version "1.99.1" + bundle "config --delete major_deprecations" + end + + it "prints major deprecations without being configured" do + ruby <<-R + require "bundler" + Bundler::SharedHelpers.major_deprecation(Bundler::VERSION) + R + + expect(warnings).to have_major_deprecation("1.99.1") + end + end + before do bundle "config major_deprecations true" diff --git a/spec/other/trampoline_spec.rb b/spec/other/trampoline_spec.rb index 2aac0a2c1d..9a8e0a4a5d 100644 --- a/spec/other/trampoline_spec.rb +++ b/spec/other/trampoline_spec.rb @@ -59,6 +59,24 @@ describe "bundler version trampolining" do end end + context "without BUNDLE_ENABLE_TRAMPOLINE" do + before { ENV["BUNDLE_ENABLE_TRAMPOLINE"] = nil } + + context "when the version is >= 2" do + let(:version) { "2.7182818285" } + before do + simulate_bundler_version version do + install_gemfile! "" + end + end + + it "trampolines automatically", :realworld => true do + bundle "--version" + expect(err).to include("Installing locked Bundler version #{version}...") + end + end + end + context "installing missing bundler versions", :realworld => true do before do ENV["BUNDLER_VERSION"] = "1.12.3" diff --git a/spec/resolver/basic_spec.rb b/spec/resolver/basic_spec.rb index b7b8b4c3b8..3e8883d1d4 100644 --- a/spec/resolver/basic_spec.rb +++ b/spec/resolver/basic_spec.rb @@ -92,15 +92,18 @@ describe "Resolving" do gem "bar", "2.0.0" do |s| s.required_ruby_version = "~> 2.0.0" end + + gem "ruby\0", "1.8.7" end dep "foo" + dep "ruby\0", "1.8.7" deps = [] @deps.each do |d| deps << Bundler::DepProxy.new(d, "ruby") end - should_resolve_and_include %w(foo-1.0.0 bar-1.0.0), [{}, [], Bundler::RubyVersion.new("1.8.7", nil, nil, nil)] + should_resolve_and_include %w(foo-1.0.0 bar-1.0.0), [{}, []] end context "conservative" do diff --git a/spec/resolver/platform_spec.rb b/spec/resolver/platform_spec.rb index d5f217684c..fa91eab9c2 100644 --- a/spec/resolver/platform_spec.rb +++ b/spec/resolver/platform_spec.rb @@ -50,7 +50,7 @@ describe "Resolving platform craziness" do # mingw is _not_ hardcoded to add CPU x86 in rubygems platforms "x86-mingw32" dep "thin" - should_resolve_as %w(thin-1.2.7-x86-mingw32) + should_resolve_as %w(thin-1.2.7-mingw32) end it "finds x64-mingw gems" do diff --git a/spec/runtime/inline_spec.rb b/spec/runtime/inline_spec.rb index 3119045be4..4d9a1f7fe4 100644 --- a/spec/runtime/inline_spec.rb +++ b/spec/runtime/inline_spec.rb @@ -173,4 +173,24 @@ describe "bundler/inline#gemfile" do expect(err).to be_empty expect(exitstatus).to be_zero if exitstatus end + + it "allows calling gemfile twice" do + script <<-RUBY + gemfile do + path "#{lib_path}" do + gem "two" + end + end + + gemfile do + path "#{lib_path}" do + gem "four" + end + end + RUBY + + expect(out).to eq("two\nfour") + expect(err).to be_empty + expect(exitstatus).to be_zero if exitstatus + end end diff --git a/spec/runtime/platform_spec.rb b/spec/runtime/platform_spec.rb index 666864a88c..4fd09cf4b7 100644 --- a/spec/runtime/platform_spec.rb +++ b/spec/runtime/platform_spec.rb @@ -88,4 +88,20 @@ describe "Bundler.setup with multi platform stuff" do expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 x86-darwin-100" end + + it "allows specifying only-ruby-platform" do + simulate_platform "java" + + install_gemfile! <<-G + source "file://#{gem_repo1}" + gem "nokogiri" + gem "platform_specific" + G + + bundle! "config force_ruby_platform true" + + bundle! "install" + + expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 RUBY" + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a3251ea640..642bfabc18 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -17,6 +17,10 @@ rescue LoadError abort "Run rake spec:deps to install development dependencies" end +if File.expand_path(__FILE__) =~ %r{([^\w/\.])} + abort "The bundler specs cannot be run from a path that contains special characters (particularly #{$1.inspect})" +end + require "bundler" # Require the correct version of popen for the current platform diff --git a/spec/support/artifice/compact_index.rb b/spec/support/artifice/compact_index.rb index d6b1e0cecd..0afd7fc526 100644 --- a/spec/support/artifice/compact_index.rb +++ b/spec/support/artifice/compact_index.rb @@ -78,7 +78,12 @@ class CompactIndexAPI < Endpoint reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ") CompactIndex::Dependency.new(d.name, reqs) end - CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, nil, nil, + checksum = begin + Digest::SHA256.file("#{GEM_REPO}/gems/#{spec.original_name}.gem").base64digest + rescue + nil + end + CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil, deps, spec.required_ruby_version, spec.required_rubygems_version) end CompactIndex::Gem.new(name, gem_versions) diff --git a/spec/support/artifice/compact_index_wrong_gem_checksum.rb b/spec/support/artifice/compact_index_wrong_gem_checksum.rb new file mode 100644 index 0000000000..3a12a59ae7 --- /dev/null +++ b/spec/support/artifice/compact_index_wrong_gem_checksum.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexWrongGemChecksum < CompactIndexAPI + get "/info/:name" do + etag_response do + name = params[:name] + gem = gems.find {|g| g.name == name } + checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") { "ab" * 22 } + versions = gem ? gem.versions : [] + versions.each {|v| v.checksum = checksum } + CompactIndex.info(versions) + end + end +end + +Artifice.activate_with(CompactIndexWrongGemChecksum) diff --git a/spec/support/builders.rb b/spec/support/builders.rb index 337234f14a..16ced2b920 100644 --- a/spec/support/builders.rb +++ b/spec/support/builders.rb @@ -79,8 +79,8 @@ module Spec end build_gem "platform_specific" do |s| - s.platform = Gem::Platform.local - s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 #{Gem::Platform.local}'" + s.platform = Bundler.local_platform + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 #{Bundler.local_platform}'" end build_gem "platform_specific" do |s| @@ -610,7 +610,10 @@ module Spec end def _default_files - @_default_files ||= { "lib/#{name}.rb" => "#{Builders.constantize(name)} = '#{version}'" } + @_default_files ||= begin + platform_string = " #{@spec.platform}" unless @spec.platform == Gem::Platform::RUBY + { "lib/#{name}.rb" => "#{Builders.constantize(name)} = '#{version}#{platform_string}'" } + end end def _default_path diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb index 65603cb529..6b30afd480 100644 --- a/spec/support/helpers.rb +++ b/spec/support/helpers.rb @@ -218,7 +218,11 @@ module Spec end def gemfile(*args) - create_file("Gemfile", *args) + if args.empty? + File.open("Gemfile", "r", &:read) + else + create_file("Gemfile", *args) + end end def lockfile(*args) diff --git a/spec/support/indexes.rb b/spec/support/indexes.rb index 9a7879bc74..29780014fc 100644 --- a/spec/support/indexes.rb +++ b/spec/support/indexes.rb @@ -62,7 +62,7 @@ module Spec s.level = opts.first s.strict = opts.include?(:strict) end - should_resolve_and_include specs, [{}, @base, nil, search] + should_resolve_and_include specs, [{}, @base, search] end def an_awesome_index diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index 9476f18984..9248360639 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -110,15 +110,9 @@ module Spec define_compound_matcher :read_as, [exist] do |file_contents| diffable - attr_reader :strip_whitespace - - chain :stripping_whitespace do - @strip_whitespace = true - end match do |actual| @actual = Bundler.read_file(actual) - file_contents = strip_whitespace(file_contents) if strip_whitespace values_match?(file_contents, @actual) end end diff --git a/spec/support/platforms.rb b/spec/support/platforms.rb index b1dedb05fa..a2a3afba00 100644 --- a/spec/support/platforms.rb +++ b/spec/support/platforms.rb @@ -11,6 +11,10 @@ module Spec Gem::Platform.new("x86-darwin-10") end + def x64_mac + Gem::Platform.new("x86_64-darwin-15") + end + def java Gem::Platform.new([nil, "java", nil]) end diff --git a/spec/support/the_bundle.rb b/spec/support/the_bundle.rb index 86df9cd9c7..742d393425 100644 --- a/spec/support/the_bundle.rb +++ b/spec/support/the_bundle.rb @@ -27,5 +27,10 @@ module Spec def lockfile bundle_dir.join("Gemfile.lock") end + + def locked_gems + raise "Cannot read lockfile if it doesn't exist" unless locked? + Bundler::LockfileParser.new(lockfile.read) + end end end |