diff options
95 files changed, 2105 insertions, 1095 deletions
diff --git a/.travis.yml b/.travis.yml index 842d42ba21..501f70a267 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,10 @@ branches: - 1-0-stable notifications: email: - - mail@arko.net - - hone02@gmail.com + # andre + - secure: "bCcvqJT7YrBawtkXXwHhT+jOFth7r2Qv/30PkkbhQxk6Jb3xambjCOJ3U6vJ\ngYmiL50exi5lUp3oc3SEbHN5t2CrZqOZDQ6o7P8EAmB5c0oH2RrYaFOkI5Gt\nul/jGH/96A9sj0aMwG7JfdMSfhqj1DUKAm2PnnbXPL853VfmT24=" + # terence + - secure: "MQ8eA5Jb8YzEpAo58DRGfVJklAPcEbAulpBZnTxp0am6ldneDtJHbQk21w6R\nj5GsDHlzr/lMp/GHIimtUZ7rLohfND8fj/W7fs1Dkd4eN02/ERt98x3pHlqv\nvZgSnZ39uVYv+OcphraE24QaRaGWLhWZAMYQTVe/Yz50NyG8g1U=" irc: on_success: change on_failure: always @@ -31,7 +33,7 @@ env: # we need to know if changes to rubygems will break bundler on release - RGV=master # test the latest rubygems release with all of our supported rubies - - RGV=v2.0.2 + - RGV=v2.0.3 matrix: allow_failures: # we want to know how we're doing with head, but not fail the build diff --git a/CHANGELOG.md b/CHANGELOG.md index a6be0940e1..e76b1008c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## 1.4.0 + +Features: + + - add `ENV['DEBUG_RESOLVER_TREE']` outputs resolver tree (@dblock) + - set $MANPATH so `bundle exec man name` works (#1624, @sunaku) + - add Gemfile dependency info to bundle outdated output (#2487, @rahearn) + - faster installs using gemspecs from the local system cache (#2497, @mipearson) + - add `bundle install -jN` for N parallel gem installations (#2481, @eagletmt) + - allow `require: true` as an alias for `require: <name>` (#2538, @ndbroadbent) + +Bugfixes: + + - reduce stack size while resolving, helping JRuby overflow less (#2510, @headius) + +## 1.3.6 + +Bugfixes: + + - set --no-cache when bundle install --local is called (@TimMoore) + - make gemspec path option preserve relative paths in lock file (@bwillis) + ## 1.3.5 (3 April 2013) Features: @@ -111,6 +133,7 @@ Features: - `binstubs` lists child gem bins if a gem has no binstubs - `bundle gem --edit` will open the new gemspec (@ndbroadbent) - `bundle gem --test rspec` now makes working tests (@tricknotes) + - `bundle env` prints info about bundler's environment (@peeja) - add `BUNDLE_IGNORE_CONFIG` environment variable support (@richo) Bugfixes: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 759b2dec97..89e8e5f9a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Creating Issues -If you're having a problem, please see [ISSUES](https://github.com/carlhuda/bundler/blob/master/ISSUES.md) for troubleshooting steps and a guide for how to submit a ticket that will help us solve the problem you are having as quickly as possible. +If you're having a problem, please see [ISSUES](https://github.com/bundler/bundler/blob/master/ISSUES.md) for troubleshooting steps and a guide for how to submit a ticket that will help us solve the problem you are having as quickly as possible. # Discussing Bundler @@ -8,6 +8,6 @@ If you'd like to discuss features, ask questions, or just engage in general Bund # Helping Out -If you'd like to help make Bundler better, you totally rock! Please check out the [CONTRIBUTE](https://github.com/carlhuda/bundler/blob/master/CONTRIBUTE.md) file for an introduction to the project, guidelines for contributing, and suggestions for things anyone can do that would be helpful. +If you'd like to help make Bundler better, you totally rock! Please check out the [DEVELOPMENT](https://github.com/bundler/bundler/blob/master/DEVELOPMENT.md) file for an introduction to the project, guidelines for contributing, and suggestions for things anyone can do that would be helpful. Thanks for helping us make Bundler better. diff --git a/CONTRIBUTE.md b/DEVELOPMENT.md index a12d541e1f..1e80b2d21a 100644 --- a/CONTRIBUTE.md +++ b/DEVELOPMENT.md @@ -1,6 +1,6 @@ -Great to have you here! Here are a few ways you can help out with [Bundler](http://github.com/carlhuda/bundler). +Great to have you here! Here are a few ways you can help out with [Bundler](http://github.com/bundler/bundler). -# Learn & listen +# Where should I start? You can start learning about Bundler by reading [the documentation](http://gembundler.com). If you want, you can also read a (lengthy) explanation of [why Bundler exists and what it does](http://gembundler.com/v1.2/rationale.html). You can also check out discussions about Bundler on the [Bundler mailing list](https://groups.google.com/group/ruby-bundler) and in the [Bundler IRC channel](irc://irc.freenode.net/#bundler), which is #bundler on Freenode. @@ -8,14 +8,33 @@ You can start learning about Bundler by reading [the documentation](http://gembu The Bundler core team consists of André Arko ([@indirect](http://github.com/indirect)), Terence Lee ([@hone](http://github.com/hone)), and Jessica Lynn Suttles ([@jlsuttles](http://github.com/jlsuttles)), with support and advice from original Bundler author Yehuda Katz ([@wycats](http://github.com/wycats)). +# Development setup + +Bundler doesn't use a Gemfile to list development dependencies, because when we tried it we couldn't tell if we were awake or it was just another level of dreams. To work on Bundler, you'll probably want to do a couple of things. + + 1. Install Bundler's development dependencies + + $ rake spec:deps + + 2. Run the test suite, to make sure things are working + + $ rake spec + + 3. Set up a shell alias to run Bundler from your clone, e.g. a Bash alias: + + $ alias dbundle='ruby -I /path/to/bundler/lib /path/to/bundler/bin/bundle' + + With that set up, you can test changes you've made to Bundler by running `dbundle`, without interfering with the regular `bundle` command. + + # Adding new features -When adding a new feature to Bundler, please follow these steps: +If you would like to add a new feature to Bundler, please follow these steps: - 1. [Create an issue](https://github.com/carlhuda/bundler/issues/new) to discuss your feature. + 1. [Create an issue](https://github.com/bundler/bundler/issues/new) to discuss your feature. 2. Base your commits on the master branch, since we follow [SemVer](http://semver.org) and don't add new features to old releases. 3. Commit the code and at least one test covering your changes to a feature branch in your fork. - 4. Put a line in the [CHANGELOG](https://github.com/carlhuda/bundler/blob/master/CHANGELOG.md) summarizing your changes under the next release under the "Features" heading. + 4. Put a line in the [CHANGELOG](https://github.com/bundler/bundler/blob/master/CHANGELOG.md) summarizing your changes under the next release under the "Features" heading. 5. Send us a [pull request](https://help.github.com/articles/using-pull-requests) from your feature branch. If you don't hear back immediately, don’t get discouraged! We all have day jobs, but we respond to most tickets within a day or two. @@ -24,9 +43,9 @@ If you don't hear back immediately, don’t get discouraged! We all have day job Triage is the work of processing tickets that have been opened into actionable issues, feature requests, or bug reports. That includes verifying bugs, categorizing the ticket, and ensuring there's enough information to reproduce the bug for anyone who wants to try to fix it. -We've created an [issues guide](https://github.com/carlhuda/bundler/blob/master/ISSUES.md) to walk Bundler users through the process of troubleshooting issues and reporting bugs. +We've created an [issues guide](https://github.com/bundler/bundler/blob/master/ISSUES.md) to walk Bundler users through the process of troubleshooting issues and reporting bugs. -If you'd like to help, awesome! You can [report a new bug](https://github.com/carlhuda/bundler/issues/new) or browse our [existing open tickets](https://github.com/carlhuda/bundler/issues). +If you'd like to help, awesome! You can [report a new bug](https://github.com/bundler/bundler/issues/new) or browse our [existing open tickets](https://github.com/bundler/bundler/issues). Not every ticket will point to a bug in Bundler's code, but open tickets usually mean that there is something we could improve to help that user. Sometimes that means writing additional documentation, sometimes that means making error messages clearer, and sometimes that means explaining to a user that they need to install git to use git gems. @@ -45,7 +64,7 @@ If you can reproduce an issue, you're well on your way to fixing it. :) Fixing i 1. Discuss the fix on the existing issue. Coordinating with everyone else saves duplicate work and serves as a great way to get suggestions and ideas if you need any. 2. Base your commits on the correct branch. Bugfixes for 1.x versions of Bundler should be based on the matching 1-x-stable branch. 3. Commit the code and at least one test covering your changes to a named branch in your fork. - 4. Put a line in the [CHANGELOG](https://github.com/carlhuda/bundler/blob/master/CHANGELOG.md) summarizing your changes under the next release under the “Bugfixes” heading. + 4. Put a line in the [CHANGELOG](https://github.com/bundler/bundler/blob/master/CHANGELOG.md) summarizing your changes under the next release under the “Bugfixes” heading. 5. Send us a [pull request](https://help.github.com/articles/using-pull-requests) from your bugfix branch. Finally, the ticket may be a duplicate of another older ticket. If you notice a ticket is a duplicate, simply comment on the ticket noting the original ticket’s number. For example, you could say “This is a duplicate of issue #42, and can be closed”. @@ -71,16 +90,16 @@ Bundler has two main sources of documentation: the built-in help (including usag If you’d like to submit a patch to the man pages, follow the steps for adding a feature above. All of the man pages are located in the `man` directory. Just use the “Documentation” heading when you describe what you did in the changelog. -If you have a suggestion or proposed change for [gembundler.com](http://gembundler.com), please open an issue or send a pull request to the [bundler-site-middleman](https://github.com/bundler/bundler-site-middleman) repository. +If you have a suggestion or proposed change for [gembundler.com](http://gembundler.com), please open an issue or send a pull request to the [bundler-site](https://github.com/bundler/bundler-site) repository. # Community Community is an important part of all we do. If you’d like to be part of the Bundler community, you can jump right in and start helping make Bundler better for everyone who uses it. -It would be tremendously helpful to have more people answering questions about Bundler (and often simply about Rubygems or Ruby itself) in our [issue tracker](https://github.com/carlhuda/bundler/issues) or on [Stack Overflow](http://stackoverflow.com/questions/tagged/bundler). +It would be tremendously helpful to have more people answering questions about Bundler (and often simply about Rubygems or Ruby itself) in our [issue tracker](https://github.com/bundler/bundler/issues) or on [Stack Overflow](http://stackoverflow.com/questions/tagged/bundler). -Additional documentation and explanation is always helpful, too. If you have any suggestions for the Bundler website [gembundler.com](http://www.gembundler.com), we would absolutely love it if you opened an issue or pull request on the [bundler-site-middleman](https://github.com/bundler/bundler-site-middleman) repository. +Additional documentation and explanation is always helpful, too. If you have any suggestions for the Bundler website [gembundler.com](http://www.gembundler.com), we would absolutely love it if you opened an issue or pull request on the [bundler-site](https://github.com/bundler/bundler-site) repository. Finally, sharing your experiences and discoveries by writing them up is a valuable way to help others who have similar problems or experiences in the future. You can write a blog post, create an example and commit it to Github, take screenshots, or make videos. @@ -89,7 +108,7 @@ Examples of how Bundler is used help everyone, and we’ve discovered that peopl If you let someone on the core team know you wrote about Bundler, we will add your post to the list of Bundler resources on the Github project wiki. -# Your first bugfix +# Your first commits If you’re interested in contributing to Bundler, that’s awesome! We’d love your help. @@ -78,6 +78,6 @@ If you are using Rails 2.3, please also include: If you have either `rubygems-bundler` or `open_gem` installed, please try removing them and then following the troubleshooting steps above before opening a new ticket. -[Create a gist](https://gist.github.com) containing all of that information, then visit the [Bundler issue tracker](https://github.com/carlhuda/bundler/issues) and [create a ticket](https://github.com/carlhuda/bundler/issues/new) describing your problem and linking to your gist. +[Create a gist](https://gist.github.com) containing all of that information, then visit the [Bundler issue tracker](https://github.com/bundler/bundler/issues) and [create a ticket](https://github.com/bundler/bundler/issues/new) describing your problem and linking to your gist. Thanks for reporting issues and helping make Bundler better! diff --git a/LICENSE.md b/LICENSE.md index 5e89f93c05..e356f59f94 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Portions copyright (c) 2010 Andre Arko +Portions copyright (c) 2010 Andre Arko Portions copyright (c) 2009 Engine Yard MIT License @@ -1,5 +1,7 @@ -# Bundler: a gem to bundle gems [![Build Status](https://secure.travis-ci.org/carlhuda/bundler.png?branch=1-3-stable)](http://travis-ci.org/carlhuda/bundler) +[![Code Climate](https://codeclimate.com/github/bundler/bundler.png)](https://codeclimate.com/github/bundler/bundler) +[![Build Status](https://secure.travis-ci.org/bundler/bundler.png?branch=1-3-stable)](http://travis-ci.org/bundler/bundler) +# Bundler: a gem to bundle gems Bundler keeps ruby applications running the same code on every machine. It does this by managing the gems that the application depends on. Given a list of gems, it can automatically download and install those gems, as well as any other gems needed by the gems that are listed. Before installing gems, it checks the versions of every gem to make sure that they are compatible, and can all be loaded at the same time. After the gems have been installed, Bundler can help you update some or all of them when new versions become available. Finally, it records the exact versions that have been installed, so that others can install the exact same gems. @@ -18,15 +20,15 @@ See [gembundler.com](http://gembundler.com) for the full documentation. ### Troubleshooting -For help with common problems, see [ISSUES](https://github.com/carlhuda/bundler/blob/master/ISSUES.md). +For help with common problems, see [ISSUES](https://github.com/bundler/bundler/blob/master/ISSUES.md). ### Contributing -If you'd like to contribute to Bundler, that's awesome, and we <3 you. There's a guide to contributing to Bundler (both code and general help) over in [CONTRIBUTE](https://github.com/carlhuda/bundler/blob/master/CONTRIBUTE.md) +If you'd like to contribute to Bundler, that's awesome, and we <3 you. There's a guide to contributing to Bundler (both code and general help) over in [DEVELOPMENT](https://github.com/bundler/bundler/blob/master/DEVELOPMENT.md) ### Development -To see what has changed in recent versions of Bundler, see the [CHANGELOG](https://github.com/carlhuda/bundler/blob/master/CHANGELOG.md). +To see what has changed in recent versions of Bundler, see the [CHANGELOG](https://github.com/bundler/bundler/blob/master/CHANGELOG.md). The `master` branch contains our current progress towards version 1.3. Versions 1.0 to 1.2 each have their own stable branches. Please submit bugfixes as pull requests to the stable branch for the version you would like to fix. @@ -85,10 +85,11 @@ begin system "sudo rm -rf #{File.expand_path('../tmp/sudo_gem_home', __FILE__)}" end + # Rubygems specs by version namespace :rubygems do - # Rubygems specs by version rubyopt = ENV["RUBYOPT"] - %w(master v1.3.6 v1.3.7 v1.4.2 v1.5.3 v1.6.2 v1.7.2 v1.8.25 v2.0.2).each do |rg| + # When editing this list, also edit .travis.yml! + %w(master v1.3.6 v1.3.7 v1.4.2 v1.5.3 v1.6.2 v1.7.2 v1.8.25 v2.0.3).each do |rg| desc "Run specs with Rubygems #{rg}" RSpec::Core::RakeTask.new(rg) do |t| t.rspec_opts = %w(-fs --color) @@ -219,18 +220,4 @@ end task :build => ["man:clean", "man:build"] task :release => ["man:clean", "man:build"] -namespace :vendor do - desc "Build the vendor dir" - task :build => :clean do - sh "git clone git://github.com/wycats/thor.git lib/bundler/vendor/tmp" - sh "mv lib/bundler/vendor/tmp/lib/* lib/bundler/vendor/" - rm_rf "lib/bundler/vendor/tmp" - end - - desc "Clean the vendor dir" - task :clean do - rm_rf "lib/bundler/vendor" - end -end - task :default => :spec diff --git a/bin/bundle b/bin/bundle index fc2aca105a..a6088f83b7 100755 --- a/bin/bundle +++ b/bin/bundle @@ -1,7 +1,4 @@ #!/usr/bin/env ruby - -# Trap interrupts to quit cleanly. See -# https://twitter.com/mitchellh/status/283014103189053442 Signal.trap("INT") { exit 1 } require 'bundler' @@ -16,10 +13,8 @@ $:.each do |path| end require 'bundler/cli' +# Force Thor to raise exceptions so we can exit non-zero. +ENV["THOR_DEBUG"] = "1" + require 'bundler/friendly_errors' -Bundler.with_friendly_errors { - # Set debug flag so we can rescue Thor::error's - # and set the correct exit code. - ENV["THOR_DEBUG"] = "1" - Bundler::CLI.start -} +Bundler.with_friendly_errors { Bundler::CLI.start } diff --git a/lib/bundler.rb b/lib/bundler.rb index 5e9527a2ac..883917cb3a 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -5,6 +5,8 @@ require 'bundler/gem_path_manipulation' require 'bundler/rubygems_ext' require 'bundler/rubygems_integration' require 'bundler/version' +require 'bundler/constants' +require 'bundler/current_ruby' module Bundler preserve_gem_path @@ -48,25 +50,22 @@ module Bundler end end - class GemfileNotFound < BundlerError; status_code(10) ; end - class GemNotFound < BundlerError; status_code(7) ; end - class GemfileError < BundlerError; status_code(4) ; end - class InstallError < BundlerError; status_code(5) ; end - class InstallHookError < BundlerError; status_code(6) ; end - class PathError < BundlerError; status_code(13) ; end - class GitError < BundlerError; status_code(11) ; end - class DeprecatedError < BundlerError; status_code(12) ; end - class GemspecError < BundlerError; status_code(14) ; end - class InvalidOption < BundlerError; status_code(15) ; end - class ProductionError < BundlerError; status_code(16) ; end - class HTTPError < BundlerError; status_code(17) ; end - class RubyVersionMismatch < BundlerError; status_code(18) ; end - class SecurityError < BundlerError; status_code(19) ; end - class LockfileError < BundlerError; status_code(20) ; end - - WINDOWS = RbConfig::CONFIG["host_os"] =~ %r!(msdos|mswin|djgpp|mingw)! - FREEBSD = RbConfig::CONFIG["host_os"] =~ /bsd/ - NULL = WINDOWS ? "NUL" : "/dev/null" + class GemfileNotFound < BundlerError; status_code(10) ; end + class GemNotFound < BundlerError; status_code(7) ; end + class GemfileError < BundlerError; status_code(4) ; end + class InstallError < BundlerError; status_code(5) ; end + class InstallHookError < BundlerError; status_code(6) ; end + class PathError < BundlerError; status_code(13) ; end + class GitError < BundlerError; status_code(11) ; end + class DeprecatedError < BundlerError; status_code(12) ; end + class GemspecError < BundlerError; status_code(14) ; end + class InvalidOption < BundlerError; status_code(15) ; end + class ProductionError < BundlerError; status_code(16) ; end + class HTTPError < BundlerError; status_code(17) ; end + class RubyVersionMismatch < BundlerError; status_code(18) ; end + class SecurityError < BundlerError; status_code(19) ; end + class LockfileError < BundlerError; status_code(20) ; end + class CyclicDependencyError < BundlerError; status_code(21) ; end # Internal errors, should be rescued class VersionConflict < BundlerError @@ -80,8 +79,7 @@ module Bundler status_code(6) end - class InvalidSpecSet < StandardError; end - class MarshalError < StandardError; end + class MarshalError < StandardError; end class << self attr_writer :ui, :bundle_path @@ -140,6 +138,11 @@ module Bundler Bundler::Environment.new(root, definition) end + # Returns an instance of Bundler::Definition for given Gemfile and lockfile + # + # @param unlock [Hash, Boolean, nil] Gems that have been requested + # to be updated or true if all gems should be updated + # @return [Bundler::Definition] def definition(unlock = nil) @definition = nil if unlock @definition ||= begin @@ -262,7 +265,7 @@ module Bundler bin_dir = bin_dir.parent until bin_dir.exist? # if any directory is not writable, we need sudo - dirs = [path, bin_dir] | Dir[path.join('*')] + dirs = [path, bin_dir] | Dir[path.join('*').to_s] sudo_needed = dirs.find{|d| !File.writable?(d) } end @@ -290,7 +293,19 @@ module Bundler end def sudo(str) - `sudo -p 'Enter your password to install the bundled RubyGems to your system: ' #{str}` + prompt = "\n\n" + <<-PROMPT.gsub(/^ {6}/, '').strip + " " + Your user account isn't allowed to install to the system Rubygems. + You can cancel this installation and run: + + bundle install --path vendor/bundle + + to install the gems into ./vendor/bundle/, or you can enter your password + and install the bundled gems to Rubygems using sudo. + + Password: + PROMPT + + `sudo -p "#{prompt}" #{str}` end def read_file(file) @@ -316,7 +331,7 @@ module Bundler path = Pathname.new(file) # Eval the gemspec from its parent directory, because some gemspecs # depend on "./" relative paths. - Dir.chdir(path.dirname.to_s) do + SharedHelpers.chdir(path.dirname.to_s) do contents = path.read if contents[0..2] == "---" # YAML header eval_yaml_gemspec(path, contents) diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 210313ae1d..5953f5a025 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -12,6 +12,7 @@ module Bundler rescue UnknownArgumentError => e raise InvalidOption, e.message ensure + options ||= {} Bundler.ui = UI::Shell.new(options) Bundler.ui.level = "debug" if options["verbose"] end @@ -173,6 +174,9 @@ module Bundler "Gem trust policy (like gem install -P). Must be one of " + Bundler.rubygems.security_policies.keys.join('|') unless Bundler.rubygems.security_policies.empty? + method_option "jobs", :aliases => "-j", :type => :numeric, :banner => + "Specify the number of jobs to run in parallel" + def install opts = options.dup if opts[:without] @@ -224,6 +228,8 @@ module Bundler opts[:system] = true end + opts["no-cache"] ||= opts[:local] + # Can't use Bundler.settings for this because settings needs gemfile.dirname Bundler.settings[:path] = nil if opts[:system] Bundler.settings[:path] = "vendor/bundle" if opts[:deployment] @@ -245,7 +251,7 @@ module Bundler definition = Bundler.definition definition.validate_ruby! Installer.install(Bundler.root, definition, opts) - Bundler.load.cache if Bundler.root.join("vendor/cache").exist? && !options["no-cache"] + Bundler.load.cache if Bundler.root.join("vendor/cache").exist? && !opts["no-cache"] if Bundler.settings[:path] absolute_path = File.expand_path(Bundler.settings[:path]) @@ -373,18 +379,26 @@ module Bundler "binstub destination directory (default bin)" method_option "force", :type => :boolean, :default => false, :banner => "overwrite existing binstubs if they exist" - def binstubs(gem_name) + def binstubs(*gems) Bundler.definition.validate_ruby! Bundler.settings[:bin] = options["path"] if options["path"] Bundler.settings[:bin] = nil if options["path"] && options["path"].empty? installer = Installer.new(Bundler.root, Bundler.definition) - spec = installer.specs.find{|s| s.name == gem_name } - raise GemNotFound, not_found_message(gem_name, Bundler.definition.specs) unless spec - if spec.name == "bundler" - Bundler.ui.warn "Sorry, Bundler can only be run via Rubygems." - else - installer.generate_bundler_executable_stubs(spec, :force => options[:force], :binstubs_cmd => true) + if gems.empty? + Bundler.ui.error "`bundle binstubs` needs at least one gem to run." + exit 1 + end + + gems.each do |gem_name| + spec = installer.specs.find{|s| s.name == gem_name } + raise GemNotFound, not_found_message(gem_name, Bundler.definition.specs) unless spec + + if spec.name == "bundler" + Bundler.ui.warn "Sorry, Bundler can only be run via Rubygems." + else + installer.generate_bundler_executable_stubs(spec, :force => options[:force], :binstubs_cmd => true) + end end end @@ -404,6 +418,8 @@ module Bundler Bundler.definition.validate_ruby! current_specs = Bundler.ui.silence { Bundler.load.specs } + current_dependencies = {} + Bundler.ui.silence { Bundler.load.dependencies.each { |dep| current_dependencies[dep.name] = dep } } if gems.empty? && sources.empty? # We're doing a full update @@ -417,7 +433,8 @@ module Bundler out_count = 0 # Loop through the current specs - current_specs.sort_by { |s| s.name }.each do |current_spec| + gemfile_specs, dependency_specs = current_specs.partition { |spec| current_dependencies.has_key? spec.name } + [gemfile_specs.sort_by(&:name), dependency_specs.sort_by(&:name)].flatten.each do |current_spec| next if !gems.empty? && !gems.include?(current_spec.name) active_spec = definition.index[current_spec.name].sort_by { |b| b.version } @@ -442,7 +459,9 @@ module Bundler spec_version = "#{active_spec.version}#{active_spec.git_version}" current_version = "#{current_spec.version}#{current_spec.git_version}" - Bundler.ui.info " * #{active_spec.name} (#{spec_version} > #{current_version})" + dependency = current_dependencies[current_spec.name] + dependency_version = %|Gemfile specifies "#{dependency.requirement}"| if dependency && dependency.specific? + Bundler.ui.info " * #{active_spec.name} (#{spec_version} > #{current_version}) #{dependency_version}".rstrip out_count += 1 end Bundler.ui.debug "from #{active_spec.loaded_from}" @@ -474,6 +493,7 @@ module Bundler desc "package", "Locks and then caches all of the gems into vendor/cache" method_option "no-prune", :type => :boolean, :banner => "Don't remove stale gems from the cache." method_option "all", :type => :boolean, :banner => "Include all sources (including path and git)." + method_option "quiet", :type => :boolean, :banner => "Only output warnings and errors." long_desc <<-D The package command will copy the .gem files for every gem in the bundle into the directory ./vendor/cache. If you then check that directory into your source @@ -481,6 +501,7 @@ module Bundler bundle without having to download any additional gems. D def package + Bundler.ui.level = "warn" if options[:quiet] setup_cache_all install # TODO: move cache contents here now that all bundles are locked diff --git a/lib/bundler/constants.rb b/lib/bundler/constants.rb new file mode 100644 index 0000000000..d9bf6830d4 --- /dev/null +++ b/lib/bundler/constants.rb @@ -0,0 +1,5 @@ +module Bundler + WINDOWS = RbConfig::CONFIG["host_os"] =~ %r!(msdos|mswin|djgpp|mingw)! + FREEBSD = RbConfig::CONFIG["host_os"] =~ /bsd/ + NULL = WINDOWS ? "NUL" : "/dev/null" +end diff --git a/lib/bundler/current_ruby.rb b/lib/bundler/current_ruby.rb new file mode 100644 index 0000000000..bbb9e41303 --- /dev/null +++ b/lib/bundler/current_ruby.rb @@ -0,0 +1,88 @@ +module Bundler + # Returns current version of Ruby + # + # @return [CurrentRuby] Current version of Ruby + def self.current_ruby + @current_ruby ||= CurrentRuby.new + end + + class CurrentRuby + def on_18? + RUBY_VERSION =~ /^1\.8/ + end + + def on_19? + RUBY_VERSION =~ /^1\.9/ + end + + def on_20? + RUBY_VERSION =~ /^2\.0/ + end + + def ruby? + !mswin? && (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby" || RUBY_ENGINE == "rbx" || RUBY_ENGINE == "maglev") + end + + def ruby_18? + ruby? && on_18? + end + + def ruby_19? + ruby? && on_19? + end + + def ruby_20? + ruby? && on_20? + end + + def mri? + !mswin? && (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby") + end + + def mri_18? + mri? && on_18? + end + + def mri_19? + mri? && on_19? + end + + + def mri_20? + mri? && on_20? + end + + def rbx? + ruby? && defined?(RUBY_ENGINE) && RUBY_ENGINE == "rbx" + end + + def jruby? + defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" + end + + def maglev? + defined?(RUBY_ENGINE) && RUBY_ENGINE == "maglev" + end + + def mswin? + Bundler::WINDOWS + end + + def mingw? + Bundler::WINDOWS && Gem::Platform.local.os == "mingw32" + end + + def mingw_18? + mingw? && on_18? + end + + def mingw_19? + mingw? && on_19? + end + + def mingw_20? + mingw? && on_20? + end + + end +end diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 430182b054..be0246fbfe 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -8,6 +8,13 @@ module Bundler attr_reader :dependencies, :platforms, :sources, :ruby_version, :locked_deps + # Given a gemfile and lockfile creates a Bundler definition + # + # @param gemfile [Pathname] Path to Gemfile + # @param lockfile [Pathname,nil] Path to Gemfile.lock + # @param unlock [Hash, Boolean, nil] Gems that have been requested + # to be updated or true if all gems should be updated + # @return [Bundler::Definition] def self.build(gemfile, lockfile, unlock) unlock ||= {} gemfile = Pathname.new(gemfile).expand_path @@ -19,18 +26,24 @@ module Bundler Dsl.evaluate(gemfile, lockfile, unlock) end -=begin - How does the new system work? - === - * Load information from Gemfile and Lockfile - * Invalidate stale locked specs - * All specs from stale source are stale - * All specs that are reachable only through a stale - dependency are stale. - * If all fresh dependencies are satisfied by the locked - specs, then we can try to resolve locally. -=end + # + # How does the new system work? + # + # * Load information from Gemfile and Lockfile + # * Invalidate stale locked specs + # * All specs from stale source are stale + # * All specs that are reachable only through a stale + # dependency are stale. + # * If all fresh dependencies are satisfied by the locked + # specs, then we can try to resolve locally. + # + # @param lockfile [Pathname] Path to Gemfile.lock + # @param dependencies [Array(Bundler::Dependency)] array of dependencies from Gemfile + # @param sources [Array(Bundler::Source::Rubygems)] + # @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) @unlocking = unlock == true || !unlock.empty? @@ -109,6 +122,12 @@ module Bundler specs end + # For given dependency list returns a SpecSet with Gemspec of all the required + # dependencies. + # 1. The method first resolves the dependencies specified in Gemfile + # 2. After that it tries and fetches gemspec of resolved dependencies + # + # @return [Bundler::SpecSet] def specs @specs ||= begin specs = resolve.materialize(requested_dependencies) @@ -159,6 +178,11 @@ module Bundler specs.for(expand_dependencies(deps)) end + # Resolve all the dependencies specified in Gemfile. It ensures that + # dependencies that have been already resolved via locked file and are fresh + # are reused when resolving dependencies + # + # @return [SpecSet] resolved dependencies def resolve @resolve ||= begin if Bundler.settings[:frozen] || (!@unlocking && nothing_changed?) diff --git a/lib/bundler/dependency.rb b/lib/bundler/dependency.rb index 7f153f236c..1fb6d8e1d4 100644 --- a/lib/bundler/dependency.rb +++ b/lib/bundler/dependency.rb @@ -70,7 +70,9 @@ module Bundler def current_platform? return true if @platforms.empty? - @platforms.any? { |p| send("#{p}?") } + @platforms.any? { |p| + Bundler.current_ruby.send("#{p}?") + } end def to_lock @@ -79,84 +81,11 @@ module Bundler out << "\n" end - private - def on_18? - RUBY_VERSION =~ /^1\.8/ + def specific? + super + rescue NoMethodError + requirement != ">= 0" end - - def on_19? - RUBY_VERSION =~ /^1\.9/ - end - - def on_20? - RUBY_VERSION =~ /^2\.0/ - end - - def ruby? - !mswin? && (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby" || RUBY_ENGINE == "rbx" || RUBY_ENGINE == "maglev") - end - - def ruby_18? - ruby? && on_18? - end - - def ruby_19? - ruby? && on_19? - end - - def ruby_20? - ruby? && on_20? - end - - def mri? - !mswin? && (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby") - end - - def mri_18? - mri? && on_18? - end - - def mri_19? - mri? && on_19? - end - - - def mri_20? - mri? && on_20? - end - - def rbx? - ruby? && defined?(RUBY_ENGINE) && RUBY_ENGINE == "rbx" - end - - def jruby? - defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" - end - - def maglev? - defined?(RUBY_ENGINE) && RUBY_ENGINE == "maglev" - end - - def mswin? - Bundler::WINDOWS - end - - def mingw? - Bundler::WINDOWS && Gem::Platform.local.os == "mingw32" - end - - def mingw_18? - mingw? && on_18? - end - - def mingw_19? - mingw? && on_19? - end - - def mingw_20? - mingw? && on_20? - end - end end diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index d959eea6d3..7f88a1bc5d 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -42,8 +42,9 @@ module Bundler path = opts && opts[:path] || '.' name = opts && opts[:name] || '{,*}' development_group = opts && opts[:development_group] || :development - path = File.expand_path(path, Bundler.default_gemfile.dirname) - gemspecs = Dir[File.join(path, "#{name}.gemspec")] + expanded_path = File.expand_path(path, Bundler.default_gemfile.dirname) + + gemspecs = Dir[File.join(expanded_path, "#{name}.gemspec")] case gemspecs.size when 1 @@ -56,9 +57,9 @@ module Bundler end end when 0 - raise InvalidOption, "There are no gemspecs at #{path}." + raise InvalidOption, "There are no gemspecs at #{expanded_path}." else - raise InvalidOption, "There are multiple gemspecs at #{path}. Please use the :name option to specify which one." + raise InvalidOption, "There are multiple gemspecs at #{expanded_path}. Please use the :name option to specify which one." end end @@ -229,7 +230,7 @@ module Bundler if github = opts.delete("github") github = "#{github}/#{github}" unless github.include?("/") - opts["git"] = "git://github.com/#{github}.git" + opts["git"] = "https://github.com/#{github}.git" end if gist = opts.delete("gist") diff --git a/lib/bundler/fetcher.rb b/lib/bundler/fetcher.rb index f5ad0347e7..627d7d4ade 100644 --- a/lib/bundler/fetcher.rb +++ b/lib/bundler/fetcher.rb @@ -4,11 +4,6 @@ module Bundler # Handles all the fetching with the rubygems server class Fetcher - # How many redirects to allew in one request - REDIRECT_LIMIT = 5 - # how long to wait for each gemcutter API call - API_TIMEOUT = 10 - # This error is raised if the API returns a 413 (only printed in verbose) class FallbackError < HTTPError; end # This is the error raised if OpenSSL fails the cert verification @@ -33,7 +28,7 @@ module Bundler end class << self - attr_accessor :disable_endpoint + attr_accessor :disable_endpoint, :api_timeout, :redirect_limit, :max_retries @@spec_fetch_map ||= {} @@ -65,6 +60,13 @@ module Bundler end def initialize(remote_uri) + # How many redirects to allew in one request + @redirect_limit = 5 + # How long to wait for each gemcutter API call + @api_timeout = 10 + # How many retries for the gemcutter API call + @max_retries = 3 + @remote_uri = remote_uri @public_uri = remote_uri.dup @public_uri.user, @public_uri.password = nil, nil # don't print these @@ -77,7 +79,7 @@ module Bundler raise SSLError if @remote_uri.scheme == "https" @connection = Net::HTTP.new(@remote_uri.host, @remote_uri.port) end - @connection.read_timeout = API_TIMEOUT + @connection.read_timeout = @api_timeout Socket.do_not_reverse_lookup = true end @@ -85,17 +87,28 @@ module Bundler # fetch a gem specification def fetch_spec(spec) spec = spec - [nil, 'ruby', ''] - spec_file_name = "#{spec.join '-'}.gemspec.rz" - - uri = URI.parse("#{@remote_uri}#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}") + spec_file_name = "#{spec.join '-'}.gemspec" - spec_rz = (uri.scheme == "file") ? Gem.read_binary(uri.path) : fetch(uri) - Bundler.load_marshal Gem.inflate(spec_rz) + uri = URI.parse("#{@remote_uri}#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}.rz") + if uri.scheme == 'file' + Bundler.load_marshal Gem.inflate(Gem.read_binary(uri.path)) + elsif cached_spec_path = gemspec_cached_path(spec_file_name) + Bundler.load_gemspec(cached_spec_path) + else + Bundler.load_marshal Gem.inflate(fetch(uri)) + end rescue MarshalError => e 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) index = Index.new @@ -157,14 +170,19 @@ module Bundler # 2. Marshal blob doesn't load properly # 3. One of the YAML gemspecs has the Syck::DefaultKey problem rescue HTTPError, MarshalError, GemspecError => e - @use_api = false - # new line now that the dots are over Bundler.ui.info "" unless Bundler.ui.debug? Bundler.ui.debug "Error during API request. #{e.class}: #{e.message}" Bundler.ui.debug e.backtrace.join(" ") + @current_retries ||= 0 + if @current_retries < @max_retries + @current_retries += 1 + retry + end + + @use_api = false return nil end @@ -194,15 +212,15 @@ module Bundler HTTP_ERRORS << Net::HTTP::Persistent::Error if defined?(Net::HTTP::Persistent) def fetch(uri, counter = 0) - raise HTTPError, "Too many redirects" if counter >= REDIRECT_LIMIT + raise HTTPError, "Too many redirects" if counter >= @redirect_limit begin Bundler.ui.debug "Fetching from: #{uri}" + req = Net::HTTP::Get.new uri.request_uri + req.basic_auth(uri.user, uri.password) if uri.user && uri.password if defined?(Net::HTTP::Persistent) - response = @connection.request(uri) + response = @connection.request(uri, req) else - req = Net::HTTP::Get.new uri.request_uri - req.basic_auth(uri.user, uri.password) if uri.user && uri.password response = @connection.request(req) end rescue OpenSSL::SSL::SSLError diff --git a/lib/bundler/gem_helper.rb b/lib/bundler/gem_helper.rb index 9f3763db92..6b7a1760ca 100644 --- a/lib/bundler/gem_helper.rb +++ b/lib/bundler/gem_helper.rb @@ -24,7 +24,7 @@ module Bundler def initialize(base = nil, name = nil) Bundler.ui = UI::Shell.new - @base = (base ||= Dir.pwd) + @base = (base ||= SharedHelpers.pwd) gemspecs = name ? [File.join(base, "#{name}.gemspec")] : Dir[File.join(base, "{,*}.gemspec")] raise "Unable to determine name from existing gemspec. Use :name => 'gemname' in #install_tasks to manually set it." unless gemspecs.size == 1 @spec_path = gemspecs.first @@ -153,7 +153,7 @@ module Bundler cmd << " 2>&1" outbuf = '' Bundler.ui.debug(cmd) - Dir.chdir(base) { + SharedHelpers.chdir(base) { outbuf = `#{cmd}` if $? == 0 block.call(outbuf) if block diff --git a/lib/bundler/gem_installer.rb b/lib/bundler/gem_installer.rb index 7d84939853..749ff53653 100644 --- a/lib/bundler/gem_installer.rb +++ b/lib/bundler/gem_installer.rb @@ -5,5 +5,14 @@ module Bundler def check_executable_overwrite(filename) # Bundler needs to install gems regardless of binstub overwriting end + + if Bundler.current_ruby.mswin? || Bundler.current_ruby.jruby? + def build_extensions + # Gain the lock because rubygems use Dir.chdir + SharedHelpers.chdir('.') do + super + end + end + end end end diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb index 7f19e94d19..f6c1eaefc7 100644 --- a/lib/bundler/installer.rb +++ b/lib/bundler/installer.rb @@ -1,5 +1,6 @@ require 'erb' require 'rubygems/dependency_installer' +require 'bundler/parallel_workers' module Bundler class Installer < Environment @@ -87,8 +88,14 @@ module Bundler # as dependencies might actually affect the installation of # the gem. Installer.post_install_messages = {} - specs.each do |spec| - install_gem_from_spec(spec, options[:standalone]) + + size = options[:jobs] || 1 + size = [size, 1].max + + if size > 1 && can_install_parallely? + install_in_parallel size, options[:standalone] + else + install_sequentially options[:standalone] end lock @@ -102,13 +109,12 @@ module Bundler # Fetch the build settings, if there are any settings = Bundler.settings["build.#{spec.name}"] + message = nil Bundler.rubygems.with_build_args [settings] do - spec.source.install(spec) - Bundler.ui.debug "from #{spec.loaded_from} " + message = spec.source.install(spec) + Bundler.ui.debug " #{spec.name} (#{spec.version}) from #{spec.loaded_from}" end - # newline comes after installing, some gems say "with native extensions" - Bundler.ui.info "" if Bundler.settings[:bin] && standalone generate_standalone_bundler_executable_stubs(spec) elsif Bundler.settings[:bin] @@ -116,6 +122,7 @@ module Bundler end FileUtils.rm_rf(Bundler.tmp) + message rescue Exception => e # install hook failed raise e if e.is_a?(Bundler::InstallHookError) || e.is_a?(Bundler::SecurityError) @@ -184,6 +191,16 @@ module Bundler end private + def can_install_parallely? + if Bundler.current_ruby.mri? || Bundler.rubygems.provides?(">= 2.1.0.rc") + true + else + Bundler.ui.warn "Rubygems #{Gem::VERSION} is not threadsafe, so your "\ + "gems must be installed one at a time. Upgrade to Rubygems 2.1 or "\ + "higher to enable parallel gem installation." + false + end + end def generate_standalone_bundler_executable_stubs(spec) # double-assignment to avoid warnings about variables that will be used by ERB @@ -236,5 +253,57 @@ module Bundler end end end + + def install_sequentially(standalone) + specs.each do |spec| + message = install_gem_from_spec spec, standalone + 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 = ParallelWorkers.worker_pool size, lambda { |name| + spec = name2spec[name] + message = install_gem_from_spec spec, standalone + { :name => spec.name, :post_install => message } + } + specs.each do |spec| + deps = spec.dependencies.select { |dep| dep.type != :development } + if deps.empty? + worker_pool.enq spec.name + enqueued[spec.name] = true + end + end + + 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 + remains.keys.each do |name| + next if enqueued[name] + spec = name2spec[name] + deps = spec.dependencies.select { |dep| remains[dep.name] and dep.type != :development } + if deps.empty? + worker_pool.enq name + enqueued[name] = true + end + end + end + message + ensure + worker_pool && worker_pool.stop + end end end diff --git a/lib/bundler/parallel_workers.rb b/lib/bundler/parallel_workers.rb new file mode 100644 index 0000000000..3071b49ac1 --- /dev/null +++ b/lib/bundler/parallel_workers.rb @@ -0,0 +1,18 @@ +require 'thread' + +require "bundler/parallel_workers/worker" + +module Bundler + module ParallelWorkers + autoload :UnixWorker, "bundler/parallel_workers/unix_worker" + autoload :ThreadWorker, "bundler/parallel_workers/thread_worker" + + def self.worker_pool(size, job) + if Bundler.current_ruby.mswin? || Bundler.current_ruby.jruby? + ThreadWorker.new(size, job) + else + UnixWorker.new(size, job) + end + end + end +end diff --git a/lib/bundler/parallel_workers/thread_worker.rb b/lib/bundler/parallel_workers/thread_worker.rb new file mode 100644 index 0000000000..eed69dbd3e --- /dev/null +++ b/lib/bundler/parallel_workers/thread_worker.rb @@ -0,0 +1,27 @@ +module Bundler + module ParallelWorkers + class ThreadWorker < Worker + + private + + # On platforms where fork is not available + # use Threads for parallely downloading gems + # + # @param size [Integer] Size of thread worker pool + # @param func [Proc] Job to be run inside thread worker pool + def prepare_workers(size, func) + @threads = size.times.map do |i| + Thread.start do + Thread.current.abort_on_exception = true + loop do + obj = @request_queue.deq + break if obj.equal? POISON + @response_queue.enq func.call(obj) + end + end + end + end + + end + end +end diff --git a/lib/bundler/parallel_workers/unix_worker.rb b/lib/bundler/parallel_workers/unix_worker.rb new file mode 100644 index 0000000000..1d5fee6697 --- /dev/null +++ b/lib/bundler/parallel_workers/unix_worker.rb @@ -0,0 +1,88 @@ +module Bundler + module ParallelWorkers + # UnixWorker is used only on platforms where fork is available. The way + # this code works is, it forks a preconfigured number of workers and then + # It starts preconfigured number of threads that write to the connected pipe. + class UnixWorker < Worker + + class JobHandler < Struct.new(:pid, :io_r, :io_w) + def work(obj) + Marshal.dump obj, io_w + Marshal.load io_r + rescue IOError + nil + end + end + + private + + # Start forked workers for downloading gems. This version of worker + # is only used on platforms where fork is available. + # + # @param size [Integer] Size of worker pool + # @param func [Proc] Job that should be executed in the worker + def prepare_workers(size, func) + @workers = size.times.map do + child_read, parent_write = IO.pipe + parent_read, child_write = IO.pipe + + pid = Process.fork do + begin + parent_read.close + parent_write.close + + while !child_read.eof? + obj = Marshal.load child_read + Marshal.dump func.call(obj), child_write + end + rescue Exception => e + begin + Marshal.dump WrappedException.new(e), child_write + rescue Errno::EPIPE + nil + end + ensure + child_read.close + child_write.close + end + end + + child_read.close + child_write.close + JobHandler.new pid, parent_read, parent_write + end + end + + # Start the threads whose job is basically to wait for incoming messages + # on request queue and write that message to the connected pipe. Also retrieve + # messages from child worker via connected pipe and write the message to response queue + # + # @param size [Integer] Number of threads to be started + def prepare_threads(size) + @threads = size.times.map do |i| + Thread.start do + worker = @workers[i] + Thread.current.abort_on_exception = true + loop do + obj = @request_queue.deq + break if obj.equal? POISON + @response_queue.enq worker.work(obj) + end + end + end + end + + # Kill the forked workers by sending SIGINT to them + def stop_workers + @workers.each do |worker| + worker.io_r.close + worker.io_w.close + Process.kill :INT, worker.pid + end + @workers.each do |worker| + Process.waitpid worker.pid + end + end + end + end +end diff --git a/lib/bundler/parallel_workers/worker.rb b/lib/bundler/parallel_workers/worker.rb new file mode 100644 index 0000000000..975b6a3342 --- /dev/null +++ b/lib/bundler/parallel_workers/worker.rb @@ -0,0 +1,68 @@ +module Bundler + module ParallelWorkers + class Worker + POISON = Object.new + + class WrappedException < StandardError + attr_reader :exception + def initialize(exn) + @exception = exn + end + end + + # Creates a worker pool of specified size + # + # @param size [Integer] Size of pool + # @param func [Proc] job to run in inside the worker pool + def initialize(size, func) + @request_queue = Queue.new + @response_queue = Queue.new + prepare_workers size, func + prepare_threads size + end + + # Enque a request to be executed in the worker pool + # + # @param obj [String] mostly it is name of spec that should be downloaded + def enq(obj) + @request_queue.enq obj + end + + # Retrieves results of job function being executed in worker pool + def deq + result = @response_queue.deq + if WrappedException === result + raise result.exception + end + result + end + + # Stop the forked workers and started threads + def stop + stop_workers + stop_threads + end + + private + # Stop the worker threads by sending a poison object down the request queue + # so as worker threads after retrieving it, shut themselves down + def stop_threads + @threads.each do + @request_queue.enq POISON + end + @threads.each do |thread| + thread.join + end + end + + # To be overridden by child classes + def prepare_threads(size) + end + + # To be overridden by child classes + def stop_workers + end + + end + end +end diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 60597e32a9..d2c2a0847f 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -1,4 +1,5 @@ require 'set' +require 'bundler/safe_catch' # This is the latest iteration of the gem dependency resolving algorithm. As of now, # it can resolve (as a success or failure) any set of gem dependencies we throw at it # in a reasonable amount of time. The most iterations I've seen it take is about 150. @@ -21,6 +22,9 @@ end module Bundler class Resolver + include SafeCatch + extend SafeCatch + ALL = Bundler::Dependency::PLATFORM_MAP.values.uniq.freeze class SpecGroup < Array @@ -125,7 +129,7 @@ module Bundler Bundler.ui.info "Resolving dependencies...", false base = SpecSet.new(base) unless base.is_a?(SpecSet) resolver = new(index, source_requirements, base) - result = catch(:success) do + result = safe_catch(:success) do resolver.start(requirements) raise resolver.version_conflict nil @@ -168,10 +172,10 @@ module Bundler resolve(reqs, activated) end - def resolve(reqs, activated) + def resolve(reqs, activated, depth = 0) # If the requirements are empty, then we are in a success state. Aka, all # gem dependencies have been resolved. - throw :success, successify(activated) if reqs.empty? + safe_throw :success, successify(activated) if reqs.empty? indicate_progress @@ -197,6 +201,8 @@ module Bundler # Pull off the first requirement so that we can resolve it current = reqs.shift + $stderr.puts "#{' ' * depth}#{current}" if ENV['DEBUG_RESOLVER_TREE'] + debug { "Attempting:\n #{current}"} # Check if the gem has already been activated, if it has, we will make sure @@ -228,7 +234,7 @@ module Bundler @gems_size[dep] ||= gems_size(dep) end - resolve(reqs, activated) + resolve(reqs, activated, depth + 1) else debug { " * [FAIL] Already activated" } @errors[existing.name] = [existing, current] @@ -248,7 +254,7 @@ module Bundler if parent && parent.name != 'bundler' debug { " -> Jumping to: #{parent.name}" } required_by = existing.respond_to?(:required_by) && existing.required_by.last - throw parent.name, required_by && required_by.name + safe_throw parent.name, required_by && required_by.name else # The original set of dependencies conflict with the base set of specs # passed to the resolver. This is by definition an impossible resolve. @@ -301,7 +307,7 @@ module Bundler end matching_versions.reverse_each do |spec_group| - conflict = resolve_requirement(spec_group, current, reqs.dup, activated.dup) + conflict = resolve_requirement(spec_group, current, reqs.dup, activated.dup, depth) conflicts << conflict if conflict end @@ -315,7 +321,7 @@ module Bundler # Choose the closest pivot in the stack that will affect the conflict errorpivot = (@stack & [req_name, current.required_by.last.name]).last debug { " -> Jumping to: #{errorpivot}" } - throw errorpivot, req_name + safe_throw errorpivot, req_name end end end @@ -330,14 +336,14 @@ module Bundler @stack.reverse_each do |savepoint| if conflicts.include?(savepoint) debug { " -> Jumping to: #{savepoint}" } - throw savepoint + safe_throw savepoint end end end end end - def resolve_requirement(spec_group, requirement, reqs, activated) + def resolve_requirement(spec_group, requirement, reqs, activated, depth) # We are going to try activating the spec. We need to keep track of stack of # requirements that got us to the point of activating this gem. spec_group.required_by.replace requirement.required_by @@ -366,9 +372,9 @@ module Bundler # jump back to this point and try another version of the gem. length = @stack.length @stack << requirement.name - retval = catch(requirement.name) do + retval = safe_catch(requirement.name) do # try to resolve the next option - resolve(reqs, activated) + resolve(reqs, activated, depth) end # clear the search cache since the catch means we couldn't meet the diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb index eeeed1ef16..7b808a69cc 100644 --- a/lib/bundler/rubygems_ext.rb +++ b/lib/bundler/rubygems_ext.rb @@ -52,7 +52,7 @@ module Gem def git_version if @loaded_from && File.exist?(File.join(full_gem_path, ".git")) - sha = Dir.chdir(full_gem_path){ `git rev-parse HEAD`.strip } + sha = Bundler::SharedHelpers.chdir(full_gem_path){ `git rev-parse HEAD`.strip } " #{sha[0..6]}" end end diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb index fc38cd6cde..bcd36ca4b9 100644 --- a/lib/bundler/rubygems_integration.rb +++ b/lib/bundler/rubygems_integration.rb @@ -6,6 +6,22 @@ require 'rubygems/config_file' module Bundler class RubygemsIntegration + def self.version + @version ||= Gem::Version.new(Gem::VERSION) + end + + def self.provides?(req_str) + Gem::Requirement.new(req_str).satisfied_by?(version) + end + + def version + self.class.version + end + + def provides?(req_str) + self.class.provides?(req_str) + end + def build_args Gem::Command.build_args end @@ -79,6 +95,14 @@ module Bundler Gem.path end + def spec_cache_dirs + @spec_cache_dirs ||= begin + dirs = gem_path.map {|dir| File.join(dir, 'specifications')} + dirs << Gem.spec_cache_dir if Gem.respond_to?(:spec_cache_dir) # Not in Rubygems 2.0.3 or earlier + dirs.uniq.select {|dir| File.directory? dir} + end + end + def marshal_spec_dir Gem::MARSHAL_SPEC_DIR end @@ -159,7 +183,7 @@ module Bundler end def build_gem(gem_dir, spec) - Dir.chdir(gem_dir) { build(spec) } + SharedHelpers.chdir(gem_dir) { build(spec) } end def download_gem(spec, uri, path) @@ -490,15 +514,15 @@ module Bundler end - if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.99.99') + if RubygemsIntegration.provides?(">= 1.99.99") @rubygems = RubygemsIntegration::Future.new - elsif Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.8.5') + elsif RubygemsIntegration.provides?('>= 1.8.5') @rubygems = RubygemsIntegration::Modern.new - elsif Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.8.0') + elsif RubygemsIntegration.provides?('>= 1.8.0') @rubygems = RubygemsIntegration::AlmostModern.new - elsif Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.7.0') + elsif RubygemsIntegration.provides?('>= 1.7.0') @rubygems = RubygemsIntegration::Transitional.new - elsif Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.4.0') + elsif RubygemsIntegration.provides?('>= 1.4.0') @rubygems = RubygemsIntegration::Legacy.new else # Rubygems 1.3.6 and 1.3.7 @rubygems = RubygemsIntegration::Ancient.new diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb index 9dd6a5f4f0..a5e51b15c0 100644 --- a/lib/bundler/runtime.rb +++ b/lib/bundler/runtime.rb @@ -50,6 +50,7 @@ module Bundler /^Missing \w+ (?:file\s*)?([^\s]+.rb)$/i, /^Missing API definition file in (.+)$/i, /^cannot load such file -- (.+)$/i, + /^dlopen\([^)]*\): Library not loaded: (.+)$/i, ] def require(*groups) @@ -68,6 +69,8 @@ module Bundler # dependency. If there are none, use the dependency's name # as the autorequire. Array(dep.autorequire || dep.name).each do |file| + # Allow `require: true` as an alias for `require: <name>` + file = dep.name if file == true required_file = file Kernel.require file end @@ -224,9 +227,13 @@ module Bundler rubyopt = [ENV["RUBYOPT"]].compact if rubyopt.empty? || rubyopt.first !~ /-rbundler\/setup/ rubyopt.unshift %|-rbundler/setup| - rubyopt.unshift %|-I#{File.expand_path('../..', __FILE__)}| ENV["RUBYOPT"] = rubyopt.join(' ') end + + # Set RUBYLIB + rubylib = (ENV["RUBYLIB"] || "").split(File::PATH_SEPARATOR) + rubylib.unshift File.expand_path('../..', __FILE__) + ENV["RUBYLIB"] = rubylib.uniq.join(File::PATH_SEPARATOR) end private diff --git a/lib/bundler/safe_catch.rb b/lib/bundler/safe_catch.rb new file mode 100644 index 0000000000..b71e51ad12 --- /dev/null +++ b/lib/bundler/safe_catch.rb @@ -0,0 +1,101 @@ +# SafeCatch provides a mechanism to safely deepen the stack, performing +# stack-unrolling similar to catch/throw, but using Fiber or Thread to avoid +# deepening the stack too quickly. +# +# The API is the same as that of catch/throw: SafeCatch#safe_catch takes a "tag" +# to be rescued when some code deeper in the process raises it. If the catch +# block completes successfully, that value is returned. If the tag is "thrown" +# by safe_throw, the tag's value is returned. Other exceptions propagate out as +# normal. +# +# The implementation, however, uses fibers or threads along with raise/rescue to +# handle "deepening" the stack and unrolling it. On implementations where Fiber +# is available, it will be used. If Fiber is not available, Thread will be used. +# If neither of these classes are available, Proc will be used, effectively +# deepening the stack for each recursion as in normal catch/throw. +# +# In order to avoid causing a new issue of creating too many fibers or threads, +# especially on implementations where fibers are actually backed by native +# threads, the "safe" recursion mechanism is only used every 20 recursions. +# Based on experiments with JRuby (which seems to suffer the most from +# excessively deep stacks), this appears to be a sufficient granularity to +# prevent stack overflow without spinning up excessive numbers of fibers or +# threads. This value can be adjusted with the BUNDLER_SAFE_RECURSE_EVERY env +# var; setting it to zero effectively disables safe recursion. + +module Bundler + module SafeCatch + def safe_catch(tag, &block) + if Bundler.current_ruby.jruby? + Internal.catch(tag, &block) + else + catch(tag, &block) + end + end + + def safe_throw(tag, value = nil) + if Bundler.current_ruby.jruby? + Internal.throw(tag, value) + else + throw(tag, value) + end + end + + module Internal + SAFE_RECURSE_EVERY = (ENV['BUNDLER_SAFE_RECURSE_EVERY'] || 20).to_i + + SAFE_RECURSE_CLASS, SAFE_RECURSE_START = case + when defined?(Fiber) + [Fiber, :resume] + when defined?(Thread) + [Thread, :join] + else + [Proc, :call] + end + + @recurse_count = 0 + + def self.catch(tag, &block) + @recurse_count += 1 + if SAFE_RECURSE_EVERY >= 0 && @recurse_count % SAFE_RECURSE_EVERY == 0 + SAFE_RECURSE_CLASS.new(&block).send(SAFE_RECURSE_START) + else + block.call + end + rescue Result.matcher(tag) + $!.value + end + + def self.throw(tag, value = nil) + raise Result.new(tag, value) + end + + class Result < StopIteration + def initialize(tag, value) + @tag = tag + @value = value + end + + attr_reader :tag, :value + + # The Matcher class is never instantiated; it is dup'ed and used as a + # rescue-clause argument to match Result exceptions based on their tags. + module Matcher + class << self + attr_accessor :tag + + def ===(other) + other.respond_to? :tag and @tag.equal? other.tag + end + end + end + + def self.matcher(tag) + matcher = Matcher.dup + matcher.tag = tag + matcher + end + end + end + end +end diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb index e6f4899f06..14e76a6cdb 100644 --- a/lib/bundler/shared_helpers.rb +++ b/lib/bundler/shared_helpers.rb @@ -1,7 +1,9 @@ require 'pathname' require 'rubygems' +require 'bundler/constants' require 'bundler/rubygems_integration' +require 'bundler/current_ruby' module Gem class Dependency @@ -31,6 +33,30 @@ module Bundler find_gemfile end + if Bundler.current_ruby.mswin? || Bundler.current_ruby.jruby? + require 'monitor' + @chdir_monitor = Monitor.new + def chdir(dir, &blk) + @chdir_monitor.synchronize do + Dir.chdir dir, &blk + end + end + + def pwd + @chdir_monitor.synchronize do + Dir.pwd + end + end + else + def chdir(dir, &blk) + Dir.chdir dir, &blk + end + + def pwd + Dir.pwd + end + end + private def find_gemfile @@ -38,7 +64,7 @@ module Bundler return given if given && !given.empty? previous = nil - current = File.expand_path(Dir.pwd) + current = File.expand_path(SharedHelpers.pwd) until !File.directory?(current) || current == previous if ENV['BUNDLE_SPEC_RUN'] diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb index 242d919a3f..9f308881e7 100644 --- a/lib/bundler/source/git.rb +++ b/lib/bundler/source/git.rb @@ -151,7 +151,7 @@ module Bundler end def install(spec) - Bundler.ui.info "Using #{spec.name} (#{spec.version}) from #{to_s} " + Bundler.ui.info "Using #{spec.name} (#{spec.version}) from #{to_s}" if requires_checkout? && !@copied Bundler.ui.debug " * Checking out revision: #{ref}" git_proxy.copy_to(install_path, submodules) @@ -159,6 +159,7 @@ module Bundler @copied = true end generate_bin(spec) + nil end def cache(spec) diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb index e480f15a69..800439a0fb 100644 --- a/lib/bundler/source/git/git_proxy.rb +++ b/lib/bundler/source/git/git_proxy.rb @@ -58,7 +58,7 @@ module Bundler File.chmod((0777 & ~File.umask), destination) end - Dir.chdir(destination) do + SharedHelpers.chdir(destination) do git %|fetch --force --quiet --tags "#{path}"| git "reset --hard #{@revision}" @@ -95,7 +95,7 @@ module Bundler out else raise GitError, "Bundler is trying to run a `git #{command}` at runtime. You probably need to run `bundle install`. However, " \ - "this error message could probably be more useful. Please submit a ticket at http://github.com/carlhuda/bundler/issues " \ + "this error message could probably be more useful. Please submit a ticket at http://github.com/bundler/bundler/issues " \ "with steps to reproduce as well as the following\n\nCALLER: #{caller.join("\n")}" end end @@ -127,7 +127,7 @@ module Bundler def in_path(&blk) checkout unless path.exist? - Dir.chdir(path, &blk) + SharedHelpers.chdir(path, &blk) end def allowed_in_path diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb index 625ede4ef5..e0a818b5ab 100644 --- a/lib/bundler/source/path.rb +++ b/lib/bundler/source/path.rb @@ -70,8 +70,9 @@ module Bundler end def install(spec) - Bundler.ui.info "Using #{spec.name} (#{spec.version}) from #{to_s} " + Bundler.ui.info "Using #{spec.name} (#{spec.version}) from #{to_s}" generate_bin(spec, :disable_extensions) + nil end def cache(spec) @@ -189,7 +190,7 @@ module Bundler Bundler.ui.warn "The validation message from Rubygems was:\n #{e.message}" ensure if gem_dir && gem_file - Dir.chdir(gem_dir){ FileUtils.rm_rf(gem_file) if File.exist?(gem_file) } + FileUtils.rm_rf(gem_dir.join gem_file) end end diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index 0b5d264118..76db202536 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -69,11 +69,11 @@ module Bundler def install(spec) if installed_specs[spec].any? - Bundler.ui.info "Using #{spec.name} (#{spec.version}) " + Bundler.ui.info "Using #{spec.name} (#{spec.version})" return end - Bundler.ui.info "Installing #{spec.name} (#{spec.version}) " + Bundler.ui.info "Installing #{spec.name} (#{spec.version})" path = cached_gem(spec) if Bundler.requires_sudo? install_path = Bundler.tmp @@ -94,10 +94,6 @@ module Bundler ).install end - if spec.post_install_message - Installer.post_install_messages[spec.name] = spec.post_install_message - end - # SUDO HAX if Bundler.requires_sudo? Bundler.mkdir_p "#{Bundler.rubygems.gem_dir}/gems" @@ -109,8 +105,10 @@ module Bundler Bundler.sudo "cp -R #{Bundler.tmp}/bin/#{exe} #{Bundler.system_bindir}" end end + Bundler.ui.info "Installed #{spec.name} (#{spec.version})" installed_spec.loaded_from = "#{Bundler.rubygems.gem_dir}/specifications/#{spec.full_name}.gemspec" spec.loaded_from = "#{Bundler.rubygems.gem_dir}/specifications/#{spec.full_name}.gemspec" + spec.post_install_message end def cache(spec) diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb index 2cdffdc83f..1746f6d0df 100644 --- a/lib/bundler/spec_set.rb +++ b/lib/bundler/spec_set.rb @@ -109,7 +109,22 @@ module Bundler def sorted rake = @specs.find { |s| s.name == 'rake' } - @sorted ||= ([rake] + tsort).compact.uniq + begin + @sorted ||= ([rake] + tsort).compact.uniq + rescue TSort::Cyclic => error + cgems = extract_circular_gems(error) + raise CyclicDependencyError, "Your Gemfile requires gems that depend" \ + " depend on each other, creating an infinite loop. Please remove" \ + " either gem '#{cgems[1]}' or gem '#{cgems[0]}' and try again." + end + end + + def extract_circular_gems(error) + if Bundler.current_ruby.mri? && Bundler.current_ruby.on_19? + error.message.scan(/(\w+) \([^)]/).flatten + else + error.message.scan(/@name="(.*?)"/).flatten + end end def lookup diff --git a/lib/bundler/vendor/net/http/persistent.rb b/lib/bundler/vendor/net/http/persistent.rb index dcdc5b53cb..f99f0625ed 100644 --- a/lib/bundler/vendor/net/http/persistent.rb +++ b/lib/bundler/vendor/net/http/persistent.rb @@ -1,5 +1,9 @@ require 'net/http' -require 'net/https' +begin + require 'net/https' +rescue LoadError + # net/https or openssl +end if RUBY_VERSION < '1.9' # but only for 1.8 require 'net/http/faster' require 'uri' require 'cgi' # for escaping @@ -9,6 +13,8 @@ begin rescue LoadError end +autoload :OpenSSL, 'openssl' + ## # Persistent connections for Net::HTTP # @@ -37,6 +43,11 @@ end # # perform a GET # response = http.request uri # +# # or +# +# get = Net::HTTP::Get.new uri.request_uri +# response = http.request get +# # # create a POST # post_uri = uri + 'create' # post = Net::HTTP::Post.new post_uri.path @@ -45,6 +56,10 @@ end # # perform the POST, the URI is always required # response http.request post_uri, post # +# Note that for GET, HEAD and other requests that do not have a body you want +# to use URI#request_uri not URI#path. The request_uri contains the query +# params which are sent in the body for other requests. +# # == SSL # # SSL connections are automatically created depending upon the scheme of the @@ -105,6 +120,13 @@ end # The amount of time allowed between reading two chunks from the socket. Set # through #read_timeout # +# === Max Requests +# +# The number of requests that should be made before opening a new connection. +# Typically many keep-alive capable servers tune this to 100 or less, so the +# 101st request will fail with ECONNRESET. If unset (default), this value has no +# effect, if set, connections will be reset on the request after max_requests. +# # === Open Timeout # # The amount of time to wait for a connection to be opened. Set through @@ -174,9 +196,29 @@ class Net::HTTP::Persistent EPOCH = Time.at 0 # :nodoc: ## + # Is OpenSSL available? This test works with autoload + + HAVE_OPENSSL = defined? OpenSSL::SSL # :nodoc: + + ## # The version of Net::HTTP::Persistent you are using - VERSION = '2.8' + VERSION = '2.9' + + ## + # Exceptions rescued for automatic retry on ruby 2.0.0. This overlaps with + # the exception list for ruby 1.x. + + RETRIED_EXCEPTIONS = [ # :nodoc: + (Net::ReadTimeout if Net.const_defined? :ReadTimeout), + IOError, + EOFError, + Errno::ECONNRESET, + Errno::ECONNABORTED, + Errno::EPIPE, + (OpenSSL::SSL::SSLError if HAVE_OPENSSL), + Timeout::Error, + ].compact ## # Error class for errors raised by Net::HTTP::Persistent. Various @@ -226,6 +268,8 @@ class Net::HTTP::Persistent $stderr.puts "sleeping #{sleep_time}" if $DEBUG sleep sleep_time end + rescue + # ignore StandardErrors, we've probably found the idle timeout. ensure http.shutdown @@ -288,6 +332,12 @@ class Net::HTTP::Persistent attr_accessor :idle_timeout ## + # Maximum number of requests on a connection before it is considered expired + # and automatically closed. + + attr_accessor :max_requests + + ## # The value sent in the Keep-Alive header. Defaults to 30. Not needed for # HTTP/1.1 servers. # @@ -442,6 +492,7 @@ class Net::HTTP::Persistent @open_timeout = nil @read_timeout = nil @idle_timeout = 5 + @max_requests = nil @socket_options = [] @socket_options << [Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1] if @@ -458,15 +509,22 @@ class Net::HTTP::Persistent @private_key = nil @ssl_version = nil @verify_callback = nil - @verify_mode = OpenSSL::SSL::VERIFY_PEER + @verify_mode = nil @cert_store = nil @generation = 0 # incremented when proxy URI changes @ssl_generation = 0 # incremented when SSL session variables change - @reuse_ssl_sessions = OpenSSL::SSL.const_defined? :Session + + if HAVE_OPENSSL then + @verify_mode = OpenSSL::SSL::VERIFY_PEER + @reuse_ssl_sessions = OpenSSL::SSL.const_defined? :Session + end @retry_change_requests = false + @ruby_1 = RUBY_VERSION < '2' + @retried_on_ruby_2 = !@ruby_1 + self.proxy = proxy if proxy end @@ -536,6 +594,9 @@ class Net::HTTP::Persistent use_ssl = uri.scheme.downcase == 'https' if use_ssl then + raise Net::HTTP::Persistent::Error, 'OpenSSL is not available' unless + HAVE_OPENSSL + ssl_generation = @ssl_generation ssl_cleanup ssl_generation @@ -606,10 +667,12 @@ class Net::HTTP::Persistent end ## - # Returns true if the connection should be reset due to an idle timeout, - # false otherwise. + # Returns true if the connection should be reset due to an idle timeout, or + # maximum request count, false otherwise. def expired? connection + requests = Thread.current[@request_key][connection.object_id] + return true if @max_requests && requests >= @max_requests return false unless @idle_timeout return true if @idle_timeout.zero? @@ -679,10 +742,15 @@ class Net::HTTP::Persistent end ## - # Is the request idempotent or is retry_change_requests allowed + # Is the request +req+ idempotent or is retry_change_requests allowed. + # + # If +retried_on_ruby_2+ is true, true will be returned if we are on ruby, + # retry_change_requests is allowed and the request is not idempotent. - def can_retry? req - retry_change_requests or idempotent?(req) + def can_retry? req, retried_on_ruby_2 = false + return @retry_change_requests && !idempotent?(req) if retried_on_ruby_2 + + @retry_change_requests || idempotent?(req) end if RUBY_VERSION > '1.9' then @@ -901,31 +969,14 @@ class Net::HTTP::Persistent # # +req+ must be a Net::HTTPRequest subclass (see Net::HTTP for a list). # - # If there is an error and the request is idempontent according to RFC 2616 + # If there is an error and the request is idempotent according to RFC 2616 # it will be retried automatically. def request uri, req = nil, &block retried = false bad_response = false - req = Net::HTTP::Get.new uri.request_uri unless req - - @headers.each do |pair| - req.add_field(*pair) - end - - if uri.user or uri.password - req.basic_auth uri.user, uri.password - end - - @override_headers.each do |name, value| - req[name] = value - end - - unless req['Connection'] then - req.add_field 'Connection', 'keep-alive' - req.add_field 'Keep-Alive', @keep_alive - end + req = request_setup req || uri connection = connection_for uri connection_id = connection.object_id @@ -950,23 +1001,25 @@ class Net::HTTP::Persistent bad_response = true retry - rescue IOError, EOFError, Timeout::Error, - Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE, - Errno::EINVAL, OpenSSL::SSL::SSLError => e - - if retried or not can_retry? req - due_to = "(due to #{e.message} - #{e.class})" - message = error_message connection + rescue *RETRIED_EXCEPTIONS => e # retried on ruby 2 + request_failed e, req, connection if + retried or not can_retry? req, @retried_on_ruby_2 - finish connection + reset connection - raise Error, "too many connection resets #{due_to} #{message}" - end + retried = true + retry + rescue Errno::EINVAL, Errno::ETIMEDOUT => e # not retried on ruby 2 + request_failed e, req, connection if retried or not can_retry? req reset connection retried = true retry + rescue Exception => e + finish connection + + raise ensure Thread.current[@timeout_key][connection_id] = Time.now end @@ -977,6 +1030,51 @@ class Net::HTTP::Persistent end ## + # Raises an Error for +exception+ which resulted from attempting the request + # +req+ on the +connection+. + # + # Finishes the +connection+. + + def request_failed exception, req, connection # :nodoc: + due_to = "(due to #{exception.message} - #{exception.class})" + message = "too many connection resets #{due_to} #{error_message connection}" + + finish connection + + + raise Error, message, exception.backtrace + end + + ## + # Creates a GET request if +req_or_uri+ is a URI and adds headers to the + # request. + # + # Returns the request. + + def request_setup req_or_uri # :nodoc: + req = if URI === req_or_uri then + Net::HTTP::Get.new req_or_uri.request_uri + else + req_or_uri + end + + @headers.each do |pair| + req.add_field(*pair) + end + + @override_headers.each do |name, value| + req[name] = value + end + + unless req['Connection'] then + req.add_field 'Connection', 'keep-alive' + req.add_field 'Keep-Alive', @keep_alive + end + + req + end + + ## # Shuts down all connections for +thread+. # # Uses the current thread by default. diff --git a/lib/bundler/vendor/thor.rb b/lib/bundler/vendor/thor.rb index 88b907c153..6880b6d7ae 100644 --- a/lib/bundler/vendor/thor.rb +++ b/lib/bundler/vendor/thor.rb @@ -3,21 +3,32 @@ require 'thor/base' class Thor class << self - # Sets the default task when thor is executed without an explicit task to be called. + # Allows for custom "Command" package naming. + # + # === Parameters + # name<String> + # options<Hash> + # + def package_name(name, options={}) + @package_name = name.nil? || name == '' ? nil : name + end + + # Sets the default command when thor is executed without an explicit command to be called. # # ==== Parameters - # meth<Symbol>:: name of the default task - # - def default_task(meth=nil) - case meth - when :none - @default_task = 'help' - when nil - @default_task ||= from_superclass(:default_task, 'help') - else - @default_task = meth.to_s + # meth<Symbol>:: name of the default command + # + def default_command(meth=nil) + @default_command = case meth + when :none + 'help' + when nil + @default_command || from_superclass(:default_command, 'help') + else + meth.to_s end end + alias default_task default_command # Registers another Thor subclass as a command. # @@ -36,7 +47,7 @@ class Thor end end - # Defines the usage and the description of the next task. + # Defines the usage and the description of the next command. # # ==== Parameters # usage<String> @@ -45,29 +56,29 @@ class Thor # def desc(usage, description, options={}) if options[:for] - task = find_and_refresh_task(options[:for]) - task.usage = usage if usage - task.description = description if description + command = find_and_refresh_command(options[:for]) + command.usage = usage if usage + command.description = description if description else @usage, @desc, @hide = usage, description, options[:hide] || false end end - # Defines the long description of the next task. + # Defines the long description of the next command. # # ==== Parameters # long description<String> # def long_desc(long_description, options={}) if options[:for] - task = find_and_refresh_task(options[:for]) - task.long_description = long_description if long_description + command = find_and_refresh_command(options[:for]) + command.long_description = long_description if long_description else @long_desc = long_description end end - # Maps an input to a task. If you define: + # Maps an input to a command. If you define: # # map "-T" => "list" # @@ -75,10 +86,10 @@ class Thor # # thor -T # - # Will invoke the list task. + # Will invoke the list command. # # ==== Parameters - # Hash[String|Array => Symbol]:: Maps the string or the strings in the array to the given task. + # Hash[String|Array => Symbol]:: Maps the string or the strings in the array to the given command. # def map(mappings=nil) @map ||= from_superclass(:map, {}) @@ -96,7 +107,7 @@ class Thor @map end - # Declares the options for the next task to be declared. + # Declares the options for the next command to be declared. # # ==== Parameters # Hash[Symbol => Object]:: The hash key is the name of the option and the value @@ -112,15 +123,15 @@ class Thor alias options method_options # Adds an option to the set of method options. If :for is given as option, - # it allows you to change the options from a previous defined task. + # it allows you to change the options from a previous defined command. # - # def previous_task + # def previous_command # # magic # end # - # method_option :foo => :bar, :for => :previous_task + # method_option :foo => :bar, :for => :previous_command # - # def next_task + # def next_command # # magic # end # @@ -139,38 +150,38 @@ class Thor # def method_option(name, options={}) scope = if options[:for] - find_and_refresh_task(options[:for]).options + find_and_refresh_command(options[:for]).options else method_options end build_option(name, options, scope) end - alias option method_option - # Prints help information for the given task. + # Prints help information for the given command. # # ==== Parameters # shell<Thor::Shell> - # task_name<String> + # command_name<String> # - def task_help(shell, task_name) - meth = normalize_task_name(task_name) - task = all_tasks[meth] - handle_no_task_error(meth) unless task + def command_help(shell, command_name) + meth = normalize_command_name(command_name) + command = all_commands[meth] + handle_no_command_error(meth) unless command shell.say "Usage:" - shell.say " #{banner(task)}" + shell.say " #{banner(command)}" shell.say - class_options_help(shell, nil => task.options.map { |_, o| o }) - if task.long_description + class_options_help(shell, nil => command.options.map { |_, o| o }) + if command.long_description shell.say "Description:" - shell.print_wrapped(task.long_description, :indent => 2) + shell.print_wrapped(command.long_description, :indent => 2) else - shell.say task.description + shell.say command.description end end + alias task_help command_help # Prints help information for this class. # @@ -178,32 +189,39 @@ class Thor # shell<Thor::Shell> # def help(shell, subcommand = false) - list = printable_tasks(true, subcommand) + list = printable_commands(true, subcommand) Thor::Util.thor_classes_in(self).each do |klass| - list += klass.printable_tasks(false) + list += klass.printable_commands(false) end list.sort!{ |a,b| a[0] <=> b[0] } - shell.say "Tasks:" + if @package_name + shell.say "#{@package_name} commands:" + else + shell.say "Commands:" + end + shell.print_table(list, :indent => 2, :truncate => true) shell.say class_options_help(shell) end - # Returns tasks ready to be printed. - def printable_tasks(all = true, subcommand = false) - (all ? all_tasks : tasks).map do |_, task| - next if task.hidden? + # Returns commands ready to be printed. + def printable_commands(all = true, subcommand = false) + (all ? all_commands : commands).map do |_, command| + next if command.hidden? item = [] - item << banner(task, false, subcommand) - item << (task.description ? "# #{task.description.gsub(/\s+/m,' ')}" : "") + item << banner(command, false, subcommand) + item << (command.description ? "# #{command.description.gsub(/\s+/m,' ')}" : "") item end.compact end + alias printable_tasks printable_commands def subcommands @subcommands ||= from_superclass(:subcommands, []) end + alias subtasks subcommands def subcommand(subcommand, subcommand_class) self.subcommands << subcommand.to_s @@ -211,9 +229,10 @@ class Thor define_method(subcommand) do |*args| args, opts = Thor::Arguments.split(args) - invoke subcommand_class, args, opts, :invoked_via_subcommand => true + invoke subcommand_class, args, opts, :invoked_via_subcommand => true, :class_options => options end end + alias subtask subcommand # Extend check unknown options to accept a hash of conditions. # @@ -236,10 +255,10 @@ class Thor options = check_unknown_options return false unless options - task = config[:current_task] - return true unless task + command = config[:current_command] + return true unless command - name = task.name + name = command.name if subcommands.include?(name) false @@ -253,16 +272,16 @@ class Thor end # Stop parsing of options as soon as an unknown option or a regular - # argument is encountered. All remaining arguments are passed to the task. - # This is useful if you have a task that can receive arbitrary additional + # argument is encountered. All remaining arguments are passed to the command. + # This is useful if you have a command that can receive arbitrary additional # options, and where those additional options should not be handled by # Thor. # # ==== Example # - # To better understand how this is useful, let's consider a task that calls + # To better understand how this is useful, let's consider a command that calls # an external command. A user may want to pass arbitrary options and - # arguments to that command. The task itself also accepts some options, + # arguments to that command. The command itself also accepts some options, # which should be handled by Thor. # # class_option "verbose", :type => :boolean @@ -288,163 +307,167 @@ class Thor # --verbose foo # # ==== Parameters - # Symbol ...:: A list of tasks that should be affected. - def stop_on_unknown_option!(*task_names) + # Symbol ...:: A list of commands that should be affected. + def stop_on_unknown_option!(*command_names) @stop_on_unknown_option ||= Set.new - @stop_on_unknown_option.merge(task_names) + @stop_on_unknown_option.merge(command_names) end - def stop_on_unknown_option?(task) #:nodoc: - !!@stop_on_unknown_option && @stop_on_unknown_option.include?(task.name.to_sym) + def stop_on_unknown_option?(command) #:nodoc: + command && !@stop_on_unknown_option.nil? && @stop_on_unknown_option.include?(command.name.to_sym) end - protected - - # The method responsible for dispatching given the args. - def dispatch(meth, given_args, given_opts, config) #:nodoc: - # There is an edge case when dispatching from a subcommand. - # A problem occurs invoking the default task. This case occurs - # when arguments are passed and a default task is defined, and - # the first given_args does not match the default task. - # Thor use "help" by default so we skip that case. - # Note the call to retrieve_task_name. It's called with - # given_args.dup since that method calls args.shift. Then lookup - # the task normally. If the first item in given_args is not - # a task then use the default task. The given_args will be - # intact later since dup was used. - if config[:invoked_via_subcommand] && given_args.size >= 1 && default_task != "help" && given_args.first != default_task - meth ||= retrieve_task_name(given_args.dup) - task = all_tasks[normalize_task_name(meth)] - task ||= all_tasks[normalize_task_name(default_task)] - else - meth ||= retrieve_task_name(given_args) - task = all_tasks[normalize_task_name(meth)] - end + protected + + # The method responsible for dispatching given the args. + def dispatch(meth, given_args, given_opts, config) #:nodoc: + # There is an edge case when dispatching from a subcommand. + # A problem occurs invoking the default command. This case occurs + # when arguments are passed and a default command is defined, and + # the first given_args does not match the default command. + # Thor use "help" by default so we skip that case. + # Note the call to retrieve_command_name. It's called with + # given_args.dup since that method calls args.shift. Then lookup + # the command normally. If the first item in given_args is not + # a command then use the default command. The given_args will be + # intact later since dup was used. + if config[:invoked_via_subcommand] && given_args.size >= 1 && default_command != "help" && given_args.first != default_command + meth ||= retrieve_command_name(given_args.dup) + command = all_commands[normalize_command_name(meth)] + command ||= all_commands[normalize_command_name(default_command)] + else + meth ||= retrieve_command_name(given_args) + command = all_commands[normalize_command_name(meth)] + end - if task - args, opts = Thor::Options.split(given_args) - if stop_on_unknown_option?(task) && !args.empty? - # given_args starts with a non-option, so we treat everything as - # ordinary arguments - args.concat opts - opts.clear - end - else - args, opts = given_args, nil - task = Thor::DynamicTask.new(meth) + if command + args, opts = Thor::Options.split(given_args) + if stop_on_unknown_option?(command) && !args.empty? + # given_args starts with a non-option, so we treat everything as + # ordinary arguments + args.concat opts + opts.clear end - - opts = given_opts || opts || [] - config.merge!(:current_task => task, :task_options => task.options) - - instance = new(args, opts, config) - yield instance if block_given? - args = instance.args - trailing = args[Range.new(arguments.size, -1)] - instance.invoke_task(task, trailing || []) + else + args, opts = given_args, nil + command = Thor::DynamicCommand.new(meth) end - # The banner for this class. You can customize it if you are invoking the - # thor class by another ways which is not the Thor::Runner. It receives - # the task that is going to be invoked and a boolean which indicates if - # the namespace should be displayed as arguments. - # - def banner(task, namespace = nil, subcommand = false) - "#{basename} #{task.formatted_usage(self, $thor_runner, subcommand)}" - end + opts = given_opts || opts || [] + config.merge!(:current_command => command, :command_options => command.options) - def baseclass #:nodoc: - Thor - end + instance = new(args, opts, config) + yield instance if block_given? + args = instance.args + trailing = args[Range.new(arguments.size, -1)] + instance.invoke_command(command, trailing || []) + end - def create_task(meth) #:nodoc: - @long_desc ||= nil - @usage ||= nil - if @usage && @desc - base_class = @hide ? Thor::HiddenTask : Thor::Task - tasks[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options) - @usage, @desc, @long_desc, @method_options, @hide = nil - true - elsif self.all_tasks[meth] || meth == "method_missing" - true - else - puts "[WARNING] Attempted to create task #{meth.inspect} without usage or description. " << - "Call desc if you want this method to be available as task or declare it inside a " << - "no_tasks{} block. Invoked from #{caller[1].inspect}." - false - end - end + # The banner for this class. You can customize it if you are invoking the + # thor class by another ways which is not the Thor::Runner. It receives + # the command that is going to be invoked and a boolean which indicates if + # the namespace should be displayed as arguments. + # + def banner(command, namespace = nil, subcommand = false) + "#{basename} #{command.formatted_usage(self, $thor_runner, subcommand)}" + end - def initialize_added #:nodoc: - class_options.merge!(method_options) - @method_options = nil - end + def baseclass #:nodoc: + Thor + end - # Retrieve the task name from given args. - def retrieve_task_name(args) #:nodoc: - meth = args.first.to_s unless args.empty? - if meth && (map[meth] || meth !~ /^\-/) - args.shift - else - nil - end + def create_command(meth) #:nodoc: + if @usage && @desc + base_class = @hide ? Thor::HiddenCommand : Thor::Command + commands[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options) + @usage, @desc, @long_desc, @method_options, @hide = nil + true + elsif self.all_commands[meth] || meth == "method_missing" + true + else + puts "[WARNING] Attempted to create command #{meth.inspect} without usage or description. " << + "Call desc if you want this method to be available as command or declare it inside a " << + "no_commands{} block. Invoked from #{caller[1].inspect}." + false end + end + alias create_task create_command - # receives a (possibly nil) task name and returns a name that is in - # the tasks hash. In addition to normalizing aliases, this logic - # will determine if a shortened command is an unambiguous substring of - # a task or alias. - # - # +normalize_task_name+ also converts names like +animal-prison+ - # into +animal_prison+. - def normalize_task_name(meth) #:nodoc: - return default_task.to_s.gsub('-', '_') unless meth - - possibilities = find_task_possibilities(meth) - if possibilities.size > 1 - raise ArgumentError, "Ambiguous task #{meth} matches [#{possibilities.join(', ')}]" - elsif possibilities.size < 1 - meth = meth || default_task - elsif map[meth] - meth = map[meth] - else - meth = possibilities.first - end + def initialize_added #:nodoc: + class_options.merge!(method_options) + @method_options = nil + end - meth.to_s.gsub('-','_') # treat foo-bar as foo_bar + # Retrieve the command name from given args. + def retrieve_command_name(args) #:nodoc: + meth = args.first.to_s unless args.empty? + if meth && (map[meth] || meth !~ /^\-/) + args.shift + else + nil end - - # this is the logic that takes the task name passed in by the user - # and determines whether it is an unambiguous substrings of a task or - # alias name. - def find_task_possibilities(meth) - len = meth.to_s.length - possibilities = all_tasks.merge(map).keys.select { |n| meth == n[0, len] }.sort - unique_possibilities = possibilities.map { |k| map[k] || k }.uniq - - if possibilities.include?(meth) - [meth] - elsif unique_possibilities.size == 1 - unique_possibilities - else - possibilities - end + end + alias retrieve_task_name retrieve_command_name + + # receives a (possibly nil) command name and returns a name that is in + # the commands hash. In addition to normalizing aliases, this logic + # will determine if a shortened command is an unambiguous substring of + # a command or alias. + # + # +normalize_command_name+ also converts names like +animal-prison+ + # into +animal_prison+. + def normalize_command_name(meth) #:nodoc: + return default_command.to_s.gsub('-', '_') unless meth + + possibilities = find_command_possibilities(meth) + if possibilities.size > 1 + raise AmbiguousTaskError, "Ambiguous command #{meth} matches [#{possibilities.join(', ')}]" + elsif possibilities.size < 1 + meth = meth || default_command + elsif map[meth] + meth = map[meth] + else + meth = possibilities.first end - def subcommand_help(cmd) - desc "help [COMMAND]", "Describe subcommands or one specific subcommand" - class_eval <<-RUBY - def help(task = nil, subcommand = true); super; end - RUBY + meth.to_s.gsub('-','_') # treat foo-bar as foo_bar + end + alias normalize_task_name normalize_command_name + + # this is the logic that takes the command name passed in by the user + # and determines whether it is an unambiguous substrings of a command or + # alias name. + def find_command_possibilities(meth) + len = meth.to_s.length + possibilities = all_commands.merge(map).keys.select { |n| meth == n[0, len] }.sort + unique_possibilities = possibilities.map { |k| map[k] || k }.uniq + + if possibilities.include?(meth) + [meth] + elsif unique_possibilities.size == 1 + unique_possibilities + else + possibilities end + end + alias find_task_possibilities find_command_possibilities + + def subcommand_help(cmd) + desc "help [COMMAND]", "Describe subcommands or one specific subcommand" + class_eval <<-RUBY + def help(command = nil, subcommand = true); super; end + RUBY + end + alias subtask_help subcommand_help + end include Thor::Base map HELP_MAPPINGS => :help - desc "help [TASK]", "Describe available tasks or one specific task" - def help(task = nil, subcommand = false) - task ? self.class.task_help(shell, task) : self.class.help(shell, subcommand) + desc "help [COMMAND]", "Describe available commands or one specific command" + def help(command = nil, subcommand = false) + command ? self.class.command_help(shell, command) : self.class.help(shell, subcommand) end end diff --git a/lib/bundler/vendor/thor/actions.rb b/lib/bundler/vendor/thor/actions.rb index 8cc16e6896..7e574d8bde 100644 --- a/lib/bundler/vendor/thor/actions.rb +++ b/lib/bundler/vendor/thor/actions.rb @@ -1,6 +1,6 @@ require 'fileutils' require 'uri' -require 'thor/core_ext/file_binary_read' +require 'thor/core_ext/io_binary_read' require 'thor/actions/create_file' require 'thor/actions/create_link' require 'thor/actions/directory' @@ -73,13 +73,13 @@ class Thor # def initialize(args=[], options={}, config={}) self.behavior = case config[:behavior].to_s - when "force", "skip" - _cleanup_options_and_set(options, config[:behavior]) - :invoke - when "revoke" - :revoke - else - :invoke + when "force", "skip" + _cleanup_options_and_set(options, config[:behavior]) + :invoke + when "revoke" + :revoke + else + :invoke end super @@ -227,7 +227,7 @@ class Thor # ==== Parameters # command<String>:: the command to be executed. # config<Hash>:: give :verbose => false to not log the status, :capture => true to hide to output. Specify :with - # to append an executable to command executation. + # to append an executable to command execution. # # ==== Example # @@ -268,8 +268,8 @@ class Thor # switches. # # ==== Parameters - # task<String>:: the task to be invoked - # args<Array>:: arguments to the task + # command<String>:: the command to be invoked + # args<Array>:: arguments to the command # config<Hash>:: give :verbose => false to not log the status, :capture => true to hide to output. # Other options are given as parameter to Thor. # @@ -282,13 +282,13 @@ class Thor # thor :list, :all => true, :substring => 'rails' # #=> thor list --all --substring=rails # - def thor(task, *args) + def thor(command, *args) config = args.last.is_a?(Hash) ? args.pop : {} verbose = config.key?(:verbose) ? config.delete(:verbose) : true pretend = config.key?(:pretend) ? config.delete(:pretend) : false capture = config.key?(:capture) ? config.delete(:capture) : false - args.unshift task + args.unshift(command) args.push Thor::Options.to_switches(config) command = args.join(' ').strip @@ -305,12 +305,12 @@ class Thor def _cleanup_options_and_set(options, key) #:nodoc: case options - when Array - %w(--force -f --skip -s).each { |i| options.delete(i) } - options << "--#{key}" - when Hash - [:force, :skip, "force", "skip"].each { |i| options.delete(i) } - options.merge!(key => true) + when Array + %w(--force -f --skip -s).each { |i| options.delete(i) } + options << "--#{key}" + when Hash + [:force, :skip, "force", "skip"].each { |i| options.delete(i) } + options.merge!(key => true) end end diff --git a/lib/bundler/vendor/thor/actions/create_link.rb b/lib/bundler/vendor/thor/actions/create_link.rb index 864a1e9923..fba3915155 100644 --- a/lib/bundler/vendor/thor/actions/create_link.rb +++ b/lib/bundler/vendor/thor/actions/create_link.rb @@ -52,6 +52,9 @@ class Thor given_destination end + def exists? + super || File.symlink?(destination) + end end end end diff --git a/lib/bundler/vendor/thor/actions/directory.rb b/lib/bundler/vendor/thor/actions/directory.rb index 8e64513d6b..7f8fd97c9b 100644 --- a/lib/bundler/vendor/thor/actions/directory.rb +++ b/lib/bundler/vendor/thor/actions/directory.rb @@ -39,6 +39,7 @@ class Thor # config<Hash>:: give :verbose => false to not log the status. # If :recursive => false, does not look for paths recursively. # If :mode => :preserve, preserve the file mode from the source. + # If :exclude_pattern => /regexp/, prevents copying files that match that regexp. # # ==== Examples # @@ -74,26 +75,45 @@ class Thor def execute! lookup = Util.escape_globs(source) lookup = config[:recursive] ? File.join(lookup, '**') : lookup - lookup = File.join(lookup, '{*,.[a-z]*}') + lookup = file_level_lookup(lookup) - Dir[lookup].sort.each do |file_source| + files(lookup).sort.each do |file_source| next if File.directory?(file_source) + next if config[:exclude_pattern] && file_source.match(config[:exclude_pattern]) file_destination = File.join(given_destination, file_source.gsub(source, '.')) file_destination.gsub!('/./', '/') case file_source - when /\.empty_directory$/ - dirname = File.dirname(file_destination).gsub(/\/\.$/, '') - next if dirname == given_destination - base.empty_directory(dirname, config) - when /\.tt$/ - destination = base.template(file_source, file_destination[0..-4], config, &@block) - else - destination = base.copy_file(file_source, file_destination, config, &@block) + when /\.empty_directory$/ + dirname = File.dirname(file_destination).gsub(/\/\.$/, '') + next if dirname == given_destination + base.empty_directory(dirname, config) + when /\.tt$/ + destination = base.template(file_source, file_destination[0..-4], config, &@block) + else + destination = base.copy_file(file_source, file_destination, config, &@block) end end end + if RUBY_VERSION < '2.0' + def file_level_lookup(previous_lookup) + File.join(previous_lookup, '{*,.[a-z]*}') + end + + def files(lookup) + Dir[lookup] + end + else + def file_level_lookup(previous_lookup) + File.join(previous_lookup, '*') + end + + def files(lookup) + Dir.glob(lookup, File::FNM_DOTMATCH) + end + end + end end end diff --git a/lib/bundler/vendor/thor/actions/empty_directory.rb b/lib/bundler/vendor/thor/actions/empty_directory.rb index 93d3e2a839..d9970abae4 100644 --- a/lib/bundler/vendor/thor/actions/empty_directory.rb +++ b/lib/bundler/vendor/thor/actions/empty_directory.rb @@ -97,28 +97,12 @@ class Thor # # user.rb # - # The method referenced by %-string SHOULD be public. Otherwise you - # get the exception with the corresponding error message. + # The method referenced can be either public or private. # def convert_encoded_instructions(filename) filename.gsub(/%(.*?)%/) do |initial_string| - call_public_method($1.strip) or initial_string - end - end - - # Calls `base`'s public method `sym`. - # Returns:: result of `base.sym` or `nil` if `sym` wasn't found in - # `base` - # Raises:: Thor::PrivateMethodEncodedError if `sym` references - # a private method. - def call_public_method(sym) - if base.respond_to?(sym) - base.send(sym) - elsif base.respond_to?(sym, true) - raise Thor::PrivateMethodEncodedError, - "Method #{base.class}##{sym} should be public, not private" - else - nil + method = $1.strip + base.respond_to?(method, true) ? base.send(method) : initial_string end end diff --git a/lib/bundler/vendor/thor/actions/file_manipulation.rb b/lib/bundler/vendor/thor/actions/file_manipulation.rb index d9d0d6f62a..a6effbec29 100644 --- a/lib/bundler/vendor/thor/actions/file_manipulation.rb +++ b/lib/bundler/vendor/thor/actions/file_manipulation.rb @@ -129,7 +129,7 @@ class Thor # # ==== Example # - # chmod "script/*", 0755 + # chmod "script/server", 0755 # def chmod(path, mode, config={}) return unless behavior == :invoke @@ -251,7 +251,7 @@ class Thor def uncomment_lines(path, flag, *args) flag = flag.respond_to?(:source) ? flag.source : flag - gsub_file(path, /^(\s*)#\s*(.*#{flag})/, '\1\2', *args) + gsub_file(path, /^(\s*)#[[:blank:]]*(.*#{flag})/, '\1\2', *args) end # Comment all lines matching a given regex. It will leave the space @@ -293,8 +293,11 @@ class Thor end alias :remove_dir :remove_file - attr_accessor :output_buffer + attr_accessor :output_buffer + private :output_buffer, :output_buffer= + private + def concat(string) @output_buffer.concat(string) end diff --git a/lib/bundler/vendor/thor/base.rb b/lib/bundler/vendor/thor/base.rb index aa1a6f65bf..272dae417b 100644 --- a/lib/bundler/vendor/thor/base.rb +++ b/lib/bundler/vendor/thor/base.rb @@ -1,10 +1,10 @@ +require 'thor/command' require 'thor/core_ext/hash_with_indifferent_access' require 'thor/core_ext/ordered_hash' require 'thor/error' -require 'thor/shell' require 'thor/invocation' require 'thor/parser' -require 'thor/task' +require 'thor/shell' require 'thor/util' class Thor @@ -47,8 +47,8 @@ class Thor # first two parameters. if options.is_a?(Array) - task_options = config.delete(:task_options) # hook for start - parse_options = parse_options.merge(task_options) if task_options + command_options = config.delete(:command_options) # hook for start + parse_options = parse_options.merge(command_options) if command_options array_options, hash_options = options, {} else # Handle the case where the class was explicitly instantiated @@ -59,9 +59,10 @@ class Thor # Let Thor::Options parse the options first, so it can remove # declared options from the array. This will leave us with # a list of arguments that weren't declared. - stop_on_unknown = self.class.stop_on_unknown_option? config[:current_task] + stop_on_unknown = self.class.stop_on_unknown_option? config[:current_command] opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown) self.options = opts.parse(array_options) + self.options = config[:class_options].merge(self.options) if config[:class_options] # If unknown options are disallowed, make sure that none of the # remaining arguments looks like an option. @@ -119,15 +120,15 @@ class Thor module ClassMethods def attr_reader(*) #:nodoc: - no_tasks { super } + no_commands { super } end def attr_writer(*) #:nodoc: - no_tasks { super } + no_commands { super } end def attr_accessor(*) #:nodoc: - no_tasks { super } + no_commands { super } end # If you want to raise an error for unknown options, call check_unknown_options! @@ -146,8 +147,8 @@ class Thor # If true, option parsing is suspended as soon as an unknown option or a # regular argument is encountered. All remaining arguments are passed to - # the task as regular arguments. - def stop_on_unknown_option?(task_name) #:nodoc: + # the command as regular arguments. + def stop_on_unknown_option?(command_name) #:nodoc: false end @@ -172,11 +173,11 @@ class Thor # is how they are parsed from the command line, arguments are retrieved # from position: # - # thor task NAME + # thor command NAME # # Instead of: # - # thor task --name=NAME + # thor command --name=NAME # # Besides, arguments are used inside your code as an accessor (self.argument), # while options are all kept in a hash (self.options). @@ -203,7 +204,7 @@ class Thor # def argument(name, options={}) is_thor_reserved_word?(name, :argument) - no_tasks { attr_accessor name } + no_commands { attr_accessor name } required = if options.key?(:optional) !options[:optional] @@ -307,88 +308,92 @@ class Thor end # Defines the group. This is used when thor list is invoked so you can specify - # that only tasks from a pre-defined group will be shown. Defaults to standard. + # that only commands from a pre-defined group will be shown. Defaults to standard. # # ==== Parameters # name<String|Symbol> # def group(name=nil) - case name - when nil - @group ||= from_superclass(:group, 'standard') - else - @group = name.to_s + @group = case name + when nil + @group || from_superclass(:group, 'standard') + else + name.to_s end end - # Returns the tasks for this Thor class. + # Returns the commands for this Thor class. # # ==== Returns - # OrderedHash:: An ordered hash with tasks names as keys and Thor::Task + # OrderedHash:: An ordered hash with commands names as keys and Thor::Command # objects as values. # - def tasks - @tasks ||= Thor::CoreExt::OrderedHash.new + def commands + @commands ||= Thor::CoreExt::OrderedHash.new end + alias tasks commands - # Returns the tasks for this Thor class and all subclasses. + # Returns the commands for this Thor class and all subclasses. # # ==== Returns - # OrderedHash:: An ordered hash with tasks names as keys and Thor::Task + # OrderedHash:: An ordered hash with commands names as keys and Thor::Command # objects as values. # - def all_tasks - @all_tasks ||= from_superclass(:all_tasks, Thor::CoreExt::OrderedHash.new) - @all_tasks.merge(tasks) + def all_commands + @all_commands ||= from_superclass(:all_commands, Thor::CoreExt::OrderedHash.new) + @all_commands.merge(commands) end + alias all_tasks all_commands - # Removes a given task from this Thor class. This is usually done if you + # Removes a given command from this Thor class. This is usually done if you # are inheriting from another class and don't want it to be available # anymore. # - # By default it only remove the mapping to the task. But you can supply + # By default it only remove the mapping to the command. But you can supply # :undefine => true to undefine the method from the class as well. # # ==== Parameters - # name<Symbol|String>:: The name of the task to be removed - # options<Hash>:: You can give :undefine => true if you want tasks the method + # name<Symbol|String>:: The name of the command to be removed + # options<Hash>:: You can give :undefine => true if you want commands the method # to be undefined from the class as well. # - def remove_task(*names) + def remove_command(*names) options = names.last.is_a?(Hash) ? names.pop : {} names.each do |name| - tasks.delete(name.to_s) - all_tasks.delete(name.to_s) + commands.delete(name.to_s) + all_commands.delete(name.to_s) undef_method name if options[:undefine] end end + alias remove_task remove_command - # All methods defined inside the given block are not added as tasks. + # All methods defined inside the given block are not added as commands. # # So you can do: # # class MyScript < Thor - # no_tasks do - # def this_is_not_a_task + # no_commands do + # def this_is_not_a_command # end # end # end # - # You can also add the method and remove it from the task list: + # You can also add the method and remove it from the command list: # # class MyScript < Thor - # def this_is_not_a_task + # def this_is_not_a_command # end - # remove_task :this_is_not_a_task + # remove_command :this_is_not_a_command # end # - def no_tasks - @no_tasks = true + def no_commands + @no_commands = true yield ensure - @no_tasks = false + @no_commands = false end + alias no_tasks no_commands # Sets the namespace for the Thor or Thor::Group class. By default the # namespace is retrieved from the class name. If your Thor class is named @@ -400,7 +405,7 @@ class Thor # # namespace :my_scripts # - # You change how your tasks are invoked: + # You change how your commands are invoked: # # thor my_scripts -h # @@ -408,26 +413,25 @@ class Thor # # namespace :default # - # Your tasks can be invoked with a shortcut. Instead of: + # Your commands can be invoked with a shortcut. Instead of: # - # thor :my_task + # thor :my_command # def namespace(name=nil) - case name - when nil - @namespace ||= Thor::Util.namespace_from_thor_class(self) - else + if name @namespace = name.to_s + else + @namespace ||= Thor::Util.namespace_from_thor_class(self) end end - # Parses the task and options from the given args, instantiate the class - # and invoke the task. This method is used when the arguments must be parsed + # Parses the command and options from the given args, instantiate the class + # and invoke the command. This method is used when the arguments must be parsed # from an array. If you are inside Ruby and want to use a Thor class, you # can simply initialize it: # # script = MyScript.new(args, options, config) - # script.invoke(:task, first_arg, second_arg, third_arg) + # script.invoke(:command, first_arg, second_arg, third_arg) # def start(given_args=ARGV, config={}) config[:shell] ||= Thor::Base.shell.new @@ -436,48 +440,44 @@ class Thor ENV["THOR_DEBUG"] == "1" ? (raise e) : config[:shell].error(e.message) exit(1) if exit_on_failure? rescue Errno::EPIPE - # This happens if a thor task is piped to something like `head`, + # This happens if a thor command is piped to something like `head`, # which closes the pipe when it's done reading. This will also # mean that if the pipe is closed, further unnecessary # computation will not occur. exit(0) end - # Allows to use private methods from parent in child classes as tasks. + # Allows to use private methods from parent in child classes as commands. # # ==== Parameters - # names<Array>:: Method names to be used as tasks + # names<Array>:: Method names to be used as commands # # ==== Examples # - # public_task :foo - # public_task :foo, :bar, :baz + # public_command :foo + # public_command :foo, :bar, :baz # - def public_task(*names) + def public_command(*names) names.each do |name| class_eval "def #{name}(*); super end" end end + alias public_task public_command - def handle_no_task_error(task, has_namespace = $thor_runner) #:nodoc: + def handle_no_command_error(command, has_namespace = $thor_runner) #:nodoc: if has_namespace - raise UndefinedTaskError, "Could not find task #{task.inspect} in #{namespace.inspect} namespace." + raise UndefinedCommandError, "Could not find command #{command.inspect} in #{namespace.inspect} namespace." else - raise UndefinedTaskError, "Could not find task #{task.inspect}." + raise UndefinedCommandError, "Could not find command #{command.inspect}." end end + alias handle_no_task_error handle_no_command_error - def handle_argument_error(task, error, arity=nil) #:nodoc: - msg = "#{basename} #{task.name}" - if arity - required = arity < 0 ? (-1 - arity) : arity - msg << " requires at least #{required} argument" - msg << "s" if required > 1 - else - msg = "call #{msg} as" - end - - msg << ": #{self.banner(task).inspect}." + def handle_argument_error(command, error, args, arity) #:nodoc: + msg = "ERROR: \"#{basename} #{command.name}\" was called with " + msg << 'no arguments' if args.empty? + msg << 'arguments ' << args.inspect if !args.empty? + msg << "\nUsage: #{self.banner(command).inspect}" raise InvocationError, msg end @@ -555,28 +555,29 @@ class Thor end end - # Finds a task with the given name. If the task belongs to the current + # Finds a command with the given name. If the command belongs to the current # class, just return it, otherwise dup it and add the fresh copy to the - # current task hash. - def find_and_refresh_task(name) #:nodoc: - task = if task = tasks[name.to_s] - task - elsif task = all_tasks[name.to_s] - tasks[name.to_s] = task.clone + # current command hash. + def find_and_refresh_command(name) #:nodoc: + command = if command = commands[name.to_s] + command + elsif command = all_commands[name.to_s] + commands[name.to_s] = command.clone else - raise ArgumentError, "You supplied :for => #{name.inspect}, but the task #{name.inspect} could not be found." + raise ArgumentError, "You supplied :for => #{name.inspect}, but the command #{name.inspect} could not be found." end end + alias find_and_refresh_task find_and_refresh_command # Everytime someone inherits from a Thor class, register the klass # and file into baseclass. def inherited(klass) Thor::Base.register_klass_file(klass) - klass.instance_variable_set(:@no_tasks, false) + klass.instance_variable_set(:@no_commands, false) end # Fire this callback whenever a method is added. Added methods are - # tracked as tasks by invoking the create_task method. + # tracked as commands by invoking the create_command method. def method_added(meth) meth = meth.to_s @@ -586,12 +587,11 @@ class Thor end # Return if it's not a public instance method - return unless public_instance_methods.include?(meth) || - public_instance_methods.include?(meth.to_sym) + return unless public_method_defined?(meth.to_sym) - return if (defined?(@no_tasks) && @no_tasks) || !create_task(meth) + return if @no_commands || !create_command(meth) - is_thor_reserved_word?(meth, :task) + is_thor_reserved_word?(meth, :command) Thor::Base.register_klass_file(self) end @@ -603,13 +603,16 @@ class Thor else value = superclass.send(method) - if value - if value.is_a?(TrueClass) || value.is_a?(Symbol) - value - else - value.dup - end + # Ruby implements `dup` on Object, but raises a `TypeError` + # if the method is called on immediates. As a result, we + # don't have a good way to check whether dup will succeed + # without calling it and rescuing the TypeError. + begin + value.dup + rescue TypeError + value end + end end @@ -630,10 +633,11 @@ class Thor def baseclass #:nodoc: end - # SIGNATURE: Creates a new task if valid_task? is true. This method is + # SIGNATURE: Creates a new command if valid_command? is true. This method is # called when a new method is added to the class. - def create_task(meth) #:nodoc: + def create_command(meth) #:nodoc: end + alias create_task create_command # SIGNATURE: Defines behavior when the initialize method is added to the # class. @@ -641,7 +645,7 @@ class Thor end # SIGNATURE: The hook invoked by start. - def dispatch(task, given_args, given_opts, config) #:nodoc: + def dispatch(command, given_args, given_opts, config) #:nodoc: raise NotImplementedError end diff --git a/lib/bundler/vendor/thor/task.rb b/lib/bundler/vendor/thor/command.rb index 92fb557f43..e56bd44c0b 100644 --- a/lib/bundler/vendor/thor/task.rb +++ b/lib/bundler/vendor/thor/command.rb @@ -1,5 +1,5 @@ class Thor - class Task < Struct.new(:name, :description, :long_description, :usage, :options) + class Command < Struct.new(:name, :description, :long_description, :usage, :options) FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/ def initialize(name, description, long_description, usage, options=nil) @@ -15,27 +15,27 @@ class Thor false end - # By default, a task invokes a method in the thor class. You can change this - # implementation to create custom tasks. + # By default, a command invokes a method in the thor class. You can change this + # implementation to create custom commands. def run(instance, args=[]) arity = nil if private_method?(instance) - instance.class.handle_no_task_error(name) + instance.class.handle_no_command_error(name) elsif public_method?(instance) arity = instance.method(name).arity instance.__send__(name, *args) elsif local_method?(instance, :method_missing) instance.__send__(:method_missing, name.to_sym, *args) else - instance.class.handle_no_task_error(name) + instance.class.handle_no_command_error(name) end rescue ArgumentError => e handle_argument_error?(instance, e, caller) ? - instance.class.handle_argument_error(self, e, arity) : (raise e) + instance.class.handle_argument_error(self, e, args, arity) : (raise e) rescue NoMethodError => e handle_no_method_error?(instance, e, caller) ? - instance.class.handle_no_task_error(name) : (raise e) + instance.class.handle_no_command_error(name) : (raise e) end # Returns the formatted usage by injecting given required arguments @@ -107,26 +107,30 @@ class Thor error.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/ end end + Task = Command - # A task that is hidden in help messages but still invocable. - class HiddenTask < Task + # A command that is hidden in help messages but still invocable. + class HiddenCommand < Command def hidden? true end end + HiddenTask = HiddenCommand - # A dynamic task that handles method missing scenarios. - class DynamicTask < Task + # A dynamic command that handles method missing scenarios. + class DynamicCommand < Command def initialize(name, options=nil) - super(name.to_s, "A dynamically-generated task", name.to_s, name.to_s, options) + super(name.to_s, "A dynamically-generated command", name.to_s, name.to_s, options) end def run(instance, args=[]) if (instance.methods & [name.to_s, name.to_sym]).empty? super else - instance.class.handle_no_task_error(name) + instance.class.handle_no_command_error(name) end end end + DynamicTask = DynamicCommand + end diff --git a/lib/bundler/vendor/thor/core_ext/dir_escape.rb b/lib/bundler/vendor/thor/core_ext/dir_escape.rb deleted file mode 100644 index e69de29bb2..0000000000 --- a/lib/bundler/vendor/thor/core_ext/dir_escape.rb +++ /dev/null diff --git a/lib/bundler/vendor/thor/core_ext/file_binary_read.rb b/lib/bundler/vendor/thor/core_ext/file_binary_read.rb deleted file mode 100644 index d6af7e44b0..0000000000 --- a/lib/bundler/vendor/thor/core_ext/file_binary_read.rb +++ /dev/null @@ -1,9 +0,0 @@ -class File #:nodoc: - - unless File.respond_to?(:binread) - def self.binread(file) - File.open(file, 'rb') { |f| f.read } - end - end - -end diff --git a/lib/bundler/vendor/thor/core_ext/io_binary_read.rb b/lib/bundler/vendor/thor/core_ext/io_binary_read.rb new file mode 100644 index 0000000000..a824f1b2b1 --- /dev/null +++ b/lib/bundler/vendor/thor/core_ext/io_binary_read.rb @@ -0,0 +1,12 @@ +class IO #:nodoc: + class << self + + def binread(file, *args) + raise ArgumentError, "wrong number of arguments (#{1 + args.size} for 1..3)" unless args.size < 3 + File.open(file, 'rb') do |f| + f.read(*args) + end + end unless method_defined? :binread + + end +end diff --git a/lib/bundler/vendor/thor/error.rb b/lib/bundler/vendor/thor/error.rb index 532db462b8..3174c57eac 100644 --- a/lib/bundler/vendor/thor/error.rb +++ b/lib/bundler/vendor/thor/error.rb @@ -5,17 +5,19 @@ class Thor # Errors that are caused by the developer, like declaring a method which # overwrites a thor keyword, it SHOULD NOT raise a Thor::Error. This way, we # ensure that developer errors are shown with full backtrace. - # class Error < StandardError end - # Raised when a task was not found. - # - class UndefinedTaskError < Error + # Raised when a command was not found. + class UndefinedCommandError < Error end + UndefinedTaskError = UndefinedCommandError - # Raised when a task was found, but not invoked properly. - # + class AmbiguousCommandError < Error + end + AmbiguousTaskError = AmbiguousCommandError + + # Raised when a command was found, but not invoked properly. class InvocationError < Error end @@ -27,9 +29,4 @@ class Thor class MalformattedArgumentError < InvocationError end - - # Raised when a user tries to call a private method encoded in templated filename. - # - class PrivateMethodEncodedError < Error - end end diff --git a/lib/bundler/vendor/thor/group.rb b/lib/bundler/vendor/thor/group.rb index 874ac47a1e..2aaee73778 100644 --- a/lib/bundler/vendor/thor/group.rb +++ b/lib/bundler/vendor/thor/group.rb @@ -1,9 +1,9 @@ require 'thor/base' # Thor has a special class called Thor::Group. The main difference to Thor class -# is that it invokes all tasks at once. It also include some methods that allows +# is that it invokes all commands at once. It also include some methods that allows # invocations to be done at the class method, which are not available to Thor -# tasks. +# commands. class Thor::Group class << self # The description for this Thor::Group. If none is provided, but a source root @@ -14,11 +14,11 @@ class Thor::Group # description<String>:: The description for this Thor::Group. # def desc(description=nil) - case description - when nil - @desc ||= from_superclass(:desc, nil) - else - @desc = description + @desc = case description + when nil + @desc || from_superclass(:desc, nil) + else + description end end @@ -48,7 +48,7 @@ class Thor::Group end # Invoke the given namespace or class given. It adds an instance - # method that will invoke the klass and task. You can give a block to + # method that will invoke the klass and command. You can give a block to # configure how it will be invoked. # # The namespace/class given will have its options showed on the help @@ -64,12 +64,12 @@ class Thor::Group class_eval <<-METHOD, __FILE__, __LINE__ def _invoke_#{name.to_s.gsub(/\W/, '_')} - klass, task = self.class.prepare_for_invocation(nil, #{name.inspect}) + klass, command = self.class.prepare_for_invocation(nil, #{name.inspect}) if klass say_status :invoke, #{name.inspect}, #{verbose.inspect} block = self.class.invocation_blocks[#{name.inspect}] - _invoke_for_class_method klass, task, &block + _invoke_for_class_method klass, command, &block else say_status :error, %(#{name.inspect} [not found]), :red end @@ -100,7 +100,7 @@ class Thor::Group # In some cases you want to customize how a specified hook is going to be # invoked. You can do that by overwriting the class method # prepare_for_invocation. The class method must necessarily return a klass - # and an optional task. + # and an optional command. # # ==== Custom invocations # @@ -127,12 +127,12 @@ class Thor::Group value = options[#{name.inspect}] value = #{name.inspect} if TrueClass === value - klass, task = self.class.prepare_for_invocation(#{name.inspect}, value) + klass, command = self.class.prepare_for_invocation(#{name.inspect}, value) if klass say_status :invoke, value, #{verbose.inspect} block = self.class.invocation_blocks[#{name.inspect}] - _invoke_for_class_method klass, task, &block + _invoke_for_class_method klass, command, &block else say_status :error, %(\#{value} [not found]), :red end @@ -149,7 +149,7 @@ class Thor::Group # def remove_invocation(*names) names.each do |name| - remove_task(name) + remove_command(name) remove_class_option(name) invocations.delete(name) invocation_blocks.delete(name) @@ -196,30 +196,26 @@ class Thor::Group end end - # Returns tasks ready to be printed. - def printable_tasks(*) + # Returns commands ready to be printed. + def printable_commands(*) item = [] item << banner item << (desc ? "# #{desc.gsub(/\s+/m,' ')}" : "") [item] end + alias printable_tasks printable_commands - def handle_argument_error(task, error, arity=nil) #:nodoc: - if arity > 0 - msg = "#{basename} #{task.name} takes #{arity} argument" - msg << "s" if arity > 1 - msg << ", but it should not." - else - msg = "You should not pass arguments to #{basename} #{task.name}." - end - + def handle_argument_error(command, error, args, arity) #:nodoc: + msg = "#{basename} #{command.name} takes #{arity} argument" + msg << "s" if arity > 1 + msg << ", but it should not." raise error, msg end protected # The method responsible for dispatching given the args. - def dispatch(task, given_args, given_opts, config) #:nodoc: + def dispatch(command, given_args, given_opts, config) #:nodoc: if Thor::HELP_MAPPINGS.include?(given_args.first) help(config[:shell]) return @@ -230,10 +226,9 @@ class Thor::Group instance = new(args, opts, config) yield instance if block_given? - args = instance.args - if task - instance.invoke_task(all_tasks[task]) + if command + instance.invoke_command(all_commands[command]) else instance.invoke_all end @@ -242,22 +237,24 @@ class Thor::Group # The banner for this class. You can customize it if you are invoking the # thor class by another ways which is not the Thor::Runner. def banner - "#{basename} #{self_task.formatted_usage(self, false)}" + "#{basename} #{self_command.formatted_usage(self, false)}" end - # Represents the whole class as a task. - def self_task #:nodoc: - Thor::DynamicTask.new(self.namespace, class_options) + # Represents the whole class as a command. + def self_command #:nodoc: + Thor::DynamicCommand.new(self.namespace, class_options) end + alias self_task self_command def baseclass #:nodoc: Thor::Group end - def create_task(meth) #:nodoc: - tasks[meth.to_s] = Thor::Task.new(meth, nil, nil, nil, nil) + def create_command(meth) #:nodoc: + commands[meth.to_s] = Thor::Command.new(meth, nil, nil, nil, nil) true end + alias create_task create_command end include Thor::Base @@ -266,19 +263,19 @@ class Thor::Group # Shortcut to invoke with padding and block handling. Use internally by # invoke and invoke_from_option class methods. - def _invoke_for_class_method(klass, task=nil, *args, &block) #:nodoc: + def _invoke_for_class_method(klass, command=nil, *args, &block) #:nodoc: with_padding do if block case block.arity when 3 - block.call(self, klass, task) + block.call(self, klass, command) when 2 block.call(self, klass) when 1 instance_exec(klass, &block) end else - invoke klass, task, *args + invoke klass, command, *args end end end diff --git a/lib/bundler/vendor/thor/invocation.rb b/lib/bundler/vendor/thor/invocation.rb index 71db7c81de..a9adeb560c 100644 --- a/lib/bundler/vendor/thor/invocation.rb +++ b/lib/bundler/vendor/thor/invocation.rb @@ -6,12 +6,12 @@ class Thor module ClassMethods # This method is responsible for receiving a name and find the proper - # class and task for it. The key is an optional parameter which is + # class and command for it. The key is an optional parameter which is # available only in class methods invocations (i.e. in Thor::Group). def prepare_for_invocation(key, name) #:nodoc: case name when Symbol, String - Thor::Util.find_class_and_task_by_namespace(name.to_s, !key) + Thor::Util.find_class_and_command_by_namespace(name.to_s, !key) else name end @@ -25,15 +25,15 @@ class Thor super end - # Receives a name and invokes it. The name can be a string (either "task" or - # "namespace:task"), a Thor::Task, a Class or a Thor instance. If the task - # cannot be guessed by name, it can also be supplied as second argument. + # Receives a name and invokes it. The name can be a string (either "command" or + # "namespace:command"), a Thor::Command, a Class or a Thor instance. If the + # command cannot be guessed by name, it can also be supplied as second argument. # # You can also supply the arguments, options and configuration values for - # the task to be invoked, if none is given, the same values used to + # the command to be invoked, if none is given, the same values used to # initialize the invoker are used to initialize the invoked. # - # When no name is given, it will invoke the default task of the current class. + # When no name is given, it will invoke the default command of the current class. # # ==== Examples # @@ -54,16 +54,16 @@ class Thor # end # end # - # You can notice that the method "foo" above invokes two tasks: "bar", + # You can notice that the method "foo" above invokes two commands: "bar", # which belongs to the same class and "hello" which belongs to the class B. # - # By using an invocation system you ensure that a task is invoked only once. + # By using an invocation system you ensure that a command is invoked only once. # In the example above, invoking "foo" will invoke "b:hello" just once, even # if it's invoked later by "bar" method. # # When class A invokes class B, all arguments used on A initialization are # supplied to B. This allows lazy parse of options. Let's suppose you have - # some rspec tasks: + # some rspec commands: # # class Rspec < Thor::Group # class_option :mock_framework, :type => :string, :default => :rr @@ -100,30 +100,31 @@ class Thor end args.unshift(nil) if Array === args.first || NilClass === args.first - task, args, opts, config = args + command, args, opts, config = args - klass, task = _retrieve_class_and_task(name, task) + klass, command = _retrieve_class_and_command(name, command) raise "Expected Thor class, got #{klass}" unless klass <= Thor::Base args, opts, config = _parse_initialization_options(args, opts, config) - klass.send(:dispatch, task, args, opts, config) do |instance| + klass.send(:dispatch, command, args, opts, config) do |instance| instance.parent_options = options end end - # Invoke the given task if the given args. - def invoke_task(task, *args) #:nodoc: + # Invoke the given command if the given args. + def invoke_command(command, *args) #:nodoc: current = @_invocations[self.class] - unless current.include?(task.name) - current << task.name - task.run(self, *args) + unless current.include?(command.name) + current << command.name + command.run(self, *args) end end + alias invoke_task invoke_command - # Invoke all tasks for the current instance. + # Invoke all commands for the current instance. def invoke_all #:nodoc: - self.class.all_tasks.map { |_, task| invoke_task(task) } + self.class.all_commands.map { |_, command| invoke_command(command) } end # Invokes using shell padding. @@ -138,21 +139,22 @@ class Thor { :invocations => @_invocations } end - # This method simply retrieves the class and task to be invoked. - # If the name is nil or the given name is a task in the current class, + # This method simply retrieves the class and command to be invoked. + # If the name is nil or the given name is a command in the current class, # use the given name and return self as class. Otherwise, call # prepare_for_invocation in the current class. - def _retrieve_class_and_task(name, sent_task=nil) #:nodoc: + def _retrieve_class_and_command(name, sent_command=nil) #:nodoc: case when name.nil? [self.class, nil] - when self.class.all_tasks[name.to_s] + when self.class.all_commands[name.to_s] [self.class, name.to_s] else - klass, task = self.class.prepare_for_invocation(nil, name) - [klass, task || sent_task] + klass, command = self.class.prepare_for_invocation(nil, name) + [klass, command || sent_command] end end + alias _retrieve_class_and_task _retrieve_class_and_command # Initialize klass using values stored in the @_initializer. def _parse_initialization_options(args, opts, config) #:nodoc: diff --git a/lib/bundler/vendor/thor/parser/options.rb b/lib/bundler/vendor/thor/parser/options.rb index 5e16c52318..9542e816c5 100644 --- a/lib/bundler/vendor/thor/parser/options.rb +++ b/lib/bundler/vendor/thor/parser/options.rb @@ -11,16 +11,16 @@ class Thor def self.to_switches(options) options.map do |key, value| case value - when true - "--#{key}" - when Array - "--#{key} #{value.map{ |v| v.inspect }.join(' ')}" - when Hash - "--#{key} #{value.map{ |k,v| "#{k}:#{v}" }.join(' ')}" - when nil, false - "" - else - "--#{key} #{value.inspect}" + when true + "--#{key}" + when Array + "--#{key} #{value.map{ |v| v.inspect }.join(' ')}" + when Hash + "--#{key} #{value.map{ |k,v| "#{k}:#{v}" }.join(' ')}" + when nil, false + "" + else + "--#{key} #{value.inspect}" end end.join(" ") end @@ -46,7 +46,8 @@ class Thor @switches[option.switch_name] = option option.aliases.each do |short| - @shorts[short.to_s] ||= option.switch_name + name = short.to_s.sub(/^(?!\-)/, '-') + @shorts[name] ||= option.switch_name end end end @@ -79,20 +80,21 @@ class Thor if is_switch case shifted - when SHORT_SQ_RE - unshift($1.split('').map { |f| "-#{f}" }) - next - when EQ_RE, SHORT_NUM - unshift($2) - switch = $1 - when LONG_RE, SHORT_RE - switch = $1 + when SHORT_SQ_RE + unshift($1.split('').map { |f| "-#{f}" }) + next + when EQ_RE, SHORT_NUM + unshift($2) + switch = $1 + when LONG_RE, SHORT_RE + switch = $1 end switch = normalize_switch(switch) option = switch_option(switch) @assigns[option.human_name] = parse_peek(switch, option) elsif @stop_on_unknown + @parsing_options = false @extra << shifted @extra << shift while peek break diff --git a/lib/bundler/vendor/thor/rake_compat.rb b/lib/bundler/vendor/thor/rake_compat.rb index c86e840578..fcb3b24d9d 100644 --- a/lib/bundler/vendor/thor/rake_compat.rb +++ b/lib/bundler/vendor/thor/rake_compat.rb @@ -6,12 +6,13 @@ class Thor # rake package tasks. For example, to use rspec rake tasks, one can do: # # require 'thor/rake_compat' + # require 'rspec/core/rake_task' # # class Default < Thor # include Thor::RakeCompat # - # Spec::Rake::SpecTask.new(:spec) do |t| - # t.spec_opts = ['--options', "spec/spec.opts"] + # RSpec::Core::RakeTask.new(:spec) do |t| + # t.spec_opts = ['--options', "./.rspec"] # t.spec_files = FileList['spec/**/*_spec.rb'] # end # end diff --git a/lib/bundler/vendor/thor/runner.rb b/lib/bundler/vendor/thor/runner.rb index e32ae9ca0f..bef3d57bc7 100644 --- a/lib/bundler/vendor/thor/runner.rb +++ b/lib/bundler/vendor/thor/runner.rb @@ -1,6 +1,6 @@ require 'thor' require 'thor/group' -require 'thor/core_ext/file_binary_read' +require 'thor/core_ext/io_binary_read' require 'fileutils' require 'open-uri' @@ -16,33 +16,33 @@ class Thor::Runner < Thor #:nodoc: def help(meth = nil) if meth && !self.respond_to?(meth) initialize_thorfiles(meth) - klass, task = Thor::Util.find_class_and_task_by_namespace(meth) - self.class.handle_no_task_error(task, false) if klass.nil? - klass.start(["-h", task].compact, :shell => self.shell) + klass, command = Thor::Util.find_class_and_command_by_namespace(meth) + self.class.handle_no_command_error(command, false) if klass.nil? + klass.start(["-h", command].compact, :shell => self.shell) else super end end - # If a task is not found on Thor::Runner, method missing is invoked and - # Thor::Runner is then responsible for finding the task in all classes. + # If a command is not found on Thor::Runner, method missing is invoked and + # Thor::Runner is then responsible for finding the command in all classes. # def method_missing(meth, *args) meth = meth.to_s initialize_thorfiles(meth) - klass, task = Thor::Util.find_class_and_task_by_namespace(meth) - self.class.handle_no_task_error(task, false) if klass.nil? - args.unshift(task) if task + klass, command = Thor::Util.find_class_and_command_by_namespace(meth) + self.class.handle_no_command_error(command, false) if klass.nil? + args.unshift(command) if command klass.start(args, :shell => self.shell) end - desc "install NAME", "Install an optionally named Thor file into your system tasks" + desc "install NAME", "Install an optionally named Thor file into your system commands" method_options :as => :string, :relative => :boolean, :force => :boolean def install(name) initialize_thorfiles # If a directory name is provided as the argument, look for a 'main.thor' - # task in said directory. + # command in said directory. begin if File.directory?(File.expand_path(name)) base, package = File.join(name, "main.thor"), :directory @@ -143,14 +143,14 @@ class Thor::Runner < Thor #:nodoc: end end - desc "installed", "List the installed Thor modules and tasks" + desc "installed", "List the installed Thor modules and commands" method_options :internal => :boolean def installed initialize_thorfiles(nil, true) display_klasses(true, options["internal"]) end - desc "list [SEARCH]", "List the available thor tasks (--substring means .*SEARCH)" + desc "list [SEARCH]", "List the available thor commands (--substring means .*SEARCH)" method_options :substring => :boolean, :group => :string, :all => :boolean, :debug => :boolean def list(search="") initialize_thorfiles @@ -168,8 +168,8 @@ class Thor::Runner < Thor #:nodoc: private - def self.banner(task, all = false, subcommand = false) - "thor " + task.formatted_usage(self, all, subcommand) + def self.banner(command, all = false, subcommand = false) + "thor " + command.formatted_usage(self, all, subcommand) end def thor_root @@ -206,7 +206,7 @@ class Thor::Runner < Thor #:nodoc: # in the thor_root instead of loading them all. # # By default, it also traverses the current path until find Thor files, as - # described in thorfiles. This look up can be skipped by suppliying + # described in thorfiles. This look up can be skipped by supplying # skip_lookup true. # def initialize_thorfiles(relevant_to=nil, skip_lookup=false) @@ -276,25 +276,25 @@ class Thor::Runner < Thor #:nodoc: def display_klasses(with_modules=false, show_internal=false, klasses=Thor::Base.subclasses) klasses -= [Thor, Thor::Runner, Thor::Group] unless show_internal - raise Error, "No Thor tasks available" if klasses.empty? + raise Error, "No Thor commands available" if klasses.empty? show_modules if with_modules && !thor_yaml.empty? list = Hash.new { |h,k| h[k] = [] } groups = klasses.select { |k| k.ancestors.include?(Thor::Group) } # Get classes which inherit from Thor - (klasses - groups).each { |k| list[k.namespace.split(":").first] += k.printable_tasks(false) } + (klasses - groups).each { |k| list[k.namespace.split(":").first] += k.printable_commands(false) } # Get classes which inherit from Thor::Base - groups.map! { |k| k.printable_tasks(false).first } + groups.map! { |k| k.printable_commands(false).first } list["root"] = groups # Order namespaces with default coming first list = list.sort{ |a,b| a[0].sub(/^default/, '') <=> b[0].sub(/^default/, '') } - list.each { |n, tasks| display_tasks(n, tasks) unless tasks.empty? } + list.each { |n, commands| display_commands(n, commands) unless commands.empty? } end - def display_tasks(namespace, list) #:nodoc: + def display_commands(namespace, list) #:nodoc: list.sort!{ |a,b| a[0] <=> b[0] } say shell.set_color(namespace, :blue, true) @@ -303,6 +303,7 @@ class Thor::Runner < Thor #:nodoc: print_table(list, :truncate => true) say end + alias display_tasks display_commands def show_modules #:nodoc: info = [] diff --git a/lib/bundler/vendor/thor/shell/basic.rb b/lib/bundler/vendor/thor/shell/basic.rb index 9f8ad24383..d9119caeff 100644 --- a/lib/bundler/vendor/thor/shell/basic.rb +++ b/lib/bundler/vendor/thor/shell/basic.rb @@ -47,8 +47,13 @@ class Thor # def ask(statement, *args) options = args.last.is_a?(Hash) ? args.pop : {} + color = args.first - options[:limited_to] ? ask_filtered(statement, options[:limited_to], *args) : ask_simply(statement, *args) + if options[:limited_to] + ask_filtered(statement, color, options) + else + ask_simply(statement, color, options) + end end # Say (print) something to the user. If the sentence ends with a whitespace @@ -61,7 +66,7 @@ class Thor def say(message="", color=nil, force_new_line=(message.to_s !~ /( |\t)\Z/)) message = message.to_s - message = set_color(message, *color) if color + message = set_color(message, *color) if color && can_display_colors? spaces = " " * padding @@ -226,20 +231,20 @@ class Thor answer = ask %[Overwrite #{destination}? (enter "h" for help) #{options}] case answer - when is?(:yes), is?(:force), "" - return true - when is?(:no), is?(:skip) - return false - when is?(:always) - return @always_force = true - when is?(:quit) - say 'Aborting...' - raise SystemExit - when is?(:diff) - show_diff(destination, yield) if block_given? - say 'Retrying...' - else - say file_collision_help + when is?(:yes), is?(:force), "" + return true + when is?(:no), is?(:skip) + return false + when is?(:always) + return @always_force = true + when is?(:quit) + say 'Aborting...' + raise SystemExit + when is?(:diff) + show_diff(destination, yield) if block_given? + say 'Retrying...' + else + say file_collision_help end end end @@ -275,6 +280,10 @@ class Thor protected + def can_display_colors? + false + end + def lookup_color(color) return color unless color.is_a?(Symbol) self.class.const_get(color.to_s.upcase) @@ -368,17 +377,30 @@ HELP end end - def ask_simply(statement, color=nil) - say("#{statement} ", color) - stdin.gets.tap{|text| text.strip! if text} + def ask_simply(statement, color, options) + default = options[:default] + message = [statement, ("(#{default})" if default), nil].uniq.join(" ") + say(message, color) + result = stdin.gets + + return unless result + + result.strip! + + if default && result == "" + default + else + result + end end - def ask_filtered(statement, answer_set, *args) + def ask_filtered(statement, color, options) + answer_set = options[:limited_to] correct_answer = nil until correct_answer - answer = ask_simply("#{statement} #{answer_set.inspect}", *args) + answers = answer_set.join(", ") + answer = ask_simply("#{statement} [#{answers}]", color, options) correct_answer = answer_set.include?(answer) ? answer : nil - answers = answer_set.map(&:inspect).join(", ") say("Your response must be one of: [#{answers}]. Please try again.") unless correct_answer end correct_answer diff --git a/lib/bundler/vendor/thor/shell/color.rb b/lib/bundler/vendor/thor/shell/color.rb index f24ab6ccb1..fcf9c25d7d 100644 --- a/lib/bundler/vendor/thor/shell/color.rb +++ b/lib/bundler/vendor/thor/shell/color.rb @@ -94,6 +94,10 @@ class Thor protected + def can_display_colors? + stdout.tty? + end + # Overwrite show_diff to show diff with colors if Diff::LCS is # available. # @@ -112,15 +116,15 @@ class Thor def output_diff_line(diff) #:nodoc: case diff.action - when '-' - say "- #{diff.old_element.chomp}", :red, true - when '+' - say "+ #{diff.new_element.chomp}", :green, true - when '!' - say "- #{diff.old_element.chomp}", :red, true - say "+ #{diff.new_element.chomp}", :green, true - else - say " #{diff.old_element.chomp}", nil, true + when '-' + say "- #{diff.old_element.chomp}", :red, true + when '+' + say "+ #{diff.new_element.chomp}", :green, true + when '!' + say "- #{diff.old_element.chomp}", :red, true + say "+ #{diff.new_element.chomp}", :green, true + else + say " #{diff.old_element.chomp}", nil, true end end diff --git a/lib/bundler/vendor/thor/shell/html.rb b/lib/bundler/vendor/thor/shell/html.rb index 678bc89b87..2a1bb3840a 100644 --- a/lib/bundler/vendor/thor/shell/html.rb +++ b/lib/bundler/vendor/thor/shell/html.rb @@ -73,6 +73,10 @@ class Thor protected + def can_display_colors? + true + end + # Overwrite show_diff to show diff with colors if Diff::LCS is # available. # @@ -91,15 +95,15 @@ class Thor def output_diff_line(diff) #:nodoc: case diff.action - when '-' - say "- #{diff.old_element.chomp}", :red, true - when '+' - say "+ #{diff.new_element.chomp}", :green, true - when '!' - say "- #{diff.old_element.chomp}", :red, true - say "+ #{diff.new_element.chomp}", :green, true - else - say " #{diff.old_element.chomp}", nil, true + when '-' + say "- #{diff.old_element.chomp}", :red, true + when '+' + say "+ #{diff.new_element.chomp}", :green, true + when '!' + say "- #{diff.old_element.chomp}", :red, true + say "+ #{diff.new_element.chomp}", :green, true + else + say " #{diff.old_element.chomp}", nil, true end end diff --git a/lib/bundler/vendor/thor/util.rb b/lib/bundler/vendor/thor/util.rb index d45843dde0..2510630f05 100644 --- a/lib/bundler/vendor/thor/util.rb +++ b/lib/bundler/vendor/thor/util.rb @@ -16,251 +16,255 @@ class Thor # module Util - # Receives a namespace and search for it in the Thor::Base subclasses. - # - # ==== Parameters - # namespace<String>:: The namespace to search for. - # - def self.find_by_namespace(namespace) - namespace = "default#{namespace}" if namespace.empty? || namespace =~ /^:/ - Thor::Base.subclasses.find { |klass| klass.namespace == namespace } - end + class << self - # Receives a constant and converts it to a Thor namespace. Since Thor tasks - # can be added to a sandbox, this method is also responsable for removing - # the sandbox namespace. - # - # This method should not be used in general because it's used to deal with - # older versions of Thor. On current versions, if you need to get the - # namespace from a class, just call namespace on it. - # - # ==== Parameters - # constant<Object>:: The constant to be converted to the thor path. - # - # ==== Returns - # String:: If we receive Foo::Bar::Baz it returns "foo:bar:baz" - # - def self.namespace_from_thor_class(constant) - constant = constant.to_s.gsub(/^Thor::Sandbox::/, "") - constant = snake_case(constant).squeeze(":") - constant - end + # Receives a namespace and search for it in the Thor::Base subclasses. + # + # ==== Parameters + # namespace<String>:: The namespace to search for. + # + def find_by_namespace(namespace) + namespace = "default#{namespace}" if namespace.empty? || namespace =~ /^:/ + Thor::Base.subclasses.find { |klass| klass.namespace == namespace } + end - # Given the contents, evaluate it inside the sandbox and returns the - # namespaces defined in the sandbox. - # - # ==== Parameters - # contents<String> - # - # ==== Returns - # Array[Object] - # - def self.namespaces_in_content(contents, file=__FILE__) - old_constants = Thor::Base.subclasses.dup - Thor::Base.subclasses.clear + # Receives a constant and converts it to a Thor namespace. Since Thor + # commands can be added to a sandbox, this method is also responsable for + # removing the sandbox namespace. + # + # This method should not be used in general because it's used to deal with + # older versions of Thor. On current versions, if you need to get the + # namespace from a class, just call namespace on it. + # + # ==== Parameters + # constant<Object>:: The constant to be converted to the thor path. + # + # ==== Returns + # String:: If we receive Foo::Bar::Baz it returns "foo:bar:baz" + # + def namespace_from_thor_class(constant) + constant = constant.to_s.gsub(/^Thor::Sandbox::/, "") + constant = snake_case(constant).squeeze(":") + constant + end - load_thorfile(file, contents) + # Given the contents, evaluate it inside the sandbox and returns the + # namespaces defined in the sandbox. + # + # ==== Parameters + # contents<String> + # + # ==== Returns + # Array[Object] + # + def namespaces_in_content(contents, file=__FILE__) + old_constants = Thor::Base.subclasses.dup + Thor::Base.subclasses.clear - new_constants = Thor::Base.subclasses.dup - Thor::Base.subclasses.replace(old_constants) + load_thorfile(file, contents) - new_constants.map!{ |c| c.namespace } - new_constants.compact! - new_constants - end + new_constants = Thor::Base.subclasses.dup + Thor::Base.subclasses.replace(old_constants) - # Returns the thor classes declared inside the given class. - # - def self.thor_classes_in(klass) - stringfied_constants = klass.constants.map { |c| c.to_s } - Thor::Base.subclasses.select do |subclass| - next unless subclass.name - stringfied_constants.include?(subclass.name.gsub("#{klass.name}::", '')) + new_constants.map!{ |c| c.namespace } + new_constants.compact! + new_constants end - end - - # Receives a string and convert it to snake case. SnakeCase returns snake_case. - # - # ==== Parameters - # String - # - # ==== Returns - # String - # - def self.snake_case(str) - return str.downcase if str =~ /^[A-Z_]+$/ - str.gsub(/\B[A-Z]/, '_\&').squeeze('_') =~ /_*(.*)/ - return $+.downcase - end - - # Receives a string and convert it to camel case. camel_case returns CamelCase. - # - # ==== Parameters - # String - # - # ==== Returns - # String - # - def self.camel_case(str) - return str if str !~ /_/ && str =~ /[A-Z]+.*/ - str.split('_').map { |i| i.capitalize }.join - end - # Receives a namespace and tries to retrieve a Thor or Thor::Group class - # from it. It first searches for a class using the all the given namespace, - # if it's not found, removes the highest entry and searches for the class - # again. If found, returns the highest entry as the class name. - # - # ==== Examples - # - # class Foo::Bar < Thor - # def baz - # end - # end - # - # class Baz::Foo < Thor::Group - # end - # - # Thor::Util.namespace_to_thor_class("foo:bar") #=> Foo::Bar, nil # will invoke default task - # Thor::Util.namespace_to_thor_class("baz:foo") #=> Baz::Foo, nil - # Thor::Util.namespace_to_thor_class("foo:bar:baz") #=> Foo::Bar, "baz" - # - # ==== Parameters - # namespace<String> - # - def self.find_class_and_task_by_namespace(namespace, fallback = true) - if namespace.include?(?:) # look for a namespaced task - pieces = namespace.split(":") - task = pieces.pop - klass = Thor::Util.find_by_namespace(pieces.join(":")) - end - unless klass # look for a Thor::Group with the right name - klass, task = Thor::Util.find_by_namespace(namespace), nil + # Returns the thor classes declared inside the given class. + # + def thor_classes_in(klass) + stringfied_constants = klass.constants.map { |c| c.to_s } + Thor::Base.subclasses.select do |subclass| + next unless subclass.name + stringfied_constants.include?(subclass.name.gsub("#{klass.name}::", '')) + end end - if !klass && fallback # try a task in the default namespace - task = namespace - klass = Thor::Util.find_by_namespace('') + + # Receives a string and convert it to snake case. SnakeCase returns snake_case. + # + # ==== Parameters + # String + # + # ==== Returns + # String + # + def snake_case(str) + return str.downcase if str =~ /^[A-Z_]+$/ + str.gsub(/\B[A-Z]/, '_\&').squeeze('_') =~ /_*(.*)/ + return $+.downcase end - return klass, task - end - # Receives a path and load the thor file in the path. The file is evaluated - # inside the sandbox to avoid namespacing conflicts. - # - def self.load_thorfile(path, content=nil, debug=false) - content ||= File.binread(path) + # Receives a string and convert it to camel case. camel_case returns CamelCase. + # + # ==== Parameters + # String + # + # ==== Returns + # String + # + def camel_case(str) + return str if str !~ /_/ && str =~ /[A-Z]+.*/ + str.split('_').map { |i| i.capitalize }.join + end - begin - Thor::Sandbox.class_eval(content, path) - rescue Exception => e - $stderr.puts("WARNING: unable to load thorfile #{path.inspect}: #{e.message}") - if debug - $stderr.puts(*e.backtrace) - else - $stderr.puts(e.backtrace.first) + # Receives a namespace and tries to retrieve a Thor or Thor::Group class + # from it. It first searches for a class using the all the given namespace, + # if it's not found, removes the highest entry and searches for the class + # again. If found, returns the highest entry as the class name. + # + # ==== Examples + # + # class Foo::Bar < Thor + # def baz + # end + # end + # + # class Baz::Foo < Thor::Group + # end + # + # Thor::Util.namespace_to_thor_class("foo:bar") #=> Foo::Bar, nil # will invoke default command + # Thor::Util.namespace_to_thor_class("baz:foo") #=> Baz::Foo, nil + # Thor::Util.namespace_to_thor_class("foo:bar:baz") #=> Foo::Bar, "baz" + # + # ==== Parameters + # namespace<String> + # + def find_class_and_command_by_namespace(namespace, fallback = true) + if namespace.include?(?:) # look for a namespaced command + pieces = namespace.split(":") + command = pieces.pop + klass = Thor::Util.find_by_namespace(pieces.join(":")) + end + unless klass # look for a Thor::Group with the right name + klass, command = Thor::Util.find_by_namespace(namespace), nil + end + if !klass && fallback # try a command in the default namespace + command = namespace + klass = Thor::Util.find_by_namespace('') end + return klass, command end - end + alias find_class_and_task_by_namespace find_class_and_command_by_namespace + + # Receives a path and load the thor file in the path. The file is evaluated + # inside the sandbox to avoid namespacing conflicts. + # + def load_thorfile(path, content=nil, debug=false) + content ||= File.binread(path) - def self.user_home - @@user_home ||= if ENV["HOME"] - ENV["HOME"] - elsif ENV["USERPROFILE"] - ENV["USERPROFILE"] - elsif ENV["HOMEDRIVE"] && ENV["HOMEPATH"] - File.join(ENV["HOMEDRIVE"], ENV["HOMEPATH"]) - elsif ENV["APPDATA"] - ENV["APPDATA"] - else begin - File.expand_path("~") - rescue - if File::ALT_SEPARATOR - "C:/" + Thor::Sandbox.class_eval(content, path) + rescue Exception => e + $stderr.puts("WARNING: unable to load thorfile #{path.inspect}: #{e.message}") + if debug + $stderr.puts(*e.backtrace) else - "/" + $stderr.puts(e.backtrace.first) end end end - end - # Returns the root where thor files are located, depending on the OS. - # - def self.thor_root - File.join(user_home, ".thor").gsub(/\\/, '/') - end + def user_home + @@user_home ||= if ENV["HOME"] + ENV["HOME"] + elsif ENV["USERPROFILE"] + ENV["USERPROFILE"] + elsif ENV["HOMEDRIVE"] && ENV["HOMEPATH"] + File.join(ENV["HOMEDRIVE"], ENV["HOMEPATH"]) + elsif ENV["APPDATA"] + ENV["APPDATA"] + else + begin + File.expand_path("~") + rescue + if File::ALT_SEPARATOR + "C:/" + else + "/" + end + end + end + end + + # Returns the root where thor files are located, depending on the OS. + # + def thor_root + File.join(user_home, ".thor").gsub(/\\/, '/') + end - # Returns the files in the thor root. On Windows thor_root will be something - # like this: - # - # C:\Documents and Settings\james\.thor - # - # If we don't #gsub the \ character, Dir.glob will fail. - # - def self.thor_root_glob - files = Dir["#{escape_globs(thor_root)}/*"] + # Returns the files in the thor root. On Windows thor_root will be something + # like this: + # + # C:\Documents and Settings\james\.thor + # + # If we don't #gsub the \ character, Dir.glob will fail. + # + def thor_root_glob + files = Dir["#{escape_globs(thor_root)}/*"] - files.map! do |file| - File.directory?(file) ? File.join(file, "main.thor") : file + files.map! do |file| + File.directory?(file) ? File.join(file, "main.thor") : file + end end - end - # Where to look for Thor files. - # - def self.globs_for(path) - path = escape_globs(path) - ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"] - end + # Where to look for Thor files. + # + def globs_for(path) + path = escape_globs(path) + ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"] + end - # Return the path to the ruby interpreter taking into account multiple - # installations and windows extensions. - # - def self.ruby_command - @ruby_command ||= begin - ruby_name = RbConfig::CONFIG['ruby_install_name'] - ruby = File.join(RbConfig::CONFIG['bindir'], ruby_name) - ruby << RbConfig::CONFIG['EXEEXT'] + # Return the path to the ruby interpreter taking into account multiple + # installations and windows extensions. + # + def ruby_command + @ruby_command ||= begin + ruby_name = RbConfig::CONFIG['ruby_install_name'] + ruby = File.join(RbConfig::CONFIG['bindir'], ruby_name) + ruby << RbConfig::CONFIG['EXEEXT'] - # avoid using different name than ruby (on platforms supporting links) - if ruby_name != 'ruby' && File.respond_to?(:readlink) - begin - alternate_ruby = File.join(RbConfig::CONFIG['bindir'], 'ruby') - alternate_ruby << RbConfig::CONFIG['EXEEXT'] + # avoid using different name than ruby (on platforms supporting links) + if ruby_name != 'ruby' && File.respond_to?(:readlink) + begin + alternate_ruby = File.join(RbConfig::CONFIG['bindir'], 'ruby') + alternate_ruby << RbConfig::CONFIG['EXEEXT'] - # ruby is a symlink - if File.symlink? alternate_ruby - linked_ruby = File.readlink alternate_ruby + # ruby is a symlink + if File.symlink? alternate_ruby + linked_ruby = File.readlink alternate_ruby - # symlink points to 'ruby_install_name' - ruby = alternate_ruby if linked_ruby == ruby_name || linked_ruby == ruby + # symlink points to 'ruby_install_name' + ruby = alternate_ruby if linked_ruby == ruby_name || linked_ruby == ruby + end + rescue NotImplementedError + # just ignore on windows end - rescue NotImplementedError - # just ignore on windows end + + # escape string in case path to ruby executable contain spaces. + ruby.sub!(/.*\s.*/m, '"\&"') + ruby end + end - # escape string in case path to ruby executable contain spaces. - ruby.sub!(/.*\s.*/m, '"\&"') - ruby + # Returns a string that has had any glob characters escaped. + # The glob characters are `* ? { } [ ]`. + # + # ==== Examples + # + # Thor::Util.escape_globs('[apps]') # => '\[apps\]' + # + # ==== Parameters + # String + # + # ==== Returns + # String + # + def escape_globs(path) + path.to_s.gsub(/[*?{}\[\]]/, '\\\\\\&') end - end - # Returns a string that has had any glob characters escaped. - # The glob characters are `* ? { } [ ]`. - # - # ==== Examples - # - # Thor::Util.escape_globs('[apps]') # => '\[apps\]' - # - # ==== Parameters - # String - # - # ==== Returns - # String - # - def self.escape_globs(path) - path.to_s.gsub(/[*?{}\[\]]/, '\\\\\\&') end - end end diff --git a/lib/bundler/vendor/thor/version.rb b/lib/bundler/vendor/thor/version.rb index 4765b35d20..646cd37dec 100644 --- a/lib/bundler/vendor/thor/version.rb +++ b/lib/bundler/vendor/thor/version.rb @@ -1,3 +1,3 @@ class Thor - VERSION = "0.16.0" + VERSION = "0.18.1" end diff --git a/man/bundle-install.ronn b/man/bundle-install.ronn index 1bf9066308..4bbfc8c969 100644 --- a/man/bundle-install.ronn +++ b/man/bundle-install.ronn @@ -10,6 +10,7 @@ bundle-install(1) -- Install the dependencies specified in your Gemfile [--binstubs[=DIRECTORY]] [--standalone[=GROUP1[ GROUP2...]]] [--trust-policy=POLICY] + [--jobs=SIZE] [--no-cache] [--quiet] @@ -59,7 +60,7 @@ update process below under [CONSERVATIVE UPDATING][]. Do not attempt to connect to `rubygems.org`, instead using just the gems already present in Rubygems' cache or in `vendor/cache`. Note that if a more appropriate platform-specific gem exists on - `rubygems.org`, it will not be found. + `rubygems.org`, it will not be found. This option implies `--no-cache`. * `--deployment`: Switches bundler's defaults into [deployment mode][DEPLOYMENT MODE]. @@ -88,6 +89,9 @@ update process below under [CONSERVATIVE UPDATING][]. HighSecurity, MediumSecurity, LowSecurity, or NoSecurity. For more detail, see the Rubygems signing documentation, linked below in [SEE ALSO][]. +* `--jobs=[<size>]`: + Install gems parallely by starting <size> number of parallel workers. + * `--no-cache`: Do not update the cache in `vendor/cache` with the newly bundled gems. This does not remove any existing cached gems, only stops the newly bundled gems diff --git a/man/gemfile.5.ronn b/man/gemfile.5.ronn index 53c1c341aa..17b0ce2c1a 100644 --- a/man/gemfile.5.ronn +++ b/man/gemfile.5.ronn @@ -74,16 +74,19 @@ Each _gem_ `MAY` have one or more version specifiers. ### REQUIRE AS (:require) Each _gem_ `MAY` specify files that should be used when autorequiring via -`Bundler.require`. You may pass an array with multiple files, or `false` to +`Bundler.require`. You may pass an array with multiple files or `true` if file +you want `required` has same name as _gem_ or `false` to prevent any file from being autorequired. gem "redis", :require => ["redis/connection/hiredis", "redis"] gem "webmock", :require => false + gem "debugger", :require => true The argument defaults to the name of the gem. For example, these are identical: gem "nokogiri" gem "nokogiri", :require => "nokogiri" + gem "nokogiri", :require => true ### GROUPS (:group or :groups) @@ -258,6 +261,10 @@ Are both equivalent to gem "rails", :git => "git://github.com/rails/rails.git" +In addition, if you wish to choose a specific branch: + + gem "rails", :github => "rails/rails", :branch => "branch_name" + ### PATH (:path) You can specify that a gem is located in a particular location diff --git a/spec/bundler/cli_rspec.rb b/spec/bundler/cli_rspec.rb index e476c74498..0f7711144c 100644 --- a/spec/bundler/cli_rspec.rb +++ b/spec/bundler/cli_rspec.rb @@ -1,13 +1,13 @@ require 'spec_helper' -describe 'bundle executable' do - it 'returns non-zero exit status when passed unrecognized options' do +describe "bundle executable" do + it "returns non-zero exit status when passed unrecognized options" do bundle '--invalid_argument', :exitstatus => true expect(exitstatus).to_not be_zero end - it 'returns non-zero exit status when passed unrecognized task' do + it "returns non-zero exit status when passed unrecognized task" do bundle 'unrecognized-tast', :exitstatus => true expect(exitstatus).to_not be_zero end diff --git a/spec/bundler/dsl_spec.rb b/spec/bundler/dsl_spec.rb index 17ce5246fb..1b05eb48b4 100644 --- a/spec/bundler/dsl_spec.rb +++ b/spec/bundler/dsl_spec.rb @@ -2,14 +2,14 @@ require 'spec_helper' describe Bundler::Dsl do before do - @rubygems = mock("rubygems") + @rubygems = double("rubygems") Bundler::Source::Rubygems.stub(:new){ @rubygems } end - describe '#_normalize_options' do + describe "#_normalize_options" do it "converts :github to :git" do subject.gem("sparks", :github => "indirect/sparks") - github_uri = "git://github.com/indirect/sparks.git" + github_uri = "https://github.com/indirect/sparks.git" expect(subject.dependencies.first.source.uri).to eq(github_uri) end @@ -27,13 +27,13 @@ describe Bundler::Dsl do it "converts 'rails' to 'rails/rails'" do subject.gem("rails", :github => "rails") - github_uri = "git://github.com/rails/rails.git" + github_uri = "https://github.com/rails/rails.git" expect(subject.dependencies.first.source.uri).to eq(github_uri) end end - describe '#method_missing' do - it 'raises an error for unknown DSL methods' do + describe "#method_missing" do + it "raises an error for unknown DSL methods" do Bundler.should_receive(:read_file).with("Gemfile").and_return("unknown") error_msg = "Undefined local variable or method `unknown'" \ " for Gemfile\\s+from Gemfile:1" @@ -51,7 +51,7 @@ describe Bundler::Dsl do end describe "syntax errors" do - it "raise a Bundler::GemfileError" 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) diff --git a/spec/bundler/gem_helper_spec.rb b/spec/bundler/gem_helper_spec.rb index 134fc87099..d3181e076c 100644 --- a/spec/bundler/gem_helper_spec.rb +++ b/spec/bundler/gem_helper_spec.rb @@ -62,7 +62,7 @@ describe "Bundler::GemHelper tasks" do expect(Bundler.ui).to be_a(Bundler::UI::Shell) end - describe 'install_tasks' do + describe "install_tasks" do before(:each) do @saved, Rake.application = Rake.application, Rake::Application.new end @@ -92,7 +92,7 @@ describe "Bundler::GemHelper tasks" do end end - describe 'build' do + describe "build" do it "builds" do mock_build_message @helper.build_gem @@ -106,7 +106,7 @@ describe "Bundler::GemHelper tasks" do end end - describe 'install' do + describe "install" do it "installs" do mock_build_message mock_confirm_message "test (0.0.1) installed." @@ -127,7 +127,15 @@ describe "Bundler::GemHelper tasks" do end end - describe 'release' do + describe "release" do + before do + Dir.chdir(@app) do + `git init` + `git config user.email "you@example.com"` + `git config user.name "name"` + end + end + it "shouldn't push if there are unstaged files" do expect { @helper.release_gem }.to raise_error(/files that need to be committed/) end @@ -137,18 +145,11 @@ describe "Bundler::GemHelper tasks" do expect { @helper.release_gem }.to raise_error(/files that need to be committed/) end - it 'raises an appropriate error if there is no git remote' do + it "raises an appropriate error if there is no git remote" do Bundler.ui.stub(:confirm => nil, :error => nil) # silence messages - Dir.chdir(gem_repo1) { - `git init --bare` - } - Dir.chdir(@app) { - `git init` - `git config user.email "you@example.com"` - `git config user.name "name"` - `git commit -a -m "initial commit"` - } + Dir.chdir(gem_repo1) { `git init --bare` } + Dir.chdir(@app) { `git commit -a -m "initial commit"` } expect { @helper.release_gem }.to raise_error end @@ -160,18 +161,13 @@ describe "Bundler::GemHelper tasks" do @helper.should_receive(:rubygem_push).with(bundled_app('test/pkg/test-0.0.1.gem').to_s) - Dir.chdir(gem_repo1) { - `git init --bare` - } - Dir.chdir(@app) { - `git init` - `git config user.email "you@example.com"` - `git config user.name "name"` + Dir.chdir(gem_repo1) { `git init --bare` } + Dir.chdir(@app) do `git remote add origin file://#{gem_repo1}` `git commit -a -m "initial commit"` sys_exec("git push origin master", true) `git commit -a -m "another commit"` - } + end @helper.release_gem end diff --git a/spec/bundler/safe_catch_spec.rb b/spec/bundler/safe_catch_spec.rb new file mode 100644 index 0000000000..40b6ec092c --- /dev/null +++ b/spec/bundler/safe_catch_spec.rb @@ -0,0 +1,37 @@ +# encoding: utf-8 +require 'spec_helper' +require 'bundler' +require "bundler/safe_catch" +require "bundler/current_ruby" + +class RecursiveTmpResolver + include Bundler::SafeCatch +end + +describe Bundler::SafeCatch do + let(:resolver) { RecursiveTmpResolver.new() } + + it "should use safe_catch on jruby" do + if Bundler.current_ruby.jruby? + Bundler::SafeCatch::Internal.should_receive(:catch).and_call_original + Bundler::SafeCatch::Internal.should_receive(:throw).and_call_original + + retval = resolver.safe_catch(:resolve) do + resolver.safe_throw(:resolve, "good bye world") + end + expect(retval).to eq("good bye world") + end + end + + it "should use regular catch/throw on MRI" do + if Bundler.current_ruby.mri? + Bundler::SafeCatch::Internal.should_not_receive(:catch) + Bundler::SafeCatch::Internal.should_not_receive(:throw) + + retval = resolver.safe_catch(:resolve) do + resolver.safe_throw(:resolve, "good bye world") + end + expect(retval).to eq("good bye world") + end + end +end diff --git a/spec/install/gems/dependency_api_spec.rb b/spec/install/gems/dependency_api_spec.rb index f89c865eba..d56d788c21 100644 --- a/spec/install/gems/dependency_api_spec.rb +++ b/spec/install/gems/dependency_api_spec.rb @@ -325,7 +325,7 @@ describe "gemcutter's dependency API" do expect(out).to include("Fetching gem metadata from #{source_uri}") end - fit "should install when EndpointSpecification with a bin dir owned by root", :sudo => true do + it "should install when EndpointSpecification has a bin dir owned by root", :sudo => true do sudo "mkdir -p #{system_gem_path("bin")}" sudo "chown -R root #{system_gem_path("bin")}" @@ -334,7 +334,6 @@ describe "gemcutter's dependency API" do gem "rails" G bundle :install, :artifice => "endpoint" - puts out, err should_be_installed "rails 2.3.2" end diff --git a/spec/install/gems/packed_spec.rb b/spec/install/gems/packed_spec.rb index cce3b4ab26..1992f5d342 100644 --- a/spec/install/gems/packed_spec.rb +++ b/spec/install/gems/packed_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" describe "bundle install with gem sources" do describe "when cached and locked" do - it "does not hit the remote at all" do + it "does not hit the remote at all if --local is passed" do build_repo2 install_gemfile <<-G source "file://#{gem_repo2}" @@ -14,10 +14,11 @@ describe "bundle install with gem sources" do FileUtils.rm_rf gem_repo2 bundle "install --local" + expect(out).not_to include("Updating files in vendor/cache") should_be_installed "rack 1.0.0" end - it "does not hit the remote at all" do + it "does not hit the remote at all if --deployment is passed" do build_repo2 install_gemfile <<-G source "file://#{gem_repo2}" @@ -29,6 +30,7 @@ describe "bundle install with gem sources" do FileUtils.rm_rf gem_repo2 bundle "install --deployment" + expect(out).not_to include("Updating files in vendor/cache") should_be_installed "rack 1.0.0" end diff --git a/spec/install/gems/post_install_spec.rb b/spec/install/gems/post_install_spec.rb index f7cad665e1..49ee8d8385 100644 --- a/spec/install/gems/post_install_spec.rb +++ b/spec/install/gems/post_install_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe 'bundle install with gem sources' do - describe 'when gems include post install messages' do +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}" @@ -20,7 +20,7 @@ describe 'bundle install with gem sources' do end end - describe 'when gems do not include post install messages' do + 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}" diff --git a/spec/install/gems/resolving_spec.rb b/spec/install/gems/resolving_spec.rb index edf8b326ae..c459becfca 100644 --- a/spec/install/gems/resolving_spec.rb +++ b/spec/install/gems/resolving_spec.rb @@ -67,6 +67,43 @@ describe "bundle install with gem sources" do should_be_installed "net_a 1.0", "net_b 1.0", "net_c 1.0", "net_d 1.0", "net_e 1.0" end + + context "with ENV['DEBUG_RESOLVER'] set" do + before do + ENV['DEBUG_RESOLVER'] = '1' + end + it "produces debug output" do + expect(capture(:stdout) do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "net_c" + gem "net_e" + G + end).to include "==== Iterating ====" + end + after do + ENV['DEBUG_RESOLVER'] = nil + end + end + + context "with ENV['DEBUG_RESOLVER_TREE'] set" do + before do + ENV['DEBUG_RESOLVER_TREE'] = '1' + end + it "produces debug output" do + expect(capture(:stdout) do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "net_c" + gem "net_e" + G + end).to include " net_b (>= 0) ruby" + end + after do + ENV['DEBUG_RESOLVER_TREE'] = nil + end + end + end describe "when some gems require a different version of ruby" do diff --git a/spec/install/gems/simple_case_spec.rb b/spec/install/gems/simple_case_spec.rb index 4c38e77b82..630ea083c1 100644 --- a/spec/install/gems/simple_case_spec.rb +++ b/spec/install/gems/simple_case_spec.rb @@ -814,4 +814,22 @@ describe "bundle install with gem sources" do end end + it "should use gemspecs in the system cache when available" do + gemfile <<-G + source "http://localtestserver.gem" + gem 'rack' + G + + FileUtils.mkdir_p "#{tmp}/gems/system/specifications" + File.open("#{tmp}/gems/system/specifications/rack-1.0.0.gemspec", 'w+') do |f| + spec = Gem::Specification.new do |s| + s.name = 'rack' + s.version = '1.0.0' + s.add_runtime_dependency 'activesupport', '2.3.2' + end + f.write spec.to_ruby + end + bundle :install, :artifice => 'endpoint_marshal_fail' # force gemspec load + should_be_installed "activesupport 2.3.2" + end end diff --git a/spec/install/gems/win32_spec.rb b/spec/install/gems/win32_spec.rb index 771d3faa4a..6224220281 100644 --- a/spec/install/gems/win32_spec.rb +++ b/spec/install/gems/win32_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe "bundle install with win32-generated lockfile" do - it 'should read lockfile' do + it "should read lockfile" do File.open(bundled_app('Gemfile.lock'), 'wb') do |f| f << "GEM\r\n" f << " remote: file:#{gem_repo1}/\r\n" diff --git a/spec/install/security_policy_spec.rb b/spec/install/security_policy_spec.rb index c0cadcf9ba..941e14de27 100644 --- a/spec/install/security_policy_spec.rb +++ b/spec/install/security_policy_spec.rb @@ -14,37 +14,37 @@ describe "policies with unsigned gems" do G end - it "works after you try to deploy without a lock" do + it "will work after you try to deploy without a lock" do bundle "install --deployment" bundle :install, :exitstatus => true expect(exitstatus).to eq(0) should_be_installed "rack 1.0", "signed_gem 1.0" end - it "fails when given invalid security policy" do + it "will fail when given invalid security policy" do bundle "install --trust-policy=InvalidPolicyName" expect(out).to include("Rubygems doesn't know about trust policy") end - it "fails with High Security setting due to presence of unsigned gem" do + it "will fail with High Security setting due to presence of unsigned gem" do bundle "install --trust-policy=HighSecurity" expect(out).to include("security policy didn't allow") end # This spec will fail on Rubygems 2 rc1 due to a bug in policy.rb. the bug is fixed in rc3. - it "fails with Medium Security setting due to presence of unsigned gem", :unless => ENV['RGV'] == "v2.0.0.rc.1" do + it "will fail with Medium Security setting due to presence of unsigned gem", :unless => ENV['RGV'] == "v2.0.0.rc.1" do bundle "install --trust-policy=MediumSecurity" expect(out).to include("security policy didn't allow") end - it "succeeds with no policy" do + it "will succeed with no policy" do bundle "install", :exitstatus => true expect(exitstatus).to eq(0) end end -describe "policies with signed gems, no CA" do +describe "policies with signed gems and no CA" do before do build_security_repo gemfile <<-G @@ -53,26 +53,25 @@ describe "policies with signed gems, no CA" do G end - it "fails with High Security setting, gem is self-signed" do + it "will fail with High Security setting, gem is self-signed" do bundle "install --trust-policy=HighSecurity" expect(out).to include("security policy didn't allow") end - it "fails with Medium Security setting, gem is self-signed" do + it "will fail with Medium Security setting, gem is self-signed" do bundle "install --trust-policy=MediumSecurity" expect(out).to include("security policy didn't allow") end - it "succeeds with Low Security setting, low security accepts self signed gem" do + it "will succeed with Low Security setting, low security accepts self signed gem" do bundle "install --trust-policy=LowSecurity", :exitstatus => true expect(exitstatus).to eq(0) should_be_installed "signed_gem 1.0" end - it "succeeds with no policy" do + it "will succeed with no policy" do bundle "install", :exitstatus => true expect(exitstatus).to eq(0) should_be_installed "signed_gem 1.0" end - end diff --git a/spec/integration/inject.rb b/spec/integration/inject.rb index f7ffff6083..2737b1a4c7 100644 --- a/spec/integration/inject.rb +++ b/spec/integration/inject.rb @@ -21,7 +21,7 @@ describe "bundle inject" do bundle "install" end - it "adds the injected gems to the gemfile" do + it "adds the injected gems to the Gemfile" do expect(bundled_app("Gemfile").read).not_to match(/rack-obama/) bundle "inject 'rack-obama' '> 0'" expect(bundled_app("Gemfile").read).to match(/rack-obama/) diff --git a/spec/lock/git_spec.rb b/spec/lock/git_spec.rb index 23b7f3112c..9d4ecf9bc7 100644 --- a/spec/lock/git_spec.rb +++ b/spec/lock/git_spec.rb @@ -31,5 +31,4 @@ describe "bundle lock with git gems" do RUBY expect(out).to eq(bundle("show foo")) end - end diff --git a/spec/lock/lockfile_spec.rb b/spec/lock/lockfile_spec.rb index 30c4b22483..779fd1c613 100644 --- a/spec/lock/lockfile_spec.rb +++ b/spec/lock/lockfile_spec.rb @@ -292,7 +292,7 @@ describe "the lockfile format" do G end - it "order dependencies of dependencies in alphabetical order" do + it "orders dependencies' dependencies in alphabetical order" do install_gemfile <<-G source "file://#{gem_repo1}" @@ -328,7 +328,7 @@ describe "the lockfile format" do G end - it "orders dependencies according to version" do + it "orders dependencies by version" do install_gemfile <<-G source "file://#{gem_repo1}" gem 'double_deps' @@ -472,6 +472,30 @@ describe "the lockfile format" do G end + it "stores relative paths when the path is provided for gemspec" do + build_lib("foo", :path => tmp.join("foo")) + + install_gemfile <<-G + gemspec :path => "../foo" + G + + lockfile_should_be <<-G + PATH + remote: ../foo + specs: + foo (1.0) + + GEM + specs: + + PLATFORMS + #{generic(Gem::Platform.local)} + + DEPENDENCIES + foo! + G + end + it "keeps existing platforms in the lockfile" do lockfile <<-G GEM @@ -638,7 +662,7 @@ describe "the lockfile format" do end - it "raises if two different versions are used" do + it "raises if two different sources are used" do install_gemfile <<-G source "file://#{gem_repo1}" gem "rack" @@ -676,7 +700,7 @@ describe "the lockfile format" do # * multiple copies of the same GIT section appeared in the lockfile # * when this happened, those sections got multiple copies of gems # in those sections. - it "fix corrupted lockfiles" do + it "fixes corrupted lockfiles" do build_git "omg", :path => lib_path('omg') revision = revision_for(lib_path('omg')) @@ -740,7 +764,7 @@ describe "the lockfile format" do L end - describe "line endings" do + describe "a line ending" do def set_lockfile_mtime_to_known_value time = Time.local(2000, 1, 1, 0, 0, 0) File.utime(time, time, bundled_app('Gemfile.lock')) diff --git a/spec/other/binstubs_spec.rb b/spec/other/binstubs_spec.rb index f5e1347a39..3d3ed0fc58 100644 --- a/spec/other/binstubs_spec.rb +++ b/spec/other/binstubs_spec.rb @@ -26,6 +26,30 @@ describe "bundle binstubs <gem>" do expect(bundled_app("bin/rails")).to exist end + it "does install multiple binstubs" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + gem "rails" + G + + bundle "binstubs rails rack" + + expect(bundled_app("bin/rackup")).to exist + expect(bundled_app("bin/rails")).to exist + end + + it "displays an error when used without any gem" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle "binstubs", :exitstatus => true + expect(exitstatus).to eq(1) + expect(out).to eq("`bundle binstubs` needs at least one gem to run.") + end + it "does not bundle the bundler binary" do install_gemfile <<-G source "file://#{gem_repo1}" @@ -37,7 +61,7 @@ describe "bundle binstubs <gem>" do expect(out).to eq("Sorry, Bundler can only be run via Rubygems.") end - it "install binstubs from git gems" do + it "installs binstubs from git gems" do FileUtils.mkdir_p(lib_path("foo/bin")) FileUtils.touch(lib_path("foo/bin/foo")) build_git "foo", "1.0", :path => lib_path("foo") do |s| @@ -95,7 +119,7 @@ describe "bundle binstubs <gem>" do end context "when the bin already exists" do - it "don't override it and warn" do + it "doesn't overwrite and warns" do FileUtils.mkdir_p(bundled_app("bin")) File.open(bundled_app("bin/rackup"), 'wb') do |file| file.print "OMG" @@ -115,7 +139,7 @@ describe "bundle binstubs <gem>" do end context "when using --force" do - it "overrides the binstub" do + it "overwrites the binstub" do FileUtils.mkdir_p(bundled_app("bin")) File.open(bundled_app("bin/rackup"), 'wb') do |file| file.print "OMG" diff --git a/spec/other/bundle_ruby_spec.rb b/spec/other/bundle_ruby_spec.rb index 164a23c6ae..5bb563a74f 100644 --- a/spec/other/bundle_ruby_spec.rb +++ b/spec/other/bundle_ruby_spec.rb @@ -83,7 +83,7 @@ describe "bundle_ruby" do expect(out).to eq("Please define :engine") end - it "raises an error if engine version doesn't match ruby version for mri" do + it "raises an error if engine version doesn't match ruby version for MRI" do gemfile <<-G source "file://#{gem_repo1}" ruby "1.8.7", :engine => 'ruby', :engine_version => '1.2.4' diff --git a/spec/other/check_spec.rb b/spec/other/check_spec.rb index 5f36fa26fe..248b5c2eed 100644 --- a/spec/other/check_spec.rb +++ b/spec/other/check_spec.rb @@ -23,7 +23,7 @@ describe "bundle check" do expect(out).to eq("The Gemfile's dependencies are satisfied") end - it "creates a Gemfile.lock by default if one did not exist" do + it "creates a Gemfile.lock by default if one does not exist" do install_gemfile <<-G source "file://#{gem_repo1}" gem "rails" diff --git a/spec/other/clean_spec.rb b/spec/other/clean_spec.rb index 393902ba08..cee1312a5f 100644 --- a/spec/other/clean_spec.rb +++ b/spec/other/clean_spec.rb @@ -100,7 +100,7 @@ describe "bundle clean" do expect(vendored_gems("bin/rackup")).to exist end - it "remove gems in bundle without groups" do + it "removes gems in bundle without groups" do gemfile <<-G source "file://#{gem_repo1}" @@ -288,7 +288,7 @@ describe "bundle clean" do end # handling bundle clean upgrade path from the pre's - it "removes .gem/.gemspec file even if there's no corresponding gem dir is already moved" do + it "removes .gem/.gemspec file even if there's no corresponding gem dir" do gemfile <<-G source "file://#{gem_repo1}" diff --git a/spec/other/exec_spec.rb b/spec/other/exec_spec.rb index b5504249cc..4ed9f2f6aa 100644 --- a/spec/other/exec_spec.rb +++ b/spec/other/exec_spec.rb @@ -108,13 +108,13 @@ describe "bundle exec" do should_not_be_installed "rack_middleware 1.0" end - it "should not duplicate already exec'ed RUBYOPT or PATH" do + it "does not duplicate already exec'ed RUBYOPT" do install_gemfile <<-G gem "rack" G rubyopt = ENV['RUBYOPT'] - rubyopt = "-I#{bundler_path} -rbundler/setup #{rubyopt}" + rubyopt = "-rbundler/setup #{rubyopt}" bundle "exec 'echo $RUBYOPT'" expect(out).to have_rubyopts(rubyopt) @@ -123,6 +123,22 @@ describe "bundle exec" do expect(out).to have_rubyopts(rubyopt) end + it "does not duplicate already exec'ed RUBYLIB" do + install_gemfile <<-G + gem "rack" + G + + rubylib = ENV['RUBYLIB'] + rubylib = "#{rubylib}".split(File::PATH_SEPARATOR).unshift "#{bundler_path}" + rubylib = rubylib.uniq.join(File::PATH_SEPARATOR) + + bundle "exec 'echo $RUBYLIB'" + expect(out).to eq(rubylib) + + bundle "exec 'echo $RUBYLIB'", :env => {"RUBYLIB" => rubylib} + expect(out).to eq(rubylib) + end + it "errors nicely when the argument doesn't exist" do install_gemfile <<-G gem "rack" @@ -244,6 +260,5 @@ describe "bundle exec" do expect(out).to eq("1.0") end end - end end diff --git a/spec/other/init_spec.rb b/spec/other/init_spec.rb index 1c686461e6..8bd566dcf5 100644 --- a/spec/other/init_spec.rb +++ b/spec/other/init_spec.rb @@ -36,5 +36,4 @@ describe "bundle init" do expect(gemfile.scan(/gem "rspec", "= 1.2"/).size).to eq(1) expect(gemfile.scan(/group :development/).size).to eq(1) end - end diff --git a/spec/other/outdated_spec.rb b/spec/other/outdated_spec.rb index 54676fcf0e..b498510ff7 100644 --- a/spec/other/outdated_spec.rb +++ b/spec/other/outdated_spec.rb @@ -1,7 +1,6 @@ require "spec_helper" describe "bundle outdated" do - before :each do build_repo2 do build_git "foo", :path => lib_path("foo") @@ -26,7 +25,7 @@ describe "bundle outdated" do bundle "outdated" - expect(out).to include("activesupport (3.0 > 2.3.5)") + expect(out).to include("activesupport (3.0 > 2.3.5) Gemfile specifies \"= 2.3.5\"") expect(out).to include("foo (1.0") # Gem names are one per-line, between "*" and their parenthesized version. @@ -93,7 +92,7 @@ describe "bundle outdated" do end bundle "outdated --pre" - expect(out).to include("activesupport (3.0.0.beta > 2.3.5)") + expect(out).to include("activesupport (3.0.0.beta > 2.3.5) Gemfile specifies \"= 2.3.5\"") end end @@ -110,9 +109,8 @@ describe "bundle outdated" do G bundle "outdated" - expect(out).to include("activesupport (3.0.0.beta.2 > 3.0.0.beta.1)") + expect(out).to include("activesupport (3.0.0.beta.2 > 3.0.0.beta.1) Gemfile specifies \"= 3.0.0.beta.1\"") end end end - end diff --git a/spec/other/platform_spec.rb b/spec/other/platform_spec.rb index f0329a53e9..4efac30e8e 100644 --- a/spec/other/platform_spec.rb +++ b/spec/other/platform_spec.rb @@ -81,7 +81,7 @@ G expect(out).to eq("ruby 1.9.3") end - it "engine defaults to MRI" do + it "defaults to MRI" do gemfile <<-G source "file://#{gem_repo1}" ruby "1.9.3" @@ -146,7 +146,7 @@ G expect(exitstatus).not_to eq(0) end - it "raises an error if engine version doesn't match ruby version for mri" do + it "raises an error if engine version doesn't match ruby version for MRI" do gemfile <<-G source "file://#{gem_repo1}" ruby "1.8.7", :engine => 'ruby', :engine_version => '1.2.4' diff --git a/spec/quality_spec.rb b/spec/quality_spec.rb index a9d3d6f4f8..840f2be97f 100644 --- a/spec/quality_spec.rb +++ b/spec/quality_spec.rb @@ -5,6 +5,18 @@ if defined?(Encoding) && Encoding.default_external != "UTF-8" end describe "The library itself" do + def check_for_spec_defs_with_single_quotes(filename) + failing_lines = [] + + File.readlines(filename).each_with_index do |line,number| + failing_lines << number + 1 if line =~ /^ *(describe|it|context) {1}'{1}/ + end + + unless failing_lines.empty? + "#{filename} uses inconsistent single quotes on lines #{failing_lines.join(', ')}" + end + end + def check_for_tab_characters(filename) failing_lines = [] File.readlines(filename).each_with_index do |line,number| @@ -20,6 +32,7 @@ describe "The library itself" do failing_lines = [] File.readlines(filename).each_with_index do |line,number| next if line =~ /^\s+#.*\s+\n$/ + next if %w(LICENCE.md).include?(line) failing_lines << number + 1 if line =~ /\s+\n$/ end @@ -39,10 +52,11 @@ describe "The library itself" do end it "has no malformed whitespace" do + exempt = /\.gitmodules|\.marshal|fixtures|vendor|ssl_certs|LICENSE/ error_messages = [] Dir.chdir(File.expand_path("../..", __FILE__)) do `git ls-files`.split("\n").each do |filename| - next if filename =~ /\.gitmodules|\.marshal|fixtures|vendor|ssl_certs/ + next if filename =~ exempt error_messages << check_for_tab_characters(filename) error_messages << check_for_extra_spaces(filename) end @@ -50,6 +64,18 @@ describe "The library itself" do expect(error_messages.compact).to be_well_formed end + it "uses double-quotes consistently in specs" do + included = /spec/ + error_messages = [] + Dir.chdir(File.expand_path("../", __FILE__)) do + `git ls-files`.split("\n").each do |filename| + next unless filename =~ included + error_messages << check_for_spec_defs_with_single_quotes(filename) + end + end + expect(error_messages.compact).to be_well_formed + end + it "can still be built" do Dir.chdir(root) do `gem build bundler.gemspec` diff --git a/spec/realworld/dependency_api_spec.rb b/spec/realworld/dependency_api_spec.rb index ad4756c2f6..5d32947223 100644 --- a/spec/realworld/dependency_api_spec.rb +++ b/spec/realworld/dependency_api_spec.rb @@ -48,7 +48,7 @@ describe "gemcutter's dependency API", :realworld => true do gem "rack" old_v, $VERBOSE = $VERBOSE, nil - Bundler::Fetcher::API_TIMEOUT = 1 + Bundler::Fetcher.api_timeout = 1 $VERBOSE = old_v G @@ -57,5 +57,4 @@ describe "gemcutter's dependency API", :realworld => true do should_be_installed "rack 1.0.0" end end - end diff --git a/spec/realworld/edgecases_spec.rb b/spec/realworld/edgecases_spec.rb index 57c2252c74..fd5b607ba6 100644 --- a/spec/realworld/edgecases_spec.rb +++ b/spec/realworld/edgecases_spec.rb @@ -10,7 +10,7 @@ describe "real world edgecases", :realworld => true do expect(err).to eq("") end - # https://github.com/carlhuda/bundler/issues/1202 + # https://github.com/bundler/bundler/issues/1202 it "bundle cache works with rubygems 1.3.7 and pre gems", :ruby => "1.8" do install_gemfile <<-G source :rubygems @@ -21,7 +21,7 @@ describe "real world edgecases", :realworld => true do expect(out).not_to include("Removing outdated .gem files from vendor/cache") end - # https://github.com/carlhuda/bundler/issues/1486 + # https://github.com/bundler/bundler/issues/1486 # this is a hash collision that only manifests on 1.8.7 it "finds the correct child versions", :ruby => "1.8" do install_gemfile <<-G @@ -35,7 +35,7 @@ describe "real world edgecases", :realworld => true do expect(out).to include("activemodel (3.0.5)") end - # https://github.com/carlhuda/bundler/issues/1500 + # https://github.com/bundler/bundler/issues/1500 it "does not fail install because of gem plugins" do realworld_system_gems("open_gem --version 1.4.2", "rake --version 0.9.2") gemfile <<-G diff --git a/spec/realworld/parallel_install_spec.rb b/spec/realworld/parallel_install_spec.rb new file mode 100644 index 0000000000..656d029bb3 --- /dev/null +++ b/spec/realworld/parallel_install_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe "installing dependencies parallely", :realworld => true do + it "installs gems parallely" do + gemfile <<-G + source "https://rubygems.org" + + gem 'activesupport', '~> 3.2.13' + gem 'faker', '~> 1.1.2' + G + + bundle :install, :jobs => 4 + bundle "show activesupport" + expect(out).to match(/activesupport/) + + bundle "show faker" + expect(out).to match(/faker/) + end +end diff --git a/spec/resolver/basic_spec.rb b/spec/resolver/basic_spec.rb index 6371dc518b..22dc2f0fbe 100644 --- a/spec/resolver/basic_spec.rb +++ b/spec/resolver/basic_spec.rb @@ -18,9 +18,20 @@ describe "Resolving" do should_resolve_as %w(actionpack-2.3.5 activesupport-2.3.5 rack-1.0) end - it "resolve a conflicting index" do + it "resolves a conflicting index" do @index = a_conflict_index dep "my_app" should_resolve_as %w(activemodel-3.2.11 builder-3.0.4 grape-0.2.6 my_app-1.0.0) end + + it "should throw error in case of circular dependencies" do + @index = a_circular_index + dep "circular_app" + + got = resolve + expect { + got = got.map { |s| s.full_name }.sort + }.to raise_error(Bundler::CyclicDependencyError, /please remove either gem 'foo' or gem 'bar'/i) + end + end diff --git a/spec/runtime/require_spec.rb b/spec/runtime/require_spec.rb index db72ff70f0..e1c93897a5 100644 --- a/spec/runtime/require_spec.rb +++ b/spec/runtime/require_spec.rb @@ -33,6 +33,10 @@ describe "Bundler.require" do s.write "lib/seven.rb", "puts 'seven'" end + build_lib "eight", "1.0.0" do |s| + s.write "lib/eight.rb", "puts 'eight'" + end + gemfile <<-G path "#{lib_path}" gem "one", :group => :bar, :require => %w(baz qux) @@ -42,6 +46,7 @@ describe "Bundler.require" do gem "five" gem "six", :group => "string" gem "seven", :group => :not + gem "eight", :require => true, :group => :require_true G end @@ -69,6 +74,10 @@ describe "Bundler.require" do # required in resolver order instead of gemfile order run("Bundler.require(:not)") expect(out.split("\n").sort).to eq(['seven', 'three']) + + # test require: true + run "Bundler.require(:require_true)" + expect(out).to eq("eight") end it "allows requiring gems with non standard names explicitly" do diff --git a/spec/runtime/with_clean_env_spec.rb b/spec/runtime/with_clean_env_spec.rb index 7a77db0b4d..193a6e5b0c 100644 --- a/spec/runtime/with_clean_env_spec.rb +++ b/spec/runtime/with_clean_env_spec.rb @@ -88,5 +88,4 @@ describe "Bundler.with_env helpers" do expect($?.exitstatus).to eq(42) end end - end diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb index 0fd74bde73..27654b7650 100644 --- a/spec/support/helpers.rb +++ b/spec/support/helpers.rb @@ -94,10 +94,6 @@ module Spec requires_str = requires.map{|r| "-r#{r}"}.join(" ") env = (options.delete(:env) || {}).map{|k,v| "#{k}='#{v}' "}.join - args = options.map do |k,v| - v == true ? " --#{k}" : " --#{k} #{v}" if v - end.join - cmd = "#{env}#{Gem.ruby} -I#{lib} #{requires_str} #{bundle_bin}" if exitstatus diff --git a/spec/support/indexes.rb b/spec/support/indexes.rb index 2ca67a637a..418f7f32db 100644 --- a/spec/support/indexes.rb +++ b/spec/support/indexes.rb @@ -130,5 +130,23 @@ module Spec end end end + + def a_circular_index + build_index do + gem "rack", "1.0.1" + gem("foo", '0.2.6') do + dep "bar", ">= 0" + end + + gem("bar", "1.0.0") do + dep "foo", ">= 0" + end + + gem("circular_app", '1.0.0') do + dep "foo", ">= 0" + dep "bar", ">= 0" + end + end + end end end diff --git a/spec/support/streams.rb b/spec/support/streams.rb new file mode 100644 index 0000000000..610e99986a --- /dev/null +++ b/spec/support/streams.rb @@ -0,0 +1,13 @@ +require 'stringio' + +def capture(*streams) + streams.map! { |stream| stream.to_s } + begin + result = StringIO.new + streams.each { |stream| eval "$#{stream} = result" } + yield + ensure + streams.each { |stream| eval("$#{stream} = #{stream.upcase}") } + end + result.string +end diff --git a/spec/update/gems_spec.rb b/spec/update/gems_spec.rb index 3616fcdd89..26a367d027 100644 --- a/spec/update/gems_spec.rb +++ b/spec/update/gems_spec.rb @@ -34,12 +34,12 @@ describe "bundle update" do end describe "--quiet argument" do - it 'shows UI messages without --quiet argument' do + it "shows UI messages without --quiet argument" do bundle "update" expect(out).to include("Fetching source") end - it 'does not show UI messages with --quiet argument' do + it "does not show UI messages with --quiet argument" do bundle "update --quiet" expect(out).not_to include("Fetching source") end @@ -56,7 +56,7 @@ describe "bundle update" do end end - describe "with a unknown dependency" do + describe "with an unknown dependency" do it "should inform the user" do bundle "update halting-problem-solver", :expect_err=>true expect(out).to include "Could not find gem 'halting-problem-solver'" diff --git a/spec/update/git_spec.rb b/spec/update/git_spec.rb index 53d6e5b275..cba5a1f72b 100644 --- a/spec/update/git_spec.rb +++ b/spec/update/git_spec.rb @@ -137,7 +137,7 @@ describe "bundle update" do end end - it "it unlocks the source when submodules is added to a git source" do + it "it unlocks the source when submodules are added to a git source" do install_gemfile <<-G git "#{lib_path('has_submodule-1.0')}" do gem "has_submodule" @@ -157,7 +157,7 @@ describe "bundle update" do expect(out).to eq('GIT') end - it "it unlocks the source when submodules is removed from git source" do + it "it unlocks the source when submodules are removed from git source" do pending "This would require actually removing the submodule from the clone" install_gemfile <<-G git "#{lib_path('has_submodule-1.0')}", :submodules => true do diff --git a/spec/update/source_spec.rb b/spec/update/source_spec.rb index 6134c7700c..22f3225428 100644 --- a/spec/update/source_spec.rb +++ b/spec/update/source_spec.rb @@ -47,5 +47,4 @@ describe "bundle update" do should_be_installed "rack 1.0" end end - end |