diff options
73 files changed, 2043 insertions, 762 deletions
diff --git a/.travis.yml b/.travis.yml index ea99cf89..ffd598e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,7 @@ rvm: - 2.0.0 - 1.9.3 - 1.8.7 + - rbx-2 # Rubygems versions MUST be available as rake tasks # see Rakefile:66 for the list of possible RGV values diff --git a/CHANGELOG.md b/CHANGELOG.md index d141f605..bab74c9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ Bugfixes: - make Bundler.which stop finding directories (@nohoho) - handle Bundler prereleases correctly (#3470, @segiddins) + - add before_install to .travis.yml template for new gems (@kodnin) ## 1.9.0.pre.1 (2015-03-11) @@ -98,8 +98,8 @@ namespace :spec do end deps.sort_by{|name, _| name }.each do |name, version| - sh "#{Gem.ruby} -S gem list -i '^#{name}$' -v '#{version}' || " \ - "#{Gem.ruby} -S gem install #{name} -v '#{version}' --no-ri --no-rdoc" + sh %{#{Gem.ruby} -S gem list -i "^#{name}$" -v "#{version}" || } + + %{#{Gem.ruby} -S gem install #{name} -v "#{version}" --no-ri --no-rdoc} end # Download and install gems used inside tests diff --git a/UPGRADING.md b/UPGRADING.md deleted file mode 100644 index 665be21b..00000000 --- a/UPGRADING.md +++ /dev/null @@ -1,103 +0,0 @@ -## Bundler 0.9 to 1.0 and above - -Upgrading from Bundler 0.9 to 1.0 is relatively painless. The -Gemfile API is the same, so your old Gemfiles should continue -to work. - -The "env" file that 0.9 created at `.bundle/environment.rb` has been -removed. As a side effect of this, Passenger will only find your -bundled gems if you install with `bundle install --deployment`. -Alternatively, you can tell Passenger where you gems are installed, -[something like this](http://andre.arko.net/2010/08/16/using-passengerpane-with-gem_home-set/). - -The `bundle lock` command is no longer needed, as the -Gemfile.lock file is now automatically generated by `bundle install`. -If you have not yet done so, add your Gemfile.lock to source control -and check it in. - -Running `bundle install` no longer updates the versions of your gems. -If you need to update just one gem, run `bundle update GEMNAME`. To -update all gems to the newest versions possible, run `bundle update`. - -Bundler now supports multiple platforms, using a block syntax to -declare platform-specific gems: - - platform :jruby do - gem "jruby-maven-plugins" - end - -Deploying using Bundler is even easier than it was before, as Bundler -now includes a Capistrano recipe. Simply add this line to the top of -your deploy.rb file to run Bundler automatically as part of deploying: - - require 'bundler/capistrano' - -For more details on deploying using bundler, see the documentation -for the bundler cap task, and the [documentation on deploying](http://bundler.io/deploying.html). - - -## Bundler 0.8 to 0.9 and above - -Upgrading to Bundler 0.9 from Bundler 0.8 requires upgrading several -API calls in your Gemfile, and some workarounds if you are using Rails 2.3. - -### Gemfile Removals - -Bundler 0.9 removes the following Bundler 0.8 Gemfile APIs: - -1. `disable_system_gems`: This is now the default (and only) option - for bundler. Bundler uses the system gems you have specified - in the Gemfile, and only the system gems you have specified - (and their dependencies) -2. `disable_rubygems`: This is no longer supported. We are looking - into ways to get the fastest performance out of each supported - scenario, and we will make speed the default where possible. -3. `clear_sources`: Bundler now defaults to an empty source - list. If you want to include Rubygems, you can add the source - via source "http://gemcutter.org". If you use bundle init, this - source will be automatically added for you in the generated - Gemfile -4. `bundle_path`: You can specify this setting when installing - via `bundle install /path/to/bundle`. Bundler will remember - where you installed the dependencies to on a particular - machine for future installs, loads, setups, etc. -5. `bin_path`: Bundler no longer generates executables in the root - of your app. You should use `bundle exec` to execute executables - in the current context. - -### Gemfile Changes - -Bundler 0.9 changes the following Bundler 0.8 Gemfile APIs: - -1. Bundler 0.8 supported :only and :except as APIs for describing - groups of gems. Bundler 0.9 supports a single `group` method, - which you can use to group gems together. See the above "Group" - section for more information. - - This means that `gem "foo", :only => :production` becomes - `gem "foo", :group => :production`, and - `only :production { gem "foo" }` becomes - `group :production { gem "foo" }` - - The short version is: group your gems together logically, and - use the available commands to make use of the groups you've - created. - -2. `:require_as` becomes `:require` - -3. `:vendored_at` is fully removed; you should use `:path` - -### API Changes - -1. `Bundler.require_env(:environment)` becomes - `Bundler.require(:multiple, :groups)`. You must - now specify the default group (the default group is the - group made up of the gems not assigned to any group) - explicitly. So `Bundler.require_env(:test)` becomes - `Bundler.require(:default, :test)` - -2. `require 'vendor/gems/environment'`: In unlocked - mode, where using system gems, this becomes - `Bundler.setup(:multiple, :groups)`. If you don't - specify any groups, this puts all groups on the load - path. In locked mode, it becomes `require '.bundle/environment'` diff --git a/bin/bundle_ruby b/bin/bundle_ruby index 1e02c575..d0cfdc27 100755 --- a/bin/bundle_ruby +++ b/bin/bundle_ruby @@ -41,6 +41,8 @@ module Bundler end end +STDERR.puts "Warning: bundle_ruby will be deprecated in Bundler 2.0.0." + dsl = Bundler::Dsl.new begin dsl.eval_gemfile(Bundler::SharedHelpers.default_gemfile) diff --git a/bin/bundler b/bin/bundler index 63285e96..89c823ea 100755 --- a/bin/bundler +++ b/bin/bundler @@ -6,7 +6,7 @@ Signal.trap("INT") { exit 1 } require 'bundler' # Check if an older version of bundler is installed $LOAD_PATH.each do |path| - if path =~ %r'/bundler-0.(\d+)' && $1.to_i < 9 + if path =~ %r'/bundler-0\.(\d+)' && $1.to_i < 9 err = "Looks like you have a version of bundler that's older than 0.9.\n" err << "Please remove your old versions.\n" err << "An easy way to do this is by running `gem cleanup bundler`." diff --git a/lib/bundler.rb b/lib/bundler.rb index 38a3de0f..49d9fd68 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -12,7 +12,6 @@ module Bundler preserve_gem_path ORIGINAL_ENV = ENV.to_hash - autoload :AnonymizableURI, 'bundler/anonymizable_uri' autoload :Definition, 'bundler/definition' autoload :Dependency, 'bundler/dependency' autoload :DepProxy, 'bundler/dep_proxy' @@ -121,12 +120,7 @@ module Bundler # Load all groups, but only once @setup = load.setup else - @completed_groups ||= [] - # Figure out which groups haven't been loaded yet - unloaded = groups - @completed_groups - # Record groups that are now loaded - @completed_groups = groups - unloaded.any? ? load.setup(*groups) : load + load.setup(*groups) end end @@ -217,7 +211,7 @@ module Bundler end def cleanup - FileUtils.remove_entry_secure(@tmp) if @tmp + FileUtils.remove_entry_secure(@tmp) if defined?(@tmp) && @tmp rescue end diff --git a/lib/bundler/anonymizable_uri.rb b/lib/bundler/anonymizable_uri.rb deleted file mode 100644 index 333a568f..00000000 --- a/lib/bundler/anonymizable_uri.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Bundler - class AnonymizableURI - attr_reader :original_uri, - :without_credentials - - def initialize(original_uri, fallback_auth = nil) - @original_uri = apply_auth(original_uri, fallback_auth).freeze - @without_credentials = remove_auth(@original_uri).freeze - end - - private - - def apply_auth(uri, auth = nil) - if auth && uri.userinfo.nil? - uri = uri.dup - uri.userinfo = auth - end - - uri - end - - def remove_auth(uri) - if uri.userinfo - uri = uri.dup - uri.user = uri.password = nil - end - - uri - end - - end -end diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 6a220725..50133d29 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -20,7 +20,7 @@ module Bundler current_cmd = args.last[:current_command].name custom_gemfile = options[:gemfile] || Bundler.settings[:gemfile] ENV['BUNDLE_GEMFILE'] = File.expand_path(custom_gemfile) if custom_gemfile - Bundler::Retry.attempts = options[:retry] || Bundler.settings[:retry] || Bundler::Retry::DEFAULT_ATTEMPTS + Bundler.settings[:retry] = options[:retry] if options[:retry] Bundler.rubygems.ui = UI::RGProxy.new(Bundler.ui) auto_install if AUTO_INSTALL_CMDS.include?(current_cmd) rescue UnknownArgumentError => e @@ -134,6 +134,8 @@ module Bundler "Do not attempt to fetch gems remotely and use the gem cache instead" method_option "no-cache", :type => :boolean, :banner => "Don't update the existing gem cache." + method_option "force", :type => :boolean, :banner => + "Force downloading every gem." method_option "no-prune", :type => :boolean, :banner => "Don't remove stale gems from the cache." method_option "path", :type => :string, :banner => @@ -151,6 +153,8 @@ module Bundler Bundler.rubygems.security_policy_keys.join('|') method_option "without", :type => :array, :banner => "Exclude gems that are part of the specified named group." + method_option "with", :type => :array, :banner => + "Include gems that are part of the specified named group." def install require 'bundler/cli/install' @@ -175,6 +179,8 @@ module Bundler "Only output warnings and errors." method_option "source", :type => :array, :banner => "Update a specific source (and all gems associated with it)" + method_option "force", :type => :boolean, :banner => + "Force downloading every gem." def update(*gems) require 'bundler/cli/update' Update.new(options, gems).run @@ -379,6 +385,20 @@ module Bundler Inject.new(options, name, version, gems).run end + desc "lock", "Creates a lockfile without installing" + method_option "update", :type => :boolean, :default => false, :banner => + "ignore the existing lockfile" + method_option "local", :type => :boolean, :default => false, :banner => + "do not attempt to fetch remote gemspecs and use the local gem cache only" + method_option "print", :type => :boolean, :default => false, :banner => + "print the lockfile to STDOUT instead of writing to the file system" + method_option "lockfile", :type => :string, :default => nil, :banner => + "the path the lockfile should be written to" + def lock + require 'bundler/cli/lock' + Lock.new(options).run + end + desc "env", "Print information about the environment Bundler is running under" def env Env.new.write($stdout) diff --git a/lib/bundler/cli/gem.rb b/lib/bundler/cli/gem.rb index 1d65f448..f58e12a5 100644 --- a/lib/bundler/cli/gem.rb +++ b/lib/bundler/cli/gem.rb @@ -66,6 +66,7 @@ module Bundler "of enforcing it, so be sure that you are prepared to do that. For suggestions about " \ "how to enforce codes of conduct, see bit.ly/coc-enforcement." ) + Bundler.ui.info "Code of conduct enabled in config" templates.merge!("CODE_OF_CONDUCT.md.tt" => "CODE_OF_CONDUCT.md") end @@ -75,10 +76,12 @@ module Bundler "at choosealicense.com/licenses/mit." ) config[:mit] = true + Bundler.ui.info "MIT License enabled in config" templates.merge!("LICENSE.txt.tt" => "LICENSE.txt") end if test_framework = ask_and_set_test_framework + config[:test] = test_framework templates.merge!(".travis.yml.tt" => ".travis.yml") case test_framework @@ -90,8 +93,8 @@ module Bundler ) when 'minitest' templates.merge!( - "test/minitest_helper.rb.tt" => "test/minitest_helper.rb", - "test/test_newgem.rb.tt" => "test/test_#{namespaced_path}.rb" + "test/test_helper.rb.tt" => "test/test_helper.rb", + "test/newgem_test.rb.tt" => "test/#{namespaced_path}_test.rb" ) end end diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb index 4d671512..f7c046d4 100644 --- a/lib/bundler/cli/install.rb +++ b/lib/bundler/cli/install.rb @@ -10,10 +10,35 @@ module Bundler warn_if_root - if options[:without] - options[:without] = options[:without].map{|g| g.tr(' ', ':') } + [:with, :without].each do |option| + if options[option] + options[option] = options[option].join(":").tr(" ", ":").split(":") + end + end + + if options[:without] && options[:with] + conflicting_groups = options[:without] & options[:with] + unless conflicting_groups.empty? + Bundler.ui.error "You can't list a group in both, --with and --without." \ + "The offending groups are: #{conflicting_groups.join(", ")}." + exit 1 + end end + Bundler.settings.with = [] if options[:with] && options[:with].empty? + Bundler.settings.without = [] if options[:without] && options[:without].empty? + + with = options.fetch("with", []) + with |= Bundler.settings.with.map {|group| group.to_s } + with -= options[:without] if options[:without] + + without = options.fetch("without", []) + without |= Bundler.settings.without.map {|group| group.to_s } + without -= options[:with] if options[:with] + + options[:with] = with + options[:without] = without + ENV['RB_USER_INSTALL'] = '1' if Bundler::FREEBSD # Just disable color in deployment mode @@ -69,6 +94,7 @@ module Bundler Bundler.settings[:no_install] = true if options["no-install"] Bundler.settings[:clean] = options["clean"] if options["clean"] Bundler.settings.without = options[:without] + Bundler.settings.with = options[:with] Bundler::Fetcher.disable_endpoint = options["full-index"] Bundler.settings[:disable_shared_gems] = Bundler.settings[:path] ? '1' : nil @@ -91,9 +117,10 @@ module Bundler Bundler.ui.confirm "Use `bundle show [gemname]` to see where a bundled gem is installed." end - Installer.post_install_messages.to_a.each do |name, msg| - Bundler.ui.confirm "Post-install message from #{name}:" - Bundler.ui.info msg + unless Bundler.settings["ignore_messages"] + Installer.post_install_messages.to_a.each do |name, msg| + print_post_install_message(name, msg) unless Bundler.settings["ignore_messages.#{name}"] + end end Installer.ambiguous_gems.to_a.each do |name, installed_from_uri, *also_found_in_uris| @@ -152,5 +179,10 @@ module Bundler "#{count} #{count == 1 ? 'gem' : 'gems'} now installed" end + def print_post_install_message(name, msg) + Bundler.ui.confirm "Post-install message from #{name}:" + Bundler.ui.info msg + end + end end diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb new file mode 100644 index 00000000..543deb59 --- /dev/null +++ b/lib/bundler/cli/lock.rb @@ -0,0 +1,36 @@ +module Bundler + class CLI::Lock + attr_reader :options + + def initialize(options) + @options = options + end + + def run + unless Bundler.default_gemfile + Bundler.ui.error "Unable to find a Gemfile to lock" + exit 1 + end + + print = options[:print] + ui = Bundler.ui + Bundler.ui = UI::Silent.new if print + + unlock = options[:update] + definition = Bundler.definition(unlock) + definition.resolve_remotely! unless options[:local] + + if print + puts definition.to_lock + else + file = options[:lockfile] + file = file ? File.expand_path(file) : Bundler.default_lockfile + puts "Writing lockfile to #{file}" + definition.lock(file) + end + + Bundler.ui = ui + end + + end +end diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb index f8779d76..7e60621f 100644 --- a/lib/bundler/cli/outdated.rb +++ b/lib/bundler/cli/outdated.rb @@ -62,8 +62,15 @@ module Bundler spec_version = "#{active_spec.version}#{active_spec.git_version}" current_version = "#{current_spec.version}#{current_spec.git_version}" - dependency_version = %|Gemfile specifies "#{dependency.requirement}"| if dependency && dependency.specific? - Bundler.ui.info " * #{active_spec.name} (#{spec_version} > #{current_version}) #{dependency_version}".rstrip + dependency_version = %|, requested #{dependency.requirement}| if dependency && dependency.specific? + + if options["verbose"] + groups = dependency.groups.join(", ") + pl = (dependency.groups.length > 1) ? "s" : "" + groups = " in group#{pl} \"#{groups}\"" + end + + Bundler.ui.info " * #{active_spec.name} (newest #{spec_version}, installed #{current_version}#{dependency_version})#{groups}".rstrip out_count += 1 end Bundler.ui.debug "from #{active_spec.loaded_from}" diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 70e2efda..d8aab4fb 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -43,10 +43,11 @@ module Bundler # @param unlock [Hash, Boolean, nil] Gems that have been requested # to be updated or true if all gems should be updated # @param ruby_version [Bundler::RubyVersion, nil] Requested Ruby Version - def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil) + # @param optional_groups [Array(String)] A list of optional groups + def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = []) @unlocking = unlock == true || !unlock.empty? - @dependencies, @sources, @unlock = dependencies, sources, unlock + @dependencies, @sources, @unlock, @optional_groups = dependencies, sources, unlock, optional_groups @remote = false @specs = nil @lockfile_contents = "" @@ -56,6 +57,7 @@ module Bundler @lockfile_contents = Bundler.read_file(lockfile) locked = LockfileParser.new(@lockfile_contents) @platforms = locked.platforms + @locked_bundler_version = locked.bundler_version if unlock != true @locked_deps = locked.dependencies @@ -161,7 +163,7 @@ module Bundler def requested_specs @requested_specs ||= begin - groups = self.groups - Bundler.settings.without + groups = requested_groups groups.map! { |g| g.to_sym } specs_for(groups) end @@ -255,6 +257,16 @@ module Bundler "#{File.expand_path(file)}" end + # Returns the version of Bundler that is creating or has created + # Gemfile.lock. Used in #to_lock. + def lock_version + if @locked_bundler_version && @locked_bundler_version < Gem::Version.new(Bundler::VERSION) + new_version = Bundler::VERSION + end + + new_version || @locked_bundler_version || Bundler::VERSION + end + def to_lock out = "" @@ -293,6 +305,10 @@ module Bundler handled << dep.name end + # Record the version of Bundler that was used to create the lockfile + out << "\nBUNDLED WITH\n" + out << " #{lock_version}\n" + out end @@ -590,7 +606,7 @@ module Bundler end def requested_dependencies - groups = self.groups - Bundler.settings.without + groups = requested_groups groups.map! { |g| g.to_sym } dependencies.reject { |d| !d.should_include? || (d.groups & groups).empty? } end @@ -624,5 +640,8 @@ module Bundler names end + def requested_groups + self.groups - Bundler.settings.without - @optional_groups + Bundler.settings.with + end end end diff --git a/lib/bundler/deployment.rb b/lib/bundler/deployment.rb index 44a1b867..072843d7 100644 --- a/lib/bundler/deployment.rb +++ b/lib/bundler/deployment.rb @@ -33,6 +33,7 @@ module Bundler set :bundle_dir, File.join(fetch(:shared_path), 'bundle') set :bundle_flags, "--deployment --quiet" set :bundle_without, [:development, :test] + set :bundle_with, [:mysql] set :bundle_cmd, "bundle" # e.g. "/opt/ruby/bin/bundle" set :bundle_roles, #{role_default} # e.g. [:app, :batch] DESC @@ -42,6 +43,7 @@ module Bundler bundle_dir = context.fetch(:bundle_dir, File.join(context.fetch(:shared_path), 'bundle')) bundle_gemfile = context.fetch(:bundle_gemfile, "Gemfile") bundle_without = [*context.fetch(:bundle_without, [:development, :test])].compact + bundle_with = [*context.fetch(:bundle_with, [])].compact app_path = context.fetch(:latest_release) if app_path.to_s.empty? raise error_type.new("Cannot detect current release path - make sure you have deployed at least once.") @@ -50,6 +52,7 @@ module Bundler args << "--path #{bundle_dir}" unless bundle_dir.to_s.empty? args << bundle_flags.to_s args << "--without #{bundle_without.join(" ")}" unless bundle_without.empty? + args << "--with #{bundle_with.join(" ")}" unless bundle_with.empty? run "cd #{app_path} && #{bundle_cmd} install #{args.join(' ')}" end diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index e6c4b975..cc1e19bd 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -21,6 +21,7 @@ module Bundler @git_sources = {} @dependencies = [] @groups = [] + @optional_groups = [] @platforms = [] @env = nil @ruby_version = nil @@ -30,18 +31,14 @@ module Bundler def eval_gemfile(gemfile, contents = nil) contents ||= Bundler.read_file(gemfile.to_s) instance_eval(contents, gemfile.to_s, 1) - rescue SyntaxError => e - syntax_msg = e.message.gsub("#{gemfile}:", 'on line ') - raise GemfileError, "Gemfile syntax error #{syntax_msg}" - rescue ScriptError, RegexpError, NameError, ArgumentError, RuntimeError => e - e.backtrace[0] = "#{e.backtrace[0]}: #{e.message} (#{e.class})" - Bundler.ui.warn e.backtrace.join("\n ") - raise GemfileError, "There was an error in your Gemfile," \ - " and Bundler cannot continue." + rescue Exception => e + message = "There was an error parsing `#{File.basename gemfile.to_s}`: #{e.message}" + raise DSLError.new(message, gemfile, e.backtrace, contents) end def gemspec(opts = nil) path = opts && opts[:path] || '.' + glob = opts && opts[:glob] name = opts && opts[:name] || '{,*}' development_group = opts && opts[:development_group] || :development expanded_path = File.expand_path(path, Bundler.default_gemfile.dirname) @@ -52,7 +49,7 @@ module Bundler when 1 spec = Bundler.load_gemspec(gemspecs.first) raise InvalidOption, "There was an error loading the gemspec at #{gemspecs.first}." unless spec - gem spec.name, :path => path + gem spec.name, :path => path, :glob => glob group(development_group) do spec.development_dependencies.each do |dep| gem dep.name, *(dep.requirement.as_list + [:type => :development]) @@ -158,11 +155,20 @@ module Bundler end def to_definition(lockfile, unlock) - Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version) + Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups) end def group(*args, &blk) + opts = Hash === args.last ? args.pop.dup : {} + normalize_group_options(opts, args) + @groups.concat args + + if opts["optional"] + optional_groups = args - @optional_groups + @optional_groups.concat optional_groups + end + yield ensure args.each { @groups.pop } @@ -184,9 +190,7 @@ module Bundler end def method_missing(name, *args) - location = caller[0].split(':')[0..1].join(':') - raise GemfileError, "Undefined local variable or method `#{name}' for Gemfile\n" \ - " from #{location}" + raise GemfileError, "Undefined local variable or method `#{name}' for Gemfile" end private @@ -224,7 +228,7 @@ module Bundler end def valid_keys - @valid_keys ||= %w(group groups git path name branch ref tag require submodules platform platforms type source) + @valid_keys ||= %w(group groups git path glob name branch ref tag require submodules platform platforms type source) end def normalize_options(name, version, opts) @@ -238,19 +242,7 @@ module Bundler normalize_hash(opts) git_names = @git_sources.keys.map(&:to_s) - - invalid_keys = opts.keys - (valid_keys + git_names) - if invalid_keys.any? - message = "You passed #{invalid_keys.map{|k| ':'+k }.join(", ")} " - message << if invalid_keys.size > 1 - "as options for gem '#{name}', but they are invalid." - else - "as an option for gem '#{name}', but it is invalid." - end - - message << " Valid options are: #{valid_keys.join(", ")}" - raise InvalidOption, message - end + validate_keys("gem '#{name}'", opts, valid_keys + git_names) groups = @groups.dup opts["group"] = opts.delete("groups") || opts["group"] @@ -295,6 +287,30 @@ module Bundler opts["group"] = groups end + def normalize_group_options(opts, groups) + normalize_hash(opts) + + groups = groups.map {|group| ":#{group}" }.join(", ") + validate_keys("group #{groups}", opts, %w(optional)) + + opts["optional"] ||= false + end + + def validate_keys(command, opts, valid_keys) + invalid_keys = opts.keys - valid_keys + if invalid_keys.any? + message = "You passed #{invalid_keys.map{|k| ':'+k }.join(", ")} " + message << if invalid_keys.size > 1 + "as options for #{command}, but they are invalid." + else + "as an option for #{command}, but it is invalid." + end + + message << " Valid options are: #{valid_keys.join(", ")}" + raise InvalidOption, message + end + end + def normalize_source(source) case source when :gemcutter, :rubygems, :rubyforge @@ -327,5 +343,107 @@ module Bundler end end + class DSLError < GemfileError + # @return [String] the description that should be presented to the user. + # + attr_reader :description + + # @return [String] the path of the dsl file that raised the exception. + # + attr_reader :dsl_path + + # @return [Exception] the backtrace of the exception raised by the + # evaluation of the dsl file. + # + attr_reader :backtrace + + # @param [Exception] backtrace @see backtrace + # @param [String] dsl_path @see dsl_path + # + def initialize(description, dsl_path, backtrace, contents = nil) + @status_code = $!.respond_to?(:status_code) && $!.status_code + + @description = description + @dsl_path = dsl_path + @backtrace = backtrace + @contents = contents + end + + def status_code + @status_code || super + end + + # @return [String] the contents of the DSL that cause the exception to + # be raised. + # + def contents + @contents ||= begin + dsl_path && File.exist?(dsl_path) && File.read(dsl_path) + end + end + + # The message of the exception reports the content of podspec for the + # line that generated the original exception. + # + # @example Output + # + # Invalid podspec at `RestKit.podspec` - undefined method + # `exclude_header_search_paths=' for #<Pod::Specification for + # `RestKit/Network (0.9.3)`> + # + # from spec-repos/master/RestKit/0.9.3/RestKit.podspec:36 + # ------------------------------------------- + # # because it would break: #import <CoreData/CoreData.h> + # > ns.exclude_header_search_paths = 'Code/RestKit.h' + # end + # ------------------------------------------- + # + # @return [String] the message of the exception. + # + def message + @message ||= begin + trace_line, description = parse_line_number_from_description + + m = "\n[!] " + m << description + m << ". Bundler cannot continue.\n" + + return m unless backtrace && dsl_path && contents + + trace_line = backtrace.find { |l| l.include?(dsl_path.to_s) } || trace_line + return m unless trace_line + line_numer = trace_line.split(':')[1].to_i - 1 + return m unless line_numer + + lines = contents.lines.to_a + indent = ' # ' + indicator = indent.gsub('#', '>') + first_line = (line_numer.zero?) + last_line = (line_numer == (lines.count - 1)) + + m << "\n" + m << "#{indent}from #{trace_line.gsub(/:in.*$/, '')}\n" + m << "#{indent}-------------------------------------------\n" + m << "#{indent}#{ lines[line_numer - 1] }" unless first_line + m << "#{indicator}#{ lines[line_numer] }" + m << "#{indent}#{ lines[line_numer + 1] }" unless last_line + m << "\n" unless m.end_with?("\n") + m << "#{indent}-------------------------------------------\n" + end + end + + private + + def parse_line_number_from_description + description = self.description + if dsl_path && description =~ /((#{Regexp.quote File.expand_path(dsl_path)}|#{Regexp.quote dsl_path.to_s}):\d+)/ + trace_line = Regexp.last_match[1] + description = description.sub(/#{Regexp.quote trace_line}:\s*/, '').sub("\n", ' - ') + end + [trace_line, description] + end + end + end + end diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb index 71d5e115..31365e5b 100644 --- a/lib/bundler/endpoint_specification.rb +++ b/lib/bundler/endpoint_specification.rb @@ -4,7 +4,7 @@ module Bundler include MatchPlatform attr_reader :name, :version, :platform, :dependencies - attr_accessor :source, :source_uri + attr_accessor :source, :remote def initialize(name, version, platform, dependencies) @name = name diff --git a/lib/bundler/fetcher.rb b/lib/bundler/fetcher.rb index 46ac8e44..42c6eb67 100644 --- a/lib/bundler/fetcher.rb +++ b/lib/bundler/fetcher.rb @@ -1,11 +1,15 @@ require 'bundler/vendored_persistent' -require 'securerandom' require 'cgi' +require 'securerandom' module Bundler # Handles all the fetching with the rubygems server class Fetcher + autoload :Downloader, 'bundler/fetcher/downloader' + autoload :Dependency, 'bundler/fetcher/dependency' + autoload :Index, 'bundler/fetcher/index' + # This error is raised when it looks like the network is down class NetworkDownError < HTTPError; end # This error is raised if the API returns a 413 (only printed in verbose) @@ -52,95 +56,21 @@ module Bundler class << self attr_accessor :disable_endpoint, :api_timeout, :redirect_limit, :max_retries - - def download_gem_from_uri(spec, uri) - spec.fetch_platform - - download_path = Bundler.requires_sudo? ? Bundler.tmp(spec.full_name) : Bundler.rubygems.gem_dir - gem_path = "#{Bundler.rubygems.gem_dir}/cache/#{spec.full_name}.gem" - - FileUtils.mkdir_p("#{download_path}/cache") - Bundler.rubygems.download_gem(spec, uri, download_path) - - if Bundler.requires_sudo? - Bundler.mkdir_p "#{Bundler.rubygems.gem_dir}/cache" - Bundler.sudo "mv #{Bundler.tmp(spec.full_name)}/cache/#{spec.full_name}.gem #{gem_path}" - end - - gem_path - end - - def user_agent - @user_agent ||= begin - ruby = Bundler.ruby_version - - agent = "bundler/#{Bundler::VERSION}" - agent << " rubygems/#{Gem::VERSION}" - agent << " ruby/#{ruby.version}" - agent << " (#{ruby.host})" - agent << " command/#{ARGV.first}" - - if ruby.engine != "ruby" - # engine_version raises on unknown engines - engine_version = ruby.engine_version rescue "???" - agent << " #{ruby.engine}/#{engine_version}" - end - - agent << " options/#{Bundler.settings.all.join(",")}" - - # add a random ID so we can consolidate runs server-side - agent << " " << SecureRandom.hex(8) - - # add any user agent strings set in the config - extra_ua = Bundler.settings[:user_agent] - agent << " " << extra_ua if extra_ua - - agent - end - end - end - def initialize(remote_uri) - @redirect_limit = 5 # How many redirects to allow in one request - @api_timeout = 10 # How long to wait for each API call - @max_retries = 3 # How many retries for the API call + self.redirect_limit = Bundler.settings[:redirect] # How many redirects to allow in one request + self.api_timeout = Bundler.settings[:timeout] # How long to wait for each API call + self.max_retries = Bundler.settings[:retry] # How many retries for the API call - @anonymizable_uri = configured_uri_for(remote_uri) + def initialize(remote) + @remote = remote Socket.do_not_reverse_lookup = true connection # create persistent connection end - def connection - @connection ||= begin - needs_ssl = remote_uri.scheme == "https" || - Bundler.settings[:ssl_verify_mode] || - Bundler.settings[:ssl_client_cert] - raise SSLError if needs_ssl && !defined?(OpenSSL::SSL) - - con = Net::HTTP::Persistent.new 'bundler', :ENV - - if remote_uri.scheme == "https" - con.verify_mode = (Bundler.settings[:ssl_verify_mode] || - OpenSSL::SSL::VERIFY_PEER) - con.cert_store = bundler_cert_store - end - - if Bundler.settings[:ssl_client_cert] - pem = File.read(Bundler.settings[:ssl_client_cert]) - con.cert = OpenSSL::X509::Certificate.new(pem) - con.key = OpenSSL::PKey::RSA.new(pem) - end - - con.read_timeout = @api_timeout - con.override_headers["User-Agent"] = self.class.user_agent - con - end - end - def uri - @anonymizable_uri.without_credentials + @remote.anonymized_uri end # fetch a gem specification @@ -154,37 +84,26 @@ module Bundler elsif cached_spec_path = gemspec_cached_path(spec_file_name) Bundler.load_gemspec(cached_spec_path) else - Bundler.load_marshal Gem.inflate(fetch(uri)) + Bundler.load_marshal Gem.inflate(downloader.fetch uri) end rescue MarshalError raise HTTPError, "Gemspec #{spec} contained invalid data.\n" \ "Your network or your gem server is probably having issues right now." end - # cached gem specification path, if one exists - def gemspec_cached_path spec_file_name - paths = Bundler.rubygems.spec_cache_dirs.map { |dir| File.join(dir, spec_file_name) } - paths = paths.select {|path| File.file? path } - paths.first - end - # return the specs in the bundler format as an index def specs(gem_names, source) old = Bundler.rubygems.sources - index = Index.new - - if gem_names && use_api - specs = fetch_remote_specs(gem_names) - end - - if specs.nil? - # API errors mean we should treat this as a non-API source - @use_api = false + index = Bundler::Index.new - specs = Bundler::Retry.new("source fetch", AUTH_ERRORS).attempts do - fetch_all_remote_specs + specs = {} + fetchers.dup.each do |f| + unless f.api_fetcher? && !gem_names + break if specs = f.specs(gem_names) end + fetchers.delete(f) end + @use_api = false if fetchers.none?(&:api_fetcher?) specs[remote_uri].each do |name, version, platform, dependencies| next if name == 'bundler' @@ -195,189 +114,111 @@ module Bundler spec = RemoteSpecification.new(name, version, platform, self) end spec.source = source - spec.source_uri = @anonymizable_uri + spec.remote = @remote index << spec end index - rescue CertificateFailureError => e + rescue CertificateFailureError Bundler.ui.info "" if gem_names && use_api # newline after dots - raise e + raise ensure Bundler.rubygems.sources = old end - # fetch index - def fetch_remote_specs(gem_names, full_dependency_list = [], last_spec_list = []) - query_list = gem_names - full_dependency_list - - # only display the message on the first run - if Bundler.ui.debug? - Bundler.ui.debug "Query List: #{query_list.inspect}" - else - Bundler.ui.info ".", false - end - - return {remote_uri => last_spec_list} if query_list.empty? - - remote_specs = Bundler::Retry.new("dependency api", AUTH_ERRORS).attempts do - fetch_dependency_remote_specs(query_list) - end - - spec_list, deps_list = remote_specs - returned_gems = spec_list.map {|spec| spec.first }.uniq - fetch_remote_specs(deps_list, full_dependency_list + returned_gems, spec_list + last_spec_list) - rescue HTTPError, MarshalError, GemspecError - Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over - Bundler.ui.debug "could not fetch from the dependency API, trying the full index" - @use_api = false - return nil - end - def use_api return @use_api if defined?(@use_api) if remote_uri.scheme == "file" || Bundler::Fetcher.disable_endpoint @use_api = false - elsif fetch(dependency_api_uri) - @use_api = true + else + fetchers.reject! { |f| f.api_fetcher? && !f.api_available? } + @use_api = fetchers.any?(&:api_fetcher?) end - rescue NetworkDownError => e - raise HTTPError, e.message - rescue AuthenticationRequiredError - # We got a 401 from the server. Don't fall back to the full index, just fail. - raise - rescue HTTPError - @use_api = false end - def inspect - "#<#{self.class}:0x#{object_id} uri=#{uri}>" - end + def user_agent + @user_agent ||= begin + ruby = Bundler.ruby_version - private + agent = "bundler/#{Bundler::VERSION}" + agent << " rubygems/#{Gem::VERSION}" + agent << " ruby/#{ruby.version}" + agent << " (#{ruby.host})" + agent << " command/#{ARGV.first}" - HTTP_ERRORS = [ - Timeout::Error, EOFError, SocketError, Errno::ENETDOWN, - Errno::EINVAL, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EAGAIN, - Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, - Net::HTTP::Persistent::Error - ] + if ruby.engine != "ruby" + # engine_version raises on unknown engines + engine_version = ruby.engine_version rescue "???" + agent << " #{ruby.engine}/#{engine_version}" + end - def fetch(uri, counter = 0) - raise HTTPError, "Too many redirects" if counter >= @redirect_limit + agent << " options/#{Bundler.settings.all.join(",")}" - response = request(uri) - Bundler.ui.debug("HTTP #{response.code} #{response.message}") + # add a random ID so we can consolidate runs server-side + agent << " " << SecureRandom.hex(8) - case response - when Net::HTTPRedirection - new_uri = URI.parse(response["location"]) - if new_uri.host == uri.host - new_uri.user = uri.user - new_uri.password = uri.password - end - fetch(new_uri, counter + 1) - when Net::HTTPSuccess - response.body - when Net::HTTPRequestEntityTooLarge - raise FallbackError, response.body - when Net::HTTPUnauthorized - raise AuthenticationRequiredError, remote_uri.host - else - raise HTTPError, "#{response.class}: #{response.body}" + # add any user agent strings set in the config + extra_ua = Bundler.settings[:user_agent] + agent << " " << extra_ua if extra_ua + + agent end end - def request(uri) - Bundler.ui.debug "HTTP GET #{uri}" - req = Net::HTTP::Get.new uri.request_uri - if uri.user - user = CGI.unescape(uri.user) - password = uri.password ? CGI.unescape(uri.password) : nil - req.basic_auth(user, password) - end - connection.request(uri, req) - rescue OpenSSL::SSL::SSLError - raise CertificateFailureError.new(uri) - rescue *HTTP_ERRORS => e - Bundler.ui.trace e - case e.message - when /host down:/, /getaddrinfo: nodename nor servname provided/ - raise NetworkDownError, "Could not reach host #{uri.host}. Check your network " \ - "connection and try again." - else - raise HTTPError, "Network error while fetching #{uri}" - end + def fetchers + @fetchers ||= FETCHERS.map { |f| f.new(downloader, remote_uri, fetch_uri, uri) } end - def dependency_api_uri(gem_names = []) - uri = fetch_uri + "api/v1/dependencies" - uri.query = "gems=#{URI.encode(gem_names.join(","))}" if gem_names.any? - uri + def inspect + "#<#{self.class}:0x#{object_id} uri=#{uri}>" end - # fetch from Gemcutter Dependency Endpoint API - def fetch_dependency_remote_specs(gem_names) - Bundler.ui.debug "Query Gemcutter Dependency Endpoint API: #{gem_names.join(',')}" - gem_list = [] - deps_list = [] + private - gem_names.each_slice(Source::Rubygems::API_REQUEST_SIZE) do |names| - marshalled_deps = fetch dependency_api_uri(names) - gem_list += Bundler.load_marshal(marshalled_deps) - end + FETCHERS = [Dependency, Index] - spec_list = gem_list.map do |s| - dependencies = s[:dependencies].map do |name, requirement| - dep = well_formed_dependency(name, requirement.split(", ")) - deps_list << dep.name - dep - end + def connection + @connection ||= begin + needs_ssl = remote_uri.scheme == "https" || + Bundler.settings[:ssl_verify_mode] || + Bundler.settings[:ssl_client_cert] + raise SSLError if needs_ssl && !defined?(OpenSSL::SSL) - [s[:name], Gem::Version.new(s[:number]), s[:platform], dependencies] - end + con = Net::HTTP::Persistent.new 'bundler', :ENV - [spec_list, deps_list.uniq] - end + if remote_uri.scheme == "https" + con.verify_mode = (Bundler.settings[:ssl_verify_mode] || + OpenSSL::SSL::VERIFY_PEER) + con.cert_store = bundler_cert_store + end - # fetch from modern index: specs.4.8.gz - def fetch_all_remote_specs - old_sources = Bundler.rubygems.sources - Bundler.rubygems.sources = [remote_uri.to_s] - Bundler.rubygems.fetch_all_remote_specs - rescue Gem::RemoteFetcher::FetchError, OpenSSL::SSL::SSLError => e - case e.message - when /certificate verify failed/ - raise CertificateFailureError.new(uri) - when /401/ - raise AuthenticationRequiredError, remote_uri - when /403/ - if remote_uri.userinfo - raise BadAuthenticationError, remote_uri - else - raise AuthenticationRequiredError, remote_uri + if Bundler.settings[:ssl_client_cert] + pem = File.read(Bundler.settings[:ssl_client_cert]) + con.cert = OpenSSL::X509::Certificate.new(pem) + con.key = OpenSSL::PKey::RSA.new(pem) end - else - Bundler.ui.trace e - raise HTTPError, "Could not fetch specs from #{uri}" + + con.read_timeout = Fetcher.api_timeout + con.override_headers["User-Agent"] = user_agent + con end - ensure - Bundler.rubygems.sources = old_sources end - def well_formed_dependency(name, *requirements) - Gem::Dependency.new(name, *requirements) - rescue ArgumentError => e - illformed = 'Ill-formed requirement ["#<YAML::Syck::DefaultKey' - raise e unless e.message.include?(illformed) - puts # we shouldn't print the error message on the "fetching info" status line - raise GemspecError, - "Unfortunately, the gem #{s[:name]} (#{s[:number]}) has an invalid " \ - "gemspec. \nPlease ask the gem author to yank the bad version to fix " \ - "this issue. For more information, see http://bit.ly/syck-defaultkey." + # cached gem specification path, if one exists + def gemspec_cached_path spec_file_name + paths = Bundler.rubygems.spec_cache_dirs.map { |dir| File.join(dir, spec_file_name) } + paths = paths.select {|path| File.file? path } + paths.first end + HTTP_ERRORS = [ + Timeout::Error, EOFError, SocketError, Errno::ENETDOWN, + Errno::EINVAL, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EAGAIN, + Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, + Net::HTTP::Persistent::Error + ] + def bundler_cert_store store = OpenSSL::X509::Store.new if Bundler.settings[:ssl_ca_cert] @@ -396,12 +237,6 @@ module Bundler private - def configured_uri_for(uri) - uri = Bundler::Source.mirror_for(uri) - config_auth = Bundler.settings[uri.to_s] || Bundler.settings[uri.host] - AnonymizableURI.new(uri, config_auth) - end - def fetch_uri @fetch_uri ||= begin if remote_uri.host == "rubygems.org" @@ -415,7 +250,12 @@ module Bundler end def remote_uri - @anonymizable_uri.original_uri + @remote.uri + end + + def downloader + @downloader ||= Downloader.new(connection, self.class.redirect_limit) end + end end diff --git a/lib/bundler/fetcher/base.rb b/lib/bundler/fetcher/base.rb new file mode 100644 index 00000000..31d6f8a0 --- /dev/null +++ b/lib/bundler/fetcher/base.rb @@ -0,0 +1,27 @@ +module Bundler + class Fetcher + class Base + attr_reader :downloader + attr_reader :remote_uri + attr_reader :fetch_uri + attr_reader :display_uri + + def initialize(downloader, remote_uri, fetch_uri, display_uri) + raise 'Abstract class' if self.class == Base + @downloader = downloader + @remote_uri = remote_uri + @fetch_uri = fetch_uri + @display_uri = display_uri + end + + def api_available? + api_fetcher? + end + + def api_fetcher? + false + end + + end + end +end diff --git a/lib/bundler/fetcher/dependency.rb b/lib/bundler/fetcher/dependency.rb new file mode 100644 index 00000000..ca36dcb5 --- /dev/null +++ b/lib/bundler/fetcher/dependency.rb @@ -0,0 +1,88 @@ +require 'bundler/fetcher/base' + +module Bundler + class Fetcher + class Dependency < Base + def api_available? + downloader.fetch(dependency_api_uri) + rescue NetworkDownError => e + raise HTTPError, e.message + rescue AuthenticationRequiredError + # We got a 401 from the server. Just fail. + raise + rescue HTTPError + end + + def api_fetcher? + true + end + + def specs(gem_names, full_dependency_list = [], last_spec_list = []) + query_list = gem_names - full_dependency_list + + # only display the message on the first run + if Bundler.ui.debug? + Bundler.ui.debug "Query List: #{query_list.inspect}" + else + Bundler.ui.info ".", false + end + + return {remote_uri => last_spec_list} if query_list.empty? + + remote_specs = Bundler::Retry.new("dependency api", AUTH_ERRORS).attempts do + dependency_specs(query_list) + end + + spec_list, deps_list = remote_specs + returned_gems = spec_list.map(&:first).uniq + specs(deps_list, full_dependency_list + returned_gems, spec_list + last_spec_list) + rescue HTTPError, MarshalError, GemspecError + Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over + Bundler.ui.debug "could not fetch from the dependency API, trying the full index" + return nil + end + + def dependency_specs(gem_names) + Bundler.ui.debug "Query Gemcutter Dependency Endpoint API: #{gem_names.join(',')}" + gem_list = [] + deps_list = [] + + gem_names.each_slice(Source::Rubygems::API_REQUEST_SIZE) do |names| + marshalled_deps = downloader.fetch dependency_api_uri(names) + gem_list += Bundler.load_marshal(marshalled_deps) + end + + spec_list = gem_list.map do |s| + dependencies = s[:dependencies].map do |name, requirement| + dep = well_formed_dependency(name, requirement.split(", ")) + deps_list << dep.name + dep + end + + [s[:name], Gem::Version.new(s[:number]), s[:platform], dependencies] + end + + [spec_list, deps_list.uniq] + end + + def dependency_api_uri(gem_names = []) + uri = fetch_uri + "api/v1/dependencies" + uri.query = "gems=#{URI.encode(gem_names.join(","))}" if gem_names.any? + uri + end + + def well_formed_dependency(name, *requirements) + Gem::Dependency.new(name, *requirements) + rescue ArgumentError => e + illformed = 'Ill-formed requirement ["#<YAML::Syck::DefaultKey' + raise e unless e.message.include?(illformed) + puts # we shouldn't print the error message on the "fetching info" status line + raise GemspecError, + "Unfortunately, the gem #{s[:name]} (#{s[:number]}) has an invalid " \ + "gemspec. \nPlease ask the gem author to yank the bad version to fix " \ + "this issue. For more information, see http://bit.ly/syck-defaultkey." + end + + end + end +end diff --git a/lib/bundler/fetcher/downloader.rb b/lib/bundler/fetcher/downloader.rb new file mode 100644 index 00000000..b480b4ba --- /dev/null +++ b/lib/bundler/fetcher/downloader.rb @@ -0,0 +1,61 @@ +module Bundler + class Fetcher + class Downloader + attr_reader :connection + attr_reader :redirect_limit + + def initialize(connection, redirect_limit) + @connection = connection + @redirect_limit = redirect_limit + end + + def fetch(uri, counter = 0) + raise HTTPError, "Too many redirects" if counter >= redirect_limit + + response = request(uri) + Bundler.ui.debug("HTTP #{response.code} #{response.message}") + + case response + when Net::HTTPRedirection + new_uri = URI.parse(response["location"]) + if new_uri.host == uri.host + new_uri.user = uri.user + new_uri.password = uri.password + end + fetch(new_uri, counter + 1) + when Net::HTTPSuccess + response.body + when Net::HTTPRequestEntityTooLarge + raise FallbackError, response.body + when Net::HTTPUnauthorized + raise AuthenticationRequiredError, uri.host + else + raise HTTPError, "#{response.class}: #{response.body}" + end + end + + def request(uri) + Bundler.ui.debug "HTTP GET #{uri}" + req = Net::HTTP::Get.new uri.request_uri + if uri.user + user = CGI.unescape(uri.user) + password = uri.password ? CGI.unescape(uri.password) : nil + req.basic_auth(user, password) + end + connection.request(uri, req) + rescue OpenSSL::SSL::SSLError + raise CertificateFailureError.new(uri) + rescue *HTTP_ERRORS => e + Bundler.ui.trace e + case e.message + when /host down:/, /getaddrinfo: nodename nor servname provided/ + raise NetworkDownError, "Could not reach host #{uri.host}. Check your network " \ + "connection and try again." + else + raise HTTPError, "Network error while fetching #{uri}" + end + end + + end + end +end
\ No newline at end of file diff --git a/lib/bundler/fetcher/index.rb b/lib/bundler/fetcher/index.rb new file mode 100644 index 00000000..f9bd279d --- /dev/null +++ b/lib/bundler/fetcher/index.rb @@ -0,0 +1,31 @@ +require 'bundler/fetcher/base' + +module Bundler + class Fetcher + class Index < Base + def specs(_gem_names) + old_sources = Bundler.rubygems.sources + Bundler.rubygems.sources = [remote_uri.to_s] + Bundler.rubygems.fetch_all_remote_specs + rescue Gem::RemoteFetcher::FetchError, OpenSSL::SSL::SSLError => e + case e.message + when /certificate verify failed/ + raise CertificateFailureError.new(display_uri) + when /401/ + raise AuthenticationRequiredError, remote_uri + when /403/ + if remote_uri.userinfo + raise BadAuthenticationError, remote_uri + else + raise AuthenticationRequiredError, remote_uri + end + else + Bundler.ui.trace e + raise HTTPError, "Could not fetch specs from #{display_uri}" + end + ensure + Bundler.rubygems.sources = old_sources + end + end + end +end diff --git a/lib/bundler/friendly_errors.rb b/lib/bundler/friendly_errors.rb index fcd600db..9d4389e3 100644 --- a/lib/bundler/friendly_errors.rb +++ b/lib/bundler/friendly_errors.rb @@ -5,6 +5,9 @@ require "bundler/vendored_thor" module Bundler def self.with_friendly_errors yield + rescue Bundler::Dsl::DSLError => e + Bundler.ui.error e.message + exit e.status_code rescue Bundler::BundlerError => e Bundler.ui.error e.message, :wrap => true Bundler.ui.trace e diff --git a/lib/bundler/inline.rb b/lib/bundler/inline.rb new file mode 100644 index 00000000..b38a925d --- /dev/null +++ b/lib/bundler/inline.rb @@ -0,0 +1,50 @@ +# Allows for declaring a Gemfile inline in a ruby script, optionally installing +# any gems that aren't already installed on the user's system. +# +# @note Every gem that is specified in this 'Gemfile' will be `require`d, as if +# the user had manually called `Bundler.require`. To avoid a requested gem +# being automatically required, add the `:require => false` option to the +# `gem` dependency declaration. +# +# @param install [Boolean] whether gems that aren't already installed on the +# user's system should be installed. +# Defaults to `false`. +# +# @param gemfile [Proc] a block that is evaluated as a `Gemfile`. +# +# @example Using an inline Gemfile +# +# #!/usr/bin/env ruby +# +# require 'bundler/inline' +# +# gemfile do +# source 'https://rubygems.org' +# gem 'json', require: false +# gem 'nap', require: 'rest' +# gem 'cocoapods', '~> 0.34.1' +# end +# +# puts Pod::VERSION => "0.34.4" +# +def gemfile(install = false, &gemfile) + require 'bundler' + old_root = Bundler.method(:root) + def Bundler.root + Pathname.pwd.expand_path + end + ENV['BUNDLE_GEMFILE'] ||= 'Gemfile' + + builder = Bundler::Dsl.new + builder.instance_eval(&gemfile) + definition = builder.to_definition(nil, true) + def definition.lock(file); end + definition.validate_ruby! + Bundler::Installer.install(Bundler.root, definition, :system => true) if install + runtime = Bundler::Runtime.new(nil, definition) + runtime.setup_environment + runtime.setup.require + + bundler_module = class << Bundler; self; end + bundler_module.send(:define_method, :root, old_root) +end diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb index 0654d91f..0e72f5a1 100644 --- a/lib/bundler/installer.rb +++ b/lib/bundler/installer.rb @@ -79,32 +79,36 @@ module Bundler options["local"] ? @definition.resolve_with_cache! : @definition.resolve_remotely! end + force = options["force"] + # the order that the resolver provides is significant, since # dependencies might actually affect the installation of a gem. # that said, it's a rare situation (other than rake), and parallel # installation is just SO MUCH FASTER. so we let people opt in. jobs = [Bundler.settings[:jobs].to_i-1, 1].max if jobs > 1 && can_install_in_parallel? - install_in_parallel jobs, options[:standalone] + require 'bundler/installer/parallel_installer' + install_in_parallel jobs, options[:standalone], force else - install_sequentially options[:standalone] + install_sequentially options[:standalone], force end lock unless Bundler.settings[:frozen] generate_standalone(options[:standalone]) if options[:standalone] end - def install_gem_from_spec(spec, standalone = false, worker = 0) + def install_gem_from_spec(spec, standalone = false, worker = 0, force = false) # Fetch the build settings, if there are any settings = Bundler.settings["build.#{spec.name}"] messages = nil if settings + # Build arguments are global, so this is mutexed Bundler.rubygems.with_build_args [settings] do - messages = spec.source.install(spec) + messages = spec.source.install(spec, force) end else - messages = spec.source.install(spec) + messages = spec.source.install(spec, force) end install_message, post_install_message, debug_message = *messages @@ -264,68 +268,17 @@ module Bundler end end - def install_sequentially(standalone) + def install_sequentially(standalone, force = false) specs.each do |spec| - message = install_gem_from_spec spec, standalone, 0 + message = install_gem_from_spec spec, standalone, 0, force if message Installer.post_install_messages[spec.name] = message end end end - def install_in_parallel(size, standalone) - name2spec = {} - remains = {} - enqueued = {} - specs.each do |spec| - name2spec[spec.name] = spec - remains[spec.name] = true - end - - worker_pool = Worker.new size, lambda { |name, worker_num| - spec = name2spec[name] - message = install_gem_from_spec spec, standalone, worker_num - { :name => spec.name, :post_install => message } - } - - # Keys in the remains hash represent uninstalled gems specs. - # We enqueue all gem specs that do not have any dependencies. - # Later we call this lambda again to install specs that depended on - # previously installed specifications. We continue until all specs - # are installed. - enqueue_remaining_specs = lambda do - remains.keys.each do |name| - next if enqueued[name] - spec = name2spec[name] - if ready_to_install?(spec, remains) - worker_pool.enq name - enqueued[name] = true - end - end - end - enqueue_remaining_specs.call - - until remains.empty? - message = worker_pool.deq - remains.delete message[:name] - if message[:post_install] - Installer.post_install_messages[message[:name]] = message[:post_install] - end - enqueue_remaining_specs.call - end - message - ensure - worker_pool && worker_pool.stop - end - - # We only want to install a gem spec if all its dependencies are met. - # If the dependency is no longer in the `remains` hash then it has been met. - # If a dependency is only development or is self referential it can be ignored. - def ready_to_install?(spec, remains) - spec.dependencies.none? do |dep| - next if dep.type == :development || dep.name == spec.name - remains[dep.name] - end + def install_in_parallel(size, standalone, force = false) + ParallelInstaller.call(self, specs, size, standalone, force) end def create_bundle_path diff --git a/lib/bundler/installer/parallel_installer.rb b/lib/bundler/installer/parallel_installer.rb new file mode 100644 index 00000000..9c08c83b --- /dev/null +++ b/lib/bundler/installer/parallel_installer.rb @@ -0,0 +1,117 @@ +require 'bundler/worker' + + +class ParallelInstaller + + class SpecInstallation + + attr_accessor :spec, :name, :post_install_message, :state + def initialize(spec) + @spec, @name = spec, spec.name + @state = :none + @post_install_message = "" + end + + def installed? + state == :installed + end + + def enqueued? + state == :enqueued + end + + # Only true when spec in neither installed nor already enqueued + def ready_to_enqueue? + !installed? && !enqueued? + end + + def has_post_install_message? + !post_install_message.empty? + end + + def ignorable_dependency?(dep) + dep.type == :development || dep.name == @name + end + + # Checks installed dependencies against spec's dependencies to make + # sure needed dependencies have been installed. + def dependencies_installed?(remaining_specs) + installed_specs = remaining_specs.reject(&:installed?).map(&:name) + already_installed = lambda {|dep| installed_specs.include? dep.name } + dependencies.all? {|d| already_installed[d] } + end + + # Represents only the non-development dependencies and the ones that + # are itself. + def dependencies + @dependencies ||= all_dependencies.reject {|dep| ignorable_dependency? dep } + end + + # Represents all dependencies + def all_dependencies + @spec.dependencies + end + end + + def self.call(*args) + new(*args).call + end + + # Returns max number of threads machine can handle with a min of 1 + def self.max_threads + [Bundler.settings[:jobs].to_i-1, 1].max + end + + def initialize(installer, all_specs, size, standalone, force) + @installer = installer + @size = size + @standalone = standalone + @force = force + @specs = all_specs.map { |s| SpecInstallation.new(s) } + end + + def call + enqueue_specs + process_specs until @specs.all?(&:installed?) + ensure + worker_pool && worker_pool.stop + end + + def worker_pool + @worker_pool ||= Bundler::Worker.new @size, lambda { |spec_install, worker_num| + message = @installer.install_gem_from_spec spec_install.spec, @standalone, worker_num, @force + spec_install.post_install_message = message unless message.nil? + spec_install + } + end + + # Dequeue a spec and save its post-install message and then enqueue the + # remaining specs. + # Some specs might've had to wait til this spec was installed to be + # processed so the call to `enqueue_specs` is important after every + # dequeue. + def process_specs + spec = worker_pool.deq + spec.state = :installed + collect_post_install_message spec if spec.has_post_install_message? + enqueue_specs + end + + def collect_post_install_message(spec) + Bundler::Installer.post_install_messages[spec.name] = spec.post_install_message + end + + # Keys in the remains hash represent uninstalled gems specs. + # We enqueue all gem specs that do not have any dependencies. + # Later we call this lambda again to install specs that depended on + # previously installed specifications. We continue until all specs + # are installed. + def enqueue_specs + @specs.select(&:ready_to_enqueue?).each do |spec| + if spec.dependencies_installed? @specs + worker_pool.enq spec + spec.state = :enqueued + end + end + end +end diff --git a/lib/bundler/lazy_specification.rb b/lib/bundler/lazy_specification.rb index 8fc0e968..1119bdee 100644 --- a/lib/bundler/lazy_specification.rb +++ b/lib/bundler/lazy_specification.rb @@ -7,7 +7,7 @@ module Bundler include MatchPlatform attr_reader :name, :version, :dependencies, :platform - attr_accessor :source, :source_uri + attr_accessor :source, :remote def initialize(name, version, platform, source = nil) @name = name diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb index 70305089..dcd966b9 100644 --- a/lib/bundler/lockfile_parser.rb +++ b/lib/bundler/lockfile_parser.rb @@ -12,8 +12,9 @@ require "strscan" module Bundler class LockfileParser - attr_reader :sources, :dependencies, :specs, :platforms + attr_reader :sources, :dependencies, :specs, :platforms, :bundler_version + BUNDLED = "BUNDLED WITH" DEPENDENCIES = "DEPENDENCIES" PLATFORMS = "PLATFORMS" GIT = "GIT" @@ -41,18 +42,32 @@ module Bundler @state = :dependency elsif line == PLATFORMS @state = :platform + elsif line == BUNDLED + @state = :bundled_with else send("parse_#{@state}", line) end end @sources << @rubygems_aggregate @specs = @specs.values + warn_for_outdated_bundler_version rescue ArgumentError => e Bundler.ui.debug(e) raise LockfileError, "Your lockfile is unreadable. Run `rm Gemfile.lock` " \ "and then `bundle install` to generate a new lockfile." end + def warn_for_outdated_bundler_version + return unless bundler_version + prerelease_text = bundler_version.prerelease? ? " --pre" : "" + if Gem::Version.new(Bundler::VERSION) < Gem::Version.new(bundler_version) + Bundler.ui.warn "Warning: the running version of Bundler is older " \ + "than the version that created the lockfile. We suggest you " \ + "upgrade to the latest version of Bundler by running `gem " \ + "install bundler#{prerelease_text}`.\n" + end + end + private TYPES = { @@ -157,5 +172,12 @@ module Bundler end end + def parse_bundled_with(line) + line = line.strip + if Gem::Version.correct?(line) + @bundler_version = Gem::Version.create(line) + end + end + end end diff --git a/lib/bundler/remote_specification.rb b/lib/bundler/remote_specification.rb index d08bd9d3..456686dc 100644 --- a/lib/bundler/remote_specification.rb +++ b/lib/bundler/remote_specification.rb @@ -10,7 +10,7 @@ module Bundler include MatchPlatform attr_reader :name, :version, :platform - attr_accessor :source, :source_uri + attr_accessor :source, :remote def initialize(name, version, platform, spec_fetcher) @name = name @@ -54,4 +54,24 @@ module Bundler end end end + + class StubSpecification < RemoteSpecification + def self.from_stub(stub) + spec = new(stub.name, stub.version, stub.platform, nil) + spec.stub = stub + spec + end + + attr_accessor :stub + + def to_yaml + _remote_specification.to_yaml + end + + private + + def _remote_specification + stub.to_spec + end + end end diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 5ec46acc..511a7413 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -323,7 +323,8 @@ module Bundler message << "Source does not contain any versions of '#{requirement}'" end else - message = "Could not find gem '#{requirement}' in any of the gem sources listed in your Gemfile or installed on this machine." + message = "Could not find gem '#{requirement}' in any of the gem sources " \ + "listed in your Gemfile or available on this machine." end raise GemNotFound, message end diff --git a/lib/bundler/retry.rb b/lib/bundler/retry.rb index ea4d7577..585888f8 100644 --- a/lib/bundler/retry.rb +++ b/lib/bundler/retry.rb @@ -1,23 +1,24 @@ module Bundler # General purpose class for retrying code that may fail class Retry - DEFAULT_ATTEMPTS = 2 attr_accessor :name, :total_runs, :current_run class << self - attr_accessor :attempts + def default_attempts + default_retries + 1 + end + alias_method :attempts, :default_attempts + + def default_retries + Bundler.settings[:retry] + end end - def initialize(name, exceptions = nil, attempts = nil) + def initialize(name, exceptions = nil, retries = self.class.default_retries) @name = name - attempts ||= default_attempts + @retries = retries @exceptions = Array(exceptions) || [] - @total_runs = attempts.next # will run once, then upto attempts.times - end - - def default_attempts - return Integer(self.class.attempts) if self.class.attempts - DEFAULT_ATTEMPTS + @total_runs = @retries + 1 # will run once, then upto attempts.times end def attempt(&block) diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb index 1a9acb54..bf1b3cb3 100644 --- a/lib/bundler/rubygems_ext.rb +++ b/lib/bundler/rubygems_ext.rb @@ -13,7 +13,7 @@ module Gem @loaded_stacks = Hash.new { |h,k| h[k] = [] } class Specification - attr_accessor :source_uri, :location, :relative_loaded_from + attr_accessor :remote, :location, :relative_loaded_from remove_method :source if instance_methods(false).include?(:source) attr_accessor :source diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb index 01076630..047aa891 100644 --- a/lib/bundler/rubygems_integration.rb +++ b/lib/bundler/rubygems_integration.rb @@ -131,6 +131,19 @@ module Bundler yield end + def loaded_gem_paths + # RubyGems 2.2+ can put binary extension into dedicated folders, + # therefore use RubyGems facilities to obtain their load paths. + if Gem::Specification.method_defined? :full_require_paths + loaded_gem_paths = Gem.loaded_specs.map {|n, s| s.full_require_paths} + loaded_gem_paths.flatten + else + $LOAD_PATH.select do |p| + Bundler.rubygems.gem_path.any?{|gp| p =~ /^#{Regexp.escape(gp)}/ } + end + end + end + def ui=(obj) Gem::DefaultUserInteraction.ui = obj end @@ -204,7 +217,7 @@ module Bundler end def download_gem(spec, uri, path) - uri = Bundler::Source.mirror_for(uri) + uri = Bundler.settings.mirror_for(uri) fetcher = Gem::RemoteFetcher.new(configuration[:http_proxy]) fetcher.download(spec, uri, path) end @@ -538,7 +551,7 @@ module Bundler def download_gem(spec, uri, path) require 'resolv' - uri = Bundler::Source.mirror_for(uri) + uri = Bundler.settings.mirror_for(uri) proxy, dns = configuration[:http_proxy], Resolv::DNS.new fetcher = Gem::RemoteFetcher.new(proxy, dns) fetcher.download(spec, uri, path) @@ -561,12 +574,20 @@ module Bundler end end + # RubyGems 2.1.0 class MoreFuture < Future def initialize super backport_ext_builder_monitor end + def all_specs + require 'bundler/remote_specification' + Gem::Specification.stubs.map do |stub| + StubSpecification.from_stub(stub) + end + end + def backport_ext_builder_monitor require 'rubygems/ext' @@ -586,10 +607,16 @@ module Bundler Gem::Ext::Builder::CHDIR_MONITOR end - def find_name(name) - Gem::Specification.stubs.find_all do |spec| - spec.name == name - end.map(&:to_spec) + if Gem::Specification.respond_to?(:stubs_for) + def find_name(name) + Gem::Specification.stubs_for(name).map(&:to_spec) + end + else + def find_name(name) + Gem::Specification.stubs.find_all do |spec| + spec.name == name + end.map(&:to_spec) + end end end diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index a29e7104..7a58c8af 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -2,7 +2,9 @@ require 'uri' module Bundler class Settings - BOOL_KEYS = %w(frozen cache_all no_prune disable_local_branch_check gem.mit gem.coc).freeze + BOOL_KEYS = %w(frozen cache_all no_prune disable_local_branch_check ignore_messages gem.mit gem.coc).freeze + NUMBER_KEYS = %w(retry timeout redirect).freeze + DEFAULT_CONFIG = {:retry => 3, :timeout => 10, :redirect => 5} def initialize(root = nil) @root = root @@ -12,10 +14,13 @@ module Bundler def [](name) key = key_for(name) - value = (@local_config[key] || ENV[key] || @global_config[key]) + value = (@local_config[key] || ENV[key] || @global_config[key] || DEFAULT_CONFIG[name]) - if !value.nil? && is_bool(name) + case + when !value.nil? && is_bool(name) to_bool(value) + when !value.nil? && is_num(name) + value.to_i else value end @@ -56,6 +61,18 @@ module Bundler repos end + def mirror_for(uri) + uri = URI(uri.to_s) unless uri.is_a?(URI) + + # Settings keys are all downcased + normalized_key = normalize_uri(uri.to_s.downcase) + gem_mirrors[normalized_key] || uri + end + + def credentials_for(uri) + self[uri.to_s] || self[uri.host] + end + def gem_mirrors all.inject({}) do |h, k| if k =~ /^mirror\./ @@ -72,6 +89,7 @@ module Bundler locations[:local] = @local_config[key] if @local_config.key?(key) locations[:env] = ENV[key] if ENV[key] locations[:global] = @global_config[key] if @global_config.key?(key) + locations[:default] = DEFAULT_CONFIG[key] if DEFAULT_CONFIG.key?(key) locations end @@ -96,11 +114,19 @@ module Bundler end def without=(array) - self[:without] = (array.empty? ? nil : array.join(":")) if array + set_array(:without, array) + end + + def with=(array) + set_array(:with, array) end def without - self[:without] ? self[:without].split(":").map { |w| w.to_sym } : [] + get_array(:without) + end + + def with + get_array(:with) end # @local_config["BUNDLE_PATH"] should be prioritized over ENV["BUNDLE_PATH"] @@ -141,14 +167,38 @@ module Bundler "BUNDLE_#{key}" end - def is_bool(key) - BOOL_KEYS.include?(key.to_s) + def parent_setting_for(name) + split_specfic_setting_for(name)[0] + end + + def specfic_gem_for(name) + split_specfic_setting_for(name)[1] + end + + def split_specfic_setting_for(name) + name.split(".") + end + + def is_bool(name) + BOOL_KEYS.include?(name.to_s) || BOOL_KEYS.include?(parent_setting_for(name.to_s)) end def to_bool(value) !(value.nil? || value == '' || value =~ /^(false|f|no|n|0)$/i || value == false) end + def is_num(value) + NUMBER_KEYS.include?(value.to_s) + end + + def get_array(key) + self[key] ? self[key].split(":").map { |w| w.to_sym } : [] + end + + def set_array(key, array) + self[key] = (array.empty? ? nil : array.join(":")) if array + end + def set_key(key, value, hash, file) key = key_for(key) diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb index cd4ad717..49544e9b 100644 --- a/lib/bundler/shared_helpers.rb +++ b/lib/bundler/shared_helpers.rb @@ -135,10 +135,13 @@ module Bundler # handle 1.9 where system gems are always on the load path if defined?(::Gem) me = File.expand_path("../../", __FILE__) + me = /^#{Regexp.escape(me)}/ + + loaded_gem_paths = Bundler.rubygems.loaded_gem_paths + $LOAD_PATH.reject! do |p| - next if File.expand_path(p) =~ /^#{Regexp.escape(me)}/ - p != File.dirname(__FILE__) && - Bundler.rubygems.gem_path.any?{|gp| p =~ /^#{Regexp.escape(gp)}/ } + next if File.expand_path(p) =~ me + loaded_gem_paths.delete(p) end $LOAD_PATH.uniq! end diff --git a/lib/bundler/source.rb b/lib/bundler/source.rb index b544451d..869931a1 100644 --- a/lib/bundler/source.rb +++ b/lib/bundler/source.rb @@ -4,16 +4,6 @@ module Bundler autoload :Path, 'bundler/source/path' autoload :Git, 'bundler/source/git' - def self.mirror_for(uri) - uri = URI(uri.to_s) unless uri.is_a?(URI) - - # Settings keys are all downcased - mirrors = Bundler.settings.gem_mirrors - normalized_key = URI(uri.to_s.downcase) - - mirrors[normalized_key] || uri - end - attr_accessor :dependency_names def unmet_deps diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb index 62d61508..230aa8fc 100644 --- a/lib/bundler/source/git.rb +++ b/lib/bundler/source/git.rb @@ -159,9 +159,9 @@ module Bundler local_specs end - def install(spec) + def install(spec, force = false) debug = nil - if requires_checkout? && !@copied + if requires_checkout? && !@copied && !force debug = " * Checking out revision: #{ref}" git_proxy.copy_to(install_path, submodules) serialize_gemspecs_in(install_path) diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb index 2b53bc78..862fbcb9 100644 --- a/lib/bundler/source/path.rb +++ b/lib/bundler/source/path.rb @@ -69,7 +69,7 @@ module Bundler File.basename(expanded_path.to_s) end - def install(spec) + def install(spec, force = false) generate_bin(spec, :disable_extensions) ["Using #{version_message(spec)} from #{to_s}", nil] end diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index 925bec66..684c6c20 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -5,6 +5,8 @@ require 'rubygems/spec_fetcher' module Bundler class Source class Rubygems < Source + autoload :Remote, "bundler/source/rubygems/remote" + # Use the API when installing less than X gems API_REQUEST_LIMIT = 500 # Ask for X gems per API request @@ -83,15 +85,15 @@ module Bundler end end - def install(spec) - return ["Using #{version_message(spec)}", nil] if installed_specs[spec].any? + def install(spec, force = false) + return ["Using #{version_message(spec)}", nil] if installed_specs[spec].any? && !force # Download the gem to get the spec, because some specs that are returned # by rubygems.org are broken and wrong. - if spec.source_uri + if spec.remote # Check for this spec from other sources - uris = [spec.source_uri.without_credentials] - uris += source_uris_for_spec(spec) + uris = [spec.remote.anonymized_uri] + uris += remotes_for_spec(spec).map { |remote| remote.anonymized_uri } uris.uniq! Installer.ambiguous_gems << [spec.name, *uris] if uris.length > 1 @@ -198,7 +200,8 @@ module Bundler def fetchers @fetchers ||= remotes.map do |uri| - Bundler::Fetcher.new(uri) + remote = Source::Rubygems::Remote.new(uri) + Bundler::Fetcher.new(remote) end end @@ -208,11 +211,9 @@ module Bundler remotes.map(&method(:suppress_configured_credentials)) end - private - - def source_uris_for_spec(spec) + def remotes_for_spec(spec) specs.search_all(spec.name).inject([]) do |uris, s| - uris << s.source_uri.without_credentials if s.source_uri + uris << s.remote if s.remote uris end end @@ -298,7 +299,7 @@ module Bundler end def api_fetchers - fetchers.select{|f| f.use_api } + fetchers.select(&:use_api) end def remote_specs @@ -339,7 +340,7 @@ module Bundler end end until idxcount == idx.size - if api_fetchers.any? && api_fetchers.all?{|f| f.use_api } + if api_fetchers.any? # it's possible that gems from one source depend on gems from some # other source, so now we download gemspecs and iterate over those # dependencies, looking for gems we don't have info on yet. @@ -356,7 +357,7 @@ module Bundler end end - if !allow_api + unless allow_api api_fetchers.each do |f| Bundler.ui.info "Fetching source index from #{f.uri}" idx.use f.specs(nil, self) @@ -366,8 +367,22 @@ module Bundler end def fetch_gem(spec) - return false unless spec.source_uri - Fetcher.download_gem_from_uri(spec, spec.source_uri.original_uri) + return false unless spec.remote + uri = spec.remote.uri + spec.fetch_platform + + download_path = Bundler.requires_sudo? ? Bundler.tmp(spec.full_name) : Bundler.rubygems.gem_dir + gem_path = "#{Bundler.rubygems.gem_dir}/cache/#{spec.full_name}.gem" + + FileUtils.mkdir_p("#{download_path}/cache") + Bundler.rubygems.download_gem(spec, uri, download_path) + + if Bundler.requires_sudo? + Bundler.mkdir_p "#{Bundler.rubygems.gem_dir}/cache" + Bundler.sudo "mv #{Bundler.tmp(spec.full_name)}/cache/#{spec.full_name}.gem #{gem_path}" + end + + gem_path end def builtin_gem?(spec) diff --git a/lib/bundler/source/rubygems/remote.rb b/lib/bundler/source/rubygems/remote.rb new file mode 100644 index 00000000..82ce1a4f --- /dev/null +++ b/lib/bundler/source/rubygems/remote.rb @@ -0,0 +1,39 @@ +module Bundler + class Source + class Rubygems + class Remote + attr_reader :uri, + :anonymized_uri + + def initialize(uri) + uri = Bundler.settings.mirror_for(uri) + fallback_auth = Bundler.settings.credentials_for(uri) + + @uri = apply_auth(uri, fallback_auth).freeze + @anonymized_uri = remove_auth(@uri).freeze + end + + private + + def apply_auth(uri, auth) + if auth && uri.userinfo.nil? + uri = uri.dup + uri.userinfo = auth + end + + uri + end + + def remove_auth(uri) + if uri.userinfo + uri = uri.dup + uri.user = uri.password = nil + end + + uri + end + + end + end + end +end diff --git a/lib/bundler/templates/newgem/.travis.yml.tt b/lib/bundler/templates/newgem/.travis.yml.tt index 4c7eba63..d78885d0 100644 --- a/lib/bundler/templates/newgem/.travis.yml.tt +++ b/lib/bundler/templates/newgem/.travis.yml.tt @@ -1,3 +1,4 @@ language: ruby rvm: - <%= RUBY_VERSION %> +before_install: gem install bundler -v <%= Bundler::VERSION %> diff --git a/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt b/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt index c5393d14..ce9bee75 100644 --- a/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt +++ b/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt @@ -2,7 +2,7 @@ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. -We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. @@ -10,4 +10,4 @@ Project maintainers have the right and responsibility to remove, edit, or reject Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. -This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) diff --git a/lib/bundler/templates/newgem/Rakefile.tt b/lib/bundler/templates/newgem/Rakefile.tt index e0e87e59..3d3fb674 100644 --- a/lib/bundler/templates/newgem/Rakefile.tt +++ b/lib/bundler/templates/newgem/Rakefile.tt @@ -4,6 +4,8 @@ require "rake/testtask" Rake::TestTask.new(:test) do |t| t.libs << "test" + t.libs << "lib" + t.test_files = FileList['test/**/*_test.rb'] end task :default => :test diff --git a/lib/bundler/templates/newgem/test/test_newgem.rb.tt b/lib/bundler/templates/newgem/test/newgem_test.rb.tt index d50f7da2..95e33a34 100644 --- a/lib/bundler/templates/newgem/test/test_newgem.rb.tt +++ b/lib/bundler/templates/newgem/test/newgem_test.rb.tt @@ -1,6 +1,6 @@ -require 'minitest_helper' +require 'test_helper' -class Test<%= config[:constant_name] %> < Minitest::Test +class <%= config[:constant_name] %>Test < Minitest::Test def test_that_it_has_a_version_number refute_nil ::<%= config[:constant_name] %>::VERSION end diff --git a/lib/bundler/templates/newgem/test/minitest_helper.rb.tt b/lib/bundler/templates/newgem/test/test_helper.rb.tt index 49a56c18..49a56c18 100644 --- a/lib/bundler/templates/newgem/test/minitest_helper.rb.tt +++ b/lib/bundler/templates/newgem/test/test_helper.rb.tt diff --git a/man/bundle-config.ronn b/man/bundle-config.ronn index 201672eb..1a923085 100644 --- a/man/bundle-config.ronn +++ b/man/bundle-config.ronn @@ -35,6 +35,10 @@ local and global sources. Not compatible with --global or --local flag. Executing bundle with the `BUNDLE_IGNORE_CONFIG` environment variable set will cause it to ignore all configuration. +Executing `bundle config disable_multisource true` upgrades the warning about +the Gemfile containing multiple primary sources to an error. Executing `bundle +config --delete disable_multisource` downgrades this error to a warning. + ## BUILD OPTIONS You can use `bundle config` to give bundler the flags to pass to the gem @@ -108,6 +112,9 @@ learn more about their operation in [bundle install(1)][bundle-install]. * `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. +* `ignore_messages` (`BUNDLE_IGNORE_MESSAGES`): When set, no post install + messages will be printed. To silence a single gem, use dot notation like + `ignore_messages.httparty true`. In general, you should set these settings per-application by using the applicable flag to the [bundle install(1)][bundle-install] or [bundle package(1)][bundle-package] command. diff --git a/man/bundle-install.ronn b/man/bundle-install.ronn index 49099294..381b714b 100644 --- a/man/bundle-install.ronn +++ b/man/bundle-install.ronn @@ -20,6 +20,7 @@ bundle-install(1) -- Install the dependencies specified in your Gemfile [--standalone[=GROUP[ GROUP...]]] [--trust-policy=POLICY] [--without=GROUP[ GROUP...]] + [--with=GROUP[ GROUP...]] ## DESCRIPTION @@ -127,8 +128,16 @@ update process below under [CONSERVATIVE UPDATING][]. * `--without=<list>`: A space-separated list of groups referencing gems to skip during installation. + If a group is given that is in the remembered list of groups given + to --with, it is removed from that list. This is a [remembered option][REMEMBERED OPTIONS]. +* `--with=<list>`: + A space-separated list of groups referencing gems to install. If an + optional group is given it is installed. If a group is given that is + in the remembered list of groups given to --without, it is removed + from that list. This is a [remembered option][REMEMBERED OPTIONS]. + ## DEPLOYMENT MODE diff --git a/man/bundle.ronn b/man/bundle.ronn index 7b69c673..f7b9e94f 100644 --- a/man/bundle.ronn +++ b/man/bundle.ronn @@ -67,6 +67,9 @@ We divide `bundle` subcommands into primary commands and utilities. * `bundle open(1)`: Open an installed gem in the editor +* `bundle lock(1)`: + Generate a lockfile for your dependencies + * `bundle viz(1)`: Generate a visual representation of your dependencies @@ -92,7 +95,4 @@ and execute it, passing down any extra arguments to it. These commands are obsolete and should no longer be used -* `bundle lock(1)` -* `bundle unlock(1)` * `bundle cache(1)` - diff --git a/man/gemfile.5.ronn b/man/gemfile.5.ronn index 876c9273..89911695 100644 --- a/man/gemfile.5.ronn +++ b/man/gemfile.5.ronn @@ -430,11 +430,15 @@ applied to a group of gems by using block form. gem "sqlite3" end - group :development do + group :development, :optional => true do gem "wirble" gem "faker" end +In the case of the group block form the :optional option can be given +to prevent a group from being installed unless listed in the `--with` +option given to the `bundle install` command. + In the case of the `git` block form, the `:ref`, `:branch`, `:tag`, and `:submodules` options may be passed to the `git` method, and all gems in the block will inherit those options. @@ -453,10 +457,10 @@ files in your test code as you would if the project were installed as a gem; you need not manipulate the load path manually or require project files via relative paths. -The `gemspec` method supports optional `:path`, `:name`, and `:development_group` -options, which control where bundler looks for the `.gemspec`, what named -`.gemspec` it uses (if more than one is present), and which group development -dependencies are included in. +The `gemspec` method supports optional `:path`, `:glob`, `:name`, and `:development_group` +options, which control where bundler looks for the `.gemspec`, the glob it uses to look +for the gemspec (defaults to: "{,*,*/*}.gemspec"), what named `.gemspec` it uses +(if more than one is present), and which group development dependencies are included in. ## SOURCE PRIORITY diff --git a/spec/bundler/anonymizable_uri_spec.rb b/spec/bundler/anonymizable_uri_spec.rb deleted file mode 100644 index c444ea04..00000000 --- a/spec/bundler/anonymizable_uri_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'spec_helper' -require 'bundler/anonymizable_uri' - -describe Bundler::AnonymizableURI do - def auri(uri, auth = nil) - Bundler::AnonymizableURI.new(uri, auth) - end - - describe "#without_credentials" do - context "when the original URI has no credentials" do - let(:original_uri) { URI('https://rubygems.org') } - - it "returns the original URI" do - expect(auri(original_uri).without_credentials).to eq(original_uri) - end - - it "applies given credentials" do - with_auth = original_uri.dup - with_auth.userinfo = "user:pass" - expect(auri(original_uri, "user:pass").original_uri).to eq(with_auth) - end - end - - context "when the original URI has a username and password" do - let(:original_uri) { URI("https://username:password@gems.example.com") } - - it "returns the URI without username and password" do - expect(auri(original_uri).without_credentials).to eq(URI("https://gems.example.com")) - end - - it "does not apply given credentials" do - expect(auri(original_uri, "other:stuff").original_uri).to eq(original_uri) - end - end - - context "when the original URI has only a username" do - let(:original_uri) { URI("https://SeCrEt-ToKeN@gem.fury.io/me/") } - - it "returns the URI without username and password" do - expect(auri(original_uri).without_credentials).to eq(URI("https://gem.fury.io/me/")) - end - end - end -end diff --git a/spec/bundler/cli_spec.rb b/spec/bundler/cli_spec.rb index 7917726e..2ebe69d2 100644 --- a/spec/bundler/cli_spec.rb +++ b/spec/bundler/cli_spec.rb @@ -2,8 +2,6 @@ require 'spec_helper' require 'bundler/cli' describe "bundle executable" do - let(:source_uri) { "http://localgemserver.test" } - it "returns non-zero exit status when passed unrecognized options" do bundle '--invalid_argument' expect(exitstatus).to_not be_zero if exitstatus diff --git a/spec/bundler/dsl_spec.rb b/spec/bundler/dsl_spec.rb index 81313c38..5c6be9ef 100644 --- a/spec/bundler/dsl_spec.rb +++ b/spec/bundler/dsl_spec.rb @@ -15,7 +15,7 @@ describe Bundler::Dsl do expect(subject.dependencies.first.source.uri).to eq(example_uri) end - it "raises expection on invalid hostname" do + it "raises exception on invalid hostname" do expect { subject.git_source(:group){ |repo_name| "git@git.example.com:#{repo_name}.git" } }.to raise_error(Bundler::InvalidOption) @@ -69,8 +69,7 @@ describe Bundler::Dsl do expect(Bundler).to receive(:read_file).with("Gemfile"). and_return("unknown") - error_msg = "Undefined local variable or method `unknown'" \ - " for Gemfile\\s+from Gemfile:1" + error_msg = "There was an error parsing `Gemfile`: Undefined local variable or method `unknown' for Gemfile. Bundler cannot continue." expect { subject.eval_gemfile("Gemfile") }. to raise_error(Bundler::GemfileError, Regexp.new(error_msg)) end @@ -80,7 +79,7 @@ describe Bundler::Dsl do it "handles syntax errors with a useful message" do expect(Bundler).to receive(:read_file).with("Gemfile").and_return("}") expect { subject.eval_gemfile("Gemfile") }. - to raise_error(Bundler::GemfileError, /Gemfile syntax error/) + to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: (syntax error, unexpected tSTRING_DEND|(compile error - )?syntax error, unexpected '}'). Bundler cannot continue./) end end @@ -177,7 +176,7 @@ describe Bundler::Dsl do it "will raise a Bundler::GemfileError" do gemfile "gem 'foo', :path => /unquoted/string/syntax/error" expect { Bundler::Dsl.evaluate(bundled_app("Gemfile"), nil, true) }. - to raise_error(Bundler::GemfileError, /Gemfile syntax error/) + to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`:( compile error -)? unknown regexp options - trg. Bundler cannot continue./) end end @@ -185,7 +184,7 @@ describe Bundler::Dsl do it "will raise a Bundler::GemfileError" do gemfile "s = 'foo'.freeze; s.strip!" expect { Bundler::Dsl.evaluate(bundled_app("Gemfile"), nil, true) }. - to raise_error(Bundler::GemfileError, /There was an error in your Gemfile/) + to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: can't modify frozen String. Bundler cannot continue./) end end end diff --git a/spec/bundler/fetcher_spec.rb b/spec/bundler/fetcher_spec.rb index 4640f635..0d47bc5e 100644 --- a/spec/bundler/fetcher_spec.rb +++ b/spec/bundler/fetcher_spec.rb @@ -1,6 +1,9 @@ require 'spec_helper' +require 'bundler/fetcher' describe Bundler::Fetcher do + subject(:fetcher) { Bundler::Fetcher.new(double("remote", :uri => URI("https://example.com"))) } + before do allow(Bundler).to receive(:root){ Pathname.new("root") } end @@ -8,10 +11,10 @@ describe Bundler::Fetcher do describe "#user_agent" do it "builds user_agent with current ruby version and Bundler settings" do allow(Bundler.settings).to receive(:all).and_return(["foo", "bar"]) - expect(described_class.user_agent).to match(/bundler\/(\d.)/) - expect(described_class.user_agent).to match(/rubygems\/(\d.)/) - expect(described_class.user_agent).to match(/ruby\/(\d.)/) - expect(described_class.user_agent).to match(/options\/foo,bar/) + expect(fetcher.user_agent).to match(/bundler\/(\d.)/) + expect(fetcher.user_agent).to match(/rubygems\/(\d.)/) + expect(fetcher.user_agent).to match(/ruby\/(\d.)/) + expect(fetcher.user_agent).to match(/options\/foo,bar/) end end end diff --git a/spec/bundler/retry_spec.rb b/spec/bundler/retry_spec.rb index 8e35a101..aa4e5e42 100644 --- a/spec/bundler/retry_spec.rb +++ b/spec/bundler/retry_spec.rb @@ -11,17 +11,6 @@ describe Bundler::Retry do expect(attempts).to eq(1) end - it "defaults to retrying twice" do - attempts = 0 - expect { - Bundler::Retry.new(nil).attempt do - attempts += 1 - raise "nope" - end - }.to raise_error("nope") - expect(attempts).to eq(3) - end - it "returns the first valid result" do jobs = [Proc.new{ raise "foo" }, Proc.new{ :bar }, Proc.new{ raise "foo" }] attempts = 0 diff --git a/spec/bundler/settings_spec.rb b/spec/bundler/settings_spec.rb index 56ee87da..cb844068 100644 --- a/spec/bundler/settings_spec.rb +++ b/spec/bundler/settings_spec.rb @@ -2,8 +2,12 @@ require 'spec_helper' require 'bundler/settings' describe Bundler::Settings do + subject(:settings) { described_class.new(bundled_app) } + describe "#set_local" do context "when the local config file is not found" do + subject(:settings) { described_class.new(nil) } + it "raises a GemfileNotFound error with explanation" do expect{ subject.set_local("foo", "bar") }. to raise_error(Bundler::GemfileNotFound, "Could not locate Gemfile") @@ -11,9 +15,98 @@ describe Bundler::Settings do end end - describe "URI normalization" do - let(:settings) { described_class.new(bundled_app) } + describe "#[]" do + context "when not set" do + context "when default value present" do + it "retrieves value" do + expect(settings[:retry]).to be 3 + end + end + + it "returns nil" do + expect(settings[:buttermilk]).to be nil + end + end + + context "when is boolean" do + it "returns a boolean" do + settings[:frozen] = "true" + expect(settings[:frozen]).to be true + end + context "when specific gem is configured" do + it "returns a boolean" do + settings["ignore_messages.foobar"] = "true" + expect(settings["ignore_messages.foobar"]).to be true + end + end + end + end + + + describe "#mirror_for" do + let(:uri) { URI("https://rubygems.org/") } + + context "with no configured mirror" do + it "returns the original URI" do + expect(settings.mirror_for(uri)).to eq(uri) + end + + it "converts a string parameter to a URI" do + expect(settings.mirror_for("https://rubygems.org/")).to eq(uri) + end + end + + context "with a configured mirror" do + let(:mirror_uri) { URI("https://rubygems-mirror.org/") } + + before { settings["mirror.https://rubygems.org/"] = mirror_uri.to_s } + + it "returns the mirror URI" do + expect(settings.mirror_for(uri)).to eq(mirror_uri) + end + it "converts a string parameter to a URI" do + expect(settings.mirror_for("https://rubygems.org/")).to eq(mirror_uri) + end + + it "normalizes the URI" do + expect(settings.mirror_for("https://rubygems.org")).to eq(mirror_uri) + end + + it "is case insensitive" do + expect(settings.mirror_for("HTTPS://RUBYGEMS.ORG/")).to eq(mirror_uri) + end + end + end + + describe "#credentials_for" do + let(:uri) { URI("https://gemserver.example.org/") } + let(:credentials) { "username:password" } + + context "with no configured credentials" do + it "returns nil" do + expect(settings.credentials_for(uri)).to be_nil + end + end + + context "with credentials configured by URL" do + before { settings["https://gemserver.example.org/"] = credentials } + + it "returns the configured credentials" do + expect(settings.credentials_for(uri)).to eq(credentials) + end + end + + context "with credentials configured by hostname" do + before { settings["gemserver.example.org"] = credentials } + + it "returns the configured credentials" do + expect(settings.credentials_for(uri)).to eq(credentials) + end + end + end + + describe "URI normalization" do it "normalizes HTTP URIs in credentials configuration" do settings["http://gemserver.example.org"] = "username:password" expect(settings.all).to include("http://gemserver.example.org/") diff --git a/spec/bundler/source/rubygems/remote_spec.rb b/spec/bundler/source/rubygems/remote_spec.rb new file mode 100644 index 00000000..cec243bf --- /dev/null +++ b/spec/bundler/source/rubygems/remote_spec.rb @@ -0,0 +1,105 @@ +require "spec_helper" +require "bundler/source/rubygems/remote" + +describe Bundler::Source::Rubygems::Remote do + def remote(uri) + Bundler::Source::Rubygems::Remote.new(uri) + end + + let(:uri_no_auth) { URI("https://gems.example.com") } + let(:uri_with_auth) { URI("https://#{credentials}@gems.example.com") } + let(:credentials) { "username:password" } + + context "when the original URI has no credentials" do + describe "#uri" do + it "returns the original URI" do + expect(remote(uri_no_auth).uri).to eq(uri_no_auth) + end + + it "applies configured credentials" do + Bundler.settings[uri_no_auth.to_s] = credentials + expect(remote(uri_no_auth).uri).to eq(uri_with_auth) + end + end + + describe "#anonymized_uri" do + it "returns the original URI" do + expect(remote(uri_no_auth).anonymized_uri).to eq(uri_no_auth) + end + + it "does not apply given credentials" do + Bundler.settings[uri_no_auth.to_s] = credentials + expect(remote(uri_no_auth).anonymized_uri).to eq(uri_no_auth) + end + end + end + + context "when the original URI has a username and password" do + describe "#uri" do + it "returns the original URI" do + expect(remote(uri_with_auth).uri).to eq(uri_with_auth) + end + + it "does not apply configured credentials" do + Bundler.settings[uri_no_auth.to_s] = "other:stuff" + expect(remote(uri_with_auth).uri).to eq(uri_with_auth) + end + end + + describe "#anonymized_uri" do + it "returns the URI without username and password" do + expect(remote(uri_with_auth).anonymized_uri).to eq(uri_no_auth) + end + + it "does not apply given credentials" do + Bundler.settings[uri_no_auth.to_s] = "other:stuff" + expect(remote(uri_with_auth).anonymized_uri).to eq(uri_no_auth) + end + end + end + + context "when the original URI has only a username" do + let(:uri) { URI("https://SeCrEt-ToKeN@gem.fury.io/me/") } + + describe "#anonymized_uri" do + it "returns the URI without username and password" do + expect(remote(uri).anonymized_uri).to eq(URI("https://gem.fury.io/me/")) + end + end + end + + context "when a mirror with inline credentials is configured for the URI" do + let(:uri) { URI("https://rubygems.org/") } + let(:mirror_uri_with_auth) { URI("https://username:password@rubygems-mirror.org/") } + let(:mirror_uri_no_auth) { URI("https://rubygems-mirror.org/") } + + before { Bundler.settings["mirror.https://rubygems.org/"] = mirror_uri_with_auth.to_s } + + specify "#uri returns the mirror URI with credentials" do + expect(remote(uri).uri).to eq(mirror_uri_with_auth) + end + + specify "#anonymized_uri returns the mirror URI without credentials" do + expect(remote(uri).anonymized_uri).to eq(mirror_uri_no_auth) + end + end + + context "when a mirror with configured credentials is configured for the URI" do + let(:uri) { URI("https://rubygems.org/") } + let(:mirror_uri_with_auth) { URI("https://#{credentials}@rubygems-mirror.org/") } + let(:mirror_uri_no_auth) { URI("https://rubygems-mirror.org/") } + + before do + Bundler.settings["mirror.https://rubygems.org/"] = mirror_uri_no_auth.to_s + Bundler.settings[mirror_uri_no_auth.to_s] = credentials + end + + specify "#uri returns the mirror URI with credentials" do + expect(remote(uri).uri).to eq(mirror_uri_with_auth) + end + + specify "#anonymized_uri returns the mirror URI without credentials" do + expect(remote(uri).anonymized_uri).to eq(mirror_uri_no_auth) + end + end +end diff --git a/spec/commands/lock_spec.rb b/spec/commands/lock_spec.rb new file mode 100644 index 00000000..07d92fb6 --- /dev/null +++ b/spec/commands/lock_spec.rb @@ -0,0 +1,97 @@ +require "spec_helper" + +describe "bundle lock" do + def strip_lockfile(lockfile) + strip_whitespace(lockfile).sub(/\n\Z/, '') + end + + def read_lockfile(file = "Gemfile.lock") + strip_lockfile bundled_app(file).read + end + + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + gem "with_license" + gem "foo" + G + + @lockfile = strip_lockfile <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + actionmailer (2.3.2) + activesupport (= 2.3.2) + actionpack (2.3.2) + activesupport (= 2.3.2) + activerecord (2.3.2) + activesupport (= 2.3.2) + activeresource (2.3.2) + activesupport (= 2.3.2) + activesupport (2.3.2) + foo (1.0) + rails (2.3.2) + actionmailer (= 2.3.2) + actionpack (= 2.3.2) + activerecord (= 2.3.2) + activeresource (= 2.3.2) + rake (= 10.0.2) + rake (10.0.2) + with_license (1.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo + rails + with_license + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "prints a lockfile when there is no existing lockfile with --print" do + bundle "lock --print" + + expect(out).to eq(@lockfile) + end + + it "prints a lockfile when there is an existing lockfile with --print" do + lockfile @lockfile + + bundle "lock --print" + + expect(out).to eq(@lockfile) + end + + it "writes a lockfile when there is no existing lockfile" do + bundle "lock" + + expect(read_lockfile).to eq(@lockfile) + end + + 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" + + expect(read_lockfile).to eq(@lockfile) + end + + it "does not fetch remote specs when using the --local option" do + bundle "lock --update --local" + + expect(out).to include("available on this machine.") + end + + it "writes to a custom location using --lockfile" do + bundle "lock --lockfile=lock" + + expect(out).to match(/Writing lockfile to.+lock/) + expect(read_lockfile "lock").to eq(@lockfile) + expect { read_lockfile }.to raise_error + end +end diff --git a/spec/commands/newgem_spec.rb b/spec/commands/newgem_spec.rb index ae294726..9dd54176 100644 --- a/spec/commands/newgem_spec.rb +++ b/spec/commands/newgem_spec.rb @@ -273,8 +273,8 @@ describe "bundle gem" do end it "builds spec skeleton" do - expect(bundled_app("test_gem/test/test_test_gem.rb")).to exist - expect(bundled_app("test_gem/test/minitest_helper.rb")).to exist + expect(bundled_app("test_gem/test/test_gem_test.rb")).to exist + expect(bundled_app("test_gem/test/test_helper.rb")).to exist end end @@ -286,20 +286,20 @@ describe "bundle gem" do end it "builds spec skeleton" do - expect(bundled_app("test_gem/test/test_test_gem.rb")).to exist - expect(bundled_app("test_gem/test/minitest_helper.rb")).to exist + expect(bundled_app("test_gem/test/test_gem_test.rb")).to exist + expect(bundled_app("test_gem/test/test_helper.rb")).to exist end it "requires 'test-gem'" do - expect(bundled_app("test_gem/test/minitest_helper.rb").read).to include("require 'test_gem'") + expect(bundled_app("test_gem/test/test_helper.rb").read).to include("require 'test_gem'") end it "requires 'minitest_helper'" do - expect(bundled_app("test_gem/test/test_test_gem.rb").read).to include("require 'minitest_helper'") + expect(bundled_app("test_gem/test/test_gem_test.rb").read).to include("require 'test_helper'") end it "creates a default test which fails" do - expect(bundled_app("test_gem/test/test_test_gem.rb").read).to include("assert false") + expect(bundled_app("test_gem/test/test_gem_test.rb").read).to include("assert false") end end @@ -515,20 +515,20 @@ describe "bundle gem" do end it "builds spec skeleton" do - expect(bundled_app("test-gem/test/test_test/gem.rb")).to exist - expect(bundled_app("test-gem/test/minitest_helper.rb")).to exist + expect(bundled_app("test-gem/test/test/gem_test.rb")).to exist + expect(bundled_app("test-gem/test/test_helper.rb")).to exist end it "requires 'test/gem'" do - expect(bundled_app("test-gem/test/minitest_helper.rb").read).to match(/require 'test\/gem'/) + expect(bundled_app("test-gem/test/test_helper.rb").read).to match(/require 'test\/gem'/) end - it "requires 'minitest_helper'" do - expect(bundled_app("test-gem/test/test_test/gem.rb").read).to match(/require 'minitest_helper'/) + it "requires 'test_helper'" do + expect(bundled_app("test-gem/test/test/gem_test.rb").read).to match(/require 'test_helper'/) end it "creates a default test which fails" do - expect(bundled_app("test-gem/test/test_test/gem.rb").read).to match(/assert false/) + expect(bundled_app("test-gem/test/test/gem_test.rb").read).to match(/assert false/) end it "creates a default rake task to run the test suite" do @@ -538,6 +538,8 @@ describe "bundle gem" do Rake::TestTask.new(:test) do |t| t.libs << "test" + t.libs << "lib" + t.test_files = FileList['test/**/*_test.rb'] end task :default => :test @@ -645,6 +647,17 @@ describe "bundle gem" do end expect(bundled_app("foobar/spec/spec_helper.rb")).to exist + rakefile = strip_whitespace <<-RAKEFILE + require "bundler/gem_tasks" + require "rspec/core/rake_task" + + RSpec::Core::RakeTask.new(:spec) + + task :default => :spec + RAKEFILE + + expect(bundled_app("foobar/Rakefile").read).to eq(rakefile) + expect(bundled_app("foobar/foobar.gemspec").read).to include('spec.add_development_dependency "rspec"') end it "asks about MIT license" do diff --git a/spec/commands/outdated_spec.rb b/spec/commands/outdated_spec.rb index 4d8e6d5c..9ecdc067 100644 --- a/spec/commands/outdated_spec.rb +++ b/spec/commands/outdated_spec.rb @@ -27,9 +27,9 @@ describe "bundle outdated" do bundle "outdated" - expect(out).to include("activesupport (3.0 > 2.3.5) Gemfile specifies \"= 2.3.5\"") - expect(out).to include("weakling (0.2 > 0.0.3) Gemfile specifies \"~> 0.0.1\"") - expect(out).to include("foo (1.0") + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)") + expect(out).to include("weakling (newest 0.2, installed 0.0.3, requested ~> 0.0.1)") + expect(out).to include("foo (newest 1.0") # Gem names are one per-line, between "*" and their parenthesized version. gem_list = out.split("\n").map { |g| g[ /\* (.*) \(/, 1] }.compact @@ -54,6 +54,26 @@ describe "bundle outdated" do end end + describe "with --verbose option" do + it "adds gem group to dependency output when repo is updated" do + + install_gemfile <<-G + source "file://#{gem_repo2}" + + group :development, :test do + gem 'activesupport', '2.3.5' + end + G + + update_repo2 { build_gem "activesupport", "3.0" } + + bundle "outdated --verbose" + + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5) in groups \"development, test\"") + + end + end + describe "with --local option" do it "doesn't hit repo2" do FileUtils.rm_rf(gem_repo2) @@ -71,8 +91,8 @@ describe "bundle outdated" do end bundle "outdated foo" - expect(out).not_to include("activesupport (3.0 > 2.3.5)") - expect(out).to include("foo (1.0") + expect(out).not_to include("activesupport (newest") + expect(out).to include("foo (newest 1.0") end end @@ -95,7 +115,7 @@ describe "bundle outdated" do end bundle "outdated --pre" - expect(out).to include("activesupport (3.0.0.beta > 2.3.5) Gemfile specifies \"= 2.3.5\"") + expect(out).to include("activesupport (newest 3.0.0.beta, installed 2.3.5, requested = 2.3.5)") end end @@ -112,7 +132,7 @@ describe "bundle outdated" do G bundle "outdated" - expect(out).to include("activesupport (3.0.0.beta.2 > 3.0.0.beta.1) Gemfile specifies \"= 3.0.0.beta.1\"") + expect(out).to include("(newest 3.0.0.beta.2, installed 3.0.0.beta.1, requested = 3.0.0.beta.1)") end end end @@ -126,8 +146,8 @@ describe "bundle outdated" do bundle "outdated --strict" - expect(out).to_not include("activesupport (3.0 > 2.3.5) Gemfile specifies \"= 2.3.5\"") - expect(out).to include("weakling (0.0.5 > 0.0.3) Gemfile specifies \"~> 0.0.1\"") + expect(out).to_not include("activesupport (newest") + expect(out).to include("(newest 0.0.5, installed 0.0.3, requested ~> 0.0.1)") end it "only reports gem dependencies when they can actually be updated" do @@ -138,7 +158,7 @@ describe "bundle outdated" do bundle "outdated --strict" - expect(out).to_not include("rack (1.2 > 0.9.1)") + expect(out).to_not include("rack (1.2") end end diff --git a/spec/install/force_spec.rb b/spec/install/force_spec.rb new file mode 100644 index 00000000..26939099 --- /dev/null +++ b/spec/install/force_spec.rb @@ -0,0 +1,20 @@ +require "spec_helper" + +describe "bundle install" do + describe "with --force" do + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "install" + end + + it "re-installs installed gems" do + bundle "install --force" + expect(out).to include "Installing rack 1.0.0" + should_be_installed "rack 1.0.0" + end + end +end diff --git a/spec/install/gems/flex_spec.rb b/spec/install/gems/flex_spec.rb index 691a6a86..bd6ba2db 100644 --- a/spec/install/gems/flex_spec.rb +++ b/spec/install/gems/flex_spec.rb @@ -267,6 +267,9 @@ describe "bundle flex_install" do DEPENDENCIES rack + + BUNDLED WITH + #{Bundler::VERSION} L end end diff --git a/spec/install/gems/groups_spec.rb b/spec/install/gems/groups_spec.rb index 23af4aee..5d0972d1 100644 --- a/spec/install/gems/groups_spec.rb +++ b/spec/install/gems/groups_spec.rb @@ -80,6 +80,9 @@ describe "bundle install with groups" do group :emo do gem "activesupport", "2.3.5" end + group :debugging, :optional => true do + gem "thin" + end G end @@ -159,6 +162,69 @@ describe "bundle install with groups" do bundle :install should_not_be_installed "activesupport 2.3.5" end + + it "does not install gems from the optional group" do + bundle :install + should_not_be_installed "thin 1.0" + end + + it "does install gems from the optional group when requested" do + bundle :install, :with => "debugging" + should_be_installed "thin 1.0" + end + + it "does install gems from the previously requested group" do + bundle :install, :with => "debugging" + should_be_installed "thin 1.0" + bundle :install + should_be_installed "thin 1.0" + end + + it "does install gems from the optional groups requested with BUNDLE_WITH" do + ENV["BUNDLE_WITH"] = "debugging" + bundle :install + should_be_installed "thin 1.0" + ENV["BUNDLE_WITH"] = nil + end + + it "clears with when passed an empty list" do + bundle :install, :with => "debugging" + bundle 'install --with ""' + should_not_be_installed "thin 1.0" + end + + it "does remove groups from without when passed at with" do + bundle :install, :without => "emo" + bundle :install, :with => "emo" + should_be_installed "activesupport 2.3.5" + end + + it "does remove groups from with when passed at without" do + bundle :install, :with => "debugging" + bundle :install, :without => "debugging" + should_not_be_installed "thin 1.0" + end + + it "errors out when passing a group to with and without" do + bundle :install, :with => "emo debugging", :without => "emo" + expect(out).to include("The offending groups are: emo") + end + + it "can add and remove a group at the same time" do + bundle :install, :with => "debugging", :without => "emo" + should_be_installed "thin 1.0" + should_not_be_installed "activesupport 2.3.5" + end + + it "does have no effect when listing a not optional group in with" do + bundle :install, :with => "emo" + should_be_installed "activesupport 2.3.5" + end + + it "does have no effect when listing an optional group in without" do + bundle :install, :without => "debugging" + should_not_be_installed "thin 1.0" + end end describe "with gems assigned to multiple groups" do diff --git a/spec/install/gems/post_install_spec.rb b/spec/install/gems/post_install_spec.rb index 69841fea..76a3a6ca 100644 --- a/spec/install/gems/post_install_spec.rb +++ b/spec/install/gems/post_install_spec.rb @@ -1,121 +1,150 @@ require 'spec_helper' -describe "bundle install with gem sources" do - describe "when gems include post install messages" do - it "should display the post-install messages after installing" do - gemfile <<-G - source "file://#{gem_repo1}" - gem 'rack' - gem 'thin' - gem 'rack-obama' - G - - bundle :install - expect(out).to include("Post-install message from rack:") - expect(out).to include("Rack's post install message") - expect(out).to include("Post-install message from thin:") - expect(out).to include("Thin's post install message") - expect(out).to include("Post-install message from rack-obama:") - expect(out).to include("Rack-obama's post install message") +describe "bundle install" do + context "with gem sources" do + context "when gems include post install messages" do + it "should display the post-install messages after installing" do + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack' + gem 'thin' + gem 'rack-obama' + G + + bundle :install + expect(out).to include("Post-install message from rack:") + expect(out).to include("Rack's post install message") + expect(out).to include("Post-install message from thin:") + expect(out).to include("Thin's post install message") + expect(out).to include("Post-install message from rack-obama:") + expect(out).to include("Rack-obama's post install message") + end end - end - describe "when gems do not include post install messages" do - it "should not display any post-install messages" do - gemfile <<-G - source "file://#{gem_repo1}" - gem "activesupport" - G + context "when gems do not include post install messages" do + it "should not display any post-install messages" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "activesupport" + G - bundle :install - expect(out).not_to include("Post-install message") + bundle :install + expect(out).not_to include("Post-install message") + end end - end - describe "when a dependecy includes a post install message" do - it "should display the post install message" do - gemfile <<-G - source "file://#{gem_repo1}" - gem 'rack_middleware' - G + context "when a dependecy includes a post install message" do + it "should display the post install message" do + gemfile <<-G + source "file://#{gem_repo1}" + gem 'rack_middleware' + G - bundle :install - expect(out).to include("Post-install message from rack:") - expect(out).to include("Rack's post install message") + bundle :install + expect(out).to include("Post-install message from rack:") + expect(out).to include("Rack's post install message") + end end end -end -describe "bundle install with git sources" do - describe "when gems include post install messages" do - it "should display the post-install messages after installing" do - build_git "foo" do |s| - s.post_install_message = "Foo's post install message" + context "with git sources" do + context "when gems include post install messages" do + it "should display the post-install messages after installing" do + build_git "foo" do |s| + s.post_install_message = "Foo's post install message" + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + + bundle :install + expect(out).to include("Post-install message from foo:") + expect(out).to include("Foo's post install message") end - gemfile <<-G - source "file://#{gem_repo1}" - gem 'foo', :git => '#{lib_path("foo-1.0")}' - G - - bundle :install - expect(out).to include("Post-install message from foo:") - expect(out).to include("Foo's post install message") - end - it "should display the post-install messages if repo is updated" do - build_git "foo" do |s| - s.post_install_message = "Foo's post install message" + it "should display the post-install messages if repo is updated" do + build_git "foo" do |s| + s.post_install_message = "Foo's post install message" + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + bundle :install + + build_git "foo", "1.1" do |s| + s.post_install_message = "Foo's 1.1 post install message" + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.1")}' + G + bundle :install + + expect(out).to include("Post-install message from foo:") + expect(out).to include("Foo's 1.1 post install message") end - gemfile <<-G - source "file://#{gem_repo1}" - gem 'foo', :git => '#{lib_path("foo-1.0")}' - G - bundle :install - build_git "foo", "1.1" do |s| - s.post_install_message = "Foo's 1.1 post install message" + it "should not display the post-install messages if repo is not updated" do + build_git "foo" do |s| + s.post_install_message = "Foo's post install message" + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + + bundle :install + expect(out).to include("Post-install message from foo:") + expect(out).to include("Foo's post install message") + + bundle :install + expect(out).not_to include("Post-install message") end - gemfile <<-G - source "file://#{gem_repo1}" - gem 'foo', :git => '#{lib_path("foo-1.1")}' - G - bundle :install - - expect(out).to include("Post-install message from foo:") - expect(out).to include("Foo's 1.1 post install message") end - it "should not display the post-install messages if repo is not updated" do - build_git "foo" do |s| - s.post_install_message = "Foo's post install message" + context "when gems do not include post install messages" do + it "should not display any post-install messages" do + build_git "foo" do |s| + s.post_install_message = nil + end + gemfile <<-G + source "file://#{gem_repo1}" + gem 'foo', :git => '#{lib_path("foo-1.0")}' + G + + bundle :install + expect(out).not_to include("Post-install message") end + end + end + + context "when ignore post-install messages for gem is set" do + it "doesn't display any post-install messages" do gemfile <<-G source "file://#{gem_repo1}" - gem 'foo', :git => '#{lib_path("foo-1.0")}' + gem "rack" G - bundle :install - expect(out).to include("Post-install message from foo:") - expect(out).to include("Foo's post install message") + bundle "config ignore_messages.rack true" bundle :install expect(out).not_to include("Post-install message") end end - describe "when gems do not include post install messages" do - it "should not display any post-install messages" do - build_git "foo" do |s| - s.post_install_message = nil - end + context "when ignore post-install messages for all gems" do + it "doesn't display any post-install messages" do gemfile <<-G source "file://#{gem_repo1}" - gem 'foo', :git => '#{lib_path("foo-1.0")}' + gem "rack" G + bundle "config ignore_messages true" + bundle :install expect(out).not_to include("Post-install message") end end - end diff --git a/spec/install/gems/simple_case_spec.rb b/spec/install/gems/simple_case_spec.rb index 759de088..0f05588c 100644 --- a/spec/install/gems/simple_case_spec.rb +++ b/spec/install/gems/simple_case_spec.rb @@ -17,7 +17,7 @@ describe "bundle install with gem sources" do G expect(err).to eq "" - expect(out).to match(/StandardError: FAIL/) + expect(out).to match(/StandardError, "FAIL"/) expect(bundled_app("Gemfile.lock")).not_to exist end diff --git a/spec/install/parallel/spec_installation_spec.rb b/spec/install/parallel/spec_installation_spec.rb new file mode 100644 index 00000000..97288fe0 --- /dev/null +++ b/spec/install/parallel/spec_installation_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' +require 'bundler/installer/parallel_installer' + +describe ParallelInstaller::SpecInstallation do + describe "#ready_to_enqueue?" do + + let!(:dep) do + a_spec = Object.new + def a_spec.name + "I like tests" + end + a_spec + end + + context "when in enqueued state" do + it "is falsey" do + spec = ParallelInstaller::SpecInstallation.new(dep) + spec.state = :enqueued + expect(spec.ready_to_enqueue?).to be_falsey + end + end + + context "when in installed state" do + it "returns falsey" do + spec = ParallelInstaller::SpecInstallation.new(dep) + spec.state = :installed + expect(spec.ready_to_enqueue?).to be_falsey + end + end + + it "returns truthy" do + spec = ParallelInstaller::SpecInstallation.new(dep) + expect(spec.ready_to_enqueue?).to be_truthy + end + end + +end diff --git a/spec/install/post_bundle_message_spec.rb b/spec/install/post_bundle_message_spec.rb index 408e5188..5bdf1650 100644 --- a/spec/install/post_bundle_message_spec.rb +++ b/spec/install/post_bundle_message_spec.rb @@ -94,7 +94,7 @@ describe "post bundle message" do it "should report a helpufl error message" do bundle :install expect(out).to include("Fetching gem metadata from https://rubygems.org/") - expect(out).to include("Could not find gem 'misspelled-gem-name (>= 0) ruby' in any of the gem sources listed in your Gemfile or installed on this machine.") + expect(out).to include("Could not find gem 'misspelled-gem-name (>= 0) ruby' in any of the gem sources listed in your Gemfile or available on this machine.") end end end diff --git a/spec/lock/lockfile_spec.rb b/spec/lock/lockfile_spec.rb index 364b9718..ea712de2 100644 --- a/spec/lock/lockfile_spec.rb +++ b/spec/lock/lockfile_spec.rb @@ -4,6 +4,7 @@ describe "the lockfile format" do include Bundler::GemHelpers it "generates a simple lockfile for a single source, gem" do + install_gemfile <<-G source "file://#{gem_repo1}" @@ -21,6 +22,139 @@ describe "the lockfile format" do DEPENDENCIES rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "updates the lockfile's bundler version if current ver. is newer" do + + # TODO: verno below should be one less than prev ver (unless at min) + + lockfile <<-L + GIT + remote: git://github.com/nex3/haml.git + revision: 8a2271f + specs: + + GEM + remote: file://#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic(Gem::Platform.local)} + + DEPENDENCIES + omg! + rack + + BUNDLED WITH + 1.8.2 + L + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic(Gem::Platform.local)} + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "updates the lockfile's bundler version if not present" do + + lockfile <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic(Gem::Platform.local)} + + DEPENDENCIES + rack + L + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic(Gem::Platform.local)} + + DEPENDENCIES + rack + + BUNDLED WITH + #{Bundler::VERSION} + G + end + + it "outputs a warning if the current is older than lockfile's bundler version" do + lockfile <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic(Gem::Platform.local)} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.0.0 + L + + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack" + G + + expect(out).to include("Warning: the running version of Bundler is " \ + "older than the version that created the lockfile") + + lockfile_should_be <<-G + GEM + remote: file:#{gem_repo1}/ + specs: + rack (1.0.0) + + PLATFORMS + #{generic(Gem::Platform.local)} + + DEPENDENCIES + rack + + BUNDLED WITH + 9999999.0.0 G end @@ -44,6 +178,9 @@ describe "the lockfile format" do DEPENDENCIES rack-obama + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -67,6 +204,9 @@ describe "the lockfile format" do DEPENDENCIES rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -94,6 +234,9 @@ describe "the lockfile format" do DEPENDENCIES rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -116,6 +259,9 @@ describe "the lockfile format" do DEPENDENCIES net-sftp + + BUNDLED WITH + #{Bundler::VERSION} G should_be_installed "net-sftp 1.1.1", "net-ssh 1.0.0" @@ -143,6 +289,9 @@ describe "the lockfile format" do DEPENDENCIES foo! + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -176,6 +325,9 @@ describe "the lockfile format" do DEPENDENCIES omg! rack + + BUNDLED WITH + #{Bundler::VERSION} L bundle "install" @@ -206,6 +358,9 @@ describe "the lockfile format" do DEPENDENCIES foo! + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -233,6 +388,9 @@ describe "the lockfile format" do DEPENDENCIES foo! + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -260,6 +418,9 @@ describe "the lockfile format" do DEPENDENCIES foo! + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -284,6 +445,9 @@ describe "the lockfile format" do DEPENDENCIES foo! + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -323,6 +487,9 @@ describe "the lockfile format" do bar! foo! rack + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -355,6 +522,9 @@ describe "the lockfile format" do actionpack rack-obama thin + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -391,6 +561,9 @@ describe "the lockfile format" do DEPENDENCIES rails + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -414,6 +587,9 @@ describe "the lockfile format" do DEPENDENCIES double_deps + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -437,6 +613,9 @@ describe "the lockfile format" do DEPENDENCIES rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -460,6 +639,9 @@ describe "the lockfile format" do DEPENDENCIES rack-obama (>= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -485,6 +667,9 @@ describe "the lockfile format" do DEPENDENCIES foo + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -510,6 +695,9 @@ describe "the lockfile format" do DEPENDENCIES foo + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -535,6 +723,9 @@ describe "the lockfile format" do DEPENDENCIES foo + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -559,6 +750,9 @@ describe "the lockfile format" do DEPENDENCIES foo! + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -574,6 +768,9 @@ describe "the lockfile format" do DEPENDENCIES rack + + BUNDLED WITH + #{Bundler::VERSION} G install_gemfile <<-G @@ -596,6 +793,9 @@ describe "the lockfile format" do DEPENDENCIES rack + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -622,6 +822,9 @@ describe "the lockfile format" do DEPENDENCIES platform_specific + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -650,6 +853,9 @@ describe "the lockfile format" do DEPENDENCIES activesupport rack + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -671,6 +877,9 @@ describe "the lockfile format" do DEPENDENCIES rack + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -692,6 +901,9 @@ describe "the lockfile format" do DEPENDENCIES rack (= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -713,6 +925,9 @@ describe "the lockfile format" do DEPENDENCIES rack (= 1.0) + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -756,6 +971,9 @@ describe "the lockfile format" do DEPENDENCIES rack (> 0.9, < 1.0) + + BUNDLED WITH + #{Bundler::VERSION} G end @@ -803,6 +1021,9 @@ describe "the lockfile format" do DEPENDENCIES omg! + + BUNDLED WITH + #{Bundler::VERSION} L FileUtils.rm_rf(bundled_app('vendor')) @@ -827,6 +1048,9 @@ describe "the lockfile format" do DEPENDENCIES omg! + + BUNDLED WITH + #{Bundler::VERSION} L end @@ -836,7 +1060,9 @@ describe "the lockfile format" do File.utime(time, time, bundled_app('Gemfile.lock')) end before(:each) do + build_repo2 + install_gemfile <<-G source "file://#{gem_repo2}" gem "rack" @@ -913,6 +1139,9 @@ describe "the lockfile format" do DEPENDENCIES rack + + BUNDLED WITH + #{Bundler::VERSION} L error = install_gemfile(<<-G, :expect_err => true) diff --git a/spec/other/bundle_ruby_spec.rb b/spec/other/bundle_ruby_spec.rb index 739c3d0f..1f55ebd9 100644 --- a/spec/other/bundle_ruby_spec.rb +++ b/spec/other/bundle_ruby_spec.rb @@ -1,6 +1,22 @@ require "spec_helper" describe "bundle_ruby" do + context "when run" do + it "displays a deprecation warning" do + gemfile <<-G + source "file://#{gem_repo1}" + ruby "1.9.3", :engine => 'ruby', :engine_version => '1.9.3' + + gem "foo" + G + + bundle_ruby + + expect(err).to eq("Warning: bundle_ruby will be deprecated in " \ + "Bundler 2.0.0.") + end + end + context "without patchlevel" do it "returns the ruby version" do gemfile <<-G @@ -12,7 +28,7 @@ describe "bundle_ruby" do bundle_ruby - expect(out).to eq("ruby 1.9.3") + expect(out).to include("ruby 1.9.3") end it "engine defaults to MRI" do @@ -25,7 +41,7 @@ describe "bundle_ruby" do bundle_ruby - expect(out).to eq("ruby 1.9.3") + expect(out).to include("ruby 1.9.3") end it "handles jruby" do @@ -38,7 +54,7 @@ describe "bundle_ruby" do bundle_ruby - expect(out).to eq("ruby 1.8.7 (jruby 1.6.5)") + expect(out).to include("ruby 1.8.7 (jruby 1.6.5)") end it "handles rbx" do @@ -51,7 +67,7 @@ describe "bundle_ruby" do bundle_ruby - expect(out).to eq("ruby 1.8.7 (rbx 1.2.4)") + expect(out).to include("ruby 1.8.7 (rbx 1.2.4)") end it "raises an error if engine is used but engine version is not" do @@ -66,7 +82,7 @@ describe "bundle_ruby" do expect(exitstatus).not_to eq(0) if exitstatus bundle_ruby - expect(out).to eq("Please define :engine_version") + expect(out).to include("Please define :engine_version") end it "raises an error if engine_version is used but engine is not" do @@ -81,7 +97,7 @@ describe "bundle_ruby" do expect(exitstatus).not_to eq(0) if exitstatus bundle_ruby - expect(out).to eq("Please define :engine") + expect(out).to include("Please define :engine") end it "raises an error if engine version doesn't match ruby version for MRI" do @@ -96,7 +112,7 @@ describe "bundle_ruby" do expect(exitstatus).not_to eq(0) if exitstatus bundle_ruby - expect(out).to eq("ruby_version must match the :engine_version for MRI") + expect(out).to include("ruby_version must match the :engine_version for MRI") end it "should print if no ruby version is specified" do @@ -108,7 +124,7 @@ describe "bundle_ruby" do bundle_ruby - expect(out).to eq("No ruby version specified") + expect(out).to include("No ruby version specified") end end @@ -123,7 +139,7 @@ describe "bundle_ruby" do bundle_ruby - expect(out).to eq("ruby 1.9.3p429") + expect(out).to include("ruby 1.9.3p429") end it "handles an engine" do @@ -136,7 +152,7 @@ describe "bundle_ruby" do bundle_ruby - expect(out).to eq("ruby 1.9.3p392 (jruby 1.7.4)") + expect(out).to include("ruby 1.9.3p392 (jruby 1.7.4)") end end end diff --git a/spec/other/platform_spec.rb b/spec/other/platform_spec.rb index 13b57c13..ad9390cf 100644 --- a/spec/other/platform_spec.rb +++ b/spec/other/platform_spec.rb @@ -1125,8 +1125,8 @@ G G bundle "outdated" - expect(out).to include("activesupport (3.0 > 2.3.5)") - expect(out).to include("foo (1.0") + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5") + expect(out).to include("foo (newest 1.0") end it "returns list of outdated gems when the ruby version matches for any engine" do @@ -1145,8 +1145,8 @@ G G bundle "outdated" - expect(out).to include("activesupport (3.0 > 2.3.5)") - expect(out).to include("foo (1.0") + expect(out).to include("activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)") + expect(out).to include("foo (newest 1.0") end end diff --git a/spec/realworld/parallel_spec.rb b/spec/realworld/parallel_spec.rb index 67311a7e..409c146d 100644 --- a/spec/realworld/parallel_spec.rb +++ b/spec/realworld/parallel_spec.rb @@ -6,7 +6,7 @@ describe "parallel", :realworld => true do source "https://rubygems.org" gem 'activesupport', '~> 3.2.13' gem 'faker', '~> 1.1.2' - gem 'i18n', '~> 0.6.0' # Because 1.7+ requires Ruby 1.9.3+ + gem 'i18n', '~> 0.6.0' # Because 0.7+ requires Ruby 1.9.3+ G bundle :install, :jobs => 4, :env => {"DEBUG" => "1"} @@ -38,7 +38,7 @@ describe "parallel", :realworld => true do source "https://rubygems.org" gem 'activesupport', '~> 3.2.12' gem 'faker', '~> 1.1.2' - gem 'i18n', '~> 0.6.0' # Because 1.7+ requires Ruby 1.9.3+ + gem 'i18n', '~> 0.6.0' # Because 0.7+ requires Ruby 1.9.3+ G bundle :update, :jobs => 4, :env => {"DEBUG" => "1"} diff --git a/spec/runtime/inline_spec.rb b/spec/runtime/inline_spec.rb new file mode 100644 index 00000000..176c9629 --- /dev/null +++ b/spec/runtime/inline_spec.rb @@ -0,0 +1,88 @@ +require "spec_helper" + +describe "bundler/inline#gemfile" do + def script(code, options = {}) + @out = ruby("require 'bundler/inline'\n\n" << code, options) + end + + before :each do + build_lib "one", "1.0.0" do |s| + s.write "lib/baz.rb", "puts 'baz'" + s.write "lib/qux.rb", "puts 'qux'" + end + + build_lib "two", "1.0.0" do |s| + s.write "lib/two.rb", "puts 'two'" + s.add_dependency "three", "= 1.0.0" + end + + build_lib "three", "1.0.0" do |s| + s.write "lib/three.rb", "puts 'three'" + s.add_dependency "seven", "= 1.0.0" + end + + build_lib "four", "1.0.0" do |s| + s.write "lib/four.rb", "puts 'four'" + end + + build_lib "five", "1.0.0", :no_default => true do |s| + s.write "lib/mofive.rb", "puts 'five'" + end + + build_lib "six", "1.0.0" do |s| + s.write "lib/six.rb", "puts 'six'" + end + + build_lib "seven", "1.0.0" do |s| + s.write "lib/seven.rb", "puts 'seven'" + end + + build_lib "eight", "1.0.0" do |s| + s.write "lib/eight.rb", "puts 'eight'" + end + + build_lib "four", "1.0.0" do |s| + s.write "lib/four.rb", "puts 'four'" + end + + @gemfile = <<-G + path "#{lib_path}" + gem "two" + gem "four", :require => false + G + end + + it "requires the gems" do + script <<-RUBY + gemfile do + path "#{lib_path}" + gem "two" + end + RUBY + + expect(out).to eq("two") + expect(exitstatus).to be_zero if exitstatus + + script <<-RUBY, :expect_err => true + gemfile do + path "#{lib_path}" + gem "eleven" + end + + puts "success" + RUBY + + expect(err).to include "Could not find gem 'eleven (>= 0) ruby'" + expect(out).not_to include "success" + + script <<-RUBY + gemfile(true) do + source "file://#{gem_repo1}" + gem "rack" + end + RUBY + + expect(out).to eq("Rack's post install message") + expect(exitstatus).to be_zero if exitstatus + end +end diff --git a/spec/runtime/setup_spec.rb b/spec/runtime/setup_spec.rb index 02ce0de3..efa5a693 100644 --- a/spec/runtime/setup_spec.rb +++ b/spec/runtime/setup_spec.rb @@ -90,6 +90,21 @@ describe "Bundler.setup" do expect(err).to eq("") expect(out).to match("WIN") end + + it "handles multiple non-additive invocations" do + ruby <<-RUBY + require 'bundler' + Bundler.setup(:default, :test) + Bundler.setup(:default) + require 'rack' + + puts "FAIL" + RUBY + + expect(err).to match("rack") + expect(err).to match("LoadError") + expect(out).not_to match("FAIL") + end end it "raises if the Gemfile was not yet installed" do @@ -628,6 +643,39 @@ describe "Bundler.setup" do expect(out).to eq("yay") end + it "should clean $LOAD_PATH properly" do + gem_name = 'very_simple_binary' + full_gem_name = gem_name + '-1.0' + ext_dir = File.join(tmp "extenstions", full_gem_name) + + install_gem full_gem_name + + install_gemfile <<-G + source "file://#{gem_repo1}" + G + + ruby <<-R + if Gem::Specification.method_defined? :extension_dir + s = Gem::Specification.find_by_name '#{gem_name}' + s.extension_dir = '#{ext_dir}' + + # Don't build extensions. + s.class.send(:define_method, :build_extensions) { nil } + end + + require 'bundler' + gem '#{gem_name}' + + puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} >= 2 + + Bundler.setup + + puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} == 0 + R + + expect(out).to eq("true\ntrue") + end + it "stubs out Gem.refresh so it does not reveal system gems" do system_gems "rack-1.0.0" diff --git a/spec/update/gems_spec.rb b/spec/update/gems_spec.rb index ed503e36..4989bcfd 100644 --- a/spec/update/gems_spec.rb +++ b/spec/update/gems_spec.rb @@ -99,6 +99,16 @@ describe "bundle update" do should_not_be_installed "rack 1.2" end end + + describe "in a frozen bundle" do + it "should fail loudly" do + bundle "install --deployment" + bundle "update" + + expect(out).to match(/You are trying to install in deployment mode after changing.your Gemfile/m) + expect(exitstatus).not_to eq(0) if exitstatus + end + end end describe "bundle update in more complicated situations" do |