diff options
Diffstat (limited to 'lib/bundler')
376 files changed, 29259 insertions, 11308 deletions
diff --git a/lib/bundler/.document b/lib/bundler/.document new file mode 100644 index 0000000000..238bbd8705 --- /dev/null +++ b/lib/bundler/.document @@ -0,0 +1 @@ +# not in RDoc diff --git a/lib/bundler/build_metadata.rb b/lib/bundler/build_metadata.rb index 54436f982d..49d2518078 100644 --- a/lib/bundler/build_metadata.rb +++ b/lib/bundler/build_metadata.rb @@ -4,33 +4,40 @@ module Bundler # Represents metadata from when the Bundler gem was built. module BuildMetadata # begin ivars - @release = false + @built_at = nil # end ivars # A hash representation of the build metadata. def self.to_h { - "Built At" => built_at, + "Timestamp" => timestamp, "Git SHA" => git_commit_sha, - "Released Version" => release?, } end + # A timestamp representing the date the bundler gem was built, or the + # current time if never built + def self.timestamp + @timestamp ||= @built_at || Time.now.utc.strftime("%Y-%m-%d").freeze + end + # A string representing the date the bundler gem was built. def self.built_at - @built_at ||= Time.now.utc.strftime("%Y-%m-%d").freeze + @built_at end # The SHA for the git commit the bundler gem was built from. def self.git_commit_sha - @git_commit_sha ||= Dir.chdir(File.expand_path("..", __FILE__)) do - `git rev-parse --short HEAD`.strip.freeze + return @git_commit_sha if instance_variable_defined? :@git_commit_sha + + # If Bundler has been installed without its .git directory and without a + # commit instance variable then we can't determine its commits SHA. + git_dir = File.expand_path("../../../.git", __dir__) + if File.directory?(git_dir) + return @git_commit_sha = IO.popen(%w[git rev-parse --short HEAD], { chdir: git_dir }, &:read).strip.freeze end - end - # Whether this is an official release build of Bundler. - def self.release? - @release + @git_commit_sha ||= "unknown" end end end diff --git a/lib/bundler/bundler.gemspec b/lib/bundler/bundler.gemspec new file mode 100644 index 0000000000..49319e81b4 --- /dev/null +++ b/lib/bundler/bundler.gemspec @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +begin + require_relative "lib/bundler/version" +rescue LoadError + # for Ruby core repository + require_relative "version" +end + +Gem::Specification.new do |s| + s.name = "bundler" + s.version = Bundler::VERSION + s.license = "MIT" + s.authors = [ + "André Arko", "Samuel Giddins", "Colby Swandale", "Hiroshi Shibata", + "David Rodríguez", "Grey Baker", "Stephanie Morillo", "Chris Morris", "James Wen", "Tim Moore", + "André Medeiros", "Jessica Lynn Suttles", "Terence Lee", "Carl Lerche", + "Yehuda Katz" + ] + s.email = ["team@bundler.io"] + s.homepage = "https://bundler.io" + s.summary = "The best way to manage your application's dependencies" + s.description = "Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably" + + s.metadata = { + "bug_tracker_uri" => "https://github.com/ruby/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3ABundler", + "changelog_uri" => "https://github.com/ruby/rubygems/blob/master/bundler/CHANGELOG.md", + "homepage_uri" => "https://bundler.io/", + "source_code_uri" => "https://github.com/ruby/rubygems/tree/master/bundler", + } + + s.required_ruby_version = ">= 3.2.0" + + # It should match the RubyGems version shipped with `required_ruby_version` above + s.required_rubygems_version = ">= 3.4.1" + + s.files = Dir.glob("lib/bundler{.rb,/**/*}", File::FNM_DOTMATCH).reject {|f| File.directory?(f) } + + # include the gemspec itself because warbler breaks w/o it + s.files += %w[lib/bundler/bundler.gemspec] + + s.bindir = "exe" + s.executables = %w[bundle bundler] + s.require_paths = ["lib"] +end diff --git a/lib/bundler/capistrano.rb b/lib/bundler/capistrano.rb index 1b7145b72b..6d2437d895 100644 --- a/lib/bundler/capistrano.rb +++ b/lib/bundler/capistrano.rb @@ -1,22 +1,4 @@ # frozen_string_literal: true -require "bundler/shared_helpers" -Bundler::SharedHelpers.major_deprecation 2, - "The Bundler task for Capistrano. Please use http://github.com/capistrano/bundler" - -# Capistrano task for Bundler. -# -# Add "require 'bundler/capistrano'" in your Capistrano deploy.rb, and -# Bundler will be activated after each new deployment. -require "bundler/deployment" -require "capistrano/version" - -if defined?(Capistrano::Version) && Gem::Version.new(Capistrano::Version).release >= Gem::Version.new("3.0") - raise "For Capistrano 3.x integration, please use http://github.com/capistrano/bundler" -end - -Capistrano::Configuration.instance(:must_exist).load do - before "deploy:finalize_update", "bundle:install" - Bundler::Deployment.define_task(self, :task, :except => { :no_release => true }) - set :rake, lambda { "#{fetch(:bundle_cmd, "bundle")} exec rake" } -end +require_relative "shared_helpers" +Bundler::SharedHelpers.feature_removed! "The Bundler task for Capistrano. Please use https://github.com/capistrano/bundler" diff --git a/lib/bundler/checksum.rb b/lib/bundler/checksum.rb new file mode 100644 index 0000000000..ce05818bb0 --- /dev/null +++ b/lib/bundler/checksum.rb @@ -0,0 +1,270 @@ +# frozen_string_literal: true + +module Bundler + class Checksum + ALGO_SEPARATOR = "=" + DEFAULT_ALGORITHM = "sha256" + private_constant :DEFAULT_ALGORITHM + DEFAULT_BLOCK_SIZE = 16_384 + private_constant :DEFAULT_BLOCK_SIZE + + class << self + def from_gem_package(gem_package, algo = DEFAULT_ALGORITHM) + return if Bundler.settings[:disable_checksum_validation] + return unless source = gem_package.instance_variable_get(:@gem) + return unless source.respond_to?(:with_read_io) + + source.with_read_io do |io| + from_gem(io, source.path) + ensure + io.rewind + end + end + + def from_gem(io, pathname, algo = DEFAULT_ALGORITHM) + digest = Bundler::SharedHelpers.digest(algo.upcase).new + buf = String.new(capacity: DEFAULT_BLOCK_SIZE) + digest << io.readpartial(DEFAULT_BLOCK_SIZE, buf) until io.eof? + Checksum.new(algo, digest.hexdigest!, Source.new(:gem, pathname)) + end + + def from_api(digest, source_uri, algo = DEFAULT_ALGORITHM) + return if Bundler.settings[:disable_checksum_validation] + + Checksum.new(algo, to_hexdigest(digest, algo), Source.new(:api, source_uri)) + end + + def from_lock(lock_checksum, lockfile_location) + algo, digest = lock_checksum.strip.split(ALGO_SEPARATOR, 2) + Checksum.new(algo, to_hexdigest(digest, algo), Source.new(:lock, lockfile_location)) + end + + def to_hexdigest(digest, algo = DEFAULT_ALGORITHM) + return digest unless algo == DEFAULT_ALGORITHM + return digest if digest.match?(/\A[0-9a-f]{64}\z/i) + + if digest.match?(%r{\A[-0-9a-z_+/]{43}={0,2}\z}i) + digest = digest.tr("-_", "+/") # fix urlsafe base64 + digest.unpack1("m0").unpack1("H*") + else + raise ArgumentError, "#{digest.inspect} is not a valid SHA256 hex or base64 digest" + end + end + end + + attr_reader :algo, :digest, :sources + + def initialize(algo, digest, source) + @algo = algo + @digest = digest + @sources = [source] + end + + def ==(other) + match?(other) && other.sources == sources + end + + alias_method :eql?, :== + + def same_source?(other) + sources.include?(other.sources.first) + end + + def match?(other) + other.is_a?(self.class) && other.digest == digest && other.algo == algo + end + + def hash + digest.hash + end + + def to_s + "#{to_lock} (from #{sources.first}#{", ..." if sources.size > 1})" + end + + def to_lock + "#{algo}#{ALGO_SEPARATOR}#{digest}" + end + + def merge!(other) + return nil unless match?(other) + + @sources.concat(other.sources).uniq! + self + end + + def formatted_sources + sources.join("\n and ").concat("\n") + end + + def removable? + sources.all?(&:removable?) + end + + def removal_instructions + msg = +"" + i = 1 + sources.each do |source| + msg << " #{i}. #{source.removal}\n" + i += 1 + end + msg << " #{i}. run `bundle install`\n" + end + + def inspect + abbr = "#{algo}#{ALGO_SEPARATOR}#{digest[0, 8]}" + from = "from #{sources.join(" and ")}" + "#<#{self.class}:#{object_id} #{abbr} #{from}>" + end + + class Source + attr_reader :type, :location + + def initialize(type, location) + @type = type + @location = location + end + + def removable? + [:lock, :gem].include?(type) + end + + def ==(other) + other.is_a?(self.class) && other.type == type && other.location == location + end + + # phrased so that the usual string format is grammatically correct + # rake (10.3.2) sha256=abc123 from #{to_s} + def to_s + case type + when :lock + "the lockfile CHECKSUMS at #{location}" + when :gem + "the gem at #{location}" + when :api + "the API at #{location}" + else + "#{location} (#{type})" + end + end + + # A full sentence describing how to remove the checksum + def removal + case type + when :lock + "remove the matching checksum in #{location}" + when :gem + "remove the gem at #{location}" + when :api + "checksums from #{location} cannot be locally modified, you may need to update your sources" + else + "remove #{location} (#{type})" + end + end + end + + class Store + attr_reader :store + protected :store + + def initialize + @store = {} + @store_mutex = Mutex.new + end + + def inspect + "#<#{self.class}:#{object_id} size=#{store.size}>" + end + + # Replace when the new checksum is from the same source. + # The primary purpose is registering checksums from gems where there are + # duplicates of the same gem (according to full_name) in the index. + # + # In particular, this is when 2 gems have two similar platforms, e.g. + # "darwin20" and "darwin-20", both of which resolve to darwin-20. + # In the Index, the later gem replaces the former, so we do that here. + # + # However, if the new checksum is from a different source, we register like normal. + # This ensures a mismatch error where there are multiple top level sources + # that contain the same gem with different checksums. + def replace(spec, checksum) + return unless checksum + + lock_name = spec.lock_name + @store_mutex.synchronize do + existing = fetch_checksum(lock_name, checksum.algo) + if !existing || existing.same_source?(checksum) + store_checksum(lock_name, checksum) + else + merge_checksum(lock_name, checksum, existing) + end + end + end + + def missing?(spec) + @store[spec.lock_name].nil? + end + + def empty?(spec) + return false unless spec.source.is_a?(Bundler::Source::Rubygems) + + @store[spec.lock_name].empty? + end + + def register(spec, checksum) + register_checksum(spec.lock_name, checksum) + end + + def merge!(other) + other.store.each do |lock_name, checksums| + checksums.each do |_algo, checksum| + register_checksum(lock_name, checksum) + end + end + end + + def to_lock(spec) + lock_name = spec.lock_name + checksums = @store[lock_name] + if checksums&.any? + "#{lock_name} #{checksums.values.map(&:to_lock).sort.join(",")}" + else + lock_name + end + end + + private + + def register_checksum(lock_name, checksum) + @store_mutex.synchronize do + if checksum + existing = fetch_checksum(lock_name, checksum.algo) + if existing + merge_checksum(lock_name, checksum, existing) + else + store_checksum(lock_name, checksum) + end + else + init_checksum(lock_name) + end + end + end + + def merge_checksum(lock_name, checksum, existing) + existing.merge!(checksum) || raise(ChecksumMismatchError.new(lock_name, existing, checksum)) + end + + def store_checksum(lock_name, checksum) + init_checksum(lock_name)[checksum.algo] = checksum + end + + def init_checksum(lock_name) + @store[lock_name] ||= {} + end + + def fetch_checksum(lock_name, algo) + @store[lock_name]&.fetch(algo, nil) + end + end + end +end diff --git a/lib/bundler/ci_detector.rb b/lib/bundler/ci_detector.rb new file mode 100644 index 0000000000..e5fedbdea8 --- /dev/null +++ b/lib/bundler/ci_detector.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Bundler + module CIDetector + # NOTE: Any changes made here will need to be made to both lib/rubygems/ci_detector.rb and + # bundler/lib/bundler/ci_detector.rb (which are enforced duplicates). + # TODO: Drop that duplication once bundler drops support for RubyGems 3.4 + # + # ## Recognized CI providers, their signifiers, and the relevant docs ## + # + # Travis CI - CI, TRAVIS https://docs.travis-ci.com/user/environment-variables/#default-environment-variables + # Cirrus CI - CI, CIRRUS_CI https://cirrus-ci.org/guide/writing-tasks/#environment-variables + # Circle CI - CI, CIRCLECI https://circleci.com/docs/variables/#built-in-environment-variables + # Gitlab CI - CI, GITLAB_CI https://docs.gitlab.com/ee/ci/variables/ + # AppVeyor - CI, APPVEYOR https://www.appveyor.com/docs/environment-variables/ + # CodeShip - CI_NAME https://docs.cloudbees.com/docs/cloudbees-codeship/latest/pro-builds-and-configuration/environment-variables#_default_environment_variables + # dsari - CI, DSARI https://github.com/rfinnie/dsari#running + # Jenkins - BUILD_NUMBER https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables + # TeamCity - TEAMCITY_VERSION https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html#Predefined+Server+Build+Parameters + # Appflow - CI_BUILD_ID https://ionic.io/docs/appflow/automation/environments#predefined-environments + # TaskCluster - TASKCLUSTER_ROOT_URL https://docs.taskcluster.net/docs/manual/design/env-vars + # Semaphore - CI, SEMAPHORE https://docs.semaphoreci.com/ci-cd-environment/environment-variables/ + # BuildKite - CI, BUILDKITE https://buildkite.com/docs/pipelines/environment-variables + # GoCD - GO_SERVER_URL https://docs.gocd.org/current/faq/dev_use_current_revision_in_build.html + # GH Actions - CI, GITHUB_ACTIONS https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables + # + # ### Some "standard" ENVs that multiple providers may set ### + # + # * CI - this is set by _most_ (but not all) CI providers now; it's approaching a standard. + # * CI_NAME - Not as frequently used, but some providers set this to specify their own name + + # Any of these being set is a reasonably reliable indicator that we are + # executing in a CI environment. + ENV_INDICATORS = [ + "CI", + "CI_NAME", + "CONTINUOUS_INTEGRATION", + "BUILD_NUMBER", + "CI_APP_ID", + "CI_BUILD_ID", + "CI_BUILD_NUMBER", + "RUN_ID", + "TASKCLUSTER_ROOT_URL", + ].freeze + + # For each CI, this env suffices to indicate that we're on _that_ CI's + # containers. (A few of them only supply a CI_NAME variable, which is also + # nice). And if they set "CI" but we can't tell which one they are, we also + # want to know that - a bare "ci" without another token tells us as much. + ENV_DESCRIPTORS = { + "TRAVIS" => "travis", + "CIRCLECI" => "circle", + "CIRRUS_CI" => "cirrus", + "DSARI" => "dsari", + "SEMAPHORE" => "semaphore", + "JENKINS_URL" => "jenkins", + "BUILDKITE" => "buildkite", + "GO_SERVER_URL" => "go", + "GITLAB_CI" => "gitlab", + "GITHUB_ACTIONS" => "github", + "TASKCLUSTER_ROOT_URL" => "taskcluster", + "CI" => "ci", + }.freeze + + def self.ci? + ENV_INDICATORS.any? {|var| ENV.include?(var) } + end + + def self.ci_strings + matching_names = ENV_DESCRIPTORS.select {|env, _| ENV[env] }.values + matching_names << ENV["CI_NAME"].downcase if ENV["CI_NAME"] + matching_names.reject(&:empty?).sort.uniq + end + end +end diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 05e1851c18..9d8a68fff9 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -1,24 +1,32 @@ # frozen_string_literal: true -require "bundler" -require "bundler/vendored_thor" +require_relative "vendored_thor" module Bundler class CLI < Thor - require "bundler/cli/common" + require_relative "cli/common" + require_relative "cli/install" package_name "Bundler" AUTO_INSTALL_CMDS = %w[show binstubs outdated exec open console licenses clean].freeze - PARSEABLE_COMMANDS = %w[ - check config help exec platform show version - ].freeze + PARSEABLE_COMMANDS = %w[check config help exec platform show version].freeze + EXTENSIONS = ["c", "rust", "go"].freeze + + COMMAND_ALIASES = { + "check" => "c", + "install" => "i", + "plugin" => "", + "list" => "ls", + "exec" => ["e", "ex", "exe"], + "cache" => ["package", "pack"], + "version" => ["-v", "--version"], + }.freeze def self.start(*) + check_invalid_ext_option(ARGV) if ARGV.include?("--ext") + super - rescue Exception => e - Bundler.ui = UI::Shell.new - raise e ensure Bundler::SharedHelpers.print_major_deprecations! end @@ -30,56 +38,73 @@ module Bundler end end + def self.all_aliases + @all_aliases ||= begin + command_aliases = {} + + COMMAND_ALIASES.each do |name, aliases| + Array(aliases).each do |one_alias| + command_aliases[one_alias] = name + end + end + + command_aliases + end + end + + def self.aliases_for(command_name) + COMMAND_ALIASES.select {|k, _| k == command_name }.invert + end + def initialize(*args) super - custom_gemfile = options[:gemfile] || Bundler.settings[:gemfile] - if custom_gemfile && !custom_gemfile.empty? - Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", File.expand_path(custom_gemfile) - Bundler.reset_paths! + current_cmd = args.last[:current_command].name + + # `bundle config` manages stored settings, so avoid promoting settings + # like `gemfile` or `lockfile` to environment variables before it runs. + unless current_cmd == "config" + Bundler.configure_custom_gemfile(options[:gemfile]) + + # lock --lockfile works differently than install --lockfile + unless current_cmd == "lock" + custom_lockfile = options[:lockfile] || ENV["BUNDLE_LOCKFILE"] || Bundler.settings[:lockfile] + if custom_lockfile && !custom_lockfile.empty? + Bundler::SharedHelpers.set_env "BUNDLE_LOCKFILE", File.expand_path(custom_lockfile) + reset_settings = true + end + end end + Bundler.reset_settings_and_root! if reset_settings + + Bundler.auto_switch + Bundler.settings.set_command_option_if_given :retry, options[:retry] - current_cmd = args.last[:current_command].name - auto_install if AUTO_INSTALL_CMDS.include?(current_cmd) + Bundler.auto_install if AUTO_INSTALL_CMDS.include?(current_cmd) rescue UnknownArgumentError => e raise InvalidOption, e.message ensure self.options ||= {} unprinted_warnings = Bundler.ui.unprinted_warnings Bundler.ui = UI::Shell.new(options) - Bundler.ui.level = "debug" if options["verbose"] + Bundler.ui.level = "debug" if options[:verbose] || Bundler.settings[:verbose] unprinted_warnings.each {|w| Bundler.ui.warn(w) } - - if ENV["RUBYGEMS_GEMDEPS"] && !ENV["RUBYGEMS_GEMDEPS"].empty? - Bundler.ui.warn( - "The RUBYGEMS_GEMDEPS environment variable is set. This enables RubyGems' " \ - "experimental Gemfile mode, which may conflict with Bundler and cause unexpected errors. " \ - "To remove this warning, unset RUBYGEMS_GEMDEPS.", :wrap => true - ) - end - end - - def self.deprecated_option(*args, &blk) - return if Bundler.feature_flag.forget_cli_options? - method_option(*args, &blk) end - check_unknown_options!(:except => [:config, :exec]) + check_unknown_options!(except: [:config, :exec]) stop_on_unknown_option! :exec - desc "cli_help", "Prints a summary of bundler commands", :hide => true + desc "cli_help", "Prints a summary of bundler commands", hide: true def cli_help version Bundler.ui.info "\n" - primary_commands = ["install", "update", - Bundler.feature_flag.cache_command_is_package? ? "cache" : "package", - "exec", "config", "help"] + primary_commands = ["install", "update", "cache", "exec", "config", "help"] list = self.class.printable_commands(true) - by_name = list.group_by {|name, _message| name.match(/^bundle (\w+)/)[1] } + by_name = list.group_by {|name, _message| name.match(/^bundler? (\w+)/)[1] } utilities = by_name.keys.sort - primary_commands primary_commands.map! {|name| (by_name[name] || raise("no primary command #{name}")).first } utilities.map! {|name| by_name[name].first } @@ -87,37 +112,70 @@ module Bundler shell.say "Bundler commands:\n\n" shell.say " Primary commands:\n" - shell.print_table(primary_commands, :indent => 4, :truncate => true) + shell.print_table(primary_commands, indent: 4, truncate: true) shell.say shell.say " Utilities:\n" - shell.print_table(utilities, :indent => 4, :truncate => true) + shell.print_table(utilities, indent: 4, truncate: true) shell.say self.class.send(:class_options_help, shell) end - default_task(Bundler.feature_flag.default_cli_command) - class_option "no-color", :type => :boolean, :desc => "Disable colorization in output" - class_option "retry", :type => :numeric, :aliases => "-r", :banner => "NUM", - :desc => "Specify the number of times you wish to attempt network commands" - class_option "verbose", :type => :boolean, :desc => "Enable verbose output mode", :aliases => "-V" + desc "install_or_cli_help", "Deprecated alias of install", hide: true + def install_or_cli_help + Bundler.ui.warn <<~MSG + `bundle install_or_cli_help` is a deprecated alias of `bundle install`. + It might be called due to the 'default_cli_command' being set to 'install_or_cli_help', + if so fix that by running `bundle config set default_cli_command install --global`. + MSG + invoke_other_command("install") + end + + def self.default_command(meth = nil) + return super if meth + + unless Bundler.settings[:default_cli_command] + Bundler.ui.info <<~MSG + In a future version of Bundler, running `bundle` without argument will no longer run `bundle install`. + Instead, the `cli_help` command will be displayed. Please use `bundle install` explicitly for scripts like CI/CD. + You can use the future behavior now with `bundle config set default_cli_command cli_help --global`, + or you can continue to use the current behavior with `bundle config set default_cli_command install --global`. + This message will be removed after a default_cli_command value is set. + + MSG + end + + Bundler.settings[:default_cli_command] || "install" + end + + class_option "no-color", type: :boolean, desc: "Disable colorization in output" + class_option "retry", type: :numeric, aliases: "-r", banner: "NUM", + desc: "Specify the number of times you wish to attempt network commands" + class_option "verbose", type: :boolean, desc: "Enable verbose output mode", aliases: "-V" def help(cli = nil) + cli = self.class.all_aliases[cli] if self.class.all_aliases[cli] + + if Bundler.settings[:plugins] && Bundler::Plugin.command?(cli) && !self.class.all_commands.key?(cli) + return Bundler::Plugin.exec_command(cli, ["--help"]) + end + case cli when "gemfile" then command = "gemfile" when nil then command = "bundle" else command = "bundle-#{cli}" end - man_path = File.expand_path("../../../man", __FILE__) - man_pages = Hash[Dir.glob(File.join(man_path, "*")).grep(/.*\.\d*\Z/).collect do |f| + man_path = File.expand_path("man", __dir__) + man_pages = Hash[Dir.glob(File.join(man_path, "**", "*")).grep(/.*\.\d*\Z/).collect do |f| [File.basename(f, ".*"), f] end] if man_pages.include?(command) - if Bundler.which("man") && man_path !~ %r{^file:/.+!/META-INF/jruby.home/.+} - Kernel.exec "man #{man_pages[command]}" + man_page = man_pages[command] + if Bundler.which("man") && !man_path.match?(%r{^(?:file:/.+!|uri:classloader:)/META-INF/jruby.home/.+}) + Kernel.exec("man", man_page) else - puts File.read("#{man_path}/#{File.basename(man_pages[command])}.txt") + puts File.read("#{man_path}/#{File.basename(man_page)}.ronn") end elsif command_path = Bundler.which("bundler-#{cli}") Kernel.exec(command_path, "--help") @@ -127,7 +185,7 @@ module Bundler end def self.handle_no_command_error(command, has_namespace = $thor_runner) - if Bundler.feature_flag.plugins? && Bundler::Plugin.command?(command) + if Bundler.settings[:plugins] && Bundler::Plugin.command?(command) return Bundler::Plugin.exec_command(command, ARGV[1..-1]) end @@ -142,9 +200,10 @@ module Bundler Gemfile to a gem with a gemspec, the --gemspec option will automatically add each dependency listed in the gemspec file to the newly created Gemfile. D - deprecated_option "gemspec", :type => :string, :banner => "Use the specified .gemspec to create the Gemfile" + method_option "gemspec", type: :string, banner: "Use the specified .gemspec to create the Gemfile" + method_option "gemfile", type: :string, banner: "Use the specified name for the gemfile instead of 'Gemfile'" def init - require "bundler/cli/init" + require_relative "cli/init" Init.new(options.dup).run end @@ -154,18 +213,33 @@ module Bundler all gems are found, Bundler prints a success message and exits with a status of 0. If not, the first missing gem is listed and Bundler exits status 1. D - method_option "dry-run", :type => :boolean, :default => false, :banner => - "Lock the Gemfile" - method_option "gemfile", :type => :string, :banner => - "Use the specified gemfile instead of Gemfile" - method_option "path", :type => :string, :banner => - "Specify a different path than the system default ($BUNDLE_PATH or $GEM_HOME).#{" Bundler will remember this value for future installs on this machine" unless Bundler.feature_flag.forget_cli_options?}" - map "c" => "check" + method_option "dry-run", type: :boolean, default: false, banner: "Lock the Gemfile" + method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile" + method_option "path", type: :string, banner: "Specify a different path than the system default, namely, $BUNDLE_PATH or $GEM_HOME (removed)" def check - require "bundler/cli/check" + remembered_flag_deprecation("path") + + require_relative "cli/check" Check.new(options).run end + map aliases_for("check") + + desc "remove [GEM [GEM ...]]", "Removes gems from the Gemfile" + long_desc <<-D + Removes the given gems from the Gemfile while ensuring that the resulting Gemfile is still valid. If the gem is not found, Bundler prints a error message and if gem could not be removed due to any reason Bundler will display a warning. + D + method_option "install", type: :boolean, banner: "Runs 'bundle install' after removing the gems from the Gemfile (removed)" + def remove(*gems) + if ARGV.include?("--install") + removed_message = "The `--install` flag has been removed. `bundle install` is triggered by default." + raise InvalidOption, removed_message + end + + require_relative "cli/remove" + Remove.new(gems, options).run + end + desc "install [OPTIONS]", "Install the current environment to the system" long_desc <<-D Install will install all of the gems in the current bundle, making them available @@ -177,93 +251,86 @@ module Bundler If the bundle has already been installed, bundler will tell you so and then exit. D - deprecated_option "binstubs", :type => :string, :lazy_default => "bin", :banner => - "Generate bin stubs for bundled gems to ./bin" - deprecated_option "clean", :type => :boolean, :banner => - "Run bundle clean automatically after install" - deprecated_option "deployment", :type => :boolean, :banner => - "Install using defaults tuned for deployment environments" - deprecated_option "frozen", :type => :boolean, :banner => - "Do not allow the Gemfile.lock to be updated after this install" - method_option "full-index", :type => :boolean, :banner => - "Fall back to using the single-file index of all gems" - method_option "gemfile", :type => :string, :banner => - "Use the specified gemfile instead of Gemfile" - method_option "jobs", :aliases => "-j", :type => :numeric, :banner => - "Specify the number of jobs to run in parallel" - method_option "local", :type => :boolean, :banner => - "Do not attempt to fetch gems remotely and use the gem cache instead" - deprecated_option "no-cache", :type => :boolean, :banner => - "Don't update the existing gem cache." - method_option "redownload", :type => :boolean, :aliases => - [Bundler.feature_flag.forget_cli_options? ? nil : "--force"].compact, :banner => - "Force downloading every gem." - deprecated_option "no-prune", :type => :boolean, :banner => - "Don't remove stale gems from the cache." - deprecated_option "path", :type => :string, :banner => - "Specify a different path than the system default ($BUNDLE_PATH or $GEM_HOME). Bundler will remember this value for future installs on this machine" - method_option "quiet", :type => :boolean, :banner => - "Only output warnings and errors." - deprecated_option "shebang", :type => :string, :banner => - "Specify a different shebang executable name than the default (usually 'ruby')" - method_option "standalone", :type => :array, :lazy_default => [], :banner => - "Make a bundle that can work without the Bundler runtime" - deprecated_option "system", :type => :boolean, :banner => - "Install to the system location ($BUNDLE_PATH or $GEM_HOME) even if the bundle was previously installed somewhere else for this application" - method_option "trust-policy", :alias => "P", :type => :string, :banner => - "Gem trust policy (like gem install -P). Must be one of " + - Bundler.rubygems.security_policy_keys.join("|") - deprecated_option "without", :type => :array, :banner => - "Exclude gems that are part of the specified named group." - deprecated_option "with", :type => :array, :banner => - "Include gems that are part of the specified named group." - map "i" => "install" + method_option "binstubs", type: :string, lazy_default: "bin", banner: "Generate bin stubs for bundled gems to ./bin (removed)" + method_option "clean", type: :boolean, banner: "Run bundle clean automatically after install (removed)" + method_option "deployment", type: :boolean, banner: "Install using defaults tuned for deployment environments (removed)" + method_option "frozen", type: :boolean, banner: "Do not allow the Gemfile.lock to be updated after this install (removed)" + method_option "full-index", type: :boolean, banner: "Fall back to using the single-file index of all gems" + method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile" + method_option "jobs", aliases: "-j", type: :numeric, banner: "Specify the number of jobs to run in parallel" + method_option "local", type: :boolean, banner: "Do not attempt to fetch gems remotely and use the gem cache instead" + method_option "lockfile", type: :string, banner: "Use the specified lockfile instead of the default." + method_option "prefer-local", type: :boolean, banner: "Only attempt to fetch gems remotely if not present locally, even if newer versions are available remotely" + method_option "no-cache", type: :boolean, banner: "Don't update the existing gem cache." + method_option "no-lock", type: :boolean, banner: "Don't create a lockfile." + method_option "force", type: :boolean, aliases: "--redownload", banner: "Force reinstalling every gem, even if already installed" + method_option "no-prune", type: :boolean, banner: "Don't remove stale gems from the cache (removed)." + method_option "path", type: :string, banner: "Specify a different path than the system default, namely, $BUNDLE_PATH or $GEM_HOME (removed)." + method_option "quiet", type: :boolean, banner: "Only output warnings and errors." + method_option "shebang", type: :string, banner: "Specify a different shebang executable name than the default, usually 'ruby' (removed)" + method_option "standalone", type: :array, lazy_default: [], banner: "Make a bundle that can work without the Bundler runtime" + method_option "system", type: :boolean, banner: "Install to the system location ($BUNDLE_PATH or $GEM_HOME) even if the bundle was previously installed somewhere else for this application (removed)" + method_option "trust-policy", alias: "P", type: :string, banner: "Gem trust policy (like gem install -P). Must be one of #{Bundler.rubygems.security_policy_keys.join("|")}" + method_option "target-rbconfig", type: :string, banner: "Path to rbconfig.rb for the deployment target platform" + method_option "without", type: :array, banner: "Exclude gems that are part of the specified named group (removed)." + method_option "with", type: :array, banner: "Include gems that are part of the specified named group (removed)." + method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." def install - require "bundler/cli/install" - Bundler.settings.temporary(:no_install => false) do - Install.new(options.dup).run + %w[clean deployment frozen no-prune path shebang without with].each do |option| + remembered_flag_deprecation(option) end + + print_remembered_flag_deprecation("--system", "path.system", "true") if ARGV.include?("--system") + + remembered_flag_deprecation("deployment", negative: true) + + if ARGV.include?("--binstubs") + removed_message = "The --binstubs option has been removed in favor of `bundle binstubs --all`" + raise InvalidOption, removed_message + end + + require_relative "cli/install" + options = self.options.dup + options["lockfile"] ||= ENV["BUNDLE_LOCKFILE"] + Bundler.settings.temporary(no_install: false) do + Install.new(options).run + end + rescue GemfileNotFound => error + invoke_other_command("cli_help") + raise error # re-raise to show the error and get a failing exit status end + map aliases_for("install") + desc "update [OPTIONS]", "Update the current environment" long_desc <<-D Update will install the newest versions of the gems listed in the Gemfile. Use update when you have changed the Gemfile, or if you want to get the newest possible versions of the gems in the bundle. D - method_option "full-index", :type => :boolean, :banner => - "Fall back to using the single-file index of all gems" - method_option "group", :aliases => "-g", :type => :array, :banner => - "Update a specific group" - method_option "jobs", :aliases => "-j", :type => :numeric, :banner => - "Specify the number of jobs to run in parallel" - method_option "local", :type => :boolean, :banner => - "Do not attempt to fetch gems remotely and use the gem cache instead" - method_option "quiet", :type => :boolean, :banner => - "Only output warnings and errors." - method_option "source", :type => :array, :banner => - "Update a specific source (and all gems associated with it)" - method_option "force", :type => :boolean, :banner => - "Force downloading every gem." - method_option "ruby", :type => :boolean, :banner => - "Update ruby specified in Gemfile.lock" - method_option "bundler", :type => :string, :lazy_default => "> 0.a", :banner => - "Update the locked version of bundler" - method_option "patch", :type => :boolean, :banner => - "Prefer updating only to next patch version" - method_option "minor", :type => :boolean, :banner => - "Prefer updating only to next minor version" - method_option "major", :type => :boolean, :banner => - "Prefer updating to next major version (default)" - method_option "strict", :type => :boolean, :banner => - "Do not allow any gem to be updated past latest --patch | --minor | --major" - method_option "conservative", :type => :boolean, :banner => - "Use bundle install conservative update behavior and do not allow shared dependencies to be updated." - method_option "all", :type => :boolean, :banner => - "Update everything." + method_option "full-index", type: :boolean, banner: "Fall back to using the single-file index of all gems" + method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile" + method_option "group", aliases: "-g", type: :array, banner: "Update a specific group" + method_option "jobs", aliases: "-j", type: :numeric, banner: "Specify the number of jobs to run in parallel" + method_option "local", type: :boolean, banner: "Do not attempt to fetch gems remotely and use the gem cache instead" + method_option "quiet", type: :boolean, banner: "Only output warnings and errors." + method_option "source", type: :array, banner: "Update a specific source (and all gems associated with it)" + method_option "force", type: :boolean, aliases: "--redownload", banner: "Force reinstalling every gem, even if already installed" + method_option "ruby", type: :boolean, banner: "Update ruby specified in Gemfile.lock" + method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler" + method_option "patch", type: :boolean, banner: "Prefer updating only to next patch version" + method_option "minor", type: :boolean, banner: "Prefer updating only to next minor version" + method_option "major", type: :boolean, banner: "Prefer updating to next major version (default)" + method_option "pre", type: :boolean, banner: "Always choose the highest allowed version when updating gems, regardless of prerelease status" + method_option "strict", type: :boolean, banner: "Do not allow any gem to be updated past latest --patch | --minor | --major" + method_option "conservative", type: :boolean, banner: "Use bundle install conservative update behavior and do not allow shared dependencies to be updated." + method_option "all", type: :boolean, banner: "Update everything." + method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." def update(*gems) - require "bundler/cli/update" - Update.new(options, gems).run + require_relative "cli/update" + Bundler.settings.temporary(no_install: false) do + Update.new(options, gems).run + end end desc "show GEM [OPTIONS]", "Shows all gems that are part of the bundle, or the path to a given gem" @@ -271,34 +338,35 @@ module Bundler Show lists the names and versions of all gems that are required by your Gemfile. Calling show with [GEM] will list the exact location of that gem on your machine. D - method_option "paths", :type => :boolean, - :banner => "List the paths of all gems that are required by your Gemfile." - method_option "outdated", :type => :boolean, - :banner => "Show verbose output including whether gems are outdated." + method_option "paths", type: :boolean, banner: "List the paths of all gems that are required by your Gemfile." + method_option "outdated", type: :boolean, banner: "Show verbose output including whether gems are outdated (removed)." def show(gem_name = nil) - Bundler::SharedHelpers.major_deprecation(2, "use `bundle list` instead of `bundle show`") if ARGV[0] == "show" - require "bundler/cli/show" + if ARGV.include?("--outdated") + removed_message = "the `--outdated` flag to `bundle show` has been removed in favor of `bundle show --verbose`" + raise InvalidOption, removed_message + end + require_relative "cli/show" Show.new(options, gem_name).run end - # TODO: 2.0 remove `bundle show` - if Bundler.feature_flag.list_command? - desc "list", "List all gems in the bundle" - method_option "name-only", :type => :boolean, :banner => "print only the gem names" - def list - require "bundler/cli/list" - List.new(options).run - end - - map %w[ls] => "list" - else - map %w[list] => "show" + desc "list", "List all gems in the bundle" + method_option "name-only", type: :boolean, banner: "print only the gem names" + method_option "only-group", type: :array, default: [], banner: "print gems from a given set of groups" + method_option "without-group", type: :array, default: [], banner: "print all gems except from a given set of groups" + method_option "format", type: :string, banner: "format output ('json' is the only supported format)" + method_option "paths", type: :boolean, banner: "print the path to each gem in the bundle" + def list + require_relative "cli/list" + List.new(options).run end + map aliases_for("list") + desc "info GEM [OPTIONS]", "Show information for the given gem" - method_option "path", :type => :boolean, :banner => "Print full path to gem" + method_option "path", type: :boolean, banner: "Print full path to gem" + method_option "version", type: :boolean, banner: "Print gem version" def info(gem_name) - require "bundler/cli/info" + require_relative "cli/info" Info.new(options, gem_name).run end @@ -308,16 +376,16 @@ module Bundler or the --binstubs directory if one has been set. Calling binstubs with [GEM [GEM]] will create binstubs for all given gems. D - method_option "force", :type => :boolean, :default => false, :banner => - "Overwrite existing binstubs if they exist" - method_option "path", :type => :string, :lazy_default => "bin", :banner => - "Binstub destination directory (default bin)" - method_option "shebang", :type => :string, :banner => - "Specify a different shebang executable name than the default (usually 'ruby')" - method_option "standalone", :type => :boolean, :banner => - "Make binstubs that can work without the Bundler runtime" + method_option "force", type: :boolean, default: false, banner: "Overwrite existing binstubs if they exist" + method_option "path", type: :string, lazy_default: "bin", banner: "Binstub destination directory, `bin` by default (removed)" + method_option "shebang", type: :string, banner: "Specify a different shebang executable name than the default (usually 'ruby')" + method_option "standalone", type: :boolean, banner: "Make binstubs that can work without the Bundler runtime" + method_option "all", type: :boolean, banner: "Install binstubs for all gems" + method_option "all-platforms", type: :boolean, default: false, banner: "Install binstubs for all platforms" def binstubs(*gems) - require "bundler/cli/binstubs" + remembered_flag_deprecation("path", option_name: "bin") + + require_relative "cli/binstubs" Binstubs.new(options, gems).run end @@ -325,13 +393,25 @@ module Bundler long_desc <<-D Adds the specified gem to Gemfile (if valid) and run 'bundle install' in one step. D - method_option "version", :aliases => "-v", :type => :string - method_option "group", :aliases => "-g", :type => :string - method_option "source", :aliases => "-s", :type => :string - - def add(gem_name) - require "bundler/cli/add" - Add.new(options.dup, gem_name).run + method_option "version", aliases: "-v", type: :string + method_option "group", aliases: "-g", type: :string + method_option "source", aliases: "-s", type: :string + method_option "require", aliases: "-r", type: :string, banner: "Adds require path to gem. Provide false, or a path as a string." + method_option "path", type: :string + method_option "git", type: :string + method_option "github", type: :string + method_option "branch", type: :string + method_option "ref", type: :string + method_option "glob", type: :string, banner: "The location of a dependency's .gemspec, expanded within Ruby (single quotes recommended)" + method_option "quiet", type: :boolean, banner: "Only output warnings and errors." + method_option "skip-install", type: :boolean, banner: "Adds gem to the Gemfile but does not install it" + method_option "optimistic", type: :boolean, banner: "Ignored (now default behavior)" + method_option "pessimistic", type: :boolean, banner: "Adds pessimistic declaration of version to gem" + method_option "strict", type: :boolean, banner: "Adds strict declaration of version to gem" + method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." + def add(*gems) + require_relative "cli/add" + Add.new(options.dup, gems).run end desc "outdated GEM [OPTIONS]", "List installed gems with newer versions available" @@ -342,88 +422,95 @@ module Bundler are up to date, Bundler will exit with a status of 0. Otherwise, it will exit 1. For more information on patch level options (--major, --minor, --patch, - --update-strict) see documentation on the same options on the update command. + --strict) see documentation on the same options on the update command. D - method_option "group", :type => :string, :banner => "List gems from a specific group" - method_option "groups", :type => :boolean, :banner => "List gems organized by groups" - method_option "local", :type => :boolean, :banner => - "Do not attempt to fetch gems remotely and use the gem cache instead" - method_option "pre", :type => :boolean, :banner => "Check for newer pre-release gems" - method_option "source", :type => :array, :banner => "Check against a specific source" - method_option "strict", :type => :boolean, :banner => - "Only list newer versions allowed by your Gemfile requirements" - method_option "update-strict", :type => :boolean, :banner => - "Strict conservative resolution, do not allow any gem to be updated past latest --patch | --minor | --major" - method_option "minor", :type => :boolean, :banner => "Prefer updating only to next minor version" - method_option "major", :type => :boolean, :banner => "Prefer updating to next major version (default)" - method_option "patch", :type => :boolean, :banner => "Prefer updating only to next patch version" - method_option "filter-major", :type => :boolean, :banner => "Only list major newer versions" - method_option "filter-minor", :type => :boolean, :banner => "Only list minor newer versions" - method_option "filter-patch", :type => :boolean, :banner => "Only list patch newer versions" - method_option "parseable", :aliases => "--porcelain", :type => :boolean, :banner => - "Use minimal formatting for more parseable output" + method_option "group", type: :string, banner: "List gems from a specific group" + method_option "groups", type: :boolean, banner: "List gems organized by groups" + method_option "local", type: :boolean, banner: "Do not attempt to fetch gems remotely and use the gem cache instead" + method_option "pre", type: :boolean, banner: "Check for newer pre-release gems" + method_option "source", type: :array, banner: "Check against a specific source" + method_option "filter-strict", type: :boolean, aliases: "--strict", banner: "Only list newer versions allowed by your Gemfile requirements" + method_option "update-strict", type: :boolean, banner: "Strict conservative resolution, do not allow any gem to be updated past latest --patch | --minor | --major" + method_option "minor", type: :boolean, banner: "Prefer updating only to next minor version" + method_option "major", type: :boolean, banner: "Prefer updating to next major version (default)" + method_option "patch", type: :boolean, banner: "Prefer updating only to next patch version" + method_option "filter-major", type: :boolean, banner: "Only list major newer versions" + method_option "filter-minor", type: :boolean, banner: "Only list minor newer versions" + method_option "filter-patch", type: :boolean, banner: "Only list patch newer versions" + method_option "parseable", aliases: "--porcelain", type: :boolean, banner: "Use minimal formatting for more parseable output" + method_option "only-explicit", type: :boolean, banner: "Only list gems specified in your Gemfile, not their dependencies" + method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." def outdated(*gems) - require "bundler/cli/outdated" + require_relative "cli/outdated" Outdated.new(options, gems).run end - if Bundler.feature_flag.cache_command_is_package? - map %w[cache] => :package - else - desc "cache [OPTIONS]", "Cache all the gems to vendor/cache", :hide => true - unless Bundler.feature_flag.cache_command_is_package? - method_option "all", :type => :boolean, - :banner => "Include all sources (including path and git)." - end - method_option "all-platforms", :type => :boolean, :banner => "Include gems for all platforms present in the lockfile, not only the current one" - method_option "no-prune", :type => :boolean, :banner => "Don't remove stale gems from the cache." - def cache - require "bundler/cli/cache" - Cache.new(options).run - end + desc "fund [OPTIONS]", "Lists information about gems seeking funding assistance" + method_option "group", aliases: "-g", type: :array, banner: "Fetch funding information for a specific group" + def fund + require_relative "cli/fund" + Fund.new(options).run end - desc "#{Bundler.feature_flag.cache_command_is_package? ? :cache : :package} [OPTIONS]", "Locks and then caches all of the gems into vendor/cache" - unless Bundler.feature_flag.cache_command_is_package? - method_option "all", :type => :boolean, - :banner => "Include all sources (including path and git)." - end - method_option "all-platforms", :type => :boolean, :banner => "Include gems for all platforms present in the lockfile, not only the current one" - method_option "cache-path", :type => :string, :banner => - "Specify a different cache path than the default (vendor/cache)." - method_option "gemfile", :type => :string, :banner => "Use the specified gemfile instead of Gemfile" - method_option "no-install", :type => :boolean, :banner => "Don't install the gems, only the package." - method_option "no-prune", :type => :boolean, :banner => "Don't remove stale gems from the cache." - method_option "path", :type => :string, :banner => - "Specify a different path than the system default ($BUNDLE_PATH or $GEM_HOME). Bundler will remember this value for future installs on this machine" - method_option "quiet", :type => :boolean, :banner => "Only output warnings and errors." - method_option "frozen", :type => :boolean, :banner => - "Do not allow the Gemfile.lock to be updated after this package operation's install" + desc "cache [OPTIONS]", "Locks and then caches all of the gems into vendor/cache" + method_option "all", type: :boolean, default: Bundler.settings[:cache_all], banner: "Include all sources (including path and git) (removed)." + method_option "all-platforms", type: :boolean, banner: "Include gems for all platforms present in the lockfile, not only the current one" + method_option "cache-path", type: :string, banner: "Specify a different cache path than the default (vendor/cache)." + method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile" + method_option "no-install", type: :boolean, banner: "Don't install the gems, only update the cache." + method_option "no-prune", type: :boolean, banner: "Don't remove stale gems from the cache (removed)." + method_option "path", type: :string, banner: "Specify a different path than the system default, namely, $BUNDLE_PATH or $GEM_HOME (removed)." + method_option "quiet", type: :boolean, banner: "Only output warnings and errors." + method_option "frozen", type: :boolean, banner: "Do not allow the Gemfile.lock to be updated after this bundle cache operation's install (removed)" long_desc <<-D - The package command will copy the .gem files for every gem in the bundle into the + The cache 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 control repository, others who check out your source will be able to install the bundle without having to download any additional gems. D - def package - require "bundler/cli/package" - Package.new(options).run + def cache + print_remembered_flag_deprecation("--all", "cache_all", "true") if ARGV.include?("--all") + print_remembered_flag_deprecation("--no-all", "cache_all", "false") if ARGV.include?("--no-all") + + %w[frozen no-prune].each do |option| + remembered_flag_deprecation(option) + end + + if flag_passed?("--path") + removed_message = + "The `--path` flag has been removed because its semantics were unclear. " \ + "Use `bundle config cache_path` to configure the path of your cache of gems, " \ + "and `bundle config path` to configure the path where your gems are installed, " \ + "and stop using this flag" + raise InvalidOption, removed_message + end + + require_relative "cli/cache" + Cache.new(options).run end - map %w[pack] => :package + + map aliases_for("cache") desc "exec [OPTIONS]", "Run the command in context of the bundle" - method_option :keep_file_descriptors, :type => :boolean, :default => false + method_option :keep_file_descriptors, type: :boolean, default: true, banner: "Passes all file descriptors to the new processes. Default is true, and setting it to false is not permitted (removed)." + method_option :gemfile, type: :string, required: false, banner: "Use the specified gemfile instead of Gemfile" long_desc <<-D Exec runs a command, providing it access to the gems in the bundle. While using bundle exec you can require and call the bundled gems as if they were installed into the system wide RubyGems repository. D - map "e" => "exec" def exec(*args) - require "bundler/cli/exec" + if ARGV.include?("--no-keep-file-descriptors") + removed_message = "The `--no-keep-file-descriptors` has been removed. `bundle exec` no longer mess with your file descriptors. Close them in the exec'd script if you need to" + raise InvalidOption, removed_message + end + + require_relative "cli/exec" Exec.new(options, args).run end + map aliases_for("exec") + desc "config NAME [VALUE]", "Retrieve or set a configuration value" long_desc <<-D Retrieves or sets a configuration value. If only one parameter is provided, retrieve the value. If two parameters are provided, replace the @@ -432,44 +519,41 @@ module Bundler By default, setting a configuration value sets it for all projects on the machine. - If a global setting is superceded by local configuration, this command - will show the current value, as well as any superceded values and + If a global setting is superseded by local configuration, this command + will show the current value, as well as any superseded values and where they were specified. D - method_option "parseable", :type => :boolean, :banner => "Use minimal formatting for more parseable output" - def config(*args) - require "bundler/cli/config" - Config.new(options, args, self).run - end + require_relative "cli/config" + subcommand "config", Config desc "open GEM", "Opens the source directory of the given bundled gem" + method_option "path", type: :string, lazy_default: "", banner: "Open relative path of the gem source." def open(name) - require "bundler/cli/open" + require_relative "cli/open" Open.new(options, name).run end - if Bundler.feature_flag.console_command? - desc "console [GROUP]", "Opens an IRB session with the bundle pre-loaded" - def console(group = nil) - require "bundler/cli/console" - Console.new(options, group).run - end + desc "console [GROUP]", "Opens an IRB session with the bundle pre-loaded" + def console(group = nil) + require_relative "cli/console" + Console.new(options, group).run end - desc "version", "Prints the bundler's version information" + desc "version", "Prints Bundler version information" def version cli_help = current_command.name == "cli_help" if cli_help || ARGV.include?("version") - build_info = " (#{BuildMetadata.built_at} commit #{BuildMetadata.git_commit_sha})" + build_info = " (#{BuildMetadata.timestamp} commit #{BuildMetadata.git_commit_sha})" end - if !cli_help && Bundler.feature_flag.print_only_version_number? - Bundler.ui.info "#{Bundler::VERSION}#{build_info}" + if !cli_help + Bundler.ui.info "#{Bundler.verbose_version}#{build_info}" else - Bundler.ui.info "Bundler version #{Bundler::VERSION}#{build_info}" + Bundler.ui.info "Bundler version #{Bundler.verbose_version}#{build_info}" end end - map %w[-v --version] => :version + + map aliases_for("version") desc "licenses", "Prints the license of all gems in the bundle" def licenses @@ -485,119 +569,82 @@ module Bundler end end - desc "viz [OPTIONS]", "Generates a visual dependency graph", :hide => true - long_desc <<-D - Viz generates a PNG file of the current Gemfile as a dependency graph. - Viz requires the ruby-graphviz gem (and its dependencies). - The associated gems must also be installed via 'bundle install'. - D - method_option :file, :type => :string, :default => "gem_graph", :aliases => "-f", :desc => "The name to use for the generated file. see format option" - method_option :format, :type => :string, :default => "png", :aliases => "-F", :desc => "This is output format option. Supported format is png, jpg, svg, dot ..." - method_option :requirements, :type => :boolean, :default => false, :aliases => "-R", :desc => "Set to show the version of each required dependency." - method_option :version, :type => :boolean, :default => false, :aliases => "-v", :desc => "Set to show each gem version." - method_option :without, :type => :array, :default => [], :aliases => "-W", :banner => "GROUP[ GROUP...]", :desc => "Exclude gems that are part of the specified named group." + desc "viz [OPTIONS]", "Generates a visual dependency graph", hide: true def viz - require "bundler/cli/viz" - Viz.new(options.dup).run + SharedHelpers.feature_removed! "The `viz` command has been renamed to `graph` and moved to a plugin. See https://github.com/rubygems/bundler-graph" end - old_gem = instance_method(:gem) - desc "gem NAME [OPTIONS]", "Creates a skeleton for creating a rubygem" - method_option :exe, :type => :boolean, :default => false, :aliases => ["--bin", "-b"], :desc => "Generate a binary executable for your library." - method_option :coc, :type => :boolean, :desc => "Generate a code of conduct file. Set a default with `bundle config gem.coc true`." - method_option :edit, :type => :string, :aliases => "-e", :required => false, :banner => "EDITOR", - :lazy_default => [ENV["BUNDLER_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find {|e| !e.nil? && !e.empty? }, - :desc => "Open generated gemspec in the specified editor (defaults to $EDITOR or $BUNDLER_EDITOR)" - method_option :ext, :type => :boolean, :default => false, :desc => "Generate the boilerplate for C extension code" - method_option :mit, :type => :boolean, :desc => "Generate an MIT license file. Set a default with `bundle config gem.mit true`." - method_option :test, :type => :string, :lazy_default => "rspec", :aliases => "-t", :banner => "rspec", - :desc => "Generate a test directory for your library, either rspec or minitest. Set a default with `bundle config gem.test rspec`." - def gem(name) - end + method_option :exe, type: :boolean, default: false, aliases: ["--bin", "-b"], banner: "Generate a binary executable for your library." + method_option :coc, type: :boolean, banner: "Generate a code of conduct file. Set a default with `bundle config set --global gem.coc true`." + method_option :edit, type: :string, aliases: "-e", required: false, lazy_default: [ENV["BUNDLER_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find {|e| !e.nil? && !e.empty? }, banner: "Open generated gemspec in the specified editor (defaults to $EDITOR or $BUNDLER_EDITOR)" + method_option :ext, type: :string, banner: "Generate the boilerplate for C extension code.", enum: EXTENSIONS + method_option :git, type: :boolean, default: true, banner: "Initialize a git repo inside your library." + method_option :mit, type: :boolean, banner: "Generate an MIT license file. Set a default with `bundle config set --global gem.mit true`." + method_option :rubocop, type: :boolean, banner: "Add rubocop to the generated Rakefile and gemspec. Set a default with `bundle config set --global gem.rubocop true` (removed)." + method_option :changelog, type: :boolean, banner: "Generate changelog file. Set a default with `bundle config set --global gem.changelog true`." + method_option :test, type: :string, lazy_default: Bundler.settings["gem.test"] || "", aliases: "-t", banner: "Use the specified test framework for your library", enum: %w[rspec minitest test-unit], desc: "Generate a test directory for your library, either rspec, minitest or test-unit. Set a default with `bundle config set --global gem.test (rspec|minitest|test-unit)`." + method_option :ci, type: :string, lazy_default: Bundler.settings["gem.ci"] || "", enum: %w[github gitlab circle], banner: "Generate CI configuration, either GitHub Actions, GitLab CI or CircleCI. Set a default with `bundle config set --global gem.ci (github|gitlab|circle)`" + method_option :linter, type: :string, lazy_default: Bundler.settings["gem.linter"] || "", enum: %w[rubocop standard], banner: "Add a linter and code formatter, either RuboCop or Standard. Set a default with `bundle config set --global gem.linter (rubocop|standard)`" + method_option :github_username, type: :string, default: Bundler.settings["gem.github_username"], banner: "Set your username on GitHub", desc: "Fill in GitHub username on README so that you don't have to do it manually. Set a default with `bundle config set --global gem.github_username <your_username>`." + method_option :bundle, type: :boolean, default: Bundler.settings["gem.bundle"], banner: "Automatically run `bundle install` after creation. Set a default with `bundle config set --global gem.bundle true`" - commands["gem"].tap do |gem_command| - def gem_command.run(instance, args = []) - arity = 1 # name + def gem(name) + require_relative "cli/gem" - require "bundler/cli/gem" - cmd_args = args + [instance] - cmd_args.unshift(instance.options) + raise InvalidOption, "--rubocop has been removed, use --linter=rubocop" if ARGV.include?("--rubocop") + raise InvalidOption, "--no-rubocop has been removed, use --no-linter" if ARGV.include?("--no-rubocop") - cmd = begin - Gem.new(*cmd_args) - rescue ArgumentError => e - instance.class.handle_argument_error(self, e, args, arity) - end + cmd_args = args + [self] + cmd_args.unshift(options) - cmd.run - end + Gem.new(*cmd_args).run end - undef_method(:gem) - define_method(:gem, old_gem) - private :gem - def self.source_root - File.expand_path(File.join(File.dirname(__FILE__), "templates")) + File.expand_path("templates", __dir__) end - desc "clean [OPTIONS]", "Cleans up unused gems in your bundler directory", :hide => true - method_option "dry-run", :type => :boolean, :default => false, :banner => - "Only print out changes, do not clean gems" - method_option "force", :type => :boolean, :default => false, :banner => - "Forces clean even if --path is not set" + desc "clean [OPTIONS]", "Cleans up unused gems in your bundler directory" + method_option "dry-run", type: :boolean, default: false, banner: "Only print out changes, do not clean gems" + method_option "force", type: :boolean, default: false, banner: "Forces cleaning up unused gems even if Bundler is configured to use globally installed gems. As a consequence, removes all system gems except for the ones in the current application." def clean - require "bundler/cli/clean" + require_relative "cli/clean" Clean.new(options.dup).run end desc "platform [OPTIONS]", "Displays platform compatibility information" - method_option "ruby", :type => :boolean, :default => false, :banner => - "only display ruby related platform information" + method_option "ruby", type: :boolean, default: false, banner: "only display ruby related platform information" def platform - require "bundler/cli/platform" + require_relative "cli/platform" Platform.new(options).run end - desc "inject GEM VERSION", "Add the named gem, with version requirements, to the resolved Gemfile", :hide => true - method_option "source", :type => :string, :banner => - "Install gem from the given source" - method_option "group", :type => :string, :banner => - "Install gem into a bundler group" - def inject(name, version) - SharedHelpers.major_deprecation 2, "The `inject` command has been replaced by the `add` command" - require "bundler/cli/inject" - Inject.new(options.dup, name, version).run + desc "inject GEM VERSION", "Add the named gem, with version requirements, to the resolved Gemfile", hide: true + def inject(*) + SharedHelpers.feature_removed! "The `inject` command has been replaced by the `add` command" end desc "lock", "Creates a lockfile without installing" - method_option "update", :type => :array, :lazy_default => true, :banner => - "ignore the existing lockfile, update all gems by default, or update list of given gems" - method_option "local", :type => :boolean, :default => false, :banner => - "do not attempt to fetch remote gemspecs and use the local gem cache only" - method_option "print", :type => :boolean, :default => false, :banner => - "print the lockfile to STDOUT instead of writing to the file system" - method_option "lockfile", :type => :string, :default => nil, :banner => - "the path the lockfile should be written to" - method_option "full-index", :type => :boolean, :default => false, :banner => - "Fall back to using the single-file index of all gems" - method_option "add-platform", :type => :array, :default => [], :banner => - "Add a new platform to the lockfile" - method_option "remove-platform", :type => :array, :default => [], :banner => - "Remove a platform from the lockfile" - method_option "patch", :type => :boolean, :banner => - "If updating, prefer updating only to next patch version" - method_option "minor", :type => :boolean, :banner => - "If updating, prefer updating only to next minor version" - method_option "major", :type => :boolean, :banner => - "If updating, prefer updating to next major version (default)" - method_option "strict", :type => :boolean, :banner => - "If updating, do not allow any gem to be updated past latest --patch | --minor | --major" - method_option "conservative", :type => :boolean, :banner => - "If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated" + method_option "update", type: :array, lazy_default: true, banner: "ignore the existing lockfile, update all gems by default, or update list of given gems" + method_option "local", type: :boolean, default: false, banner: "do not attempt to fetch remote gemspecs and use the local gem cache only" + method_option "print", type: :boolean, default: false, banner: "print the lockfile to STDOUT instead of writing to the file system" + method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile" + method_option "lockfile", type: :string, default: nil, banner: "the path the lockfile should be written to" + method_option "full-index", type: :boolean, default: false, banner: "Fall back to using the single-file index of all gems" + method_option "add-checksums", type: :boolean, default: false, banner: "Adds checksums to the lockfile" + method_option "add-platform", type: :array, default: [], banner: "Add a new platform to the lockfile" + method_option "remove-platform", type: :array, default: [], banner: "Remove a platform from the lockfile" + method_option "normalize-platforms", type: :boolean, default: false, banner: "Normalize lockfile platforms" + method_option "patch", type: :boolean, banner: "If updating, prefer updating only to next patch version" + method_option "minor", type: :boolean, banner: "If updating, prefer updating only to next minor version" + method_option "major", type: :boolean, banner: "If updating, prefer updating to next major version (default)" + method_option "pre", type: :boolean, banner: "If updating, always choose the highest allowed version, regardless of prerelease status" + method_option "strict", type: :boolean, banner: "If updating, do not allow any gem to be updated past latest --patch | --minor | --major" + method_option "conservative", type: :boolean, banner: "If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated" + method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler" def lock - require "bundler/cli/lock" + require_relative "cli/lock" Lock.new(options).run end @@ -607,23 +654,12 @@ module Bundler end desc "doctor [OPTIONS]", "Checks the bundle for common problems" - long_desc <<-D - Doctor scans the OS dependencies of each of the gems requested in the Gemfile. If - missing dependencies are detected, Bundler prints them and exits status 1. - Otherwise, Bundler prints a success message and exits with a status of 0. - D - method_option "gemfile", :type => :string, :banner => - "Use the specified gemfile instead of Gemfile" - method_option "quiet", :type => :boolean, :banner => - "Only output warnings and errors." - def doctor - require "bundler/cli/doctor" - Doctor.new(options).run - end + require_relative "cli/doctor" + subcommand("doctor", Doctor) desc "issue", "Learn how to report an issue in Bundler" def issue - require "bundler/cli/issue" + require_relative "cli/issue" Issue.new.run end @@ -634,12 +670,14 @@ module Bundler checkout --force`. D def pristine(*gems) - require "bundler/cli/pristine" - Pristine.new(gems).run + require_relative "cli/pristine" + Bundler.settings.temporary(no_install: false) do + Pristine.new(gems).run + end end - if Bundler.feature_flag.plugins? - require "bundler/cli/plugin" + if Bundler.settings[:plugins] + require_relative "cli/plugin" desc "plugin", "Manage the bundler plugins" subcommand "plugin", Plugin end @@ -647,12 +685,16 @@ module Bundler # Reformat the arguments passed to bundle that include a --help flag # into the corresponding `bundle help #{command}` call def self.reformatted_help_args(args) - bundler_commands = all_commands.keys + bundler_commands = (COMMAND_ALIASES.keys + COMMAND_ALIASES.values).flatten + help_flags = %w[--help -h] - exec_commands = %w[e ex exe exec] + exec_commands = ["exec"] + COMMAND_ALIASES["exec"] + help_used = args.index {|a| help_flags.include? a } exec_used = args.index {|a| exec_commands.include? a } + command = args.find {|a| bundler_commands.include? a } + if exec_used && help_used if exec_used + help_used == 1 %w[help exec] @@ -668,33 +710,57 @@ module Bundler end end - private - - # Automatically invoke `bundle install` and resume if - # Bundler.settings[:auto_install] exists. This is set through config cmd - # `bundle config auto_install 1`. - # - # Note that this method `nil`s out the global Definition object, so it - # should be called first, before you instantiate anything like an - # `Installer` that'll keep a reference to the old one instead. - def auto_install - return unless Bundler.settings[:auto_install] - - begin - Bundler.definition.specs - rescue GemNotFound - Bundler.ui.info "Automatically installing missing gems." - Bundler.reset! - invoke :install, [] - Bundler.reset! + def self.check_invalid_ext_option(arguments) + # when invalid version of `--ext` is called + if invalid_ext_value?(arguments) + removed_message = "Extensions can now be generated using C or Rust, so `--ext` with no arguments has been removed. Please select a language, e.g. `--ext=rust` to generate a Rust extension." + raise InvalidOption, removed_message end end + def self.invalid_ext_value?(arguments) + index = arguments.index("--ext") + next_argument = arguments[index + 1] + + # it is ok when --ext is followed with valid extension value + # for example `bundle gem hello --ext c` + return false if EXTENSIONS.include?(next_argument) + + # invalid call when --ext is called with no value in last position + # for example `bundle gem hello_gem --ext` + return true if next_argument.nil? + + # invalid call when --ext is followed by other parameter + # for example `bundle gem --ext --no-ci hello_gem` + return true if next_argument.start_with?("-") + + # invalid call when --ext is followed by gem name + # for example `bundle gem --ext hello_gem` + return true if next_argument + + false + end + + private + def current_command _, _, config = @_initializer config[:current_command] end + def invoke_other_command(name) + _, _, config = @_initializer + original_command = config[:current_command] + command = self.class.all_commands[name] + config[:current_command] = command + send(name) + ensure + config[:current_command] = original_command + end + + def current_command=(command) + end + def print_command return unless Bundler.ui.debug? cmd = current_command @@ -708,7 +774,7 @@ module Bundler end command << Thor::Options.to_switches(options_to_print.sort_by(&:first)).strip command.reject!(&:empty?) - Bundler.ui.info "Running `#{command * " "}` with bundler #{Bundler::VERSION}" + Bundler.ui.info "Running `#{command * " "}` with bundler #{Bundler.verbose_version}" end def warn_on_outdated_bundler @@ -717,30 +783,48 @@ module Bundler command_name = current_command.name return if PARSEABLE_COMMANDS.include?(command_name) - latest = Fetcher::CompactIndex. - new(nil, Source::Rubygems::Remote.new(URI("https://rubygems.org")), nil). - send(:compact_index_client). - instance_variable_get(:@cache). - dependencies("bundler"). - map {|d| Gem::Version.new(d.first) }. - max + return unless SharedHelpers.md5_available? + + require_relative "vendored_uri" + remote = Source::Rubygems::Remote.new(Gem::URI("https://rubygems.org")) + cache_path = Bundler.user_cache.join("compact_index", remote.cache_slug) + latest = Bundler::CompactIndexClient.new(cache_path).latest_version("bundler") return unless latest current = Gem::Version.new(VERSION) return if current >= latest - latest_installed = Bundler.rubygems.find_name("bundler").map(&:version).max - installation = "To install the latest version, run `gem install bundler#{" --pre" if latest.prerelease?}`" - if latest_installed && latest_installed > current - suggestion = "To update to the most recent installed version (#{latest_installed}), run `bundle update --bundler`" - suggestion = "#{installation}\n#{suggestion}" if latest_installed < latest - else - suggestion = installation - end - - Bundler.ui.warn "The latest bundler is #{latest}, but you are currently running #{current}.\n#{suggestion}" - rescue + Bundler.ui.warn \ + "The latest bundler is #{latest}, but you are currently running #{current}.\n" \ + "To update to the most recent version, run `bundle update --bundler`" + rescue RuntimeError nil end + + def remembered_flag_deprecation(name, negative: false, option_name: nil) + option = current_command.options[name] + flag_name = option.switch_name + flag_name = "--no-" + flag_name.gsub(/\A--/, "") if negative + return unless flag_passed?(flag_name) + + value = options[name] + value = value.join(" ").to_s if option.type == :array + value = "'#{value}'" unless option.type == :boolean + + print_remembered_flag_deprecation(flag_name, option_name || name.tr("-", "_"), value) + end + + def print_remembered_flag_deprecation(flag_name, option_name, option_value) + removed_message = + "The `#{flag_name}` flag has been removed because it relied on being " \ + "remembered across bundler invocations, which bundler no longer does. " \ + "Instead please use `bundle config set #{option_name} #{option_value}`, " \ + "and stop using this flag" + raise InvalidOption, removed_message + end + + def flag_passed?(name) + ARGV.any? {|arg| name == arg.split("=")[0] } + end end end diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb index 1fcbd22f28..20f76b59d1 100644 --- a/lib/bundler/cli/add.rb +++ b/lib/bundler/cli/add.rb @@ -2,24 +2,61 @@ module Bundler class CLI::Add - def initialize(options, gem_name) - @gem_name = gem_name + attr_reader :gems, :options, :version + + def initialize(options, gems) + @gems = gems @options = options - @options[:group] = @options[:group].split(",").map(&:strip) if !@options[:group].nil? && !@options[:group].empty? + @options[:group] = options[:group].split(",").map(&:strip) unless options[:group].nil? + @version = options[:version].split(",").map(&:strip) unless options[:version].nil? end def run - version = @options[:version].nil? ? nil : @options[:version].split(",").map(&:strip) + Bundler.ui.level = "warn" if options[:quiet] - unless version.nil? - version.each do |v| - raise InvalidOption, "Invalid gem requirement pattern '#{v}'" unless Gem::Requirement::PATTERN =~ v.to_s - end - end - dependency = Bundler::Dependency.new(@gem_name, version, @options) + Bundler::CLI::Common.validate_cooldown!(options[:cooldown]) + Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] + + validate_options! + inject_dependencies + perform_bundle_install unless options["skip-install"] + end + + private - Injector.inject([dependency], :conservative_versioning => @options[:version].nil?) # Perform conservative versioning only when version is not specified + def perform_bundle_install Installer.install(Bundler.root, Bundler.definition) + Bundler.load.cache if Bundler.app_cache.exist? + end + + def inject_dependencies + dependencies = gems.map {|g| Bundler::Dependency.new(g, version, options) } + + Injector.inject(dependencies, + conservative_versioning: options[:version].nil?, # Perform conservative versioning only when version is not specified + pessimistic: options[:pessimistic], + strict: options[:strict]) + end + + def validate_options! + raise InvalidOption, "You cannot specify `--git` and `--github` at the same time." if options["git"] && options["github"] + + unless options["git"] || options["github"] + raise InvalidOption, "You cannot specify `--branch` unless `--git` or `--github` is specified." if options["branch"] + + raise InvalidOption, "You cannot specify `--ref` unless `--git` or `--github` is specified." if options["ref"] + end + + raise InvalidOption, "You cannot specify `--branch` and `--ref` at the same time." if options["branch"] && options["ref"] + + raise InvalidOption, "You cannot specify `--strict` and `--pessimistic` at the same time." if options[:strict] && options[:pessimistic] + + # raise error when no gems are specified + raise InvalidOption, "Please specify gems to add." if gems.empty? + + version.to_a.each do |v| + raise InvalidOption, "Invalid gem requirement pattern '#{v}'" unless Gem::Requirement::PATTERN.match?(v.to_s) + end end end end diff --git a/lib/bundler/cli/binstubs.rb b/lib/bundler/cli/binstubs.rb index 449204d821..8ce138df96 100644 --- a/lib/bundler/cli/binstubs.rb +++ b/lib/bundler/cli/binstubs.rb @@ -11,12 +11,22 @@ module Bundler def run Bundler.definition.validate_runtime! path_option = options["path"] - path_option = nil if path_option && path_option.empty? + path_option = nil if path_option&.empty? Bundler.settings.set_command_option :bin, path_option if options["path"] Bundler.settings.set_command_option_if_given :shebang, options["shebang"] installer = Installer.new(Bundler.root, Bundler.definition) - if gems.empty? + installer_opts = { + force: options[:force], + binstubs_cmd: true, + all_platforms: options["all-platforms"], + } + + if options[:all] + raise InvalidOption, "Cannot specify --all with specific gems" unless gems.empty? + @gems = Bundler.definition.specs.map(&:name) + installer_opts.delete(:binstubs_cmd) + elsif gems.empty? Bundler.ui.error "`bundle binstubs` needs at least one gem to run." exit 1 end @@ -30,12 +40,16 @@ module Bundler end if options[:standalone] - next Bundler.ui.warn("Sorry, Bundler can only be run via RubyGems.") if gem_name == "bundler" - Bundler.settings.temporary(:path => (Bundler.settings[:path] || Bundler.root)) do - installer.generate_standalone_bundler_executable_stubs(spec) + if gem_name == "bundler" + Bundler.ui.warn("Sorry, Bundler can only be run via RubyGems.") unless options[:all] + next + end + + Bundler.settings.temporary(path: Bundler.settings[:path] || Bundler.root) do + installer.generate_standalone_bundler_executable_stubs(spec, installer_opts) end else - installer.generate_bundler_executable_stubs(spec, :force => options[:force], :binstubs_cmd => true) + installer.generate_bundler_executable_stubs(spec, installer_opts) end end end diff --git a/lib/bundler/cli/cache.rb b/lib/bundler/cli/cache.rb index 9d2ba87d34..59605df847 100644 --- a/lib/bundler/cli/cache.rb +++ b/lib/bundler/cli/cache.rb @@ -3,34 +3,30 @@ module Bundler class CLI::Cache attr_reader :options + def initialize(options) @options = options end def run - Bundler.definition.validate_runtime! - Bundler.definition.resolve_with_cache! - setup_cache_all - Bundler.settings.set_command_option_if_given :cache_all_platforms, options["all-platforms"] - Bundler.load.cache - Bundler.settings.set_command_option_if_given :no_prune, options["no-prune"] - Bundler.load.lock - rescue GemNotFound => e - Bundler.ui.error(e.message) - Bundler.ui.warn "Run `bundle install` to install missing gems." - exit 1 - end - - private + Bundler.ui.level = "warn" if options[:quiet] + Bundler.settings.set_command_option_if_given :cache_path, options["cache-path"] - def setup_cache_all - Bundler.settings.set_command_option_if_given :cache_all, options[:all] + install - if Bundler.definition.has_local_dependencies? && !Bundler.feature_flag.cache_all? - Bundler.ui.warn "Your Gemfile contains path and git dependencies. If you want " \ - "to package them as well, please pass the --all flag. This will be the default " \ - "on Bundler 2.0." + Bundler.settings.temporary(cache_all_platforms: options["all-platforms"]) do + Bundler.load.cache end end + + private + + def install + require_relative "install" + options = self.options.dup + options["local"] = false if Bundler.settings[:cache_all_platforms] + options["no-cache"] = true + Bundler::CLI::Install.new(options).run + end end end diff --git a/lib/bundler/cli/check.rb b/lib/bundler/cli/check.rb index e572787dc4..493eb3ec6a 100644 --- a/lib/bundler/cli/check.rb +++ b/lib/bundler/cli/check.rb @@ -11,11 +11,13 @@ module Bundler def run Bundler.settings.set_command_option_if_given :path, options[:path] + definition = Bundler.definition + definition.validate_runtime! + begin - definition = Bundler.definition - definition.validate_runtime! + definition.check! not_installed = definition.missing_specs - rescue GemNotFound, VersionConflict + rescue GemNotFound, GitError, SolveFailure Bundler.ui.error "Bundler can't satisfy your Gemfile's dependencies." Bundler.ui.warn "Install missing gems with `bundle install`." exit 1 @@ -26,11 +28,11 @@ module Bundler not_installed.each {|s| Bundler.ui.error " * #{s.name} (#{s.version})" } Bundler.ui.warn "Install missing gems with `bundle install`" exit 1 - elsif !Bundler.default_lockfile.file? && Bundler.frozen? - Bundler.ui.error "This bundle has been frozen, but there is no #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} present" + elsif !Bundler.default_lockfile.file? && Bundler.frozen_bundle? + Bundler.ui.error "This bundle has been frozen, but there is no #{SharedHelpers.relative_lockfile_path} present" exit 1 else - Bundler.load.lock(:preserve_unknown_sections => true) unless options[:"dry-run"] + definition.lock(true) unless options[:"dry-run"] Bundler.ui.info "The Gemfile's dependencies are satisfied" end end diff --git a/lib/bundler/cli/clean.rb b/lib/bundler/cli/clean.rb index 4a407fbae7..c6b0968e3e 100644 --- a/lib/bundler/cli/clean.rb +++ b/lib/bundler/cli/clean.rb @@ -13,7 +13,7 @@ module Bundler Bundler.load.clean(options[:"dry-run"]) end - protected + protected def require_path_or_force return unless Bundler.use_system_gems? && !options[:force] diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb index 9d40ee9dfd..b44fbc3096 100644 --- a/lib/bundler/cli/common.rb +++ b/lib/bundler/cli/common.rb @@ -2,6 +2,12 @@ module Bundler module CLI::Common + def self.validate_cooldown!(value) + return if value.nil? + return if value.is_a?(Integer) && value >= 0 + raise InvalidOption, "Expected `--cooldown` to be a non-negative integer, got #{value.inspect}" + end + def self.output_post_install_messages(messages) return if Bundler.settings["ignore_messages"] messages.to_a.each do |name, msg| @@ -14,17 +20,38 @@ module Bundler Bundler.ui.info msg end - def self.output_without_groups_message + def self.output_fund_metadata_summary + return if Bundler.settings["ignore_funding_requests"] + definition = Bundler.definition + current_dependencies = definition.requested_dependencies + current_specs = definition.specs + + count = current_dependencies.count {|dep| current_specs[dep.name].first.metadata.key?("funding_uri") } + + return if count.zero? + + intro = count > 1 ? "#{count} installed gems you directly depend on are" : "#{count} installed gem you directly depend on is" + message = "#{intro} looking for funding.\n Run `bundle fund` for details" + Bundler.ui.info message + end + + def self.output_without_groups_message(command) return if Bundler.settings[:without].empty? - Bundler.ui.confirm without_groups_message + Bundler.ui.confirm without_groups_message(command) end - def self.without_groups_message + def self.without_groups_message(command) + command_in_past_tense = command == :install ? "installed" : "updated" groups = Bundler.settings[:without] + "Gems in the #{verbalize_groups(groups)} were not #{command_in_past_tense}." + end + + def self.verbalize_groups(groups) + groups.map! {|g| "'#{g}'" } group_list = [groups[0...-1].join(", "), groups[-1..-1]]. reject {|s| s.to_s.empty? }.join(" and ") - group_str = (groups.size == 1) ? "group" : "groups" - "Gems in the #{group_str} #{group_list} were not installed." + group_str = groups.size == 1 ? "group" : "groups" + "#{group_str} #{group_list}" end def self.select_spec(name, regex_match = nil) @@ -33,12 +60,21 @@ module Bundler Bundler.definition.specs.each do |spec| return spec if spec.name == name - specs << spec if regexp && spec.name =~ regexp + specs << spec if regexp && spec.name.match?(regexp) end + default_spec = default_gem_spec(name) + specs << default_spec if default_spec + case specs.count when 0 - raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies) + dep_in_other_group = Bundler.definition.current_dependencies.find {|dep|dep.name == name } + + if dep_in_other_group + raise GemNotFound, "Could not find gem '#{name}', because it's in the #{verbalize_groups(dep_in_other_group.groups)}, configured to be ignored." + else + raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies) + end when 1 specs.first else @@ -48,11 +84,12 @@ module Bundler raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies) end - def self.ask_for_spec_from(specs) - if !$stdout.tty? && ENV["BUNDLE_SPEC_RUN"].nil? - raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies) - end + def self.default_gem_spec(name) + gem_spec = Gem::Specification.find_all_by_name(name).last + gem_spec if gem_spec&.default_gem? + end + def self.ask_for_spec_from(specs) specs.each_with_index do |spec, index| Bundler.ui.info "#{index.succ} : #{spec.name}", true end @@ -63,16 +100,21 @@ module Bundler end def self.gem_not_found_message(missing_gem_name, alternatives) - require "bundler/similarity_detector" message = "Could not find gem '#{missing_gem_name}'." alternate_names = alternatives.map {|a| a.respond_to?(:name) ? a.name : a } - suggestions = SimilarityDetector.new(alternate_names).similar_word_list(missing_gem_name) - message += "\nDid you mean #{suggestions}?" if suggestions + if alternate_names.include?(missing_gem_name.downcase) + message += "\nDid you mean '#{missing_gem_name.downcase}'?" + elsif defined?(DidYouMean::SpellChecker) + suggestions = DidYouMean::SpellChecker.new(dictionary: alternate_names).correct(missing_gem_name) + message += "\nDid you mean #{word_list(suggestions)}?" unless suggestions.empty? + end message end def self.ensure_all_gems_in_lockfile!(names, locked_gems = Bundler.locked_gems) - locked_names = locked_gems.specs.map(&:name) + return unless locked_gems + + locked_names = locked_gems.specs.map(&:name).uniq names.-(locked_names).each do |g| raise GemNotFound, gem_not_found_message(g, locked_names) end @@ -80,10 +122,13 @@ module Bundler def self.configure_gem_version_promoter(definition, options) patch_level = patch_level_options(options) + patch_level << :patch if patch_level.empty? && Bundler.settings[:prefer_patch] raise InvalidOption, "Provide only one of the following options: #{patch_level.join(", ")}" unless patch_level.length <= 1 + definition.gem_version_promoter.tap do |gvp| gvp.level = patch_level.first || :major - gvp.strict = options[:strict] || options["update-strict"] + gvp.strict = options[:strict] || options["filter-strict"] + gvp.pre = options[:pre] end end @@ -94,9 +139,23 @@ module Bundler def self.clean_after_install? clean = Bundler.settings[:clean] return clean unless clean.nil? - clean ||= Bundler.feature_flag.auto_clean_without_path? && Bundler.settings[:path].nil? + clean ||= Bundler.feature_flag.bundler_5_mode? && Bundler.settings[:path].nil? clean &&= !Bundler.use_system_gems? clean end + + def self.word_list(words) + if words.empty? + return "" + end + + words = words.map {|word| "'#{word}'" } + + if words.length == 1 + return words[0] + end + + [words[0..-2].join(", "), words[-1]].join(" or ") + end end end diff --git a/lib/bundler/cli/config.rb b/lib/bundler/cli/config.rb index 12f71ea8fe..976cda7484 100644 --- a/lib/bundler/cli/config.rb +++ b/lib/bundler/cli/config.rb @@ -1,119 +1,208 @@ # frozen_string_literal: true module Bundler - class CLI::Config - attr_reader :name, :options, :scope, :thor - attr_accessor :args - - def initialize(options, args, thor) - @options = options - @args = args - @thor = thor - @name = peek = args.shift - @scope = "global" - return unless peek && peek.start_with?("--") - @name = args.shift - @scope = peek[2..-1] + class CLI::Config < Thor + class_option :parseable, type: :boolean, banner: "Use minimal formatting for more parseable output" + + def self.scope_options + method_option :global, type: :boolean, banner: "Only change the global config" + method_option :local, type: :boolean, banner: "Only change the local config" end + private_class_method :scope_options - def run - unless name - confirm_all - return - end + desc "base NAME [VALUE]", "The Bundler 1 config interface", hide: true + scope_options + method_option :delete, type: :boolean, banner: "delete" + def base(name = nil, *value) + new_args = + if ARGV.size == 1 + ["config", "list"] + elsif ARGV.include?("--delete") + ARGV.map {|arg| arg == "--delete" ? "unset" : arg } + elsif ARGV.include?("--global") || ARGV.include?("--local") || ARGV.size == 3 + ["config", "set", *ARGV[1..-1]] + else + ["config", "get", ARGV[1]] + end - unless valid_scope?(scope) - Bundler.ui.error "Invalid scope --#{scope} given. Please use --local or --global." - exit 1 - end + message = "Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle #{new_args.join(" ")}` instead." + SharedHelpers.feature_deprecated! message - if scope == "delete" - Bundler.settings.set_local(name, nil) - Bundler.settings.set_global(name, nil) - return + Base.new(options, name, value, self).run + end + + desc "list", "List out all configured settings" + def list + Base.new(options, nil, nil, self).run + end + + desc "get NAME", "Returns the value for the given key" + def get(name) + Base.new(options, name, nil, self).run + end + + desc "set NAME VALUE", "Sets the given value for the given key" + scope_options + def set(name, value, *value_) + Base.new(options, name, value_.unshift(value), self).run + end + + desc "unset NAME", "Unsets the value for the given key" + scope_options + def unset(name) + options[:delete] = true + Base.new(options, name, nil, self).run + end + + default_task :base + + class Base + attr_reader :name, :value, :options, :scope, :thor + + def initialize(options, name, value, thor) + @options = options + @name = name + value = Array(value) + @value = value.empty? ? nil : value.join(" ") + @thor = thor + validate_scope! end - if args.empty? - if options[:parseable] - if value = Bundler.settings[name] - Bundler.ui.info("#{name}=#{value}") + def run + unless name + warn_unused_scope "Ignoring --#{scope}" + confirm_all + return + end + + if options[:delete] + if !explicit_scope? || scope != "global" + Bundler.settings.set_local(name, nil) + end + if !explicit_scope? || scope != "local" + Bundler.settings.set_global(name, nil) end return end - confirm(name) - return - end + if value.nil? + warn_unused_scope "Ignoring --#{scope} since no value to set was given" + current_value = Bundler.settings[name] - Bundler.ui.info(message) if message - Bundler.settings.send("set_#{scope}", name, new_value) - end + if options[:parseable] + if value = Bundler.settings[name] + Bundler.ui.info("#{name}=#{value}") + end + else + confirm(name) + end + + if current_value.nil? + exit 1 + else + return + end + end - private + Bundler.ui.info(message) if message + Bundler.settings.send("set_#{scope}", name, new_value) + end - def confirm_all - if @options[:parseable] - thor.with_padding do + def confirm_all + if @options[:parseable] + thor.with_padding do + Bundler.settings.all.each do |setting| + val = Bundler.settings[setting] + Bundler.ui.info "#{setting}=#{val}" + end + end + else + Bundler.ui.confirm "Settings are listed in order of priority. The top value will be used.\n" Bundler.settings.all.each do |setting| - val = Bundler.settings[setting] - Bundler.ui.info "#{setting}=#{val}" + Bundler.ui.confirm setting + show_pretty_values_for(setting) + Bundler.ui.confirm "" end end - else - Bundler.ui.confirm "Settings are listed in order of priority. The top value will be used.\n" - Bundler.settings.all.each do |setting| - Bundler.ui.confirm "#{setting}" - show_pretty_values_for(setting) - Bundler.ui.confirm "" - end end - end - def confirm(name) - Bundler.ui.confirm "Settings for `#{name}` in order of priority. The top value will be used" - show_pretty_values_for(name) - end + def confirm(name) + Bundler.ui.confirm "Settings for `#{name}` in order of priority. The top value will be used" + show_pretty_values_for(name) + end - def new_value - pathname = Pathname.new(args.join(" ")) - if name.start_with?("local.") && pathname.directory? - pathname.expand_path.to_s - else - args.join(" ") + def new_value + pathname = Pathname.new(value) + if name.start_with?("local.") && pathname.directory? + pathname.expand_path.to_s + else + value + end end - end - def message - locations = Bundler.settings.locations(name) - if @options[:parseable] - "#{name}=#{new_value}" if new_value - elsif scope == "global" - if locations[:local] - "Your application has set #{name} to #{locations[:local].inspect}. " \ - "This will override the global value you are currently setting" - elsif locations[:env] - "You have a bundler environment variable for #{name} set to " \ - "#{locations[:env].inspect}. This will take precedence over the global value you are setting" - elsif locations[:global] && locations[:global] != args.join(" ") - "You are replacing the current global value of #{name}, which is currently " \ - "#{locations[:global].inspect}" + def message + locations = Bundler.settings.locations(name) + if @options[:parseable] + "#{name}=#{new_value}" if new_value + elsif scope == "global" + if !locations[:local].nil? + "Your application has set #{name} to #{locations[:local].inspect}. " \ + "This will override the global value you are currently setting" + elsif locations[:env] + "You have a bundler environment variable for #{name} set to " \ + "#{locations[:env].inspect}. This will take precedence over the global value you are setting" + elsif !locations[:global].nil? && locations[:global] != value + "You are replacing the current global value of #{name}, which is currently " \ + "#{locations[:global].inspect}" + end + elsif scope == "local" && !locations[:local].nil? && locations[:local] != value + "You are replacing the current local value of #{name}, which is currently " \ + "#{locations[:local].inspect}" end - elsif scope == "local" && locations[:local] != args.join(" ") - "You are replacing the current local value of #{name}, which is currently " \ - "#{locations[:local].inspect}" end - end - def show_pretty_values_for(setting) - thor.with_padding do - Bundler.settings.pretty_values_for(setting).each do |line| - Bundler.ui.info line + def show_pretty_values_for(setting) + thor.with_padding do + Bundler.settings.pretty_values_for(setting).each do |line| + Bundler.ui.info line + end end end - end - def valid_scope?(scope) - %w[delete local global].include?(scope) + def explicit_scope? + @explicit_scope + end + + def warn_unused_scope(msg) + return unless explicit_scope? + return if options[:parseable] + + Bundler.ui.warn(msg) + end + + def validate_scope! + @explicit_scope = true + scopes = %w[global local].select {|s| options[s] } + case scopes.size + when 0 + @scope = inside_app? ? "local" : "global" + @explicit_scope = false + when 1 + @scope = scopes.first + else + raise InvalidOption, + "The options #{scopes.join " and "} were specified. Please only use one of the switches at a time." + end + end + + private + + def inside_app? + Bundler.root + true + rescue GemfileNotFound + false + end end end end diff --git a/lib/bundler/cli/console.rb b/lib/bundler/cli/console.rb index 853eca8358..2d1a2ce458 100644 --- a/lib/bundler/cli/console.rb +++ b/lib/bundler/cli/console.rb @@ -9,10 +9,7 @@ module Bundler end def run - Bundler::SharedHelpers.major_deprecation 2, "bundle console will be replaced " \ - "by `bin/console` generated by `bundle gem <name>`" - - group ? Bundler.require(:default, *(group.split.map!(&:to_sym))) : Bundler.require + group ? Bundler.require(:default, *group.split(" ").map!(&:to_sym)) : Bundler.require ARGV.clear console = get_console(Bundler.settings[:console] || "irb") @@ -23,21 +20,28 @@ module Bundler require name get_constant(name) rescue LoadError - Bundler.ui.error "Couldn't load console #{name}, falling back to irb" - require "irb" - get_constant("irb") + if name == "irb" + if defined?(Gem::BUNDLED_GEMS) && Gem::BUNDLED_GEMS.respond_to?(:force_activate) + Gem::BUNDLED_GEMS.force_activate "irb" + require name + return get_constant(name) + end + Bundler.ui.error "#{name} is not available" + exit 1 + else + Bundler.ui.error "Couldn't load console #{name}, falling back to irb" + name = "irb" + retry + end end def get_constant(name) const_name = { - "pry" => :Pry, + "pry" => :Pry, "ripl" => :Ripl, - "irb" => :IRB, + "irb" => :IRB, }[name] Object.const_get(const_name) - rescue NameError - Bundler.ui.error "Could not find constant #{const_name}" - exit 1 end end end diff --git a/lib/bundler/cli/doctor.rb b/lib/bundler/cli/doctor.rb index 7f28a5eb13..5fd6a73d91 100644 --- a/lib/bundler/cli/doctor.rb +++ b/lib/bundler/cli/doctor.rb @@ -1,94 +1,33 @@ # frozen_string_literal: true -require "rbconfig" - module Bundler - class CLI::Doctor - DARWIN_REGEX = /\s+(.+) \(compatibility / - LDD_REGEX = /\t\S+ => (\S+) \(\S+\)/ - - attr_reader :options - - def initialize(options) - @options = options - end - - def otool_available? - Bundler.which("otool") - end - - def ldd_available? - Bundler.which("ldd") - end - - def dylibs_darwin(path) - output = `/usr/bin/otool -L "#{path}"`.chomp - dylibs = output.split("\n")[1..-1].map {|l| l.match(DARWIN_REGEX).captures[0] }.uniq - # ignore @rpath and friends - dylibs.reject {|dylib| dylib.start_with? "@" } - end - - def dylibs_ldd(path) - output = `/usr/bin/ldd "#{path}"`.chomp - output.split("\n").map do |l| - match = l.match(LDD_REGEX) - next if match.nil? - match.captures[0] - end.compact - end - - def dylibs(path) - case RbConfig::CONFIG["host_os"] - when /darwin/ - return [] unless otool_available? - dylibs_darwin(path) - when /(linux|solaris|bsd)/ - return [] unless ldd_available? - dylibs_ldd(path) - else # Windows, etc. - Bundler.ui.warn("Dynamic library check not supported on this platform.") - [] - end - end - - def bundles_for_gem(spec) - Dir.glob("#{spec.full_gem_path}/**/*.bundle") - end - - def check! - require "bundler/cli/check" - Bundler::CLI::Check.new({}).run - end - - def run - Bundler.ui.level = "error" if options[:quiet] - Bundler.settings.validate! - check! - - definition = Bundler.definition - broken_links = {} - - definition.specs.each do |spec| - bundles_for_gem(spec).each do |bundle| - bad_paths = dylibs(bundle).select {|f| !File.exist?(f) } - if bad_paths.any? - broken_links[spec] ||= [] - broken_links[spec].concat(bad_paths) - end - end - end - - if broken_links.any? - message = "The following gems are missing OS dependencies:" - broken_links.map do |spec, paths| - paths.uniq.map do |path| - "\n * #{spec.name}: #{path}" - end - end.flatten.sort.each {|m| message += m } - raise ProductionError, message - else - Bundler.ui.info "No issues found with the installed bundle" - end + class CLI::Doctor < Thor + default_command(:diagnose) + + desc "diagnose [OPTIONS]", "Checks the bundle for common problems" + long_desc <<-D + Doctor scans the OS dependencies of each of the gems requested in the Gemfile. If + missing dependencies are detected, Bundler prints them and exits status 1. + Otherwise, Bundler prints a success message and exits with a status of 0. + D + method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile" + method_option "quiet", type: :boolean, banner: "Only output warnings and errors." + method_option "ssl", type: :boolean, default: false, banner: "Diagnose SSL problems." + def diagnose + require_relative "doctor/diagnose" + Diagnose.new(options).run + end + + desc "ssl [OPTIONS]", "Diagnose SSL problems" + long_desc <<-D + Diagnose SSL problems, especially related to certificates or TLS version while connecting to https://rubygems.org. + D + method_option "host", type: :string, banner: "The host to diagnose." + method_option "tls-version", type: :string, banner: "Specify the SSL/TLS version when running the diagnostic. Accepts either <1.1> or <1.2>" + method_option "verify-mode", type: :string, banner: "Specify the mode used for certification verification. Accepts either <peer> or <none>" + def ssl + require_relative "doctor/ssl" + SSL.new(options).run end end end diff --git a/lib/bundler/cli/doctor/diagnose.rb b/lib/bundler/cli/doctor/diagnose.rb new file mode 100644 index 0000000000..a878025dda --- /dev/null +++ b/lib/bundler/cli/doctor/diagnose.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +require "rbconfig" +require "shellwords" + +module Bundler + class CLI::Doctor::Diagnose + DARWIN_REGEX = /\s+(.+) \(compatibility / + LDD_REGEX = /\t\S+ => (\S+) \(\S+\)/ + + attr_reader :options + + def initialize(options) + @options = options + end + + def otool_available? + Bundler.which("otool") + end + + def ldd_available? + Bundler.which("ldd") + end + + def dylibs_darwin(path) + output = `/usr/bin/otool -L #{path.shellescape}`.chomp + dylibs = output.split("\n")[1..-1].filter_map {|l| l.match(DARWIN_REGEX)&.match(1) }.uniq + # ignore @rpath and friends + dylibs.reject {|dylib| dylib.start_with? "@" } + end + + def dylibs_ldd(path) + output = `/usr/bin/ldd #{path.shellescape}`.chomp + output.split("\n").filter_map do |l| + match = l.match(LDD_REGEX) + next if match.nil? + match.captures[0] + end + end + + def dylibs(path) + case RbConfig::CONFIG["host_os"] + when /darwin/ + return [] unless otool_available? + dylibs_darwin(path) + when /(linux|solaris|bsd)/ + return [] unless ldd_available? + dylibs_ldd(path) + else # Windows, etc. + Bundler.ui.warn("Dynamic library check not supported on this platform.") + [] + end + end + + def bundles_for_gem(spec) + Dir.glob("#{spec.full_gem_path}/**/*.bundle") + end + + def lookup_with_fiddle(path) + require "fiddle" + Fiddle.dlopen(path) + false + rescue Fiddle::DLError + true + end + + def check! + require_relative "../check" + Bundler::CLI::Check.new({}).run + end + + def diagnose_ssl + require_relative "ssl" + Bundler::CLI::Doctor::SSL.new({}).run + end + + def run + Bundler.ui.level = "warn" if options[:quiet] + Bundler.settings.validate! + check! + diagnose_ssl if options[:ssl] + + definition = Bundler.definition + broken_links = {} + + definition.specs.each do |spec| + bundles_for_gem(spec).each do |bundle| + bad_paths = dylibs(bundle).select do |f| + lookup_with_fiddle(f) + end + if bad_paths.any? + broken_links[spec] ||= [] + broken_links[spec].concat(bad_paths) + end + end + end + + permissions_valid = check_home_permissions + + if broken_links.any? + message = "The following gems are missing OS dependencies:" + broken_links.flat_map do |spec, paths| + paths.uniq.map do |path| + "\n * #{spec.name}: #{path}" + end + end.sort.each {|m| message += m } + raise ProductionError, message + elsif permissions_valid + Bundler.ui.info "No issues found with the installed bundle" + end + end + + private + + def check_home_permissions + require "find" + files_not_readable = [] + files_not_readable_and_owned_by_different_user = [] + files_not_owned_by_current_user_but_still_readable = [] + broken_symlinks = [] + Find.find(Bundler.bundle_path.to_s).each do |f| + if !File.exist?(f) + broken_symlinks << f + elsif !File.readable?(f) + if File.stat(f).uid != Process.uid + files_not_readable_and_owned_by_different_user << f + else + files_not_readable << f + end + elsif File.stat(f).uid != Process.uid + files_not_owned_by_current_user_but_still_readable << f + end + end + + ok = true + + if broken_symlinks.any? + Bundler.ui.warn "Broken links exist in the Bundler home. Please report them to the offending gem's upstream repo. These files are:\n - #{broken_symlinks.join("\n - ")}" + + ok = false + end + + if files_not_owned_by_current_user_but_still_readable.any? + Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \ + "user, but are still readable. These files are:\n - #{files_not_owned_by_current_user_but_still_readable.join("\n - ")}" + + ok = false + end + + if files_not_readable_and_owned_by_different_user.any? + Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \ + "user, and are not readable. These files are:\n - #{files_not_readable_and_owned_by_different_user.join("\n - ")}" + + ok = false + end + + if files_not_readable.any? + Bundler.ui.warn "Files exist in the Bundler home that are not " \ + "readable by the current user. These files are:\n - #{files_not_readable.join("\n - ")}" + + ok = false + end + + ok + end + end +end diff --git a/lib/bundler/cli/doctor/ssl.rb b/lib/bundler/cli/doctor/ssl.rb new file mode 100644 index 0000000000..21fc4edf2d --- /dev/null +++ b/lib/bundler/cli/doctor/ssl.rb @@ -0,0 +1,249 @@ +# frozen_string_literal: true + +require "rubygems/remote_fetcher" +require "uri" + +module Bundler + class CLI::Doctor::SSL + attr_reader :options + + def initialize(options) + @options = options + end + + def run + return unless openssl_installed? + + output_ssl_environment + bundler_success = bundler_connection_successful? + rubygem_success = rubygem_connection_successful? + + return unless net_http_connection_successful? + + Explanation.summarize(bundler_success, rubygem_success, host) + end + + private + + def host + @options[:host] || "rubygems.org" + end + + def tls_version + @options[:"tls-version"].then do |version| + "TLS#{version.sub(".", "_")}".to_sym if version + end + end + + def verify_mode + mode = @options[:"verify-mode"] || :peer + + @verify_mode ||= mode.then {|mod| OpenSSL::SSL.const_get("verify_#{mod}".upcase) } + end + + def uri + @uri ||= URI("https://#{host}") + end + + def openssl_installed? + require "openssl" + + true + rescue LoadError + Bundler.ui.warn(<<~MSG) + Oh no! Your Ruby doesn't have OpenSSL, so it can't connect to #{host}. + You'll need to recompile or reinstall Ruby with OpenSSL support and try again. + MSG + + false + end + + def output_ssl_environment + Bundler.ui.info(<<~MESSAGE) + Here's your OpenSSL environment: + + OpenSSL: #{OpenSSL::VERSION} + Compiled with: #{OpenSSL::OPENSSL_VERSION} + Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION} + MESSAGE + end + + def bundler_connection_successful? + Bundler.ui.info("\nTrying connections to #{uri}:\n") + + bundler_uri = Gem::URI(uri.to_s) + Bundler::Fetcher.new( + Bundler::Source::Rubygems::Remote.new(bundler_uri) + ).send(:connection).request(bundler_uri) + + Bundler.ui.info("Bundler: success") + + true + rescue StandardError => error + Bundler.ui.warn("Bundler: failed (#{Explanation.explain_bundler_or_rubygems_error(error)})") + + false + end + + def rubygem_connection_successful? + Gem::RemoteFetcher.fetcher.fetch_path(uri) + Bundler.ui.info("RubyGems: success") + + true + rescue StandardError => error + Bundler.ui.warn("RubyGems: failed (#{Explanation.explain_bundler_or_rubygems_error(error)})") + + false + end + + def net_http_connection_successful? + ::Gem::Net::HTTP.new(uri.host, uri.port).tap do |http| + http.use_ssl = true + http.min_version = tls_version + http.max_version = tls_version + http.verify_mode = verify_mode + end.start + + Bundler.ui.info("Ruby net/http: success") + warn_on_unsupported_tls12 + + true + rescue StandardError => error + Bundler.ui.warn(<<~MSG) + Ruby net/http: failed + + Unfortunately, this Ruby can't connect to #{host}. + + #{Explanation.explain_net_http_error(error, host, tls_version)} + MSG + + false + end + + def warn_on_unsupported_tls12 + ctx = OpenSSL::SSL::SSLContext.new + supported = true + + if ctx.respond_to?(:min_version=) + begin + ctx.min_version = ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + rescue OpenSSL::SSL::SSLError, NameError + supported = false + end + else + supported = OpenSSL::SSL::SSLContext::METHODS.include?(:TLSv1_2) # rubocop:disable Naming/VariableNumber + end + + Bundler.ui.warn(<<~EOM) unless supported + + WARNING: Although your Ruby can connect to #{host} today, your OpenSSL is very old! + WARNING: You will need to upgrade OpenSSL to use #{host}. + + EOM + end + + module Explanation + extend self + + def explain_bundler_or_rubygems_error(error) + case error.message + when /certificate verify failed/ + "certificate verification" + when /read server hello A/ + "SSL/TLS protocol version mismatch" + when /tlsv1 alert protocol version/ + "requested TLS version is too old" + else + error.message + end + end + + def explain_net_http_error(error, host, tls_version) + case error.message + # Check for certificate errors + when /certificate verify failed/ + <<~MSG + #{show_ssl_certs} + Your Ruby can't connect to #{host} because you are missing the certificate files OpenSSL needs to verify you are connecting to the genuine #{host} servers. + MSG + # Check for TLS version errors + when /read server hello A/, /tlsv1 alert protocol version/ + if tls_version.to_s == "TLS1_3" + "Your Ruby can't connect to #{host} because #{tls_version} isn't supported yet.\n" + else + <<~MSG + Your Ruby can't connect to #{host} because your version of OpenSSL is too old. + You'll need to upgrade your OpenSSL install and/or recompile Ruby to use a newer OpenSSL. + MSG + end + # OpenSSL doesn't support TLS version specified by argument + when /unknown SSL method/ + "Your Ruby can't connect because #{tls_version} isn't supported by your version of OpenSSL." + else + <<~MSG + Even worse, we're not sure why. + + Here's the full error information: + #{error.class}: #{error.message} + #{error.backtrace.join("\n ")} + + You might have more luck using Mislav's SSL doctor.rb script. You can get it here: + https://github.com/mislav/ssl-tools/blob/8b3dec4/doctor.rb + + Read more about the script and how to use it in this blog post: + https://mislav.net/2013/07/ruby-openssl/ + MSG + end + end + + def summarize(bundler_success, rubygems_success, host) + guide_url = "http://ruby.to/ssl-check-failed" + + message = if bundler_success && rubygems_success + <<~MSG + Hooray! This Ruby can connect to #{host}. + You are all set to use Bundler and RubyGems. + + MSG + elsif !bundler_success && !rubygems_success + <<~MSG + For some reason, your Ruby installation can connect to #{host}, but neither RubyGems nor Bundler can. + The most likely fix is to manually upgrade RubyGems by following the instructions at #{guide_url}. + After you've done that, run `gem install bundler` to upgrade Bundler, and then run this script again to make sure everything worked. ❣ + + MSG + elsif !bundler_success + <<~MSG + Although your Ruby installation and RubyGems can both connect to #{host}, Bundler is having trouble. + The most likely way to fix this is to upgrade Bundler by running `gem install bundler`. + Run this script again after doing that to make sure everything is all set. + If you're still having trouble, check out the troubleshooting guide at #{guide_url}. + + MSG + else + <<~MSG + It looks like Ruby and Bundler can connect to #{host}, but RubyGems itself cannot. + You can likely solve this by manually downloading and installing a RubyGems update. + Visit #{guide_url} for instructions on how to manually upgrade RubyGems. + + MSG + end + + Bundler.ui.info("\n#{message}") + end + + private + + def show_ssl_certs + ssl_cert_file = ENV["SSL_CERT_FILE"] || OpenSSL::X509::DEFAULT_CERT_FILE + ssl_cert_dir = ENV["SSL_CERT_DIR"] || OpenSSL::X509::DEFAULT_CERT_DIR + + <<~MSG + Below affect only Ruby net/http connections: + SSL_CERT_FILE: #{File.exist?(ssl_cert_file) ? "exists #{ssl_cert_file}" : "is missing #{ssl_cert_file}"} + SSL_CERT_DIR: #{Dir.exist?(ssl_cert_dir) ? "exists #{ssl_cert_dir}" : "is missing #{ssl_cert_dir}"} + MSG + end + end + end +end diff --git a/lib/bundler/cli/exec.rb b/lib/bundler/cli/exec.rb index 2fdc614fbb..2fdc416286 100644 --- a/lib/bundler/cli/exec.rb +++ b/lib/bundler/cli/exec.rb @@ -1,37 +1,30 @@ # frozen_string_literal: true -require "bundler/current_ruby" +require_relative "../current_ruby" module Bundler class CLI::Exec attr_reader :options, :args, :cmd - RESERVED_SIGNALS = %w[SEGV BUS ILL FPE VTALRM KILL STOP].freeze + TRAPPED_SIGNALS = %w[INT].freeze def initialize(options, args) @options = options @cmd = args.shift @args = args - - if Bundler.current_ruby.ruby_2? && !Bundler.current_ruby.jruby? - @args << { :close_others => !options.keep_file_descriptors? } - elsif options.keep_file_descriptors? - Bundler.ui.warn "Ruby version #{RUBY_VERSION} defaults to keeping non-standard file descriptors on Kernel#exec." - end + @args << { close_others: !options.keep_file_descriptors? } unless Bundler.current_ruby.jruby? end def run validate_cmd! SharedHelpers.set_bundle_environment if bin_path = Bundler.which(cmd) - if !Bundler.settings[:disable_exec_load] && ruby_shebang?(bin_path) - return kernel_load(bin_path, *args) - end - # First, try to exec directly to something in PATH - if Bundler.current_ruby.jruby_18? - kernel_exec(bin_path, *args) + if !Bundler.settings[:disable_exec_load] && directly_loadable?(bin_path) + bin_path.delete_suffix!(".bat") if Gem.win_platform? + kernel_load(bin_path, *args) else - kernel_exec([bin_path, cmd], *args) + bin_path = "./" + bin_path unless File.absolute_path?(bin_path) + kernel_exec(bin_path, *args) end else # exec using the given command @@ -39,7 +32,7 @@ module Bundler end end - private + private def validate_cmd! return unless cmd.nil? @@ -48,15 +41,11 @@ module Bundler end def kernel_exec(*args) - ui = Bundler.ui - Bundler.ui = nil Kernel.exec(*args) rescue Errno::EACCES, Errno::ENOEXEC - Bundler.ui = ui Bundler.ui.error "bundler: not executable: #{cmd}" exit 126 rescue Errno::ENOENT - Bundler.ui = ui Bundler.ui.error "bundler: command not found: #{cmd}" Bundler.ui.warn "Install missing gem executables with `bundle install`" exit 127 @@ -67,29 +56,49 @@ module Bundler ARGV.replace(args) $0 = file Process.setproctitle(process_title(file, args)) if Process.respond_to?(:setproctitle) - ui = Bundler.ui - Bundler.ui = nil - require "bundler/setup" - signals = Signal.list.keys - RESERVED_SIGNALS - signals.each {|s| trap(s, "DEFAULT") } + require_relative "../setup" + TRAPPED_SIGNALS.each {|s| trap(s, "DEFAULT") } Kernel.load(file) rescue SystemExit, SignalException raise - rescue Exception => e # rubocop:disable Lint/RescueException - Bundler.ui = ui + rescue Exception # rubocop:disable Lint/RescueException Bundler.ui.error "bundler: failed to load command: #{cmd} (#{file})" - backtrace = e.backtrace.take_while {|bt| !bt.start_with?(__FILE__) } - abort "#{e.class}: #{e.message}\n #{backtrace.join("\n ")}" + Bundler::FriendlyErrors.disable! + raise end def process_title(file, args) "#{file} #{args.join(" ")}".strip end + def directly_loadable?(file) + if Gem.win_platform? + script_wrapper?(file) + else + ruby_shebang?(file) + end + end + + def script_wrapper?(file) + script_file = file.delete_suffix(".bat") + return false unless File.exist?(script_file) + + if File.zero?(script_file) + Bundler.ui.warn "#{script_file} is empty" + return false + end + + header = File.open(file, "r") {|f| f.read(32) } + ruby_exe = "#{RbConfig::CONFIG["RUBY_INSTALL_NAME"]}#{RbConfig::CONFIG["EXEEXT"]}" + ruby_exe = "ruby.exe" if ruby_exe.empty? + header.include?(ruby_exe) + end + def ruby_shebang?(file) possibilities = [ "#!/usr/bin/env ruby\n", "#!/usr/bin/env jruby\n", + "#!/usr/bin/env truffleruby\n", "#!#{Gem.ruby}\n", ] diff --git a/lib/bundler/cli/fund.rb b/lib/bundler/cli/fund.rb new file mode 100644 index 0000000000..ad7f31f3d6 --- /dev/null +++ b/lib/bundler/cli/fund.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Fund + attr_reader :options + + def initialize(options) + @options = options + end + + def run + Bundler.definition.validate_runtime! + + groups = Array(options[:group]).map(&:to_sym) + + deps = if groups.any? + Bundler.definition.dependencies_for(groups) + else + Bundler.definition.requested_dependencies + end + + fund_info = deps.each_with_object([]) do |dep, arr| + spec = Bundler.definition.specs[dep.name].first + if spec.metadata.key?("funding_uri") + arr << "* #{spec.name} (#{spec.version})\n Funding: #{spec.metadata["funding_uri"]}" + end + end + + if fund_info.empty? + Bundler.ui.info "None of the installed gems you directly depend on are looking for funding." + else + Bundler.ui.info fund_info.join("\n") + end + end + end +end diff --git a/lib/bundler/cli/gem.rb b/lib/bundler/cli/gem.rb index 885578e819..c8c24c8e66 100644 --- a/lib/bundler/cli/gem.rb +++ b/lib/bundler/cli/gem.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "pathname" - module Bundler class CLI Bundler.require_thor_actions @@ -9,12 +7,9 @@ module Bundler end class CLI::Gem - TEST_FRAMEWORK_VERSIONS = { - "rspec" => "3.0", - "minitest" => "5.0" - }.freeze + DEFAULT_GITHUB_USERNAME = "[USERNAME]" - attr_reader :options, :gem_name, :thor, :name, :target + attr_reader :options, :gem_name, :thor, :name, :target, :extension def initialize(options, gem_name, thor) @options = options @@ -25,9 +20,11 @@ module Bundler thor.destination_root = nil @name = @gem_name - @target = SharedHelpers.pwd.join(gem_name) + @target = Pathname.new(SharedHelpers.pwd).join(gem_name) + + @extension = options[:ext] - validate_ext_name if options[:ext] + validate_ext_name if @extension end def run @@ -37,39 +34,61 @@ module Bundler namespaced_path = name.tr("-", "/") constant_name = name.gsub(/-[_-]*(?![_-]|$)/) { "::" }.gsub(/([_-]+|(::)|^)(.|$)/) { $2.to_s + $3.upcase } constant_array = constant_name.split("::") - - git_installed = Bundler.git_present? - - git_author_name = git_installed ? `git config user.name`.chomp : "" - github_username = git_installed ? `git config github.user`.chomp : "" - git_user_email = git_installed ? `git config user.email`.chomp : "" + minitest_constant_name = constant_array.clone.tap {|a| a[-1] = "Test#{a[-1]}" }.join("::") # Foo::Bar => Foo::TestBar + + use_git = Bundler.git_present? && options[:git] + + git_author_name = use_git ? `git config user.name`.chomp : "" + git_username = use_git ? `git config github.user`.chomp : "" + git_user_email = use_git ? `git config user.email`.chomp : "" + github_username = github_username(git_username) + + if github_username.empty? + homepage_uri = "TODO: Put your gem's website or public repo URL here." + source_code_uri = "TODO: Put your gem's public repo URL here." + changelog_uri = "TODO: Put your gem's CHANGELOG.md URL here." + else + homepage_uri = "https://github.com/#{github_username}/#{name}" + source_code_uri = "https://github.com/#{github_username}/#{name}" + changelog_uri = "https://github.com/#{github_username}/#{name}/blob/main/CHANGELOG.md" + end config = { - :name => name, - :underscored_name => underscored_name, - :namespaced_path => namespaced_path, - :makefile_path => "#{underscored_name}/#{underscored_name}", - :constant_name => constant_name, - :constant_array => constant_array, - :author => git_author_name.empty? ? "TODO: Write your name" : git_author_name, - :email => git_user_email.empty? ? "TODO: Write your email address" : git_user_email, - :test => options[:test], - :ext => options[:ext], - :exe => options[:exe], - :bundler_version => bundler_dependency_version, - :github_username => github_username.empty? ? "[USERNAME]" : github_username + name: name, + underscored_name: underscored_name, + namespaced_path: namespaced_path, + makefile_path: "#{underscored_name}/#{underscored_name}", + constant_name: constant_name, + constant_array: constant_array, + author: git_author_name.empty? ? "TODO: Write your name" : git_author_name, + email: git_user_email.empty? ? "TODO: Write your email address" : git_user_email, + test: options[:test], + ext: extension, + exe: options[:exe], + bundle: options[:bundle], + bundler_version: bundler_dependency_version, + git: use_git, + github_username: github_username.empty? ? DEFAULT_GITHUB_USERNAME : github_username, + required_ruby_version: required_ruby_version, + rust_builder_required_rubygems_version: rust_builder_required_rubygems_version, + minitest_constant_name: minitest_constant_name, + ignore_paths: %w[bin/], + homepage_uri: homepage_uri, + source_code_uri: source_code_uri, + changelog_uri: changelog_uri, } ensure_safe_gem_name(name, constant_array) templates = { - "Gemfile.tt" => "Gemfile", + "Gemfile.tt" => Bundler.preferred_gemfile_name, "lib/newgem.rb.tt" => "lib/#{namespaced_path}.rb", "lib/newgem/version.rb.tt" => "lib/#{namespaced_path}/version.rb", + "sig/newgem.rbs.tt" => "sig/#{namespaced_path}.rbs", "newgem.gemspec.tt" => "#{name}.gemspec", "Rakefile.tt" => "Rakefile", "README.md.tt" => "README.md", "bin/console.tt" => "bin/console", - "bin/setup.tt" => "bin/setup" + "bin/setup.tt" => "bin/setup", } executables = %w[ @@ -77,13 +96,21 @@ module Bundler bin/setup ] - templates.merge!("gitignore.tt" => ".gitignore") if Bundler.git_present? + case Bundler.preferred_gemfile_name + when "Gemfile" + config[:ignore_paths] << "Gemfile" + when "gems.rb" + config[:ignore_paths] << "gems.rb" + config[:ignore_paths] << "gems.locked" + end + + if use_git + templates.merge!("gitignore.tt" => ".gitignore") + config[:ignore_paths] << ".gitignore" + end if test_framework = ask_and_set_test_framework config[:test] = test_framework - config[:test_framework_version] = TEST_FRAMEWORK_VERSIONS[test_framework] - - templates.merge!("travis.yml.tt" => ".travis.yml") case test_framework when "rspec" @@ -92,20 +119,54 @@ module Bundler "spec/spec_helper.rb.tt" => "spec/spec_helper.rb", "spec/newgem_spec.rb.tt" => "spec/#{namespaced_path}_spec.rb" ) + config[:test_task] = :spec + config[:ignore_paths] << ".rspec" + config[:ignore_paths] << "spec/" when "minitest" + # Generate path for minitest target file (FileList["test/**/test_*.rb"]) + # foo => test/test_foo.rb + # foo-bar => test/foo/test_bar.rb + # foo_bar => test/test_foo_bar.rb + paths = namespaced_path.rpartition("/") + paths[2] = "test_#{paths[2]}" + minitest_namespaced_path = paths.join("") + templates.merge!( - "test/test_helper.rb.tt" => "test/test_helper.rb", - "test/newgem_test.rb.tt" => "test/#{namespaced_path}_test.rb" + "test/minitest/test_helper.rb.tt" => "test/test_helper.rb", + "test/minitest/test_newgem.rb.tt" => "test/#{minitest_namespaced_path}.rb" ) + config[:test_task] = :test + config[:ignore_paths] << "test/" + when "test-unit" + templates.merge!( + "test/test-unit/test_helper.rb.tt" => "test/test_helper.rb", + "test/test-unit/newgem_test.rb.tt" => "test/#{namespaced_path}_test.rb" + ) + config[:test_task] = :test + config[:ignore_paths] << "test/" end end - config[:test_task] = config[:test] == "minitest" ? "test" : "spec" + config[:ci] = ask_and_set_ci + case config[:ci] + when "github" + templates.merge!("github/workflows/main.yml.tt" => ".github/workflows/main.yml") + if extension == "rust" + templates.merge!("github/workflows/build-gems.yml.tt" => ".github/workflows/build-gems.yml") + end + config[:ignore_paths] << ".github/" + when "gitlab" + templates.merge!("gitlab-ci.yml.tt" => ".gitlab-ci.yml") + config[:ignore_paths] << ".gitlab-ci.yml" + when "circle" + templates.merge!("circleci/config.yml.tt" => ".circleci/config.yml") + config[:ignore_paths] << ".circleci/" + end if ask_and_set(:mit, "Do you want to license your code permissively under the MIT license?", - "This means that any other developer or company will be legally allowed to use your code " \ - "for free as long as they admit you created it. You can read more about the MIT license " \ - "at http://choosealicense.com/licenses/mit.") + "Using a MIT license means that any other developer or company will be legally allowed " \ + "to use your code for free as long as they admit you created it. You can read more about " \ + "the MIT license at https://choosealicense.com/licenses/mit.") config[:mit] = true Bundler.ui.info "MIT License enabled in config" templates.merge!("LICENSE.txt.tt" => "LICENSE.txt") @@ -113,68 +174,127 @@ module Bundler if ask_and_set(:coc, "Do you want to include a code of conduct in gems you generate?", "Codes of conduct can increase contributions to your project by contributors who " \ - "prefer collaborative, safe spaces. You can read more about the code of conduct at " \ - "contributor-covenant.org. Having a code of conduct means agreeing to the responsibility " \ - "of enforcing it, so be sure that you are prepared to do that. Be sure that your email " \ - "address is specified as a contact in the generated code of conduct so that people know " \ - "who to contact in case of a violation. For suggestions about " \ - "how to enforce codes of conduct, see http://bit.ly/coc-enforcement.") + "prefer safe, respectful, productive, and collaborative spaces. \n" \ + "See https://github.com/ruby/rubygems/blob/master/CODE_OF_CONDUCT.md") config[:coc] = true Bundler.ui.info "Code of conduct enabled in config" templates.merge!("CODE_OF_CONDUCT.md.tt" => "CODE_OF_CONDUCT.md") end - templates.merge!("exe/newgem.tt" => "exe/#{name}") if config[:exe] + if ask_and_set(:changelog, "Do you want to include a changelog?", + "A changelog is a file which contains a curated, chronologically ordered list of notable " \ + "changes for each version of a project. To make it easier for users and contributors to" \ + " see precisely what notable changes have been made between each release (or version) of" \ + " the project. Whether consumers or developers, the end users of software are" \ + " human beings who care about what's in the software. When the software changes, people " \ + "want to know why and how. see https://keepachangelog.com") + config[:changelog] = true + Bundler.ui.info "Changelog enabled in config" + templates.merge!("CHANGELOG.md.tt" => "CHANGELOG.md") + end + + config[:linter] = ask_and_set_linter + case config[:linter] + when "rubocop" + Bundler.ui.info "RuboCop enabled in config" + templates.merge!("rubocop.yml.tt" => ".rubocop.yml") + config[:ignore_paths] << ".rubocop.yml" + when "standard" + Bundler.ui.info "Standard enabled in config" + templates.merge!("standard.yml.tt" => ".standard.yml") + config[:ignore_paths] << ".standard.yml" + end - if options[:ext] + if config[:exe] + templates.merge!("exe/newgem.tt" => "exe/#{name}") + executables.push("exe/#{name}") + end + + if extension == "c" templates.merge!( - "ext/newgem/extconf.rb.tt" => "ext/#{name}/extconf.rb", + "ext/newgem/extconf-c.rb.tt" => "ext/#{name}/extconf.rb", "ext/newgem/newgem.h.tt" => "ext/#{name}/#{underscored_name}.h", "ext/newgem/newgem.c.tt" => "ext/#{name}/#{underscored_name}.c" ) end + if extension == "rust" + templates.merge!( + "Cargo.toml.tt" => "Cargo.toml", + "ext/newgem/Cargo.toml.tt" => "ext/#{name}/Cargo.toml", + "ext/newgem/build.rs.tt" => "ext/#{name}/build.rs", + "ext/newgem/extconf-rust.rb.tt" => "ext/#{name}/extconf.rb", + "ext/newgem/src/lib.rs.tt" => "ext/#{name}/src/lib.rs", + ) + end + + if extension == "go" + templates.merge!( + "ext/newgem/go.mod.tt" => "ext/#{name}/go.mod", + "ext/newgem/extconf-go.rb.tt" => "ext/#{name}/extconf.rb", + "ext/newgem/newgem.h.tt" => "ext/#{name}/#{underscored_name}.h", + "ext/newgem/newgem.go.tt" => "ext/#{name}/#{underscored_name}.go", + "ext/newgem/newgem-go.c.tt" => "ext/#{name}/#{underscored_name}.c", + ) + + config[:go_module_username] = config[:github_username] == DEFAULT_GITHUB_USERNAME ? "username" : config[:github_username] + end + + if target.exist? && !target.directory? + Bundler.ui.error "Couldn't create a new gem named `#{gem_name}` because there's an existing file named `#{gem_name}`." + exit Bundler::BundlerError.all_errors[Bundler::GenericSystemCallError] + end + + if use_git + Bundler.ui.info "\nInitializing git repo in #{target}" + require "shellwords" + `git init #{target.to_s.shellescape}` + + config[:git_default_branch] = File.read("#{target}/.git/HEAD").split("/").last.chomp + end + templates.each do |src, dst| destination = target.join(dst) - SharedHelpers.filesystem_access(destination) do - thor.template("newgem/#{src}", destination, config) - end + thor.template("newgem/#{src}", destination, config) end executables.each do |file| - SharedHelpers.filesystem_access(target.join(file)) do |path| - executable = (path.stat.mode | 0o111) - path.chmod(executable) - end + path = target.join(file) + executable = (path.stat.mode | 0o111) + path.chmod(executable) + end + + if use_git + IO.popen(%w[git add .], { chdir: target }, &:read) end - if Bundler.git_present? - Bundler.ui.info "Initializing git repo in #{target}" + if config[:bundle] + Bundler.ui.info "Running bundle install in the new gem directory." Dir.chdir(target) do - `git init` - `git add .` + system("bundle install") end end # Open gemspec in editor open_editor(options["edit"], target.join("#{name}.gemspec")) if options[:edit] - rescue Errno::EEXIST => e - raise GenericSystemCallError.new(e, "There was a conflict while creating the new gem.") + + Bundler.ui.info "\nGem '#{name}' was successfully created. " \ + "For more information on making a RubyGem visit https://guides.rubygems.org/make-your-own-gem/" end - private + private def resolve_name(name) - SharedHelpers.pwd.join(name).basename.to_s + Pathname.new(SharedHelpers.pwd).join(name).basename.to_s end - def ask_and_set(key, header, message) + def ask_and_set(key, prompt, explanation) choice = options[key] choice = Bundler.settings["gem.#{key}"] if choice.nil? if choice.nil? - Bundler.ui.confirm header - choice = Bundler.ui.yes? "#{message} y/(n):" + Bundler.ui.info "\n#{explanation}" + choice = Bundler.ui.yes? "#{prompt} y/(n):" Bundler.settings.set_global("gem.#{key}", choice) end @@ -187,18 +307,20 @@ module Bundler Bundler.ui.error "You have specified a gem name which does not conform to the \n" \ "naming guidelines for C extensions. For more information, \n" \ "see the 'Extension Naming' section at the following URL:\n" \ - "http://guides.rubygems.org/gems-with-extensions/\n" + "https://guides.rubygems.org/gems-with-extensions/\n" exit 1 end def ask_and_set_test_framework + return if skip?(:test) test_framework = options[:test] || Bundler.settings["gem.test"] - if test_framework.nil? - Bundler.ui.confirm "Do you want to generate tests with your gem?" - result = Bundler.ui.ask "Type 'rspec' or 'minitest' to generate those test files now and " \ - "in the future. rspec/minitest/(none):" - if result =~ /rspec|minitest/ + if test_framework.to_s.empty? + Bundler.ui.info "\nDo you want to generate tests with your gem?" + Bundler.ui.info hint_text("test") + + result = Bundler.ui.ask "Enter a test framework. rspec/minitest/test-unit/(none):" + if /rspec|minitest|test-unit/.match?(result) test_framework = result else test_framework = false @@ -209,9 +331,92 @@ module Bundler Bundler.settings.set_global("gem.test", test_framework) end + if options[:test] == Bundler.settings["gem.test"] + Bundler.ui.info "#{options[:test]} is already configured, ignoring --test flag." + end + test_framework end + def skip?(option) + options.key?(option) && options[option].nil? + end + + def hint_text(setting) + if Bundler.settings["gem.#{setting}"] == false + "Your choice will only be applied to this gem." + else + "Future `bundle gem` calls will use your choice. " \ + "This setting can be changed anytime with `bundle config gem.#{setting}`." + end + end + + def ask_and_set_ci + return if skip?(:ci) + ci_template = options[:ci] || Bundler.settings["gem.ci"] + + if ci_template.to_s.empty? + Bundler.ui.info "\nDo you want to set up continuous integration for your gem? " \ + "Supported services:\n" \ + "* CircleCI: https://circleci.com/\n" \ + "* GitHub Actions: https://github.com/features/actions\n" \ + "* GitLab CI: https://docs.gitlab.com/ee/ci/\n" + Bundler.ui.info hint_text("ci") + + result = Bundler.ui.ask "Enter a CI service. github/gitlab/circle/(none):" + if /github|gitlab|circle/.match?(result) + ci_template = result + else + ci_template = false + end + end + + if Bundler.settings["gem.ci"].nil? + Bundler.settings.set_global("gem.ci", ci_template) + end + + if options[:ci] == Bundler.settings["gem.ci"] + Bundler.ui.info "#{options[:ci]} is already configured, ignoring --ci flag." + end + + ci_template + end + + def ask_and_set_linter + return if skip?(:linter) + linter_template = options[:linter] || Bundler.settings["gem.linter"] + + if linter_template.to_s.empty? + Bundler.ui.info "\nDo you want to add a code linter and formatter to your gem? " \ + "Supported Linters:\n" \ + "* RuboCop: https://rubocop.org\n" \ + "* Standard: https://github.com/standardrb/standard\n" + Bundler.ui.info hint_text("linter") + + result = Bundler.ui.ask "Enter a linter. rubocop/standard/(none):" + if /rubocop|standard/.match?(result) + linter_template = result + else + linter_template = false + end + end + + if Bundler.settings["gem.linter"].nil? + Bundler.settings.set_global("gem.linter", linter_template) + end + + # Once gem.linter safely set, unset the deprecated gem.rubocop + unless Bundler.settings["gem.rubocop"].nil? + Bundler.settings.set_global("gem.rubocop", nil) + end + + if options[:linter] == Bundler.settings["gem.linter"] + Bundler.ui.info "#{options[:linter]} is already configured, ignoring --linter flag." + end + + linter_template + end + def bundler_dependency_version v = Gem::Version.new(Bundler::VERSION) req = v.segments[0..1] @@ -220,11 +425,15 @@ module Bundler end def ensure_safe_gem_name(name, constant_array) - if name =~ /^\d/ + if /^\d/.match?(name) Bundler.ui.error "Invalid gem name #{name} Please give a name which does not start with numbers." exit 1 end + if /[A-Z]/.match?(name) + Bundler.ui.warn "Gem names with capital letters are not recommended. Please use only lowercase letters, numbers, and hyphens." + end + constant_name = constant_array.join("::") existing_constant = constant_array.inject(Object) do |c, s| @@ -245,5 +454,23 @@ module Bundler def open_editor(editor, file) thor.run(%(#{editor} "#{file}")) end + + def rust_builder_required_rubygems_version + "3.3.11" + end + + def required_ruby_version + "3.2.0" + end + + def github_username(git_username) + if options[:github_username].nil? + git_username + elsif options[:github_username] == false + "" + else + options[:github_username] + end + end end end diff --git a/lib/bundler/cli/info.rb b/lib/bundler/cli/info.rb index 958b525067..cd01d4949b 100644 --- a/lib/bundler/cli/info.rb +++ b/lib/bundler/cli/info.rb @@ -9,42 +9,75 @@ module Bundler end def run + Bundler.ui.silence do + Bundler.definition.validate_runtime! + Bundler.load.lock + end + spec = spec_for_gem(gem_name) - spec_not_found(gem_name) unless spec - return print_gem_path(spec) if @options[:path] - print_gem_info(spec) + if spec + return print_gem_path(spec) if @options[:path] + return print_gem_version(spec) if @options[:version] + print_gem_info(spec) + end end - private - - def spec_for_gem(gem_name) - spec = Bundler.definition.specs.find {|s| s.name == gem_name } - spec || default_gem_spec(gem_name) - end + private - def default_gem_spec(gem_name) - return unless Gem::Specification.respond_to?(:find_all_by_name) - gem_spec = Gem::Specification.find_all_by_name(gem_name).last - return gem_spec if gem_spec && gem_spec.respond_to?(:default_gem?) && gem_spec.default_gem? + def spec_for_gem(name) + Bundler::CLI::Common.select_spec(name, :regex_match) end - def spec_not_found(gem_name) - raise GemNotFound, Bundler::CLI::Common.gem_not_found_message(gem_name, Bundler.definition.dependencies) + def print_gem_version(spec) + Bundler.ui.info spec.version.to_s end def print_gem_path(spec) - Bundler.ui.info spec.full_gem_path + name = spec.name + if name == "bundler" + path = File.expand_path("../../..", __dir__) + else + path = spec.full_gem_path + if spec.installation_missing? + return Bundler.ui.warn "The gem #{name} is missing. It should be installed at #{path}, but was not found" + end + end + + Bundler.ui.info path end def print_gem_info(spec) + metadata = spec.metadata + name = spec.name gem_info = String.new - gem_info << " * #{spec.name} (#{spec.version}#{spec.git_version})\n" + gem_info << " * #{name} (#{spec.version}#{spec.git_version})\n" gem_info << "\tSummary: #{spec.summary}\n" if spec.summary gem_info << "\tHomepage: #{spec.homepage}\n" if spec.homepage + gem_info << "\tDocumentation: #{metadata["documentation_uri"]}\n" if metadata.key?("documentation_uri") + gem_info << "\tSource Code: #{metadata["source_code_uri"]}\n" if metadata.key?("source_code_uri") + gem_info << "\tFunding: #{metadata["funding_uri"]}\n" if metadata.key?("funding_uri") + gem_info << "\tWiki: #{metadata["wiki_uri"]}\n" if metadata.key?("wiki_uri") + gem_info << "\tChangelog: #{metadata["changelog_uri"]}\n" if metadata.key?("changelog_uri") + gem_info << "\tBug Tracker: #{metadata["bug_tracker_uri"]}\n" if metadata.key?("bug_tracker_uri") + gem_info << "\tMailing List: #{metadata["mailing_list_uri"]}\n" if metadata.key?("mailing_list_uri") gem_info << "\tPath: #{spec.full_gem_path}\n" - gem_info << "\tDefault Gem: yes" if spec.respond_to?(:default_gem?) && spec.default_gem? + gem_info << "\tDefault Gem: yes\n" if spec.respond_to?(:default_gem?) && spec.default_gem? + gem_info << "\tReverse Dependencies: \n\t\t#{gem_dependencies.join("\n\t\t")}" if gem_dependencies.any? + + if name != "bundler" && spec.installation_missing? + return Bundler.ui.warn "The gem #{name} is missing. Gemspec information is still available though:\n#{gem_info}" + end + Bundler.ui.info gem_info end + + def gem_dependencies + @gem_dependencies ||= Bundler.definition.specs.filter_map do |spec| + dependency = spec.dependencies.find {|dep| dep.name == gem_name } + next unless dependency + "#{spec.name} (#{spec.version}) depends on #{gem_name} (#{dependency.requirements_list.join(", ")})" + end.sort + end end end diff --git a/lib/bundler/cli/init.rb b/lib/bundler/cli/init.rb index 50e01f54fb..246b9d6460 100644 --- a/lib/bundler/cli/init.rb +++ b/lib/bundler/cli/init.rb @@ -13,6 +13,11 @@ module Bundler exit 1 end + unless File.writable?(Dir.pwd) + Bundler.ui.error "Can not create #{gemfile} as the current directory is not writable." + exit 1 + end + if options[:gemspec] gemspec = File.expand_path(options[:gemspec]) unless File.exist?(gemspec) @@ -27,20 +32,20 @@ module Bundler file << spec.to_gemfile end else - FileUtils.cp(File.expand_path("../../templates/#{gemfile}", __FILE__), gemfile) + File.open(File.expand_path("../templates/Gemfile", __dir__), "r") do |template| + File.open(gemfile, "wb") do |destination| + IO.copy_stream(template, destination) + end + end end puts "Writing new #{gemfile} to #{SharedHelpers.pwd}/#{gemfile}" end - private + private def gemfile - @gemfile ||= begin - Bundler.default_gemfile - rescue GemfileNotFound - Bundler.feature_flag.init_gems_rb? ? "gems.rb" : "Gemfile" - end + @gemfile ||= options[:gemfile] || Bundler.preferred_gemfile_name end end end diff --git a/lib/bundler/cli/inject.rb b/lib/bundler/cli/inject.rb deleted file mode 100644 index b00675d348..0000000000 --- a/lib/bundler/cli/inject.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -module Bundler - class CLI::Inject - attr_reader :options, :name, :version, :group, :source, :gems - def initialize(options, name, version) - @options = options - @name = name - @version = version || last_version_number - @group = options[:group].split(",") unless options[:group].nil? - @source = options[:source] - @gems = [] - end - - def run - # The required arguments allow Thor to give useful feedback when the arguments - # are incorrect. This adds those first two arguments onto the list as a whole. - gems.unshift(source).unshift(group).unshift(version).unshift(name) - - # Build an array of Dependency objects out of the arguments - deps = [] - # when `inject` support addition of more than one gem, then this loop will - # help. Currently this loop is running once. - gems.each_slice(4) do |gem_name, gem_version, gem_group, gem_source| - ops = Gem::Requirement::OPS.map {|key, _val| key } - has_op = ops.any? {|op| gem_version.start_with? op } - gem_version = "~> #{gem_version}" unless has_op - deps << Bundler::Dependency.new(gem_name, gem_version, "group" => gem_group, "source" => gem_source) - end - - added = Injector.inject(deps, options) - - if added.any? - Bundler.ui.confirm "Added to Gemfile:" - Bundler.ui.confirm(added.map do |d| - name = "'#{d.name}'" - requirement = ", '#{d.requirement}'" - group = ", :group => #{d.groups.inspect}" if d.groups != Array(:default) - source = ", :source => '#{d.source}'" unless d.source.nil? - %(gem #{name}#{requirement}#{group}#{source}) - end.join("\n")) - else - Bundler.ui.confirm "All gems were already present in the Gemfile" - end - end - - private - - def last_version_number - definition = Bundler.definition(true) - definition.resolve_remotely! - specs = definition.index[name].sort_by(&:version) - unless options[:pre] - specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? } - end - spec = specs.last - spec.version.to_s - end - end -end diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb index f0b821ed84..69affd1a10 100644 --- a/lib/bundler/cli/install.rb +++ b/lib/bundler/cli/install.rb @@ -8,107 +8,80 @@ module Bundler end def run - Bundler.ui.level = "error" if options[:quiet] + Bundler.ui.level = "warn" if options[:quiet] warn_if_root - normalize_groups - - Bundler::SharedHelpers.set_env "RB_USER_INSTALL", "1" if Bundler::FREEBSD + if options[:local] + Bundler.self_manager.restart_with_locked_bundler_if_needed + else + Bundler.self_manager.install_locked_bundler_and_restart_with_it_if_needed + end - # Disable color in deployment mode - Bundler.ui.shell = Thor::Shell::Basic.new if options[:deployment] + Bundler::SharedHelpers.set_env "RB_USER_INSTALL", "1" if Gem.freebsd_platform? - check_for_options_conflicts + if target_rbconfig_path = options[:"target-rbconfig"] + Bundler.rubygems.set_target_rbconfig(target_rbconfig_path) + end check_trust_policy - if options[:deployment] || options[:frozen] || Bundler.frozen? - unless Bundler.default_lockfile.exist? - flag = "--deployment flag" if options[:deployment] - flag ||= "--frozen flag" if options[:frozen] - flag ||= "deployment setting" - raise ProductionError, "The #{flag} requires a #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}. Please make " \ - "sure you have checked your #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} into version control " \ - "before deploying." - end - - options[:local] = true if Bundler.app_cache.exist? - - if Bundler.feature_flag.deployment_means_frozen? - Bundler.settings.set_command_option :deployment, true - else - Bundler.settings.set_command_option :frozen, true - end - end - - # When install is called with --no-deployment, disable deployment mode - if options[:deployment] == false - Bundler.settings.set_command_option :frozen, nil - options[:system] = true + if Bundler.frozen_bundle? && !Bundler.default_lockfile.exist? + flag = "deployment setting" if Bundler.settings[:deployment] + flag = "frozen setting" if Bundler.settings[:frozen] + raise ProductionError, "The #{flag} requires a lockfile. Please make " \ + "sure you have checked your #{SharedHelpers.relative_lockfile_path} into version control " \ + "before deploying." end normalize_settings Bundler::Fetcher.disable_endpoint = options["full-index"] - if options["binstubs"] - Bundler::SharedHelpers.major_deprecation 2, - "The --binstubs option will be removed in favor of `bundle binstubs`" - end - - Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.feature_flag.plugins? + Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins] - definition = Bundler.definition + # For install we want to enable strict validation + # (rather than some optimizations we perform at app runtime). + definition = Bundler.definition(strict: true) definition.validate_runtime! + definition.lockfile = options["lockfile"] if options["lockfile"] + definition.lockfile = false if options["no-lock"] installer = Installer.install(Bundler.root, definition, options) - Bundler.load.cache if Bundler.app_cache.exist? && !options["no-cache"] && !Bundler.frozen? + + Bundler.settings.temporary(cache_all_platforms: options[:local] ? false : Bundler.settings[:cache_all_platforms]) do + Bundler.load.cache(nil, options[:local]) if Bundler.app_cache.exist? && !options["no-cache"] && !Bundler.frozen_bundle? + end Bundler.ui.confirm "Bundle complete! #{dependencies_count_for(definition)}, #{gems_installed_for(definition)}." - Bundler::CLI::Common.output_without_groups_message + Bundler::CLI::Common.output_without_groups_message(:install) if Bundler.use_system_gems? Bundler.ui.confirm "Use `bundle info [gemname]` to see where a bundled gem is installed." else - absolute_path = File.expand_path(Bundler.configured_bundle_path.base_path) - relative_path = absolute_path.sub(File.expand_path(".") + File::SEPARATOR, "." + File::SEPARATOR) + relative_path = Bundler.configured_bundle_path.base_path_relative_to_pwd Bundler.ui.confirm "Bundled gems are installed into `#{relative_path}`" end Bundler::CLI::Common.output_post_install_messages installer.post_install_messages - warn_ambiguous_gems - if CLI::Common.clean_after_install? - require "bundler/cli/clean" + require_relative "clean" Bundler::CLI::Clean.new(options).run end - rescue GemNotFound, VersionConflict => e - if options[:local] && Bundler.app_cache.exist? - Bundler.ui.warn "Some gems seem to be missing from your #{Bundler.settings.app_cache_path} directory." - end - unless Bundler.definition.has_rubygems_remotes? - Bundler.ui.warn <<-WARN, :wrap => true - Your Gemfile has no gem server sources. If you need gems that are \ - not already on your machine, add a line like this to your Gemfile: - source 'https://rubygems.org' - WARN - end - raise e - rescue Gem::InvalidSpecificationException => e + Bundler::CLI::Common.output_fund_metadata_summary + rescue Gem::InvalidSpecificationException Bundler.ui.warn "You have one or more invalid gemspecs that need to be fixed." - raise e + raise end - private + private def warn_if_root - return if Bundler.settings[:silence_root_warning] || Bundler::WINDOWS || !Process.uid.zero? - Bundler.ui.warn "Don't run Bundler as root. Bundler can ask for sudo " \ - "if it is needed, and installing your bundle as root will break this " \ - "application for all non-root users on this machine.", :wrap => true + return if Bundler.settings[:silence_root_warning] || Gem.win_platform? || !Process.uid.zero? + Bundler.ui.warn "Don't run Bundler as root. Installing your bundle as root " \ + "will break this application for all non-root users on this machine.", wrap: true end def dependencies_count_for(definition) @@ -117,26 +90,10 @@ module Bundler end def gems_installed_for(definition) - count = definition.specs.count + count = definition.specs.count {|spec| spec.name != "bundler" } "#{count} #{count == 1 ? "gem" : "gems"} now installed" end - def check_for_group_conflicts_in_cli_options - conflicting_groups = Array(options[:without]) & Array(options[:with]) - return if conflicting_groups.empty? - raise InvalidOption, "You can't list a group in both with and without." \ - " The offending groups are: #{conflicting_groups.join(", ")}." - end - - def check_for_options_conflicts - if (options[:path] || options[:deployment]) && options[:system] - error_message = String.new - error_message << "You have specified both --path as well as --system. Please choose only one option.\n" if options[:path] - error_message << "You have specified both --deployment as well as --system. Please choose only one option.\n" if options[:deployment] - raise InvalidOption.new(error_message) - end - end - def check_trust_policy trust_policy = options["trust-policy"] unless Bundler.rubygems.security_policies.keys.unshift(nil).include?(trust_policy) @@ -146,69 +103,25 @@ module Bundler Bundler.settings.set_command_option_if_given :"trust-policy", trust_policy end - def normalize_groups - options[:with] &&= options[:with].join(":").tr(" ", ":").split(":") - options[:without] &&= options[:without].join(":").tr(" ", ":").split(":") - - check_for_group_conflicts_in_cli_options - - Bundler.settings.set_command_option :with, nil if options[:with] == [] - Bundler.settings.set_command_option :without, nil if options[:without] == [] - - with = options.fetch(:with, []) - with |= Bundler.settings[:with].map(&:to_s) - with -= options[:without] if options[:without] - - without = options.fetch(:without, []) - without |= Bundler.settings[:without].map(&:to_s) - without -= options[:with] if options[:with] - - options[:with] = with - options[:without] = without - end - def normalize_settings - Bundler.settings.set_command_option :path, nil if options[:system] - Bundler.settings.set_command_option :path, "vendor/bundle" if options[:deployment] - Bundler.settings.set_command_option_if_given :path, options["path"] - Bundler.settings.set_command_option :path, "bundle" if options["standalone"] && Bundler.settings[:path].nil? - - bin_option = options["binstubs"] - bin_option = nil if bin_option && bin_option.empty? - Bundler.settings.set_command_option :bin, bin_option if options["binstubs"] + if options["standalone"] && Bundler.settings[:path].nil? && !options["local"] + Bundler.settings.set_command_option :path, "bundle" + end Bundler.settings.set_command_option_if_given :shebang, options["shebang"] Bundler.settings.set_command_option_if_given :jobs, options["jobs"] + Bundler::CLI::Common.validate_cooldown!(options["cooldown"]) + Bundler.settings.set_command_option_if_given :cooldown, options["cooldown"] + Bundler.settings.set_command_option_if_given :no_prune, options["no-prune"] Bundler.settings.set_command_option_if_given :no_install, options["no-install"] Bundler.settings.set_command_option_if_given :clean, options["clean"] - unless Bundler.settings[:without] == options[:without] && Bundler.settings[:with] == options[:with] - # need to nil them out first to get around validation for backwards compatibility - Bundler.settings.set_command_option :without, nil - Bundler.settings.set_command_option :with, nil - Bundler.settings.set_command_option :without, options[:without] - options[:with] - Bundler.settings.set_command_option :with, options[:with] - end - - options[:force] = options[:redownload] - end - - def warn_ambiguous_gems - Installer.ambiguous_gems.to_a.each do |name, installed_from_uri, *also_found_in_uris| - Bundler.ui.error "Warning: the gem '#{name}' was found in multiple sources." - Bundler.ui.error "Installed from: #{installed_from_uri}" - Bundler.ui.error "Also found in:" - also_found_in_uris.each {|uri| Bundler.ui.error " * #{uri}" } - Bundler.ui.error "You should add a source requirement to restrict this gem to your preferred source." - Bundler.ui.error "For example:" - Bundler.ui.error " gem '#{name}', :source => '#{installed_from_uri}'" - Bundler.ui.error "Then uninstall the gem '#{name}' (or delete all bundled gems) and then install again." - end + options[:force] = options[:redownload] if options[:redownload] end end end diff --git a/lib/bundler/cli/issue.rb b/lib/bundler/cli/issue.rb index 91f827ea99..cbfb7da2d8 100644 --- a/lib/bundler/cli/issue.rb +++ b/lib/bundler/cli/issue.rb @@ -5,24 +5,25 @@ require "rbconfig" module Bundler class CLI::Issue def run - Bundler.ui.info <<-EOS.gsub(/^ {8}/, "") + Bundler.ui.info <<~EOS Did you find an issue with Bundler? Before filing a new issue, be sure to check out these resources: 1. Check out our troubleshooting guide for quick fixes to common issues: - https://github.com/bundler/bundler/blob/master/doc/TROUBLESHOOTING.md + https://github.com/ruby/rubygems/blob/master/doc/bundler/TROUBLESHOOTING.md 2. Instructions for common Bundler uses can be found on the documentation - site: http://bundler.io/ + site: https://bundler.io/ 3. Information about each Bundler command can be found in the Bundler - man pages: http://bundler.io/man/bundle.1.html + man pages: https://bundler.io/man/bundle.1.html Hopefully the troubleshooting steps above resolved your problem! If things still aren't working the way you expect them to, please let us know so - that we can diagnose and help fix the problem you're having. Please - view the Filing Issues guide for more information: - https://github.com/bundler/bundler/blob/master/doc/contributing/ISSUES.md + that we can diagnose and help fix the problem you're having, by filling + in the new issue form located at + https://github.com/ruby/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md, + and copy and pasting the information below. EOS @@ -33,8 +34,8 @@ module Bundler end def doctor - require "bundler/cli/doctor" - Bundler::CLI::Doctor.new({}).run + require_relative "doctor/diagnose" + Bundler::CLI::Doctor::Diagnose.new({}).run end end end diff --git a/lib/bundler/cli/list.rb b/lib/bundler/cli/list.rb index b5e7c1e650..6a467f45a9 100644 --- a/lib/bundler/cli/list.rb +++ b/lib/bundler/cli/list.rb @@ -1,22 +1,97 @@ # frozen_string_literal: true +require "json" + module Bundler class CLI::List def initialize(options) @options = options + @without_group = options["without-group"].map(&:to_sym) + @only_group = options["only-group"].map(&:to_sym) + @format = options["format"] end def run - specs = Bundler.load.specs.reject {|s| s.name == "bundler" }.sort_by(&:name) - return specs.each {|s| Bundler.ui.info s.name } if @options["name-only"] + raise InvalidOption, "The `--only-group` and `--without-group` options cannot be used together" if @only_group.any? && @without_group.any? + + raise InvalidOption, "The `--name-only` and `--paths` options cannot be used together" if @options["name-only"] && @options[:paths] + + specs = if @only_group.any? || @without_group.any? + filtered_specs_by_groups + else + begin + Bundler.load.specs + rescue GemNotFound => e + Bundler.ui.error e.message + Bundler.ui.warn "Install missing gems with `bundle install`." + exit 1 + end + end.reject {|s| s.name == "bundler" }.sort_by(&:name) + + case @format + when "json" + print_json(specs: specs) + when nil + print_human(specs: specs) + else + raise InvalidOption, "Unknown option`--format=#{@format}`. Supported formats: `json`" + end + end + + private + + def print_json(specs:) + gems = if @options["name-only"] + specs.map {|s| { name: s.name } } + else + specs.map do |s| + { + name: s.name, + version: s.version.to_s, + git_version: s.git_version&.strip, + }.tap do |h| + h[:path] = s.full_gem_path if @options["paths"] + end + end + end + Bundler.ui.info({ gems: gems }.to_json) + end + def print_human(specs:) return Bundler.ui.info "No gems in the Gemfile" if specs.empty? + + return specs.each {|s| Bundler.ui.info s.name } if @options["name-only"] + return specs.each {|s| Bundler.ui.info s.full_gem_path } if @options["paths"] + Bundler.ui.info "Gems included by the bundle:" - specs.each do |s| - Bundler.ui.info " * #{s.name} (#{s.version}#{s.git_version})" - end + + specs.each {|s| Bundler.ui.info " * #{s.name} (#{s.version}#{s.git_version})" } Bundler.ui.info "Use `bundle info` to print more detailed information about a gem" end + + def verify_group_exists(groups) + (@without_group + @only_group).each do |group| + raise InvalidOption, "`#{group}` group could not be found." unless groups.include?(group) + end + end + + def filtered_specs_by_groups + definition = Bundler.definition + groups = definition.groups + + verify_group_exists(groups) + + show_groups = + if @without_group.any? + groups.reject {|g| @without_group.include?(g) } + elsif @only_group.any? + groups.select {|g| @only_group.include?(g) } + else + groups + end.map(&:to_sym) + + definition.specs_for(show_groups) + end end end diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb index 7dd078b1ef..2f78868936 100644 --- a/lib/bundler/cli/lock.rb +++ b/lib/bundler/cli/lock.rb @@ -14,50 +14,81 @@ module Bundler exit 1 end + check_for_conflicting_options + print = options[:print] - ui = Bundler.ui - Bundler.ui = UI::Silent.new if print + previous_output_stream = Bundler.ui.output_stream + Bundler.ui.output_stream = :stderr if print Bundler::Fetcher.disable_endpoint = options["full-index"] update = options[:update] + conservative = options[:conservative] + bundler = options[:bundler] + if update.is_a?(Array) # unlocking specific gems Bundler::CLI::Common.ensure_all_gems_in_lockfile!(update) - update = { :gems => update, :lock_shared_dependencies => options[:conservative] } + update = { gems: update, conservative: conservative } + elsif update && conservative + update = { conservative: conservative } + elsif update && bundler + update = { bundler: bundler } end - definition = Bundler.definition(update) - Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options) if options[:update] + Bundler.settings.temporary(frozen: false) do + definition = Bundler.definition(update, Bundler.default_lockfile) + definition.add_checksums if options["add-checksums"] - options["remove-platform"].each do |platform| - definition.remove_platform(platform) - end + Bundler::CLI::Common.configure_gem_version_promoter(definition, options) if options[:update] - options["add-platform"].each do |platform_string| - platform = Gem::Platform.new(platform_string) - if platform.to_s == "unknown" - Bundler.ui.warn "The platform `#{platform_string}` is unknown to RubyGems " \ - "and adding it will likely lead to resolution errors" + options["remove-platform"].each do |platform_string| + platform = Gem::Platform.new(platform_string) + definition.remove_platform(platform) + end + + options["add-platform"].each do |platform_string| + platform = Gem::Platform.new(platform_string) + if platform.to_s == "unknown" + Bundler.ui.error "The platform `#{platform_string}` is unknown to RubyGems and can't be added to the lockfile." + exit 1 + end + definition.add_platform(platform) + end + + if definition.platforms.empty? + raise InvalidOption, "Removing all platforms from the bundle is not allowed" end - definition.add_platform(platform) - end - if definition.platforms.empty? - raise InvalidOption, "Removing all platforms from the bundle is not allowed" + definition.remotely! unless options[:local] + + if options["normalize-platforms"] + definition.normalize_platforms + end + + if print + puts definition.to_lock + else + file = options[:lockfile] + file = file ? Pathname.new(file).expand_path : Bundler.default_lockfile + + puts "Writing lockfile to #{file}" + definition.write_lock(file, false) + end end - definition.resolve_remotely! unless options[:local] + Bundler.ui.output_stream = previous_output_stream + end + + private - if print - puts definition.to_lock - else - file = options[:lockfile] - file = file ? File.expand_path(file) : Bundler.default_lockfile - puts "Writing lockfile to #{file}" - definition.lock(file) + def check_for_conflicting_options + if options["normalize-platforms"] && options["add-platform"].any? + raise InvalidOption, "--normalize-platforms can't be used with --add-platform" end - Bundler.ui = ui + if options["normalize-platforms"] && options["remove-platform"].any? + raise InvalidOption, "--normalize-platforms can't be used with --remove-platform" + end end end end diff --git a/lib/bundler/cli/open.rb b/lib/bundler/cli/open.rb index 552fe6f128..f24693b843 100644 --- a/lib/bundler/cli/open.rb +++ b/lib/bundler/cli/open.rb @@ -1,24 +1,27 @@ # frozen_string_literal: true -require "shellwords" - module Bundler class CLI::Open - attr_reader :options, :name + attr_reader :options, :name, :path def initialize(options, name) @options = options @name = name + @path = options[:path] unless options[:path].nil? end def run + raise InvalidOption, "Cannot specify `--path` option without a value" if !@path.nil? && @path.empty? editor = [ENV["BUNDLER_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find {|e| !e.nil? && !e.empty? } return Bundler.ui.info("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR") unless editor return unless spec = Bundler::CLI::Common.select_spec(name, :regex_match) - path = spec.full_gem_path - Dir.chdir(path) do - command = Shellwords.split(editor) + [path] + if spec.default_gem? + Bundler.ui.info "Unable to open #{name} because it's a default gem, so the directory it would normally be installed to does not exist." + else + root_path = spec.full_gem_path + require "shellwords" + command = Shellwords.split(editor) << File.join([root_path, path].compact) Bundler.with_original_env do - system(*command) + system(*command, { chdir: root_path }) end || Bundler.ui.info("Could not run '#{command.join(" ")}'") end end diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb index 5125cc710b..465e56ada2 100644 --- a/lib/bundler/cli/outdated.rb +++ b/lib/bundler/cli/outdated.rb @@ -2,224 +2,255 @@ module Bundler class CLI::Outdated - attr_reader :options, :gems + attr_reader :options, :gems, :options_include_groups, :filter_options_patch, :sources, :strict + attr_accessor :outdated_gems def initialize(options, gems) @options = options @gems = gems - end + @sources = Array(options[:source]) - def run - check_for_deployment_mode + @filter_options_patch = options.keys & %w[filter-major filter-minor filter-patch] - sources = Array(options[:source]) + @outdated_gems = [] - gems.each do |gem_name| - Bundler::CLI::Common.select_spec(gem_name) + @options_include_groups = [:group, :groups].any? do |v| + options.keys.include?(v.to_s) end + # the patch level options imply strict is also true. It wouldn't make + # sense otherwise. + @strict = options["filter-strict"] || Bundler::CLI::Common.patch_level_options(options).any? + end + + def run + check_for_deployment_mode! + + Bundler::CLI::Common.validate_cooldown!(options[:cooldown]) + Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] + Bundler.definition.validate_runtime! current_specs = Bundler.ui.silence { Bundler.definition.resolve } - current_dependencies = {} - Bundler.ui.silence do - Bundler.load.dependencies.each do |dep| - current_dependencies[dep.name] = dep + + gems.each do |gem_name| + if current_specs[gem_name].empty? + raise GemNotFound, "Could not find gem '#{gem_name}'." end end + current_dependencies = Bundler.ui.silence do + Bundler.load.dependencies.map {|dep| [dep.name, dep] }.to_h + end + definition = if gems.empty? && sources.empty? # We're doing a full update Bundler.definition(true) else - Bundler.definition(:gems => gems, :sources => sources) + Bundler.definition(gems: gems, sources: sources) end Bundler::CLI::Common.configure_gem_version_promoter( Bundler.definition, - options + options.merge(strict: @strict) ) - # the patch level options imply strict is also true. It wouldn't make - # sense otherwise. - strict = options[:strict] || - Bundler::CLI::Common.patch_level_options(options).any? - - filter_options_patch = options.keys & - %w[filter-major filter-minor filter-patch] - definition_resolution = proc do options[:local] ? definition.resolve_with_cache! : definition.resolve_remotely! end if options[:parseable] - Bundler.ui.silence(&definition_resolution) + Bundler.ui.progress(&definition_resolution) else definition_resolution.call end Bundler.ui.info "" - outdated_gems_by_groups = {} - outdated_gems_list = [] # Loop through the current specs gemfile_specs, dependency_specs = current_specs.partition do |spec| current_dependencies.key? spec.name end - (gemfile_specs + dependency_specs).sort_by(&:name).each do |current_spec| - next if !gems.empty? && !gems.include?(current_spec.name) + specs = if options["only-explicit"] + gemfile_specs + else + gemfile_specs + dependency_specs + end + + specs.sort_by(&:name).uniq(&:name).each do |current_spec| + next unless gems.empty? || gems.include?(current_spec.name) - dependency = current_dependencies[current_spec.name] - active_spec = retrieve_active_spec(strict, definition, current_spec) + active_spec = retrieve_active_spec(definition, current_spec) + next unless active_spec - next if active_spec.nil? - if filter_options_patch.any? - update_present = update_present_via_semver_portions(current_spec, active_spec, options) - next unless update_present - end + next unless filter_options_patch.empty? || update_present_via_semver_portions(current_spec, active_spec, options) gem_outdated = Gem::Version.new(active_spec.version) > Gem::Version.new(current_spec.version) next unless gem_outdated || (current_spec.git_version != active_spec.git_version) - groups = nil + + dependency = current_dependencies[current_spec.name] + groups = "" if dependency && !options[:parseable] groups = dependency.groups.join(", ") end - outdated_gems_list << { :active_spec => active_spec, - :current_spec => current_spec, - :dependency => dependency, - :groups => groups } - - outdated_gems_by_groups[groups] ||= [] - outdated_gems_by_groups[groups] << { :active_spec => active_spec, - :current_spec => current_spec, - :dependency => dependency, - :groups => groups } + outdated_gems << { + active_spec: active_spec, + current_spec: current_spec, + dependency: dependency, + groups: groups, + } end - if outdated_gems_list.empty? - display_nothing_outdated_message(filter_options_patch) + relevant_outdated_gems = if options_include_groups + outdated_gems.group_by {|g| g[:groups] }.sort.flat_map do |groups, gems| + contains_group = groups.split(", ").include?(options[:group]) + next unless options[:groups] || contains_group + + gems + end.compact else - unless options[:parseable] - if options[:pre] - Bundler.ui.info "Outdated gems included in the bundle (including " \ - "pre-releases):" - else - Bundler.ui.info "Outdated gems included in the bundle:" - end - end + outdated_gems + end - options_include_groups = [:group, :groups].select do |v| - options.keys.include?(v.to_s) + if relevant_outdated_gems.empty? + unless options[:parseable] + Bundler.ui.info(nothing_outdated_message) end - - if options_include_groups.any? - ordered_groups = outdated_gems_by_groups.keys.compact.sort - [nil, ordered_groups].flatten.each do |groups| - gems = outdated_gems_by_groups[groups] - contains_group = if groups - groups.split(",").include?(options[:group]) - else - options[:group] == "group" - end - - next if (!options[:groups] && !contains_group) || gems.nil? - - unless options[:parseable] - if groups - Bundler.ui.info "===== Group #{groups} =====" - else - Bundler.ui.info "===== Without group =====" - end - end - - gems.each do |gem| - print_gem( - gem[:current_spec], - gem[:active_spec], - gem[:dependency], - groups, - options_include_groups.any? - ) - end - end + else + if options[:parseable] + print_gems(relevant_outdated_gems) else - outdated_gems_list.each do |gem| - print_gem( - gem[:current_spec], - gem[:active_spec], - gem[:dependency], - gem[:groups], - options_include_groups.any? - ) - end + print_gems_table(relevant_outdated_gems) end exit 1 end end - private + private + + def loaded_from_for(spec) + return unless spec.respond_to?(:loaded_from) + + spec.loaded_from + end + + def groups_text(group_text, groups) + "#{group_text}#{groups.split(",").size > 1 ? "s" : ""} \"#{groups}\"" + end - def retrieve_active_spec(strict, definition, current_spec) - if strict - active_spec = definition.find_resolved_spec(current_spec) + def nothing_outdated_message + if filter_options_patch.any? + display = filter_options_patch.map do |o| + o.sub("filter-", "") + end.join(" or ") + + "No #{display} updates to display.\n" else - active_specs = definition.find_indexed_specs(current_spec) - if !current_spec.version.prerelease? && !options[:pre] && active_specs.size > 1 - active_specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? } - end - active_spec = active_specs.last + "Bundle up to date!\n" end + end + + def retrieve_active_spec(definition, current_spec) + active_spec = definition.resolve.find_by_name_and_platform(current_spec.name, current_spec.platform) + return unless active_spec + + return active_spec if strict - active_spec + active_specs = active_spec.source.specs.search(current_spec.name).select {|spec| spec.installable_on_platform?(current_spec.platform) }.sort_by(&:version) + if !current_spec.version.prerelease? && !options[:pre] && active_specs.size > 1 + active_specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? } + end + active_specs.last end - def display_nothing_outdated_message(filter_options_patch) - unless options[:parseable] - if filter_options_patch.any? - display = filter_options_patch.map do |o| - o.sub("filter-", "") - end.join(" or ") + def print_gems(gems_list) + gems_list.each do |gem| + print_gem( + gem[:current_spec], + gem[:active_spec], + gem[:dependency], + gem[:groups], + ) + end + end - Bundler.ui.info "No #{display} updates to display.\n" - else - Bundler.ui.info "Bundle up to date!\n" - end + def print_gems_table(gems_list) + data = gems_list.map do |gem| + gem_column_for( + gem[:current_spec], + gem[:active_spec], + gem[:dependency], + gem[:groups], + ) end + + print_indented([table_header] + data) end - def print_gem(current_spec, active_spec, dependency, groups, options_include_groups) + def print_gem(current_spec, active_spec, dependency, groups) spec_version = "#{active_spec.version}#{active_spec.git_version}" - spec_version += " (from #{active_spec.loaded_from})" if Bundler.ui.debug? && active_spec.loaded_from + if Bundler.ui.debug? + loaded_from = loaded_from_for(active_spec) + spec_version += " (from #{loaded_from})" if loaded_from + end current_version = "#{current_spec.version}#{current_spec.git_version}" - if dependency && dependency.specific? + if dependency&.specific? dependency_version = %(, requested #{dependency.requirement}) end spec_outdated_info = "#{active_spec.name} (newest #{spec_version}, " \ - "installed #{current_version}#{dependency_version})" + "installed #{current_version}#{dependency_version}" + + release_date = release_date_for(active_spec) + spec_outdated_info += ", released #{release_date}" unless release_date.empty? + + remaining = cooldown_days_remaining(active_spec) + spec_outdated_info += ", in cooldown for #{remaining} more day#{"s" if remaining > 1}" if remaining + + spec_outdated_info += ")" output_message = if options[:parseable] spec_outdated_info.to_s - elsif options_include_groups || !groups + elsif options_include_groups || groups.empty? " * #{spec_outdated_info}" else - " * #{spec_outdated_info} in groups \"#{groups}\"" + " * #{spec_outdated_info} in #{groups_text("group", groups)}" end Bundler.ui.info output_message.rstrip end - def check_for_deployment_mode - return unless Bundler.frozen? - suggested_command = if Bundler.settings.locations("frozen")[:global] - "bundle config --delete frozen" + def gem_column_for(current_spec, active_spec, dependency, groups) + current_version = "#{current_spec.version}#{current_spec.git_version}" + spec_version = "#{active_spec.version}#{active_spec.git_version}" + remaining = cooldown_days_remaining(active_spec) + spec_version += " (cooldown #{remaining}d)" if remaining + dependency = dependency.requirement if dependency + + ret_val = [active_spec.name, current_version, spec_version, dependency.to_s, groups.to_s] + ret_val << release_date_for(active_spec) + ret_val << loaded_from_for(active_spec).to_s if Bundler.ui.debug? + ret_val + end + + def cooldown_days_remaining(spec, now = Time.now) + return nil unless spec.respond_to?(:created_at) && spec.created_at + return nil unless spec.respond_to?(:remote) && spec.remote + days = spec.remote.effective_cooldown + return nil if days.nil? || days <= 0 + remaining = days - ((now - spec.created_at) / 86_400.0) + remaining > 0 ? remaining.ceil : nil + end + + def check_for_deployment_mode! + return unless Bundler.frozen_bundle? + suggested_command = if Bundler.settings.locations("frozen").keys.&([:global, :local]).any? + "bundle config unset frozen" elsif Bundler.settings.locations("deployment").keys.&([:global, :local]).any? - "bundle config --delete deployment" - else - "bundle install --no-deployment" + "bundle config unset deployment" end raise ProductionError, "You are trying to check outdated gems in " \ "deployment mode. Run `bundle outdated` elsewhere.\n" \ @@ -254,7 +285,53 @@ module Bundler def get_version_semver_portion_value(spec, version_portion_index) version_section = spec.version.segments[version_portion_index, 1] - version_section.nil? ? 0 : (version_section.first || 0) + version_section.to_a[0].to_i + end + + def print_indented(matrix) + header = matrix[0] + data = matrix[1..-1] + + column_sizes = Array.new(header.size) do |index| + matrix.max_by {|row| row[index].length }[index].length + end + + Bundler.ui.info justify(header, column_sizes) + + data.sort_by! {|row| row[0] } + + data.each do |row| + Bundler.ui.info justify(row, column_sizes) + end + end + + def table_header + header = ["Gem", "Current", "Latest", "Requested", "Groups", "Release Date"] + header << "Path" if Bundler.ui.debug? + header + end + + def release_date_for(spec) + return "" unless spec.respond_to?(:date) + + date = spec.date + return "" unless date + + return "" unless Gem.const_defined?(:DEFAULT_SOURCE_DATE_EPOCH) + default_date = Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc + default_date = Time.utc(default_date.year, default_date.month, default_date.day) + + date = date.utc if date.respond_to?(:utc) + + return "" if date == default_date + + date.strftime("%Y-%m-%d") + end + + def justify(row, sizes) + row.each_with_index.map do |element, index| + element.ljust(sizes[index]) + end.join(" ").strip + "\n" end end end diff --git a/lib/bundler/cli/package.rb b/lib/bundler/cli/package.rb deleted file mode 100644 index 2dcd0e1e29..0000000000 --- a/lib/bundler/cli/package.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -module Bundler - class CLI::Package - attr_reader :options - - def initialize(options) - @options = options - end - - def run - Bundler.ui.level = "error" if options[:quiet] - Bundler.settings.set_command_option_if_given :path, options[:path] - Bundler.settings.set_command_option_if_given :cache_all_platforms, options["all-platforms"] - Bundler.settings.set_command_option_if_given :cache_path, options["cache-path"] - - setup_cache_all - install - - # TODO: move cache contents here now that all bundles are locked - custom_path = Bundler.settings[:path] if options[:path] - Bundler.load.cache(custom_path) - end - - private - - def install - require "bundler/cli/install" - options = self.options.dup - if Bundler.settings[:cache_all_platforms] - options["local"] = false - options["update"] = true - end - Bundler::CLI::Install.new(options).run - end - - def setup_cache_all - all = options.fetch(:all, Bundler.feature_flag.cache_command_is_package? || nil) - - Bundler.settings.set_command_option_if_given :cache_all, all - - if Bundler.definition.has_local_dependencies? && !Bundler.feature_flag.cache_all? - Bundler.ui.warn "Your Gemfile contains path and git dependencies. If you want " \ - "to package them as well, please pass the --all flag. This will be the default " \ - "on Bundler 2.0." - end - end - end -end diff --git a/lib/bundler/cli/platform.rb b/lib/bundler/cli/platform.rb index e97cad49a4..32d68abbb1 100644 --- a/lib/bundler/cli/platform.rb +++ b/lib/bundler/cli/platform.rb @@ -8,12 +8,12 @@ module Bundler end def run - platforms, ruby_version = Bundler.ui.silence do - locked_ruby_version = Bundler.locked_gems && Bundler.locked_gems.ruby_version - gemfile_ruby_version = Bundler.definition.ruby_version && Bundler.definition.ruby_version.single_version_string - [Bundler.definition.platforms.map {|p| "* #{p}" }, - locked_ruby_version || gemfile_ruby_version] + ruby_version = if Bundler.locked_gems + Bundler.locked_gems.ruby_version&.gsub(/p\d+\Z/, "") + else + Bundler.definition.ruby_version&.single_version_string end + output = [] if options[:ruby] @@ -23,7 +23,9 @@ module Bundler output << "No ruby version specified" end else - output << "Your platform is: #{RUBY_PLATFORM}" + platforms = Bundler.definition.platforms.map {|p| "* #{p}" } + + output << "Your platform is: #{Gem::Platform.local}" output << "Your app has gems that work on these platforms:\n#{platforms.join("\n")}" if ruby_version diff --git a/lib/bundler/cli/plugin.rb b/lib/bundler/cli/plugin.rb index 5488a9f28d..32fa660fe0 100644 --- a/lib/bundler/cli/plugin.rb +++ b/lib/bundler/cli/plugin.rb @@ -1,24 +1,39 @@ # frozen_string_literal: true -require "bundler/vendored_thor" +require_relative "../vendored_thor" module Bundler class CLI::Plugin < Thor desc "install PLUGINS", "Install the plugin from the source" long_desc <<-D - Install plugins either from the rubygems source provided (with --source option) or from a git source provided with (--git option). If no sources are provided, it uses Gem.sources + Install plugins either from the rubygems source provided (with --source option), from a git source provided with --git, or a local path provided with --path. If no sources are provided, it uses Gem.sources D - method_option "source", :type => :string, :default => nil, :banner => - "URL of the RubyGems source to fetch the plugin from" - method_option "version", :type => :string, :default => nil, :banner => - "The version of the plugin to fetch" - method_option "git", :type => :string, :default => nil, :banner => - "URL of the git repo to fetch from" - method_option "branch", :type => :string, :default => nil, :banner => - "The git branch to checkout" - method_option "ref", :type => :string, :default => nil, :banner => - "The git revision to check out" + method_option "source", type: :string, default: nil, banner: "URL of the RubyGems source to fetch the plugin from" + method_option "version", type: :string, default: nil, banner: "The version of the plugin to fetch" + method_option "git", type: :string, default: nil, banner: "URL of the git repo to fetch from" + method_option "local_git", type: :string, default: nil, banner: "Path of the local git repo to fetch from (removed)" + method_option "branch", type: :string, default: nil, banner: "The git branch to checkout" + method_option "ref", type: :string, default: nil, banner: "The git revision to check out" + method_option "path", type: :string, default: nil, banner: "Path of a local gem to directly use" def install(*plugins) + if options.key?(:local_git) + raise InvalidOption, "--local_git has been removed, use --git" + end + Bundler::Plugin.install(plugins, options) end + + desc "uninstall PLUGINS", "Uninstall the plugins" + long_desc <<-D + Uninstall given list of plugins. To uninstall all the plugins, use -all option. + D + method_option "all", type: :boolean, default: nil, banner: "Uninstall all the installed plugins. If no plugin is installed, then it does nothing." + def uninstall(*plugins) + Bundler::Plugin.uninstall(plugins, options) + end + + desc "list", "List the installed plugins and available commands" + def list + Bundler::Plugin.list + end end end diff --git a/lib/bundler/cli/pristine.rb b/lib/bundler/cli/pristine.rb index 9b9cdaa9b3..f463f0bce8 100644 --- a/lib/bundler/cli/pristine.rb +++ b/lib/bundler/cli/pristine.rb @@ -11,32 +11,53 @@ module Bundler definition = Bundler.definition definition.validate_runtime! installer = Bundler::Installer.new(Bundler.root, definition) + git_sources = [] - Bundler.load.specs.each do |spec| - next if spec.name == "bundler" # Source::Rubygems doesn't install bundler - next if !@gems.empty? && !@gems.include?(spec.name) + ProcessLock.lock do + installed_specs = definition.specs.reject do |spec| + next if spec.name == "bundler" # Source::Rubygems doesn't install bundler + next if !@gems.empty? && !@gems.include?(spec.name) - gem_name = "#{spec.name} (#{spec.version}#{spec.git_version})" - gem_name += " (#{spec.platform})" if !spec.platform.nil? && spec.platform != Gem::Platform::RUBY + gem_name = "#{spec.name} (#{spec.version}#{spec.git_version})" + gem_name += " (#{spec.platform})" if !spec.platform.nil? && spec.platform != Gem::Platform::RUBY - case source = spec.source - when Source::Rubygems - cached_gem = spec.cache_file - unless File.exist?(cached_gem) - Bundler.ui.error("Failed to pristine #{gem_name}. Cached gem #{cached_gem} does not exist.") + case source = spec.source + when Source::Rubygems + cached_gem = spec.cache_file + unless File.exist?(cached_gem) + Bundler.ui.error("Failed to pristine #{gem_name}. Cached gem #{cached_gem} does not exist.") + next + end + + FileUtils.rm_rf spec.full_gem_path + when Source::Git + if source.local? + Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is locally overridden.") + next + end + + source.remote! + if extension_cache_path = source.extension_cache_path(spec) + FileUtils.rm_rf extension_cache_path + end + FileUtils.rm_rf spec.extension_dir + FileUtils.rm_rf spec.full_gem_path + + next if git_sources.include?(source) + git_sources << source + else + Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is sourced from local path.") next end - FileUtils.rm_rf spec.full_gem_path - when Source::Git - source.remote! - FileUtils.rm_rf spec.full_gem_path - else - Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is sourced from local path.") - next - end + true + end.map(&:name) - Bundler::GemInstaller.new(spec, installer, false, 0, true).install_from_spec + jobs = Bundler.settings.installation_parallelization + pristine_count = definition.specs.count - installed_specs.count + # allow a pristining a single gem to skip the parallel worker + jobs = [jobs, pristine_count].min + ParallelInstaller.call(installer, definition.specs, jobs, false, true, skip: installed_specs) end end end diff --git a/lib/bundler/cli/remove.rb b/lib/bundler/cli/remove.rb new file mode 100644 index 0000000000..44a4d891dd --- /dev/null +++ b/lib/bundler/cli/remove.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Remove + def initialize(gems, options) + @gems = gems + @options = options + end + + def run + raise InvalidOption, "Please specify gems to remove." if @gems.empty? + + Injector.remove(@gems, {}) + Installer.install(Bundler.root, Bundler.definition) + end + end +end diff --git a/lib/bundler/cli/show.rb b/lib/bundler/cli/show.rb index 61756801b2..67fdcc797e 100644 --- a/lib/bundler/cli/show.rb +++ b/lib/bundler/cli/show.rb @@ -6,7 +6,7 @@ module Bundler def initialize(options, gem_name) @options = options @gem_name = gem_name - @verbose = options[:verbose] || options[:outdated] + @verbose = options[:verbose] @latest_specs = fetch_latest_specs if @verbose end @@ -18,13 +18,13 @@ module Bundler if gem_name if gem_name == "bundler" - path = File.expand_path("../../../..", __FILE__) + path = File.expand_path("../../..", __dir__) else spec = Bundler::CLI::Common.select_spec(gem_name, :regex_match) return unless spec path = spec.full_gem_path unless File.directory?(path) - Bundler.ui.warn "The gem #{gem_name} has been deleted. It was installed at:" + return Bundler.ui.warn "The gem #{gem_name} is missing. It should be installed at #{path}, but was not found" end end return Bundler.ui.info(path) @@ -40,8 +40,8 @@ module Bundler desc = " * #{s.name} (#{s.version}#{s.git_version})" if @verbose latest = latest_specs.find {|l| l.name == s.name } - Bundler.ui.info <<-END.gsub(/^ +/, "") - #{desc} + Bundler.ui.info <<~END + #{desc.lstrip} \tSummary: #{s.summary || "No description available."} \tHomepage: #{s.homepage || "No website available."} \tStatus: #{outdated?(s, latest) ? "Outdated - #{s.version} < #{latest.version}" : "Up to date"} @@ -53,16 +53,12 @@ module Bundler end end - private + private def fetch_latest_specs definition = Bundler.definition(true) - if options[:outdated] - Bundler.ui.info "Fetching remote specs for outdated check...\n\n" - Bundler.ui.silence { definition.resolve_remotely! } - else - definition.resolve_with_cache! - end + Bundler.ui.info "Fetching remote specs for outdated check...\n\n" + Bundler.ui.silence { definition.remotely! } Bundler.reset! definition.specs end diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb index 5de11e84e4..d92ffd995f 100644 --- a/lib/bundler/cli/update.rb +++ b/lib/bundler/cli/update.rb @@ -9,27 +9,36 @@ module Bundler end def run - Bundler.ui.level = "error" if options[:quiet] + Bundler.ui.level = "warn" if options[:quiet] - Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.feature_flag.plugins? + update_bundler = options[:bundler] + + Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler) if update_bundler + + Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins] sources = Array(options[:source]) groups = Array(options[:group]).map(&:to_sym) - full_update = gems.empty? && sources.empty? && groups.empty? && !options[:ruby] && !options[:bundler] + full_update = gems.empty? && sources.empty? && groups.empty? && !options[:ruby] && !update_bundler if full_update && !options[:all] - if Bundler.feature_flag.update_requires_all_flag? + if Bundler.settings[:update_requires_all_flag] raise InvalidOption, "To update everything, pass the `--all` flag." end - SharedHelpers.major_deprecation 2, "Pass --all to `bundle update` to update everything" + SharedHelpers.feature_deprecated! "Pass --all to `bundle update` to update everything" elsif !full_update && options[:all] raise InvalidOption, "Cannot specify --all along with specific options." end + conservative = options[:conservative] + if full_update - # We're doing a full update - Bundler.definition(true) + if conservative + Bundler.definition(conservative: conservative) + else + Bundler.definition(true) + end else unless Bundler.default_lockfile.exist? raise GemfileLockNotFound, "This Bundle hasn't been installed yet. " \ @@ -38,13 +47,13 @@ module Bundler Bundler::CLI::Common.ensure_all_gems_in_lockfile!(gems) if groups.any? - specs = Bundler.definition.specs_for groups - gems.concat(specs.map(&:name)) + deps = Bundler.definition.dependencies.select {|d| (d.groups & groups).any? } + gems.concat(deps.map(&:name)) end - Bundler.definition(:gems => gems, :sources => sources, :ruby => options[:ruby], - :lock_shared_dependencies => options[:conservative], - :bundler => options[:bundler]) + Bundler.definition(gems: gems, sources: sources, ruby: options[:ruby], + conservative: conservative, + bundler: update_bundler) end Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options) @@ -54,27 +63,52 @@ module Bundler opts = options.dup opts["update"] = true opts["local"] = options[:local] + opts["force"] = options[:redownload] if options[:redownload] Bundler.settings.set_command_option_if_given :jobs, opts["jobs"] + Bundler::CLI::Common.validate_cooldown!(options[:cooldown]) + Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] Bundler.definition.validate_runtime! + + if locked_gems = Bundler.definition.locked_gems + previous_locked_info = locked_gems.specs.reduce({}) do |h, s| + h[s.name] = { spec: s, version: s.version, source: s.source.identifier } + h + end + end + installer = Installer.install Bundler.root, Bundler.definition, opts Bundler.load.cache if Bundler.app_cache.exist? if CLI::Common.clean_after_install? - require "bundler/cli/clean" + require_relative "clean" Bundler::CLI::Clean.new(options).run end - if locked_gems = Bundler.definition.locked_gems + if locked_gems gems.each do |name| - locked_version = locked_gems.specs.find {|s| s.name == name }.version - new_version = Bundler.definition.specs[name].first - new_version &&= new_version.version - if !new_version - Bundler.ui.warn "Bundler attempted to update #{name} but it was removed from the bundle" - elsif new_version < locked_version - Bundler.ui.warn "Bundler attempted to update #{name} but its version regressed from #{locked_version} to #{new_version}" + locked_info = previous_locked_info[name] + next unless locked_info + + locked_spec = locked_info[:spec] + new_spec = Bundler.definition.specs[name].first + unless new_spec + unless locked_spec.installable_on_platform?(Bundler.local_platform) + Bundler.ui.warn "Bundler attempted to update #{name} but it was not considered because it is for a different platform from the current one" + end + + next + end + + locked_source = locked_info[:source] + new_source = new_spec.source.identifier + next if locked_source != new_source + + new_version = new_spec.version + locked_version = locked_info[:version] + if new_version < locked_version + Bundler.ui.warn "Note: #{name} version regressed from #{locked_version} to #{new_version}" elsif new_version == locked_version Bundler.ui.warn "Bundler attempted to update #{name} but its version stayed the same" end @@ -82,8 +116,10 @@ module Bundler end Bundler.ui.confirm "Bundle updated!" - Bundler::CLI::Common.output_without_groups_message + Bundler::CLI::Common.output_without_groups_message(:update) Bundler::CLI::Common.output_post_install_messages installer.post_install_messages + + Bundler::CLI::Common.output_fund_metadata_summary end end end diff --git a/lib/bundler/cli/viz.rb b/lib/bundler/cli/viz.rb deleted file mode 100644 index 644f9b25cf..0000000000 --- a/lib/bundler/cli/viz.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -module Bundler - class CLI::Viz - attr_reader :options, :gem_name - def initialize(options) - @options = options - end - - def run - # make sure we get the right `graphviz`. There is also a `graphviz` - # gem we're not built to support - gem "ruby-graphviz" - require "graphviz" - - options[:without] = options[:without].join(":").tr(" ", ":").split(":") - output_file = File.expand_path(options[:file]) - - graph = Graph.new(Bundler.load, output_file, options[:version], options[:requirements], options[:format], options[:without]) - graph.viz - rescue LoadError => e - Bundler.ui.error e.inspect - Bundler.ui.warn "Make sure you have the graphviz ruby gem. You can install it with:" - Bundler.ui.warn "`gem install ruby-graphviz`" - rescue StandardError => e - raise unless e.message =~ /GraphViz not installed or dot not in PATH/ - Bundler.ui.error e.message - Bundler.ui.warn "Please install GraphViz. On a Mac with Homebrew, you can run `brew install graphviz`." - end - end -end diff --git a/lib/bundler/compact_index_client.rb b/lib/bundler/compact_index_client.rb index 6c241ca07a..6865e30dbc 100644 --- a/lib/bundler/compact_index_client.rb +++ b/lib/bundler/compact_index_client.rb @@ -1,11 +1,42 @@ # frozen_string_literal: true -require "pathname" require "set" module Bundler + # The CompactIndexClient is responsible for fetching and parsing the compact index. + # + # The compact index is a set of caching optimized files that are used to fetch gem information. + # The files are: + # - names: a list of all gem names + # - versions: a list of all gem versions + # - info/[gem]: a list of all versions of a gem + # + # The client is instantiated with: + # - `directory`: the root directory where the cache files are stored. + # - `fetcher`: (optional) an object that responds to #call(uri_path, headers) and returns an http response. + # If the `fetcher` is not provided, the client will only read cached files from disk. + # + # The client is organized into: + # - `Updater`: updates the cached files on disk using the fetcher. + # - `Cache`: calls the updater, caches files, read and return them from disk + # - `Parser`: parses the compact index file data + # - `CacheFile`: a concurrency safe file reader/writer that verifies checksums + # + # The client is intended to optimize memory usage and performance. + # It is called 100s or 1000s of times, parsing files with hundreds of thousands of lines. + # It may be called concurrently without global interpreter lock in some Rubies. + # As a result, some methods may look more complex than necessary to save memory or time. class CompactIndexClient - DEBUG_MUTEX = Mutex.new + SUPPORTED_DIGESTS = { "sha-256" => :SHA256 }.freeze + DEBUG_MUTEX = Thread::Mutex.new + + # info returns an Array of INFO Arrays. Each INFO Array has the following indices: + INFO_NAME = 0 + INFO_VERSION = 1 + INFO_PLATFORM = 2 + INFO_DEPS = 3 + INFO_REQS = 4 + def self.debug return unless ENV["DEBUG_COMPACT_INDEX"] DEBUG_MUTEX.synchronize { warn("[#{self}] #{yield}") } @@ -13,97 +44,49 @@ module Bundler class Error < StandardError; end - require "bundler/compact_index_client/cache" - require "bundler/compact_index_client/updater" - - attr_reader :directory + require_relative "compact_index_client/cache" + require_relative "compact_index_client/cache_file" + require_relative "compact_index_client/parser" + require_relative "compact_index_client/updater" - # @return [Lambda] A lambda that takes an array of inputs and a block, and - # maps the inputs with the block in parallel. - # - attr_accessor :in_parallel - - def initialize(directory, fetcher) - @directory = Pathname.new(directory) - @updater = Updater.new(fetcher) - @cache = Cache.new(@directory) - @endpoints = Set.new - @info_checksums_by_name = {} - @parsed_checksums = false - @mutex = Mutex.new - @in_parallel = lambda do |inputs, &blk| - inputs.map(&blk) - end + def initialize(directory, fetcher = nil) + @cache = Cache.new(directory, fetcher) + @parser = Parser.new(@cache) end def names - Bundler::CompactIndexClient.debug { "/names" } - update(@cache.names_path, "names") - @cache.names + Bundler::CompactIndexClient.debug { "names" } + @parser.names end def versions - Bundler::CompactIndexClient.debug { "/versions" } - update(@cache.versions_path, "versions") - versions, @info_checksums_by_name = @cache.versions - versions + Bundler::CompactIndexClient.debug { "versions" } + @parser.versions end def dependencies(names) Bundler::CompactIndexClient.debug { "dependencies(#{names})" } - in_parallel.call(names) do |name| - update_info(name) - @cache.dependencies(name).map {|d| d.unshift(name) } - end.flatten(1) + names.map {|name| info(name) } end - def spec(name, version, platform = nil) - Bundler::CompactIndexClient.debug { "spec(name = #{name}, version = #{version}, platform = #{platform})" } - update_info(name) - @cache.specific_dependency(name, version, platform) - end - - def update_and_parse_checksums! - Bundler::CompactIndexClient.debug { "update_and_parse_checksums!" } - return @info_checksums_by_name if @parsed_checksums - update(@cache.versions_path, "versions") - @info_checksums_by_name = @cache.checksums - @parsed_checksums = true - end - - private - - def update(local_path, remote_path) - Bundler::CompactIndexClient.debug { "update(#{local_path}, #{remote_path})" } - unless synchronize { @endpoints.add?(remote_path) } - Bundler::CompactIndexClient.debug { "already fetched #{remote_path}" } - return - end - @updater.update(local_path, url(remote_path)) + def info(name) + Bundler::CompactIndexClient.debug { "info(#{name})" } + @parser.info(name) end - def update_info(name) - Bundler::CompactIndexClient.debug { "update_info(#{name})" } - path = @cache.info_path(name) - checksum = @updater.checksum_for_file(path) - unless existing = @info_checksums_by_name[name] - Bundler::CompactIndexClient.debug { "skipping updating info for #{name} since it is missing from versions" } - return - end - if checksum == existing - Bundler::CompactIndexClient.debug { "skipping updating info for #{name} since the versions checksum matches the local checksum" } - return - end - Bundler::CompactIndexClient.debug { "updating info for #{name} since the versions checksum #{existing} != the local checksum #{checksum}" } - update(path, "info/#{name}") + def latest_version(name) + Bundler::CompactIndexClient.debug { "latest_version(#{name})" } + @parser.info(name).map {|d| Gem::Version.new(d[INFO_VERSION]) }.max end - def url(path) - path + def available? + Bundler::CompactIndexClient.debug { "available?" } + @parser.available? end - def synchronize - @mutex.synchronize { yield } + def reset! + Bundler::CompactIndexClient.debug { "reset!" } + @cache.reset! end end end diff --git a/lib/bundler/compact_index_client/cache.rb b/lib/bundler/compact_index_client/cache.rb index f6105d3bb3..3bae6c9efd 100644 --- a/lib/bundler/compact_index_client/cache.rb +++ b/lib/bundler/compact_index_client/cache.rb @@ -1,117 +1,95 @@ # frozen_string_literal: true +require "rubygems/resolver/api_set/gem_parser" + module Bundler class CompactIndexClient class Cache attr_reader :directory - def initialize(directory) + def initialize(directory, fetcher = nil) @directory = Pathname.new(directory).expand_path - info_roots.each do |dir| - SharedHelpers.filesystem_access(dir) do - FileUtils.mkdir_p(dir) - end - end - end + @updater = Updater.new(fetcher) if fetcher + @mutex = Thread::Mutex.new + @endpoints = Set.new - def names - lines(names_path) + @info_root = mkdir("info") + @special_characters_info_root = mkdir("info-special-characters") + @info_etag_root = mkdir("info-etags") end - def names_path - directory.join("names") + def names + fetch("names", names_path, names_etag_path) end def versions - versions_by_name = Hash.new {|hash, key| hash[key] = [] } - info_checksums_by_name = {} - - lines(versions_path).each do |line| - name, versions_string, info_checksum = line.split(" ", 3) - info_checksums_by_name[name] = info_checksum || "" - versions_string.split(",").each do |version| - if version.start_with?("-") - version = version[1..-1].split("-", 2).unshift(name) - versions_by_name[name].delete(version) - else - version = version.split("-", 2).unshift(name) - versions_by_name[name] << version - end - end - end - - [versions_by_name, info_checksums_by_name] + fetch("versions", versions_path, versions_etag_path) end - def versions_path - directory.join("versions") - end - - def checksums - checksums = {} + def info(name, remote_checksum = nil) + path = info_path(name) - lines(versions_path).each do |line| - name, _, checksum = line.split(" ", 3) - checksums[name] = checksum + if remote_checksum && remote_checksum != SharedHelpers.checksum_for_file(path, :MD5) + fetch("info/#{name}", path, info_etag_path(name)) + else + Bundler::CompactIndexClient.debug { "update skipped info/#{name} (#{remote_checksum ? "versions index checksum is nil" : "versions index checksum matches local"})" } + read(path) end - - checksums end - def dependencies(name) - lines(info_path(name)).map do |line| - parse_gem(line) - end + def reset! + @mutex.synchronize { @endpoints.clear } end + private + + def names_path = directory.join("names") + def names_etag_path = directory.join("names.etag") + def versions_path = directory.join("versions") + def versions_etag_path = directory.join("versions.etag") + def info_path(name) name = name.to_s - if name =~ /[^a-z0-9_-]/ + # TODO: converge this into the info_root by hashing all filenames like info_etag_path + if /[^a-z0-9_-]/.match?(name) name += "-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}" - info_roots.last.join(name) + @special_characters_info_root.join(name) else - info_roots.first.join(name) + @info_root.join(name) end end - def specific_dependency(name, version, platform) - pattern = [version, platform].compact.join("-") - return nil if pattern.empty? - - gem_lines = info_path(name).read - gem_line = gem_lines[/^#{Regexp.escape(pattern)}\b.*/, 0] - gem_line ? parse_gem(gem_line) : nil + def info_etag_path(name) + name = name.to_s + @info_etag_root.join("#{name}-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}") end - private - - def lines(path) - return [] unless path.file? - lines = SharedHelpers.filesystem_access(path, :read, &:read).split("\n") - header = lines.index("---") - header ? lines[header + 1..-1] : lines + def mkdir(name) + directory.join(name).tap do |dir| + SharedHelpers.filesystem_access(dir) do + FileUtils.mkdir_p(dir) + end + end end - def parse_gem(string) - version_and_platform, rest = string.split(" ", 2) - version, platform = version_and_platform.split("-", 2) - dependencies, requirements = rest.split("|", 2).map {|s| s.split(",") } if rest - dependencies = dependencies ? dependencies.map {|d| parse_dependency(d) } : [] - requirements = requirements ? requirements.map {|r| parse_dependency(r) } : [] - [version, platform, dependencies, requirements] + def fetch(remote_path, path, etag_path) + if already_fetched?(remote_path) + Bundler::CompactIndexClient.debug { "already fetched #{remote_path}" } + else + Bundler::CompactIndexClient.debug { "fetching #{remote_path}" } + @updater&.update(remote_path, path, etag_path) + end + + read(path) end - def parse_dependency(string) - dependency = string.split(":") - dependency[-1] = dependency[-1].split("&") if dependency.size > 1 - dependency + def already_fetched?(remote_path) + @mutex.synchronize { !@endpoints.add?(remote_path) } end - def info_roots - [ - directory.join("info"), - directory.join("info-special-characters"), - ] + def read(path) + return unless path.file? + SharedHelpers.filesystem_access(path, :read, &:read) end end end diff --git a/lib/bundler/compact_index_client/cache_file.rb b/lib/bundler/compact_index_client/cache_file.rb new file mode 100644 index 0000000000..299d683438 --- /dev/null +++ b/lib/bundler/compact_index_client/cache_file.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +require_relative "../vendored_fileutils" +require "rubygems/package" + +module Bundler + class CompactIndexClient + # write cache files in a way that is robust to concurrent modifications + # if digests are given, the checksums will be verified + class CacheFile + DEFAULT_FILE_MODE = 0o644 + private_constant :DEFAULT_FILE_MODE + + class Error < RuntimeError; end + class ClosedError < Error; end + + class DigestMismatchError < Error + def initialize(digests, expected_digests) + super "Calculated checksums #{digests.inspect} did not match expected #{expected_digests.inspect}." + end + end + + # Initialize with a copy of the original file, then yield the instance. + def self.copy(path, &block) + new(path) do |file| + file.initialize_digests + + SharedHelpers.filesystem_access(path, :read) do + path.open("rb") do |s| + file.open {|f| IO.copy_stream(s, f) } + end + end + + yield file + end + end + + # Write data to a temp file, then replace the original file with it verifying the digests if given. + def self.write(path, data, digests = nil) + return unless data + new(path) do |file| + file.digests = digests + file.write(data) + end + end + + attr_reader :original_path, :path + + def initialize(original_path, &block) + @original_path = original_path + @perm = original_path.file? ? original_path.stat.mode : DEFAULT_FILE_MODE + @path = original_path.sub(/$/, ".#{$$}.tmp") + return unless block_given? + begin + yield self + ensure + close + end + end + + def size + path.size + end + + # initialize the digests using CompactIndexClient::SUPPORTED_DIGESTS, or a subset based on keys. + def initialize_digests(keys = nil) + @digests = keys ? SUPPORTED_DIGESTS.slice(*keys) : SUPPORTED_DIGESTS.dup + @digests.transform_values! {|algo_class| SharedHelpers.digest(algo_class).new } + end + + # reset the digests so they don't contain any previously read data + def reset_digests + @digests&.each_value(&:reset) + end + + # set the digests that will be verified at the end + def digests=(expected_digests) + @expected_digests = expected_digests + + if @expected_digests.nil? + @digests = nil + elsif @digests + @digests = @digests.slice(*@expected_digests.keys) + else + initialize_digests(@expected_digests.keys) + end + end + + def digests? + @digests&.any? + end + + # Open the temp file for writing, reusing original permissions, yielding the IO object. + def open(write_mode = "wb", perm = @perm, &block) + raise ClosedError, "Cannot reopen closed file" if @closed + SharedHelpers.filesystem_access(path, :write) do + path.open(write_mode, perm) do |f| + yield digests? ? Gem::Package::DigestIO.new(f, @digests) : f + end + end + end + + # Returns false without appending when no digests since appending is too error prone to do without digests. + def append(data) + return false unless digests? + open("a") {|f| f.write data } + verify && commit + end + + def write(data) + reset_digests + open {|f| f.write data } + commit! + end + + def commit! + verify || raise(DigestMismatchError.new(@base64digests, @expected_digests)) + commit + end + + # Verify the digests, returning true on match, false on mismatch. + def verify + return true unless @expected_digests && digests? + @base64digests = @digests.transform_values!(&:base64digest) + @digests = nil + @base64digests.all? {|algo, digest| @expected_digests[algo] == digest } + end + + # Replace the original file with the temp file without verifying digests. + # The file is permanently closed. + def commit + raise ClosedError, "Cannot commit closed file" if @closed + SharedHelpers.filesystem_access(original_path, :write) do + FileUtils.mv(path, original_path) + end + @closed = true + end + + # Remove the temp file without replacing the original file. + # The file is permanently closed. + def close + return if @closed + FileUtils.remove_file(path) if @path&.file? + @closed = true + end + end + end +end diff --git a/lib/bundler/compact_index_client/parser.rb b/lib/bundler/compact_index_client/parser.rb new file mode 100644 index 0000000000..ad0d17ed4a --- /dev/null +++ b/lib/bundler/compact_index_client/parser.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module Bundler + class CompactIndexClient + class Parser + # `compact_index` - an object responding to #names, #versions, #info(name, checksum), + # returning the file contents as a string + def initialize(compact_index) + @compact_index = compact_index + @info_checksums = nil + @versions_by_name = nil + @available = nil + @gem_parser = nil + end + + def names + lines(@compact_index.names) + end + + def versions + @versions_by_name ||= Hash.new {|hash, key| hash[key] = [] } + @info_checksums = {} + + lines(@compact_index.versions).each do |line| + name, versions_string, checksum = line.split(" ", 3) + @info_checksums[name] = checksum || "" + versions_string.split(",") do |version| + delete = version.delete_prefix!("-") + version = version.split("-", 2).unshift(name) + if delete + @versions_by_name[name].delete(version) + else + @versions_by_name[name] << version + end + end + end + + @versions_by_name + end + + def info(name) + data = @compact_index.info(name, info_checksums[name]) + lines(data).map {|line| gem_parser.parse(line).unshift(name) } + end + + def available? + return @available unless @available.nil? + @available = !info_checksums.empty? + end + + private + + def info_checksums + @info_checksums ||= lines(@compact_index.versions).each_with_object({}) do |line, checksums| + parse_version_checksum(line, checksums) + end + end + + def lines(data) + return [] if data.nil? || data.empty? + lines = data.split("\n") + header = lines.index("---") + header ? lines[header + 1..-1] : lines + end + + def gem_parser + @gem_parser ||= Gem::Resolver::APISet::GemParser.new + end + + # This is mostly the same as `split(" ", 3)` but it avoids allocating extra objects. + # This method gets called at least once for every gem when parsing versions. + def parse_version_checksum(line, checksums) + return unless (name_end = line.index(" ")) # Artifactory bug causes blank lines in artifactor index files + checksum_start = line.index(" ", name_end + 1) + return unless checksum_start + checksum_start += 1 + + checksum_end = line.size - checksum_start + + line.freeze # allows slicing into the string to not allocate a copy of the line + name = line[0, name_end] + checksum = line[checksum_start, checksum_end] + checksums[name.freeze] = checksum # freeze name since it is used as a hash key + end + end + end +end diff --git a/lib/bundler/compact_index_client/updater.rb b/lib/bundler/compact_index_client/updater.rb index 3a4e4441ca..6066fdc7c4 100644 --- a/lib/bundler/compact_index_client/updater.rb +++ b/lib/bundler/compact_index_client/updater.rb @@ -1,106 +1,104 @@ # frozen_string_literal: true -require "bundler/vendored_fileutils" -require "stringio" -require "zlib" - module Bundler class CompactIndexClient class Updater - class MisMatchedChecksumError < Error - def initialize(path, server_checksum, local_checksum) - @path = path - @server_checksum = server_checksum - @local_checksum = local_checksum - end - - def message - "The checksum of /#{@path} does not match the checksum provided by the server! Something is wrong " \ - "(local checksum is #{@local_checksum.inspect}, was expecting #{@server_checksum.inspect})." + class MismatchedChecksumError < Error + def initialize(path, message) + super "The checksum of /#{path} does not match the checksum provided by the server! Something is wrong. #{message}" end end def initialize(fetcher) @fetcher = fetcher - require "tmpdir" end - def update(local_path, remote_path, retrying = nil) - headers = {} + def update(remote_path, local_path, etag_path) + append(remote_path, local_path, etag_path) || replace(remote_path, local_path, etag_path) + rescue CacheFile::DigestMismatchError => e + raise MismatchedChecksumError.new(remote_path, e.message) + rescue Zlib::GzipFile::Error + raise Bundler::HTTPError + end - Dir.mktmpdir("bundler-compact-index-") do |local_temp_dir| - local_temp_path = Pathname.new(local_temp_dir).join(local_path.basename) + private - # first try to fetch any new bytes on the existing file - if retrying.nil? && local_path.file? - FileUtils.cp local_path, local_temp_path - headers["If-None-Match"] = etag_for(local_temp_path) - headers["Range"] = - if local_temp_path.size.nonzero? - # Subtract a byte to ensure the range won't be empty. - # Avoids 416 (Range Not Satisfiable) responses. - "bytes=#{local_temp_path.size - 1}-" - else - "bytes=#{local_temp_path.size}-" - end - else - # Fastly ignores Range when Accept-Encoding: gzip is set - headers["Accept-Encoding"] = "gzip" - end + def append(remote_path, local_path, etag_path) + return false unless local_path.file? && local_path.size.nonzero? - response = @fetcher.call(remote_path, headers) - return nil if response.is_a?(Net::HTTPNotModified) + CacheFile.copy(local_path) do |file| + etag = etag_path.read.tap(&:chomp!) if etag_path.file? - content = response.body - if response["Content-Encoding"] == "gzip" - content = Zlib::GzipReader.new(StringIO.new(content)).read - end + # Subtract a byte to ensure the range won't be empty. + # Avoids 416 (Range Not Satisfiable) responses. + response = @fetcher.call(remote_path, request_headers(etag, file.size - 1)) + break true if response.is_a?(Gem::Net::HTTPNotModified) - SharedHelpers.filesystem_access(local_temp_path) do - if response.is_a?(Net::HTTPPartialContent) && local_temp_path.size.nonzero? - local_temp_path.open("a") {|f| f << slice_body(content, 1..-1) } - else - local_temp_path.open("w") {|f| f << content } - end + file.digests = parse_digests(response) + # server may ignore Range and return the full response + if response.is_a?(Gem::Net::HTTPPartialContent) + tail = response.body.byteslice(1..-1) + break false unless tail && file.append(tail) + else + file.write(response.body) end + CacheFile.write(etag_path, etag_from_response(response)) + true + end + end - response_etag = (response["ETag"] || "").gsub(%r{\AW/}, "") - if etag_for(local_temp_path) == response_etag - SharedHelpers.filesystem_access(local_path) do - FileUtils.mv(local_temp_path, local_path) - end - return nil - end + # request without range header to get the full file or a 304 Not Modified + def replace(remote_path, local_path, etag_path) + etag = etag_path.read.tap(&:chomp!) if etag_path.file? + response = @fetcher.call(remote_path, request_headers(etag)) + return true if response.is_a?(Gem::Net::HTTPNotModified) + CacheFile.write(local_path, response.body, parse_digests(response)) + CacheFile.write(etag_path, etag_from_response(response)) + end - if retrying - raise MisMatchedChecksumError.new(remote_path, response_etag, etag_for(local_temp_path)) - end + def request_headers(etag, range_start = nil) + headers = {} + headers["Range"] = "bytes=#{range_start}-" if range_start + headers["If-None-Match"] = %("#{etag}") if etag + headers + end - update(local_path, remote_path, :retrying) - end + def etag_for_request(etag_path) + etag_path.read.tap(&:chomp!) if etag_path.file? end - def etag_for(path) - sum = checksum_for_file(path) - sum ? %("#{sum}") : nil + def etag_from_response(response) + return unless response["ETag"] + etag = response["ETag"].delete_prefix("W/") + return if etag.delete_prefix!('"') && !etag.delete_suffix!('"') + etag end - def slice_body(body, range) - if body.respond_to?(:byteslice) - body.byteslice(range) - else # pre-1.9.3 - body.unpack("@#{range.first}a#{range.end + 1}").first + # Unwraps and returns a Hash of digest algorithms and base64 values + # according to RFC 8941 Structured Field Values for HTTP. + # https://www.rfc-editor.org/rfc/rfc8941#name-parsing-a-byte-sequence + # Ignores unsupported algorithms. + def parse_digests(response) + return unless header = response["Repr-Digest"] || response["Digest"] + digests = {} + header.split(",") do |param| + algorithm, value = param.split("=", 2) + algorithm.strip! + algorithm.downcase! + next unless SUPPORTED_DIGESTS.key?(algorithm) + next unless value = byte_sequence(value) + digests[algorithm] = value end + digests.empty? ? nil : digests end - def checksum_for_file(path) - return nil unless path.file? - # This must use IO.read instead of Digest.file().hexdigest - # because we need to preserve \n line endings on windows when calculating - # the checksum - SharedHelpers.filesystem_access(path, :read) do - SharedHelpers.digest(:MD5).hexdigest(IO.read(path)) - end + # Unwrap surrounding colons (byte sequence) + # The wrapping characters must be matched or we return nil. + # Also handles quotes because right now rubygems.org sends them. + def byte_sequence(value) + return if value.delete_prefix!(":") && !value.delete_suffix!(":") + return if value.delete_prefix!('"') && !value.delete_suffix!('"') + value end end end diff --git a/lib/bundler/compatibility_guard.rb b/lib/bundler/compatibility_guard.rb deleted file mode 100644 index 750a1db04f..0000000000 --- a/lib/bundler/compatibility_guard.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: false - -require "rubygems" -require "bundler/version" - -if Bundler::VERSION.split(".").first.to_i >= 2 - if Gem::Version.new(Object::RUBY_VERSION.dup) < Gem::Version.new("2.3") - abort "Bundler 2 requires Ruby 2.3 or later. Either install bundler 1 or update to a supported Ruby version." - end - - if Gem::Version.new(Gem::VERSION.dup) < Gem::Version.new("2.5") - abort "Bundler 2 requires RubyGems 2.5 or later. Either install bundler 1 or update to a supported RubyGems version." - end -end diff --git a/lib/bundler/constants.rb b/lib/bundler/constants.rb index 2e4ebb37ee..9564771e78 100644 --- a/lib/bundler/constants.rb +++ b/lib/bundler/constants.rb @@ -1,7 +1,14 @@ # frozen_string_literal: true +require "rbconfig" + module Bundler WINDOWS = RbConfig::CONFIG["host_os"] =~ /(msdos|mswin|djgpp|mingw)/ - FREEBSD = RbConfig::CONFIG["host_os"] =~ /bsd/ - NULL = WINDOWS ? "NUL" : "/dev/null" + deprecate_constant :WINDOWS + + FREEBSD = RbConfig::CONFIG["host_os"].to_s.include?("bsd") + deprecate_constant :FREEBSD + + NULL = File::NULL + deprecate_constant :NULL end diff --git a/lib/bundler/current_ruby.rb b/lib/bundler/current_ruby.rb index 31532d108d..17c7655adb 100644 --- a/lib/bundler/current_ruby.rb +++ b/lib/bundler/current_ruby.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative "rubygems_ext" + module Bundler # Returns current version of Ruby # @@ -9,66 +11,62 @@ module Bundler end class CurrentRuby - KNOWN_MINOR_VERSIONS = %w[ - 1.8 - 1.9 - 2.0 - 2.1 - 2.2 - 2.3 - 2.4 - 2.5 - ].freeze - - KNOWN_MAJOR_VERSIONS = KNOWN_MINOR_VERSIONS.map {|v| v.split(".", 2).first }.uniq.freeze - - KNOWN_PLATFORMS = %w[ - jruby - maglev - mingw - mri - mswin - mswin64 - rbx - ruby - x64_mingw - ].freeze + ALL_RUBY_VERSIONS = [*18..27, *30..34, *40..41].freeze + KNOWN_MINOR_VERSIONS = ALL_RUBY_VERSIONS.map {|v| v.digits.reverse.join(".") }.freeze + KNOWN_MAJOR_VERSIONS = ALL_RUBY_VERSIONS.map {|v| v.digits.last.to_s }.uniq.freeze + PLATFORM_MAP = { + ruby: [Gem::Platform::RUBY, CurrentRuby::ALL_RUBY_VERSIONS], + mri: [Gem::Platform::RUBY, CurrentRuby::ALL_RUBY_VERSIONS], + rbx: [Gem::Platform::RUBY], + truffleruby: [Gem::Platform::RUBY], + jruby: [Gem::Platform::JAVA, [18, 19]], + windows: [Gem::Platform::WINDOWS, CurrentRuby::ALL_RUBY_VERSIONS], + # deprecated + mswin: [Gem::Platform::MSWIN, CurrentRuby::ALL_RUBY_VERSIONS], + mswin64: [Gem::Platform::MSWIN64, CurrentRuby::ALL_RUBY_VERSIONS - [18]], + mingw: [Gem::Platform::UNIVERSAL_MINGW, CurrentRuby::ALL_RUBY_VERSIONS], + x64_mingw: [Gem::Platform::UNIVERSAL_MINGW, CurrentRuby::ALL_RUBY_VERSIONS - [18, 19]], + }.each_with_object({}) do |(platform, spec), hash| + hash[platform] = spec[0] + spec[1]&.each {|version| hash[:"#{platform}_#{version}"] = spec[0] } + end.freeze def ruby? - !mswin? && (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby" || RUBY_ENGINE == "rbx" || RUBY_ENGINE == "maglev") + return true if Bundler::MatchPlatform.generic_local_platform_is_ruby? + + !windows? && (RUBY_ENGINE == "ruby" || RUBY_ENGINE == "rbx" || RUBY_ENGINE == "maglev" || RUBY_ENGINE == "truffleruby") end def mri? - !mswin? && (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby") + !windows? && RUBY_ENGINE == "ruby" end def rbx? - ruby? && defined?(RUBY_ENGINE) && RUBY_ENGINE == "rbx" + ruby? && RUBY_ENGINE == "rbx" end def jruby? - defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" + RUBY_ENGINE == "jruby" end def maglev? - defined?(RUBY_ENGINE) && RUBY_ENGINE == "maglev" - end - - def mswin? - Bundler::WINDOWS - end - - def mswin64? - Bundler::WINDOWS && Bundler.local_platform != Gem::Platform::RUBY && Bundler.local_platform.os == "mswin64" && Bundler.local_platform.cpu == "x64" + removed_message = + "`CurrentRuby#maglev?` was removed with no replacement. Please use the " \ + "built-in Ruby `RUBY_ENGINE` constant to check the Ruby implementation you are running on." + SharedHelpers.feature_removed!(removed_message) end - def mingw? - Bundler::WINDOWS && Bundler.local_platform != Gem::Platform::RUBY && Bundler.local_platform.os == "mingw32" && Bundler.local_platform.cpu != "x64" + def truffleruby? + RUBY_ENGINE == "truffleruby" end - def x64_mingw? - Bundler::WINDOWS && Bundler.local_platform != Gem::Platform::RUBY && Bundler.local_platform.os == "mingw32" && Bundler.local_platform.cpu == "x64" + def windows? + Gem.win_platform? end + alias_method :mswin?, :windows? + alias_method :mswin64?, :windows? + alias_method :mingw?, :windows? + alias_method :x64_mingw?, :windows? (KNOWN_MINOR_VERSIONS + KNOWN_MAJOR_VERSIONS).each do |version| trimmed_version = version.tr(".", "") @@ -76,11 +74,21 @@ module Bundler RUBY_VERSION.start_with?("#{version}.") end - KNOWN_PLATFORMS.each do |platform| + PLATFORM_MAP.keys.each do |platform| define_method(:"#{platform}_#{trimmed_version}?") do send(:"#{platform}?") && send(:"on_#{trimmed_version}?") end end + + define_method(:"maglev_#{trimmed_version}?") do + removed_message = + "`CurrentRuby##{__method__}` was removed with no replacement. Please use the " \ + "built-in Ruby `RUBY_ENGINE` and `RUBY_VERSION` constants to perform a similar check." + + SharedHelpers.feature_removed!(removed_message) + + send(:"maglev?") && send(:"on_#{trimmed_version}?") + end end end end diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index f93ed76226..7a95671471 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -1,22 +1,28 @@ # frozen_string_literal: true -require "bundler/lockfile_parser" -require "set" +require_relative "lockfile_parser" +require_relative "worker" module Bundler class Definition - include GemHelpers + class << self + # Do not create or modify a lockfile (Makes #lock a noop) + attr_accessor :no_lock + end + + attr_writer :lockfile, :overrides attr_reader( :dependencies, - :gem_version_promoter, + :locked_checksums, :locked_deps, :locked_gems, + :overrides, :platforms, - :requires, :ruby_version, :lockfile, - :gemfiles + :gemfiles, + :sources ) # Given a gemfile and lockfile creates a Bundler definition @@ -32,7 +38,10 @@ module Bundler raise GemfileNotFound, "#{gemfile} not found" unless gemfile.file? - Dsl.evaluate(gemfile, lockfile, unlock) + Plugin.hook(Plugin::Events::GEM_BEFORE_EVAL, gemfile, lockfile) + Dsl.evaluate(gemfile, lockfile, unlock).tap do |definition| + Plugin.hook(Plugin::Events::GEM_AFTER_EVAL, definition) + end end # @@ -53,109 +62,178 @@ module Bundler # to be updated or true if all gems should be updated # @param ruby_version [Bundler::RubyVersion, nil] Requested Ruby Version # @param optional_groups [Array(String)] A list of optional groups - def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = [], gemfiles = []) - if [true, false].include?(unlock) + def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = [], gemfiles = [], overrides = []) + unlock ||= {} + + if unlock == true + @unlocking_all = true + strict = false @unlocking_bundler = false @unlocking = unlock + @sources_to_unlock = [] + @unlocking_ruby = false + @explicit_unlocks = [] + conservative = false else - unlock = unlock.dup + @unlocking_all = false + strict = unlock.delete(:strict) @unlocking_bundler = unlock.delete(:bundler) - unlock.delete_if {|_k, v| Array(v).empty? } - @unlocking = !unlock.empty? + @unlocking = unlock.any? {|_k, v| !Array(v).empty? } + @sources_to_unlock = unlock.delete(:sources) || [] + @unlocking_ruby = unlock.delete(:ruby) + @explicit_unlocks = unlock.delete(:gems) || [] + conservative = unlock.delete(:conservative) end @dependencies = dependencies @sources = sources - @unlock = unlock @optional_groups = optional_groups - @remote = false + @prefer_local = false @specs = nil @ruby_version = ruby_version @gemfiles = gemfiles + @overrides = overrides @lockfile = lockfile @lockfile_contents = String.new + @locked_bundler_version = nil - @locked_ruby_version = nil + @resolved_bundler_version = nil + + @locked_ruby_version = nil + @new_platforms = [] + @removed_platforms = [] + @originally_invalid_platforms = [] - if lockfile && File.exist?(lockfile) + if lockfile_exists? @lockfile_contents = Bundler.read_file(lockfile) - @locked_gems = LockfileParser.new(@lockfile_contents) + @locked_gems = LockfileParser.new(@lockfile_contents, strict: strict) @locked_platforms = @locked_gems.platforms + @most_specific_locked_platform = @locked_gems.most_specific_locked_platform @platforms = @locked_platforms.dup @locked_bundler_version = @locked_gems.bundler_version @locked_ruby_version = @locked_gems.ruby_version + @locked_deps = @locked_gems.dependencies + Override.attach(@locked_gems.specs, @overrides) + @originally_locked_specs = SpecSet.new(@locked_gems.specs) + @originally_locked_sources = @locked_gems.sources + @locked_checksums = @locked_gems.checksums - if unlock != true - @locked_deps = @locked_gems.dependencies - @locked_specs = SpecSet.new(@locked_gems.specs) - @locked_sources = @locked_gems.sources - else - @unlock = {} - @locked_deps = {} + if @unlocking_all @locked_specs = SpecSet.new([]) @locked_sources = [] + else + @locked_specs = @originally_locked_specs + @locked_sources = @originally_locked_sources + end + + locked_gem_sources = @originally_locked_sources.select {|s| s.is_a?(Source::Rubygems) } + multisource_lockfile = locked_gem_sources.size == 1 && locked_gem_sources.first.multiple_remotes? + + if multisource_lockfile + msg = "Your lockfile contains a single rubygems source section with multiple remotes, which is insecure. Make sure you run `bundle install` in non frozen mode and commit the result to make your lockfile secure." + + Bundler::SharedHelpers.feature_removed! msg end else - @unlock = {} + @locked_gems = nil + @locked_platforms = [] + @most_specific_locked_platform = nil @platforms = [] - @locked_gems = nil @locked_deps = {} @locked_specs = SpecSet.new([]) @locked_sources = [] - @locked_platforms = [] + @originally_locked_specs = @locked_specs + @originally_locked_sources = @locked_sources + @locked_checksums = Bundler.settings[:lockfile_checksums] end - @unlock[:gems] ||= [] - @unlock[:sources] ||= [] - @unlock[:ruby] ||= if @ruby_version && locked_ruby_version_object + @unlocking_ruby ||= if @ruby_version && locked_ruby_version_object @ruby_version.diff(locked_ruby_version_object) end - @unlocking ||= @unlock[:ruby] ||= (!@locked_ruby_version ^ !@ruby_version) + @unlocking ||= @unlocking_ruby ||= (!@locked_ruby_version ^ !@ruby_version) - add_current_platform unless Bundler.frozen? + @current_platform_missing = add_current_platform unless Bundler.frozen_bundle? - converge_path_sources_to_gemspec_sources - @path_changes = converge_paths @source_changes = converge_sources + @path_changes = converge_paths - unless @unlock[:lock_shared_dependencies] - eager_unlock = expand_dependencies(@unlock[:gems]) - @unlock[:gems] = @locked_specs.for(eager_unlock).map(&:name) + if conservative + @gems_to_unlock = @explicit_unlocks.any? ? @explicit_unlocks : @dependencies.map(&:name) + else + eager_unlock = @explicit_unlocks.map {|name| Dependency.new(name, ">= 0") } + @gems_to_unlock = @locked_specs.for(eager_unlock, platforms).map(&:name).uniq end - @gem_version_promoter = create_gem_version_promoter - @dependency_changes = converge_dependencies @local_changes = converge_locals - @requires = compute_requires + check_lockfile end - def create_gem_version_promoter - locked_specs = - if unlocking? && @locked_specs.empty? && !@lockfile_contents.empty? - # Definition uses an empty set of locked_specs to indicate all gems - # are unlocked, but GemVersionPromoter needs the locked_specs - # for conservative comparison. - Bundler::SpecSet.new(@locked_gems.specs) - else - @locked_specs - end - GemVersionPromoter.new(locked_specs, @unlock[:gems]) + def gem_version_promoter + @gem_version_promoter ||= GemVersionPromoter.new + end + + def check! + # If dependencies have changed, we need to resolve remotely. Otherwise, + # since we'll be resolving with a single local source, we may end up + # locking gems under the wrong source in the lockfile, and missing lockfile + # checksums + resolve_remotely! if @dependency_changes + + # Now do a local only resolve, to verify if any gems are missing locally + sources.local_only! + resolve + end + + # + # Setup sources according to the given options and the state of the + # definition. + # + # @return [Boolean] Whether fetching remote information will be necessary or not + # + def setup_domain!(options = {}) + prefer_local! if options[:"prefer-local"] + + sources.cached! + + if options[:add_checksums] || (!options[:local] && install_needed?) + sources.remote! + true + else + Bundler.settings.set_command_option(:jobs, 1) unless install_needed? # to avoid the overhead of Bundler::Worker + sources.local! + false + end end def resolve_with_cache! - raise "Specs already loaded" if @specs + with_cache! + + resolve + end + + def with_cache! + sources.local! sources.cached! - specs end def resolve_remotely! - raise "Specs already loaded" if @specs - @remote = true + remotely! + + resolve + end + + def remotely! + sources.cached! sources.remote! - specs + end + + def prefer_local! + @prefer_local = true + + sources.prefer_local! end # For given dependency list returns a SpecSet with Gemspec of all the required @@ -165,25 +243,7 @@ module Bundler # # @return [Bundler::SpecSet] def specs - @specs ||= begin - begin - specs = resolve.materialize(Bundler.settings[:cache_all_platforms] ? dependencies : requested_dependencies) - rescue GemNotFound => e # Handle yanked gem - gem_name, gem_version = extract_gem_info(e) - locked_gem = @locked_specs[gem_name].last - raise if locked_gem.nil? || locked_gem.version.to_s != gem_version || !@remote - raise GemNotFound, "Your bundle is locked to #{locked_gem}, but that version could not " \ - "be found in any of the sources listed in your Gemfile. If you haven't changed sources, " \ - "that means the author of #{locked_gem} has removed it. You'll need to update your bundle " \ - "to a different version of #{locked_gem} that hasn't been removed in order to install." - end - unless specs["bundler"].any? - bundler = sources.metadata_source.specs.search(Gem::Dependency.new("bundler", VERSION)).last - specs["bundler"] = bundler - end - - specs - end + @specs ||= materialize(requested_dependencies) end def new_specs @@ -194,14 +254,9 @@ module Bundler @locked_specs - specs end - def new_platform? - @new_platform - end - def missing_specs - missing = [] - resolve.materialize(requested_dependencies, missing) - missing + preload_git_sources + resolve.missing_specs_for(requested_dependencies) end def missing_specs? @@ -210,31 +265,69 @@ module Bundler Bundler.ui.debug "The definition is missing #{missing.map(&:full_name)}" true rescue BundlerError => e - @index = nil @resolve = nil + @resolver = nil + @resolution_base = nil + @source_requirements = nil @specs = nil - @gem_version_promoter = create_gem_version_promoter Bundler.ui.debug "The definition is missing dependencies, failed to resolve & materialize locally (#{e})" true end def requested_specs - @requested_specs ||= begin - groups = requested_groups - groups.map!(&:to_sym) - specs_for(groups) - end + specs_for(requested_groups) + end + + def requested_dependencies + dependencies_for(requested_groups) end def current_dependencies - dependencies.select(&:should_include?) + filter_relevant(dependencies) + end + + def current_locked_dependencies + filter_relevant(locked_dependencies) + end + + def filter_relevant(dependencies) + dependencies.select do |d| + relevant_deps?(d) + end + end + + def relevant_deps?(dep) + platforms_array = [Bundler.generic_local_platform].freeze + + dep.should_include? && !dep.gem_platforms(platforms_array).empty? + end + + def locked_dependencies + @locked_deps.values + end + + def new_deps + @new_deps ||= @dependencies - locked_dependencies + end + + def deleted_deps + @deleted_deps ||= locked_dependencies - @dependencies end def specs_for(groups) - deps = dependencies.select {|d| (d.groups & groups).any? } - deps.delete_if {|d| !d.should_include? } - specs.for(expand_dependencies(deps)) + return specs if groups.empty? + deps = dependencies_for(groups) + materialize(deps) + end + + def dependencies_for(groups) + groups.map!(&:to_sym) + deps = current_dependencies # always returns a new array + deps.select! do |d| + d.groups.intersect?(groups) + end + deps end # Resolve all the dependencies specified in Gemfile. It ensures that @@ -243,123 +336,99 @@ module Bundler # # @return [SpecSet] resolved dependencies def resolve - @resolve ||= begin - last_resolve = converge_locked_specs - if Bundler.frozen? - Bundler.ui.debug "Frozen, using resolution from the lockfile" - last_resolve - elsif !unlocking? && nothing_changed? - Bundler.ui.debug("Found no changes, using resolution from the lockfile") - last_resolve + @resolve ||= if Bundler.frozen_bundle? + Bundler.ui.debug "Frozen, using resolution from the lockfile" + @locked_specs + elsif no_resolve_needed? + if deleted_deps.any? + Bundler.ui.debug "Some dependencies were deleted, using a subset of the resolution from the lockfile" + SpecSet.new(filter_specs(@locked_specs, @dependencies - deleted_deps)) else - # Run a resolve against the locally available gems - Bundler.ui.debug("Found changes from the lockfile, re-resolving dependencies because #{change_reason}") - last_resolve.merge Resolver.resolve(expanded_dependencies, index, source_requirements, last_resolve, gem_version_promoter, additional_base_requirements_for_resolve, platforms) + Bundler.ui.debug "Found no changes, using resolution from the lockfile" + if @removed_platforms.any? || @locked_gems.may_include_redundant_platform_specific_gems? + SpecSet.new(filter_specs(@locked_specs, @dependencies)) + else + @locked_specs + end end + else + Bundler.ui.debug resolve_needed_reason + + start_resolution end end - def index - @index ||= Index.build do |idx| - dependency_names = @dependencies.map(&:name) - - sources.all_sources.each do |source| - source.dependency_names = dependency_names - pinned_spec_names(source) - idx.add_source source.specs - dependency_names.concat(source.unmet_deps).uniq! - end + def spec_git_paths + sources.git_sources.filter_map {|s| File.realpath(s.path) if File.exist?(s.path) } + end - double_check_for_index(idx, dependency_names) - end + def groups + dependencies.flat_map(&:groups).uniq end - # Suppose the gem Foo depends on the gem Bar. Foo exists in Source A. Bar has some versions that exist in both - # sources A and B. At this point, the API request will have found all the versions of Bar in source A, - # but will not have found any versions of Bar from source B, which is a problem if the requested version - # of Foo specifically depends on a version of Bar that is only found in source B. This ensures that for - # each spec we found, we add all possible versions from all sources to the index. - def double_check_for_index(idx, dependency_names) - pinned_names = pinned_spec_names - loop do - idxcount = idx.size - - names = :names # do this so we only have to traverse to get dependency_names from the index once - unmet_dependency_names = lambda do - return names unless names == :names - new_names = sources.all_sources.map(&:dependency_names_to_double_check) - return names = nil if new_names.compact! - names = new_names.flatten(1).concat(dependency_names) - names.uniq! - names -= pinned_names - names - end + def lock(file_or_preserve_unknown_sections = false, preserve_unknown_sections_or_unused = false) + if [true, false, nil].include?(file_or_preserve_unknown_sections) + target_lockfile = lockfile + preserve_unknown_sections = file_or_preserve_unknown_sections + else + target_lockfile = file_or_preserve_unknown_sections + preserve_unknown_sections = preserve_unknown_sections_or_unused - sources.all_sources.each do |source| - source.double_check_for(unmet_dependency_names, :override_dupes) + suggestion = if target_lockfile == lockfile + "To fix this warning, remove it from the `Definition#lock` call." + else + "Instead, instantiate a new definition passing `#{target_lockfile}`, and call `lock` without a file argument on that definition" end - break if idxcount == idx.size - end - end - private :double_check_for_index + msg = "`Definition#lock` was passed a target file argument. #{suggestion}" - def has_rubygems_remotes? - sources.rubygems_sources.any? {|s| s.remotes.any? } - end + Bundler::SharedHelpers.feature_removed! msg + end - def has_local_dependencies? - !sources.path_sources.empty? || !sources.git_sources.empty? + write_lock(target_lockfile, preserve_unknown_sections) end - def spec_git_paths - sources.git_sources.map {|s| s.path.to_s } - end + def write_lock(file, preserve_unknown_sections) + return if Definition.no_lock || !lockfile || file.nil? - def groups - dependencies.map(&:groups).flatten.uniq - end - - def lock(file, preserve_unknown_sections = false) contents = to_lock # Convert to \r\n if the existing lock has them # i.e., Windows with `git config core.autocrlf=true` - contents.gsub!(/\n/, "\r\n") if @lockfile_contents.match("\r\n") + contents.gsub!(/\n/, "\r\n") if @lockfile_contents.match?("\r\n") if @locked_bundler_version locked_major = @locked_bundler_version.segments.first - current_major = Gem::Version.create(Bundler::VERSION).segments.first + current_major = bundler_version_to_lock.segments.first - if updating_major = locked_major < current_major - Bundler.ui.warn "Warning: the lockfile is being updated to Bundler #{current_major}, " \ - "after which you will be unable to return to Bundler #{@locked_bundler_version.segments.first}." - end + updating_major = locked_major < current_major end - preserve_unknown_sections ||= !updating_major && (Bundler.frozen? || !(unlocking? || @unlocking_bundler)) - return if lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections) + preserve_unknown_sections ||= !updating_major && (Bundler.frozen_bundle? || !(unlocking? || @unlocking_bundler)) - if Bundler.frozen? - Bundler.ui.error "Cannot write a changed lockfile while frozen." + if File.exist?(file) && lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections) + return if Bundler.frozen_bundle? + SharedHelpers.filesystem_access(file) { FileUtils.touch(file) } return end - SharedHelpers.filesystem_access(file) do |p| - File.open(p, "wb") {|f| f.puts(contents) } + if Bundler.frozen_bundle? + Bundler.ui.error "Cannot write a changed lockfile while frozen." + return end - end - def locked_bundler_version - if @locked_bundler_version && @locked_bundler_version < Gem::Version.new(Bundler::VERSION) - new_version = Bundler::VERSION + begin + SharedHelpers.filesystem_access(file) do |p| + File.open(p, "wb") {|f| f.puts(contents) } + end + rescue ReadOnlyFileSystemError + raise ProductionError, lockfile_changes_summary("file system is read-only") end - - new_version || @locked_bundler_version || Bundler::VERSION end def locked_ruby_version return unless ruby_version - if @unlock[:ruby] || !@locked_ruby_version + if @unlocking_ruby || !@locked_ruby_version Bundler::RubyVersion.system else @locked_ruby_version @@ -378,86 +447,32 @@ module Bundler end end + def bundler_version_to_lock + @resolved_bundler_version || Bundler.gem_version + end + def to_lock - require "bundler/lockfile_generator" + require_relative "lockfile_generator" LockfileGenerator.generate(self) end def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false) - msg = String.new - msg << "You are trying to install in deployment mode after changing\n" \ - "your Gemfile. Run `bundle install` elsewhere and add the\n" \ - "updated #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} to version control." - - unless explicit_flag - suggested_command = if Bundler.settings.locations("frozen")[:global] - "bundle config --delete frozen" - elsif Bundler.settings.locations("deployment").keys.&([:global, :local]).any? - "bundle config --delete deployment" - else - "bundle install --no-deployment" - end - msg << "\n\nIf this is a development machine, remove the #{Bundler.default_gemfile} " \ - "freeze \nby running `#{suggested_command}`." - end - - added = [] - deleted = [] - changed = [] - - new_platforms = @platforms - @locked_platforms - deleted_platforms = @locked_platforms - @platforms - added.concat new_platforms.map {|p| "* platform: #{p}" } - deleted.concat deleted_platforms.map {|p| "* platform: #{p}" } + return unless Bundler.frozen_bundle? - gemfile_sources = sources.lock_sources + raise ProductionError, "Frozen mode is set, but there's no lockfile" unless lockfile_exists? - new_sources = gemfile_sources - @locked_sources - deleted_sources = @locked_sources - gemfile_sources - - new_deps = @dependencies - @locked_deps.values - deleted_deps = @locked_deps.values - @dependencies - - # Check if it is possible that the source is only changed thing - if (new_deps.empty? && deleted_deps.empty?) && (!new_sources.empty? && !deleted_sources.empty?) - new_sources.reject! {|source| (source.path? && source.path.exist?) || equivalent_rubygems_remotes?(source) } - deleted_sources.reject! {|source| (source.path? && source.path.exist?) || equivalent_rubygems_remotes?(source) } - end + msg = lockfile_changes_summary("frozen mode is set") + return unless msg - if @locked_sources != gemfile_sources - if new_sources.any? - added.concat new_sources.map {|source| "* source: #{source}" } - end - - if deleted_sources.any? - deleted.concat deleted_sources.map {|source| "* source: #{source}" } + unless explicit_flag + suggested_command = unless Bundler.settings.locations("frozen").keys.include?(:env) + "bundle config set frozen false" end + msg << "\n\nIf this is a development machine, remove the #{SharedHelpers.relative_lockfile_path} " \ + "freeze by running `#{suggested_command}`." if suggested_command end - added.concat new_deps.map {|d| "* #{pretty_dep(d)}" } if new_deps.any? - if deleted_deps.any? - deleted.concat deleted_deps.map {|d| "* #{pretty_dep(d)}" } - end - - both_sources = Hash.new {|h, k| h[k] = [] } - @dependencies.each {|d| both_sources[d.name][0] = d } - @locked_deps.each {|name, d| both_sources[name][1] = d.source } - - both_sources.each do |name, (dep, lock_source)| - next unless (dep.nil? && !lock_source.nil?) || (!dep.nil? && !lock_source.nil? && !lock_source.can_lock?(dep)) - gemfile_source_name = (dep && dep.source) || "no specified source" - lockfile_source_name = lock_source || "no specified source" - changed << "* #{name} from `#{gemfile_source_name}` to `#{lockfile_source_name}`" - end - - reason = change_reason - msg << "\n\n#{reason.split(", ").map(&:capitalize).join("\n")}" unless reason.strip.empty? - msg << "\n\nYou have added to the Gemfile:\n" << added.join("\n") if added.any? - msg << "\n\nYou have deleted from the Gemfile:\n" << deleted.join("\n") if deleted.any? - msg << "\n\nYou have changed in the Gemfile:\n" << changed.join("\n") if changed.any? - msg << "\n" - - raise ProductionError, msg if added.any? || deleted.any? || changed.any? || !nothing_changed? + raise ProductionError, msg end def validate_runtime! @@ -478,12 +493,6 @@ module Bundler "Your Ruby version is #{actual}, but your Gemfile specified #{expected}" when :engine_version "Your #{Bundler::RubyVersion.system.engine} version is #{actual}, but your Gemfile specified #{ruby_version.engine} #{expected}" - when :patchlevel - if !expected.is_a?(String) - "The Ruby patchlevel in your Gemfile must be a string" - else - "Your Ruby patchlevel is #{actual}, but your Gemfile specified #{expected}" - end end raise RubyVersionMismatch, msg @@ -491,77 +500,404 @@ module Bundler end def validate_platforms! - return if @platforms.any? do |bundle_platform| - Bundler.rubygems.platforms.any? do |local_platform| - MatchPlatform.platforms_match?(bundle_platform, local_platform) - end - end + return if current_platform_locked? || @platforms.include?(Gem::Platform::RUBY) raise ProductionError, "Your bundle only supports platforms #{@platforms.map(&:to_s)} " \ - "but your local platforms are #{Bundler.rubygems.platforms.map(&:to_s)}, and " \ - "there's no compatible match between those two lists." + "but your local platform is #{Bundler.local_platform}. " \ + "Add the current platform to the lockfile with\n`bundle lock --add-platform #{Bundler.local_platform}` and try again." + end + + def normalize_platforms + resolve.normalize_platforms!(current_dependencies, platforms) + + @resolve = SpecSet.new(resolve.for(current_dependencies, @platforms)) end def add_platform(platform) - @new_platform ||= !@platforms.include?(platform) - @platforms |= [platform] + return if @platforms.include?(platform) + + @new_platforms << platform + @platforms << platform end def remove_platform(platform) - return if @platforms.delete(Gem::Platform.new(platform)) - raise InvalidOption, "Unable to remove the platform `#{platform}` since the only platforms are #{@platforms.join ", "}" + raise InvalidOption, "Unable to remove the platform `#{platform}` since the only platforms are #{@platforms.join ", "}" unless @platforms.include?(platform) + + @removed_platforms << platform + @platforms.delete(platform) end - def add_current_platform - current_platform = Bundler.local_platform - add_platform(current_platform) if Bundler.feature_flag.specific_platform? - add_platform(generic(current_platform)) + def nothing_changed? + !something_changed? end - def find_resolved_spec(current_spec) - specs.find_by_name_and_platform(current_spec.name, current_spec.platform) + def no_resolve_needed? + !resolve_needed? end - def find_indexed_specs(current_spec) - index[current_spec.name].select {|spec| spec.match_platform(current_spec.platform) }.sort_by(&:version) + def unlocking? + @unlocking end - attr_reader :sources - private :sources + def add_checksums + require "rubygems/package" - def nothing_changed? - !@source_changes && !@dependency_changes && !@new_platform && !@path_changes && !@local_changes + @locked_checksums = true + + setup_domain!(add_checksums: true) + + # force materialization to real specifications, so that checksums are fetched + specs.each do |spec| + next unless spec.source.is_a?(Bundler::Source::Rubygems) + # Checksum was fetched from the compact index API. + next if !spec.source.checksum_store.missing?(spec) && !spec.source.checksum_store.empty?(spec) + # The gem isn't installed, can't compute the checksum. + next unless spec.loaded_from + + package = Gem::Package.new(spec.source.cached_built_in_gem(spec)) + checksum = Checksum.from_gem_package(package) + spec.source.checksum_store.register(spec, checksum) + end end - def unlocking? - @unlocking + private + + def lockfile_changes_summary(update_refused_reason) + added = [] + deleted = [] + changed = [] + + added.concat @new_platforms.map {|p| "* platform: #{p}" } + deleted.concat @removed_platforms.map {|p| "* platform: #{p}" } + + added.concat new_deps.map {|d| "* #{pretty_dep(d)}" } if new_deps.any? + deleted.concat deleted_deps.map {|d| "* #{pretty_dep(d)}" } if deleted_deps.any? + + both_sources = Hash.new {|h, k| h[k] = [] } + current_dependencies.each {|d| both_sources[d.name][0] = d } + current_locked_dependencies.each {|d| both_sources[d.name][1] = d } + + both_sources.each do |name, (dep, lock_dep)| + next if dep.nil? || lock_dep.nil? + + gemfile_source = dep.source || default_source + lock_source = lock_dep.source || default_source + next if lock_source.include?(gemfile_source) + + gemfile_source_name = dep.source ? gemfile_source.to_gemfile : "no specified source" + lockfile_source_name = lock_dep.source ? lock_source.to_gemfile : "no specified source" + changed << "* #{name} from `#{lockfile_source_name}` to `#{gemfile_source_name}`" + end + + return unless added.any? || deleted.any? || changed.any? || resolve_needed? + + msg = String.new("#{change_reason[0].upcase}#{change_reason[1..-1].strip}, but ") + msg << "the lockfile " unless msg.start_with?("Your lockfile") + msg << "can't be updated because #{update_refused_reason}" + msg << "\n\nYou have added to the Gemfile:\n" << added.join("\n") if added.any? + msg << "\n\nYou have deleted from the Gemfile:\n" << deleted.join("\n") if deleted.any? + msg << "\n\nYou have changed in the Gemfile:\n" << changed.join("\n") if changed.any? + msg << "\n\nRun `bundle install` elsewhere and add the updated #{SharedHelpers.relative_lockfile_path} to version control.\n" unless unlocking? + msg end - private + def install_needed? + resolve_needed? || missing_specs? + end - def change_reason - if unlocking? - unlock_reason = @unlock.reject {|_k, v| Array(v).empty? }.map do |k, v| - if v == true - k.to_s + def something_changed? + return true unless lockfile_exists? + + @source_changes || + @dependency_changes || + @current_platform_missing || + @new_platforms.any? || + @path_changes || + @local_changes || + @missing_lockfile_dep || + @unlocking_bundler || + @locked_spec_with_missing_checksums || + @locked_spec_with_empty_checksums || + @locked_spec_with_missing_deps || + @locked_spec_with_invalid_deps + end + + def resolve_needed? + unlocking? || something_changed? + end + + def should_add_extra_platforms? + !lockfile_exists? && Bundler::MatchPlatform.generic_local_platform_is_ruby? && !Bundler.settings[:force_ruby_platform] + end + + def lockfile_exists? + lockfile && File.exist?(lockfile) + end + + def resolver + @resolver ||= new_resolver(resolution_base) + end + + def expanded_dependencies + apply_overrides_to(dependencies_with_bundler) + metadata_dependencies + end + + def apply_overrides_to(deps) + return deps if @overrides.empty? + deps.map {|dep| apply_override_to(dep) } + end + + def apply_override_to(dep) + override = Override.find_for(@overrides, dep.name, :version) + return dep unless override + new_dep = dep.dup + new_dep.instance_variable_set(:@requirement, override.apply_to(dep.requirement)) + new_dep + end + + def dependencies_with_bundler + return dependencies unless @unlocking_bundler + return dependencies if dependencies.any? {|d| d.name == "bundler" } + + [Dependency.new("bundler", @unlocking_bundler)] + dependencies + end + + def resolution_base + @resolution_base ||= begin + last_resolve = converge_locked_specs + remove_invalid_platforms! + base = new_resolution_base(last_resolve: last_resolve, unlock: @unlocking_all || @gems_to_unlock) + base = additional_base_requirements_to_prevent_downgrades(base) + base = additional_base_requirements_to_force_updates(base) + base + end + end + + def filter_specs(specs, deps, skips: []) + SpecSet.new(specs).for(deps, platforms, skips: skips) + end + + def materialize(dependencies) + specs = begin + resolve.materialize(dependencies) + rescue IncorrectLockfileDependencies => e + raise if Bundler.frozen_bundle? + + reresolve_without([e.spec]) + retry + end + + missing_specs = resolve.missing_specs + + if missing_specs.any? + missing_specs.each do |s| + locked_gem = @locked_specs[s.name].last + next if locked_gem.nil? || locked_gem.version != s.version || sources.local_mode? + + message = if sources.implicit_global_source? + "Because your Gemfile specifies no global remote source, your bundle is locked to " \ + "#{locked_gem} from #{locked_gem.source}. However, #{locked_gem} is not installed. You'll " \ + "need to either add a global remote source to your Gemfile or make sure #{locked_gem} is " \ + "available locally before rerunning Bundler." else - v = Array(v) - "#{k}: (#{v.join(", ")})" + "Your bundle is locked to #{locked_gem} from #{locked_gem.source}, but that version can " \ + "no longer be found in that source. That means the author of #{locked_gem} has removed it. " \ + "You'll need to update your bundle to a version other than #{locked_gem} that hasn't been " \ + "removed in order to install." end - end.join(", ") - return "bundler is unlocking #{unlock_reason}" + + raise GemNotFound, message + end + + missing_specs_list = missing_specs.group_by(&:source).map do |source, missing_specs_for_source| + "#{missing_specs_for_source.map(&:full_name).join(", ")} in #{source}" + end + + raise GemNotFound, "Could not find #{missing_specs_list.join(" nor ")}" + end + + partially_missing_specs = resolve.partially_missing_specs + + if partially_missing_specs.any? && !sources.local_mode? + Bundler.ui.warn "Some locked specs have possibly been yanked (#{partially_missing_specs.map(&:full_name).join(", ")}). Ignoring them..." + + resolve.delete(partially_missing_specs) end + + incomplete_specs = resolve.incomplete_specs + loop do + break if incomplete_specs.empty? + + Bundler.ui.debug("The lockfile does not have all gems needed for the current platform though, Bundler will still re-resolve dependencies") + sources.remote! + reresolve_without(incomplete_specs) + specs = resolve.materialize(dependencies) + + still_incomplete_specs = resolve.incomplete_specs + + if still_incomplete_specs == incomplete_specs + resolver.raise_incomplete! incomplete_specs + end + + incomplete_specs = still_incomplete_specs + end + + insecurely_materialized_specs = resolve.insecurely_materialized_specs + + if insecurely_materialized_specs.any? + Bundler.ui.warn "The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version:\n" \ + " * #{insecurely_materialized_specs.map(&:full_name).join("\n * ")}\n" \ + "Please run `bundle lock --normalize-platforms` and commit the resulting lockfile.\n" \ + "Alternatively, you may run `bundle lock --add-platform <list-of-platforms-that-you-want-to-support>`" + end + + bundler = sources.metadata_source.specs.search(["bundler", Bundler.gem_version]).last + specs["bundler"] = bundler + + specs + end + + def reresolve_without(incomplete_specs) + resolution_base.delete(incomplete_specs) + @resolve = start_resolution + end + + def start_resolution + local_platform_needed_for_resolvability = @most_specific_non_local_locked_platform && !@platforms.include?(Bundler.local_platform) + @platforms << Bundler.local_platform if local_platform_needed_for_resolvability + + result = SpecSet.new(resolver.start) + + @resolved_bundler_version = result.find {|spec| spec.name == "bundler" }&.version + + @new_platforms.each do |platform| + incomplete_specs = result.incomplete_specs_for_platform(current_dependencies, platform) + + if incomplete_specs.any? + resolver.raise_incomplete! incomplete_specs + end + end + + if @most_specific_non_local_locked_platform + if result.incomplete_for_platform?(current_dependencies, @most_specific_non_local_locked_platform) + @platforms.delete(@most_specific_non_local_locked_platform) + elsif local_platform_needed_for_resolvability + @platforms.delete(Bundler.local_platform) + end + end + + if should_add_extra_platforms? + result.add_extra_platforms!(platforms) + elsif @originally_invalid_platforms.any? + result.add_originally_invalid_platforms!(platforms, @originally_invalid_platforms) + end + + SpecSet.new(result.for(dependencies, @platforms | [Gem::Platform::RUBY])) + end + + def precompute_source_requirements_for_indirect_dependencies? + if sources.non_global_rubygems_sources.all?(&:dependency_api_available?) + true + else + non_dependency_api_warning + false + end + end + + def non_dependency_api_warning + non_api_sources = sources.non_global_rubygems_sources.reject(&:dependency_api_available?) + non_api_source_names = non_api_sources.map {|d| " * #{d}" }.join("\n") + + msg = String.new + msg << "Your Gemfile contains scoped sources that don't implement a dependency API, namely:\n\n" + msg << non_api_source_names + msg << "\n\nUsing the above gem servers may result in installing unexpected gems. " \ + "To resolve this warning, make sure you use gem servers that implement dependency APIs, " \ + "such as gemstash or geminabox gem servers." + Bundler.ui.warn msg + end + + def current_platform_locked? + @platforms.any? do |bundle_platform| + Bundler.generic_local_platform == bundle_platform || Bundler.local_platform === bundle_platform + end + end + + def add_current_platform + return if @platforms.include?(Bundler.local_platform) + + @most_specific_non_local_locked_platform = find_most_specific_locked_platform + return if @most_specific_non_local_locked_platform + + @platforms << Bundler.local_platform + true + end + + def find_most_specific_locked_platform + return unless current_platform_locked? + + @most_specific_locked_platform + end + + def resolve_needed_reason + if lockfile_exists? + if unlocking? + "Re-resolving dependencies because #{unlocking_reason}" + else + "Found changes from the lockfile, re-resolving dependencies because #{lockfile_changed_reason}" + end + else + "Resolving dependencies because there's no lockfile" + end + end + + def change_reason + if resolve_needed? + if unlocking? + unlocking_reason + else + lockfile_changed_reason + end + else + "some dependencies were deleted from your gemfile" + end + end + + def unlocking_reason + unlock_targets = if @gems_to_unlock.any? + ["gems", @gems_to_unlock] + elsif @sources_to_unlock.any? + ["sources", @sources_to_unlock] + end + + unlock_reason = if unlock_targets + "#{unlock_targets.first}: (#{unlock_targets.last.join(", ")})" + else + @unlocking_ruby ? "ruby" : "" + end + + "bundler is unlocking #{unlock_reason}" + end + + def lockfile_changed_reason [ [@source_changes, "the list of sources changed"], [@dependency_changes, "the dependencies in your gemfile changed"], - [@new_platform, "you added a new platform to your gemfile"], + [@current_platform_missing, "your lockfile is missing the current platform"], + [@new_platforms.any?, "you are adding a new platform to your lockfile"], [@path_changes, "the gemspecs for path gems changed"], [@local_changes, "the gemspecs for git local gems changed"], + [@missing_lockfile_dep, "your lockfile is missing \"#{@missing_lockfile_dep}\""], + [@unlocking_bundler, "an update to the version of Bundler itself was requested"], + [@locked_spec_with_missing_checksums, "your lockfile is missing a CHECKSUMS entry for \"#{@locked_spec_with_missing_checksums}\""], + [@locked_spec_with_empty_checksums, "your lockfile has an empty CHECKSUMS entry for \"#{@locked_spec_with_empty_checksums}\""], + [@locked_spec_with_missing_deps, "your lockfile includes \"#{@locked_spec_with_missing_deps}\" but not some of its dependencies"], + [@locked_spec_with_invalid_deps, "your lockfile does not satisfy dependencies of \"#{@locked_spec_with_invalid_deps}\""], ].select(&:first).map(&:last).join(", ") end - def pretty_dep(dep, source = false) - SharedHelpers.pretty_dependency(dep, source) + def pretty_dep(dep) + SharedHelpers.pretty_dependency(dep) end # Check if the specs of the given source changed @@ -572,19 +908,18 @@ module Bundler !locked || dependencies_for_source_changed?(source, locked) || specs_for_source_changed?(source) end - def dependencies_for_source_changed?(source, locked_source = source) - deps_for_source = @dependencies.select {|s| s.source == source } - locked_deps_for_source = @locked_deps.values.select {|dep| dep.source == locked_source } + def dependencies_for_source_changed?(source, locked_source) + deps_for_source = @dependencies.select {|dep| dep.source == source } + locked_deps_for_source = locked_dependencies.select {|dep| dep.source == locked_source } - Set.new(deps_for_source) != Set.new(locked_deps_for_source) + deps_for_source.uniq.sort != locked_deps_for_source.sort end def specs_for_source_changed?(source) locked_index = Index.new - locked_index.use(@locked_specs.select {|s| source.can_lock?(s) }) + locked_index.use(@locked_specs.select {|s| s.replace_source_with!(source) }) - # order here matters, since Index#== is checking source.specs.include?(locked_index) - locked_index != source.specs + !locked_index.subset?(source.specs) rescue PathError, GitError => e Bundler.ui.debug "Assuming that #{source} has not changed since fetching its specs errored (#{e})" false @@ -598,9 +933,9 @@ module Bundler Bundler.settings.local_overrides.map do |k, v| spec = @dependencies.find {|s| s.name == k } - source = spec && spec.source - if source && source.respond_to?(:local_override!) - source.unlock! if @unlock[:gems].include?(spec.name) + source = spec&.source + if source&.respond_to?(:local_override!) + source.unlock! if @gems_to_unlock.include?(spec.name) locals << [source, source.local_override!(v)] end end @@ -608,336 +943,322 @@ module Bundler sources_with_changes = locals.select do |source, changed| changed || specs_changed?(source) end.map(&:first) - !sources_with_changes.each {|source| @unlock[:sources] << source.name }.empty? + !sources_with_changes.each {|source| @sources_to_unlock << source.name }.empty? end - def converge_paths - sources.path_sources.any? do |source| - specs_changed?(source) - end - end + def check_lockfile + @locked_spec_with_invalid_deps = nil + @locked_spec_with_missing_deps = nil + @locked_spec_with_missing_checksums = nil + @locked_spec_with_empty_checksums = nil - def converge_path_source_to_gemspec_source(source) - return source unless source.instance_of?(Source::Path) - gemspec_source = sources.path_sources.find {|s| s.is_a?(Source::Gemspec) && s.as_path_source == source } - gemspec_source || source - end + missing_deps = [] + missing_checksums = [] + empty_checksums = [] + invalid = [] - def converge_path_sources_to_gemspec_sources - @locked_sources.map! do |source| - converge_path_source_to_gemspec_source(source) - end - @locked_specs.each do |spec| - spec.source &&= converge_path_source_to_gemspec_source(spec.source) - end - @locked_deps.each do |_, dep| - dep.source &&= converge_path_source_to_gemspec_source(dep.source) + @locked_specs.each do |s| + if @locked_checksums + checksum_store = s.source.checksum_store + + if checksum_store.missing?(s) + missing_checksums << s + elsif checksum_store.empty?(s) + empty_checksums << s + end + end + + validation = @locked_specs.validate_deps(s) + + missing_deps << s if validation == :missing + invalid << s if validation == :invalid end - end - def converge_rubygems_sources - return false if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? + @locked_spec_with_missing_checksums = missing_checksums.first.name if missing_checksums.any? + @locked_spec_with_empty_checksums = empty_checksums.first.name if empty_checksums.any? - changes = false + if missing_deps.any? + @locked_specs.delete(missing_deps) - # Get the RubyGems sources from the Gemfile.lock - locked_gem_sources = @locked_sources.select {|s| s.is_a?(Source::Rubygems) } - # Get the RubyGems remotes from the Gemfile - actual_remotes = sources.rubygems_remotes + @locked_spec_with_missing_deps = missing_deps.first.name + end - # If there is a RubyGems source in both - if !locked_gem_sources.empty? && !actual_remotes.empty? - locked_gem_sources.each do |locked_gem| - # Merge the remotes from the Gemfile into the Gemfile.lock - changes |= locked_gem.replace_remotes(actual_remotes, Bundler.settings[:allow_deployment_source_credential_changes]) - end + if invalid.any? + @locked_specs.delete(invalid) + + @locked_spec_with_invalid_deps = invalid.first.name end + end - changes + def converge_paths + sources.path_sources.any? do |source| + specs_changed?(source) + end end def converge_sources - changes = false - - changes |= converge_rubygems_sources - # Replace the sources from the Gemfile with the sources from the Gemfile.lock, # if they exist in the Gemfile.lock and are `==`. If you can't find an equivalent # source in the Gemfile.lock, use the one from the Gemfile. - changes |= sources.replace_sources!(@locked_sources) + changes = sources.replace_sources!(@locked_sources) sources.all_sources.each do |source| + # has to be done separately, because we want to keep the locked checksum + # store for a source, even when doing a full update + if @locked_checksums && @locked_gems && locked_source = @originally_locked_sources.find {|s| s == source && !s.equal?(source) } + source.checksum_store.merge!(locked_source.checksum_store) + end # If the source is unlockable and the current command allows an unlock of # the source (for example, you are doing a `bundle update <foo>` of a git-pinned # gem), unlock it. For git sources, this means to unlock the revision, which # will cause the `ref` used to be the most recent for the branch (or master) if # an explicit `ref` is not used. - if source.respond_to?(:unlock!) && @unlock[:sources].include?(source.name) + if source.respond_to?(:unlock!) && @sources_to_unlock.include?(source.name) source.unlock! changes = true end end + sources.metadata_source.checksum_store.merge!(@locked_gems.metadata_source.checksum_store) if @locked_gems + changes end def converge_dependencies - frozen = Bundler.frozen? - (@dependencies + @locked_deps.values).each do |dep| - locked_source = @locked_deps[dep.name] - # This is to make sure that if bundler is installing in deployment mode and - # after locked_source and sources don't match, we still use locked_source. - if frozen && !locked_source.nil? && - locked_source.respond_to?(:source) && locked_source.source.instance_of?(Source::Path) && locked_source.source.path.exist? - dep.source = locked_source.source - elsif dep.source + @missing_lockfile_dep = nil + @changed_dependencies = [] + + @dependencies.each do |dep| + if dep.source dep.source = sources.get(dep.source) end - if dep.source.is_a?(Source::Gemspec) - dep.platforms.concat(@platforms.map {|p| Dependency::REVERSE_PLATFORM_MAP[p] }.flatten(1)).uniq! + next unless relevant_deps?(dep) + + name = dep.name + + dep_changed = @locked_deps[name].nil? + + unless name == "bundler" + locked_specs = @originally_locked_specs[name] + + if locked_specs.empty? + @missing_lockfile_dep = name if dep_changed == false + else + if locked_specs.map(&:source).uniq.size > 1 + @locked_specs.delete(locked_specs.select {|s| s.source != dep.source }) + end + + unless apply_override_to(dep).matches_spec?(locked_specs.first) + @gems_to_unlock << name + dep_changed = true + end + end end + + @changed_dependencies << name if dep_changed end - changes = false - # We want to know if all match, but don't want to check all entries - # This means we need to return false if any dependency doesn't match - # the lock or doesn't exist in the lock. - @dependencies.each do |dependency| - unless locked_dep = @locked_deps[dependency.name] - changes = true - next - end + converge_overrides_outside_dependencies - # Gem::Dependency#== matches Gem::Dependency#type. As the lockfile - # doesn't carry a notion of the dependency type, if you use - # add_development_dependency in a gemspec that's loaded with the gemspec - # directive, the lockfile dependencies and resolved dependencies end up - # with a mismatch on #type. Work around that by setting the type on the - # dep from the lockfile. - locked_dep.instance_variable_set(:@type, dependency.type) + @changed_dependencies.any? + end - # We already know the name matches from the hash lookup - # so we only need to check the requirement now - changes ||= dependency.requirement != locked_dep.requirement - end + def converge_overrides_outside_dependencies + @overrides.each do |override| + # :all overrides are intentionally not pre-unlocked. They take effect on + # fresh resolution (no lockfile) or when the user runs `bundle update`. + # Forcing a full re-resolve from a single :all directive would surprise + # users with unrelated dependency churn. + next unless override.target.is_a?(String) - changes + name = override.target + next if @changed_dependencies.include?(name) + next if @originally_locked_specs[name].empty? + # version: overrides on direct deps are detected in the per-dep + # converge_dependencies loop via apply_override_to + matches_spec?. + # Other fields are not visible there, so they always reach here. + next if override.field == :version && @dependencies.any? {|d| d.name == name } + + @gems_to_unlock << name + @changed_dependencies << name + end end # Remove elements from the locked specs that are expired. This will most # commonly happen if the Gemfile has changed since the lockfile was last # generated def converge_locked_specs - deps = [] + converged = converge_specs(@locked_specs) - # Build a list of dependencies that are the same in the Gemfile - # and Gemfile.lock. If the Gemfile modified a dependency, but - # the gem in the Gemfile.lock still satisfies it, this is fine - # too. - @dependencies.each do |dep| - locked_dep = @locked_deps[dep.name] + resolve = SpecSet.new(converged) - # If the locked_dep doesn't match the dependency we're looking for then we ignore the locked_dep - locked_dep = nil unless locked_dep == dep + diff = nil - if in_locked_deps?(dep, locked_dep) || satisfies_locked_spec?(dep) - deps << dep - elsif dep.source.is_a?(Source::Path) && dep.current_platform? && (!locked_dep || dep.source != locked_dep.source) - @locked_specs.each do |s| - @unlock[:gems] << s.name if s.source == dep.source - end + # Now, we unlock any sources that do not have anymore gems pinned to it + sources.all_sources.each do |source| + next unless source.respond_to?(:unlock!) - dep.source.unlock! if dep.source.respond_to?(:unlock!) - dep.source.specs.each {|s| @unlock[:gems] << s.name } + unless resolve.any? {|s| s.source == source } + diff ||= @locked_specs.to_a - resolve.to_a + source.unlock! if diff.any? {|s| s.source == source } end end - unlock_source_unlocks_spec = Bundler.feature_flag.unlock_source_unlocks_spec? + resolve + end + def converge_specs(specs) converged = [] - @locked_specs.each do |s| - # Replace the locked dependency's source with the equivalent source from the Gemfile - dep = @dependencies.find {|d| s.satisfies?(d) } - s.source = (dep && dep.source) || sources.get(s.source) + deps = [] - # Don't add a spec to the list if its source is expired. For example, - # if you change a Git gem to RubyGems. - next if s.source.nil? - next if @unlock[:sources].include?(s.source.name) + specs.each do |s| + name = s.name + next if @gems_to_unlock.include?(name) - # XXX This is a backwards-compatibility fix to preserve the ability to - # unlock a single gem by passing its name via `--source`. See issue #3759 - # TODO: delete in Bundler 2 - next if unlock_source_unlocks_spec && @unlock[:sources].include?(s.name) + dep = @dependencies.find {|d| s.satisfies?(d) } + lockfile_source = s.source - # If the spec is from a path source and it doesn't exist anymore - # then we unlock it. + if dep + replacement_source = dep.source - # Path sources have special logic - if s.source.instance_of?(Source::Path) || s.source.instance_of?(Source::Gemspec) - other_sources_specs = begin - s.source.specs - rescue PathError, GitError - # if we won't need the source (according to the lockfile), - # don't error if the path/git source isn't available - next if @locked_specs. - for(requested_dependencies, [], false, true, false). - none? {|locked_spec| locked_spec.source == s.source } - - raise - end + deps << dep if !replacement_source || lockfile_source.include?(replacement_source) || new_deps.include?(dep) + else + parent_dep = @dependencies.find do |d| + next unless d.source && d.source != lockfile_source + next if d.source.is_a?(Source::Gemspec) - other = other_sources_specs[s].first + parent_locked_specs = @originally_locked_specs[d.name] - # If the spec is no longer in the path source, unlock it. This - # commonly happens if the version changed in the gemspec - next unless other + parent_locked_specs.any? do |parent_spec| + parent_spec.runtime_dependencies.any? {|rd| rd.name == s.name } + end + end - deps2 = other.dependencies.select {|d| d.type != :development } - runtime_dependencies = s.dependencies.select {|d| d.type != :development } - # If the dependencies of the path source have changed, unlock it - next unless runtime_dependencies.sort == deps2.sort + if parent_dep && parent_dep.source.is_a?(Source::Path) && parent_dep.source.specs[s]&.any? + replacement_source = parent_dep.source + else + replacement_source = sources.get(lockfile_source) + end end - converged << s - end - - resolve = SpecSet.new(converged) - resolve = resolve.for(expand_dependencies(deps, true), @unlock[:gems], false, false, false) - diff = nil + # Replace the locked dependency's source with the equivalent source from the Gemfile + s.source = replacement_source || default_source + next if s.source_changed? - # Now, we unlock any sources that do not have anymore gems pinned to it - sources.all_sources.each do |source| - next unless source.respond_to?(:unlock!) + source = s.source + next if @sources_to_unlock.include?(source.name) - unless resolve.any? {|s| s.source == source } - diff ||= @locked_specs.to_a - resolve.to_a - source.unlock! if diff.any? {|s| s.source == source } + # Path sources have special logic + if source.is_a?(Source::Path) + new_spec = source.specs[s].first + if new_spec + s.runtime_dependencies.replace(new_spec.runtime_dependencies) + else + # If the spec is no longer in the path source, unlock it. This + # commonly happens if the version changed in the gemspec + @gems_to_unlock << name + end end + + converged << s end - resolve + filter_specs(converged, deps, skips: @gems_to_unlock) end - def in_locked_deps?(dep, locked_dep) - # Because the lockfile can't link a dep to a specific remote, we need to - # treat sources as equivalent anytime the locked dep has all the remotes - # that the Gemfile dep does. - locked_dep && locked_dep.source && dep.source && locked_dep.source.include?(dep.source) + def metadata_dependencies + @metadata_dependencies ||= [ + Dependency.new("Ruby\0", Bundler::RubyVersion.system.gem_version), + Dependency.new("RubyGems\0", Gem::VERSION), + ] end - def satisfies_locked_spec?(dep) - @locked_specs[dep].any? {|s| s.satisfies?(dep) && (!dep.source || s.source.include?(dep.source)) } + def source_requirements + @source_requirements ||= find_source_requirements end - # This list of dependencies is only used in #resolve, so it's OK to add - # the metadata dependencies here - def expanded_dependencies - @expanded_dependencies ||= begin - expand_dependencies(dependencies + metadata_dependencies, @remote) - end + def preload_git_source_worker + workers = Bundler.settings.installation_parallelization + + @preload_git_source_worker ||= Bundler::Worker.new(workers, "Git source preloading", ->(source, _) { source.specs }) end - def metadata_dependencies - @metadata_dependencies ||= begin - ruby_versions = concat_ruby_version_requirements(@ruby_version) - if ruby_versions.empty? || !@ruby_version.exact? - concat_ruby_version_requirements(RubyVersion.system) - concat_ruby_version_requirements(locked_ruby_version_object) unless @unlock[:ruby] - end - [ - Dependency.new("ruby\0", ruby_versions), - Dependency.new("rubygems\0", Gem::VERSION), - ] + def preload_git_sources + if Gem.ruby_version < Gem::Version.new("3.3") + # Ruby 3.2 has a bug that incorrectly triggers a circular dependency warning. This version will continue to + # fetch git repositories one by one. + return end - end - def concat_ruby_version_requirements(ruby_version, ruby_versions = []) - return ruby_versions unless ruby_version - if ruby_version.patchlevel - ruby_versions << ruby_version.to_gem_version_with_patchlevel - else - ruby_versions.concat(ruby_version.versions.map do |version| - requirement = Gem::Requirement.new(version) - if requirement.exact? - "~> #{version}.0" - else - requirement - end - end) + begin + needed_git_sources.each {|source| preload_git_source_worker.enq(source) } + ensure + preload_git_source_worker.stop end end - def expand_dependencies(dependencies, remote = false) - sorted_platforms = Resolver.sort_platforms(@platforms) - deps = [] - dependencies.each do |dep| - dep = Dependency.new(dep, ">= 0") unless dep.respond_to?(:name) - next if !remote && !dep.current_platform? - platforms = dep.gem_platforms(sorted_platforms) - if platforms.empty? - mapped_platforms = dep.platforms.map {|p| Dependency::PLATFORM_MAP[p] } - Bundler.ui.warn \ - "The dependency #{dep} will be unused by any of the platforms Bundler is installing for. " \ - "Bundler is installing for #{@platforms.join ", "} but the dependency " \ - "is only for #{mapped_platforms.join ", "}. " \ - "To add those platforms to the bundle, " \ - "run `bundle lock --add-platform #{mapped_platforms.join " "}`." - end - platforms.each do |p| - deps << DepProxy.new(dep, p) if remote || p == generic_local_platform - end + # Git sources needed for the requested groups (excludes sources only used by --without groups) + def needed_git_sources + needed_deps = dependencies_for(requested_groups) + sources.git_sources.select do |source| + needed_deps.any? {|d| d.source == source } end - deps end - def requested_dependencies - groups = requested_groups - groups.map!(&:to_sym) - dependencies.reject {|d| !d.should_include? || (d.groups & groups).empty? } + # Git sources that should be excluded (only used by --without groups) + def excluded_git_sources + sources.git_sources - needed_git_sources end - def source_requirements - # Load all specs from remote sources - index + def find_source_requirements + preload_git_sources + + # Only safe to exclude when locked_requirements (merged below) backfills the gap. + nothing_changed = nothing_changed? + excluded = nothing_changed ? excluded_git_sources : [] # Record the specs available in each gem's source, so that those # specs will be available later when the resolver knows where to # look for that gemspec (or its dependencies) - default = sources.default_source - source_requirements = { :default => default } - default = nil unless Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? - dependencies.each do |dep| - next unless source = dep.source || default - source_requirements[dep.name] = source + source_requirements = if precompute_source_requirements_for_indirect_dependencies? + all_requirements = source_map.all_requirements(excluded) + { default: default_source }.merge(all_requirements) + else + { default: Source::RubygemsAggregate.new(sources, source_map, excluded) }.merge(source_map.direct_requirements) end + source_requirements.merge!(source_map.locked_requirements) if nothing_changed metadata_dependencies.each do |dep| source_requirements[dep.name] = sources.metadata_source end - source_requirements["bundler"] = sources.metadata_source # needs to come last to override + + default_bundler_source = source_requirements["bundler"] || default_source + + if @unlocking_bundler + default_bundler_source.add_dependency_names("bundler") + else + source_requirements[:default_bundler] = default_bundler_source + source_requirements["bundler"] = sources.metadata_source # needs to come last to override + end + source_requirements end - def pinned_spec_names(skip = nil) - pinned_names = [] - default = Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? && sources.default_source - @dependencies.each do |dep| - next unless dep_source = dep.source || default - next if dep_source == skip - pinned_names << dep.name - end - pinned_names + def default_source + sources.default_source end def requested_groups - groups - Bundler.settings[:without] - @optional_groups + Bundler.settings[:with] + values = groups - Bundler.settings[:without] - @optional_groups + Bundler.settings[:with] + values &= Bundler.settings[:only] unless Bundler.settings[:only].empty? + values end def lockfiles_equal?(current, proposed, preserve_unknown_sections) if preserve_unknown_sections sections_to_ignore = LockfileParser.sections_to_ignore(@locked_bundler_version) sections_to_ignore += LockfileParser.unknown_sections_in_lockfile(current) - sections_to_ignore += LockfileParser::ENVIRONMENT_VERSION_SECTIONS + sections_to_ignore << LockfileParser::RUBY + sections_to_ignore << LockfileParser::BUNDLED unless @unlocking_bundler pattern = /#{Regexp.union(sections_to_ignore)}\n(\s{2,}.*\n)+/ whitespace_cleanup = /\n{2,}/ current = current.gsub(pattern, "\n").gsub(whitespace_cleanup, "\n\n").strip @@ -946,39 +1267,63 @@ module Bundler current == proposed end - def extract_gem_info(error) - # This method will extract the error message like "Could not find foo-1.2.3 in any of the sources" - # to an array. The first element will be the gem name (e.g. foo), the second will be the version number. - error.message.scan(/Could not find (\w+)-(\d+(?:\.\d+)+)/).flatten + def additional_base_requirements_to_prevent_downgrades(resolution_base) + return resolution_base unless @locked_gems + @originally_locked_specs.each do |locked_spec| + next if locked_spec.source.is_a?(Source::Path) || locked_spec.source_changed? + + name = locked_spec.name + next if @changed_dependencies.include?(name) + + resolution_base.base_requirements[name] = Gem::Requirement.new(">= #{locked_spec.version}") + end + resolution_base end - def compute_requires - dependencies.reduce({}) do |requires, dep| - next requires unless dep.should_include? - requires[dep.name] = Array(dep.autorequire || dep.name).map do |file| - # Allow `require: true` as an alias for `require: <name>` - file == true ? dep.name : file - end - requires + def additional_base_requirements_to_force_updates(resolution_base) + return resolution_base if @explicit_unlocks.empty? + full_update = SpecSet.new(new_resolver_for_full_update.start) + @explicit_unlocks.each do |name| + version = full_update.version_for(name) + resolution_base.base_requirements[name] = Gem::Requirement.new("= #{version}") if version end + resolution_base end - def additional_base_requirements_for_resolve - return [] unless @locked_gems && Bundler.feature_flag.only_update_to_newer_versions? - dependencies_by_name = dependencies.group_by(&:name) - @locked_gems.specs.reduce({}) do |requirements, locked_spec| - name = locked_spec.name - next requirements if @locked_deps[name] != dependencies_by_name[name] - dep = Gem::Dependency.new(name, ">= #{locked_spec.version}") - requirements[name] = DepProxy.new(dep, locked_spec.platform) - requirements - end.values + def remove_invalid_platforms! + return if Bundler.frozen_bundle? + + skips = (@new_platforms + [Bundler.local_platform]).uniq + + # We should probably avoid removing non-ruby platforms, since that means + # lockfile will no longer install on those platforms, so a error to give + # heads up to the user may be better. However, we have tests expecting + # non ruby platform autoremoval to work, so leaving that in place for + # now. + skips |= platforms - [Gem::Platform::RUBY] if @dependency_changes + + @originally_invalid_platforms = @originally_locked_specs.remove_invalid_platforms!(current_dependencies, platforms, skips: skips) end - def equivalent_rubygems_remotes?(source) - return false unless source.is_a?(Source::Rubygems) + def source_map + @source_map ||= SourceMap.new(sources, dependencies, @locked_specs) + end + + def new_resolver_for_full_update + new_resolver(unlocked_resolution_base) + end + + def unlocked_resolution_base + new_resolution_base(last_resolve: SpecSet.new([]), unlock: true) + end + + def new_resolution_base(last_resolve:, unlock:) + new_resolution_platforms = @current_platform_missing ? @new_platforms + [Bundler.local_platform] : @new_platforms + Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local, new_platforms: new_resolution_platforms, overrides: @overrides) + end - Bundler.settings[:allow_deployment_source_credential_changes] && source.equivalent_remotes?(sources.rubygems_remotes) + def new_resolver(base) + Resolver.new(base, gem_version_promoter, @most_specific_locked_platform) end end end diff --git a/lib/bundler/dep_proxy.rb b/lib/bundler/dep_proxy.rb deleted file mode 100644 index 7a9423b14a..0000000000 --- a/lib/bundler/dep_proxy.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -module Bundler - class DepProxy - attr_reader :__platform, :dep - - def initialize(dep, platform) - @dep = dep - @__platform = platform - end - - def hash - @hash ||= dep.hash - end - - def ==(other) - return if other.nil? - dep == other.dep && __platform == other.__platform - end - - alias_method :eql?, :== - - def type - @dep.type - end - - def name - @dep.name - end - - def requirement - @dep.requirement - end - - def to_s - s = name.dup - s << " (#{requirement})" unless requirement == Gem::Requirement.default - s << " #{__platform}" unless __platform == Gem::Platform::RUBY - s - end - - private - - def method_missing(*args, &blk) - @dep.send(*args, &blk) - end - end -end diff --git a/lib/bundler/dependency.rb b/lib/bundler/dependency.rb index 24257bc113..cb9c7a76ea 100644 --- a/lib/bundler/dependency.rb +++ b/lib/bundler/dependency.rb @@ -1,124 +1,137 @@ # frozen_string_literal: true require "rubygems/dependency" -require "bundler/shared_helpers" -require "bundler/rubygems_ext" +require_relative "shared_helpers" module Bundler class Dependency < Gem::Dependency - attr_reader :autorequire - attr_reader :groups - attr_reader :platforms - - PLATFORM_MAP = { - :ruby => Gem::Platform::RUBY, - :ruby_18 => Gem::Platform::RUBY, - :ruby_19 => Gem::Platform::RUBY, - :ruby_20 => Gem::Platform::RUBY, - :ruby_21 => Gem::Platform::RUBY, - :ruby_22 => Gem::Platform::RUBY, - :ruby_23 => Gem::Platform::RUBY, - :ruby_24 => Gem::Platform::RUBY, - :ruby_25 => Gem::Platform::RUBY, - :mri => Gem::Platform::RUBY, - :mri_18 => Gem::Platform::RUBY, - :mri_19 => Gem::Platform::RUBY, - :mri_20 => Gem::Platform::RUBY, - :mri_21 => Gem::Platform::RUBY, - :mri_22 => Gem::Platform::RUBY, - :mri_23 => Gem::Platform::RUBY, - :mri_24 => Gem::Platform::RUBY, - :mri_25 => Gem::Platform::RUBY, - :rbx => Gem::Platform::RUBY, - :jruby => Gem::Platform::JAVA, - :jruby_18 => Gem::Platform::JAVA, - :jruby_19 => Gem::Platform::JAVA, - :mswin => Gem::Platform::MSWIN, - :mswin_18 => Gem::Platform::MSWIN, - :mswin_19 => Gem::Platform::MSWIN, - :mswin_20 => Gem::Platform::MSWIN, - :mswin_21 => Gem::Platform::MSWIN, - :mswin_22 => Gem::Platform::MSWIN, - :mswin_23 => Gem::Platform::MSWIN, - :mswin_24 => Gem::Platform::MSWIN, - :mswin_25 => Gem::Platform::MSWIN, - :mswin64 => Gem::Platform::MSWIN64, - :mswin64_19 => Gem::Platform::MSWIN64, - :mswin64_20 => Gem::Platform::MSWIN64, - :mswin64_21 => Gem::Platform::MSWIN64, - :mswin64_22 => Gem::Platform::MSWIN64, - :mswin64_23 => Gem::Platform::MSWIN64, - :mswin64_24 => Gem::Platform::MSWIN64, - :mswin64_25 => Gem::Platform::MSWIN64, - :mingw => Gem::Platform::MINGW, - :mingw_18 => Gem::Platform::MINGW, - :mingw_19 => Gem::Platform::MINGW, - :mingw_20 => Gem::Platform::MINGW, - :mingw_21 => Gem::Platform::MINGW, - :mingw_22 => Gem::Platform::MINGW, - :mingw_23 => Gem::Platform::MINGW, - :mingw_24 => Gem::Platform::MINGW, - :mingw_25 => Gem::Platform::MINGW, - :x64_mingw => Gem::Platform::X64_MINGW, - :x64_mingw_20 => Gem::Platform::X64_MINGW, - :x64_mingw_21 => Gem::Platform::X64_MINGW, - :x64_mingw_22 => Gem::Platform::X64_MINGW, - :x64_mingw_23 => Gem::Platform::X64_MINGW, - :x64_mingw_24 => Gem::Platform::X64_MINGW, - :x64_mingw_25 => Gem::Platform::X64_MINGW, - }.freeze - - REVERSE_PLATFORM_MAP = {}.tap do |reverse_platform_map| - PLATFORM_MAP.each do |key, value| - reverse_platform_map[value] ||= [] - reverse_platform_map[value] << key - end - - reverse_platform_map.each {|_, platforms| platforms.freeze } - end.freeze - def initialize(name, version, options = {}, &blk) type = options["type"] || :runtime super(name, version, type) - @autorequire = nil - @groups = Array(options["group"] || :default).map(&:to_sym) - @source = options["source"] - @platforms = Array(options["platforms"]) - @env = options["env"] - @should_include = options.fetch("should_include", true) + @options = options + end + + def groups + @groups ||= Array(@options["group"] || :default).map(&:to_sym) + end + + def source + return @source if defined?(@source) + + @source = @options["source"] + end + + def path + return @path if defined?(@path) + + @path = @options["path"] + end + + def git + return @git if defined?(@git) + + @git = @options["git"] + end + + def github + return @github if defined?(@github) + + @github = @options["github"] + end + + def branch + return @branch if defined?(@branch) + + @branch = @options["branch"] + end + + def ref + return @ref if defined?(@ref) - @autorequire = Array(options["require"] || []) if options.key?("require") + @ref = @options["ref"] end + def glob + return @glob if defined?(@glob) + + @glob = @options["glob"] + end + + def platforms + @platforms ||= Array(@options["platforms"]) + end + + def env + return @env if defined?(@env) + + @env = @options["env"] + end + + def should_include + @should_include ||= @options.fetch("should_include", true) + end + + def gemfile + return @gemfile if defined?(@gemfile) + + @gemfile = @options["gemfile"] + end + + def force_ruby_platform + return @force_ruby_platform if defined?(@force_ruby_platform) + + @force_ruby_platform = @options["force_ruby_platform"] + end + + def autorequire + return @autorequire if defined?(@autorequire) + + @autorequire = Array(@options["require"] || []) if @options.key?("require") + end + + RUBY_PLATFORM_ARRAY = [Gem::Platform::RUBY].freeze + private_constant :RUBY_PLATFORM_ARRAY + # Returns the platforms this dependency is valid for, in the same order as # passed in the `valid_platforms` parameter def gem_platforms(valid_platforms) - return valid_platforms if @platforms.empty? + return RUBY_PLATFORM_ARRAY if force_ruby_platform + return valid_platforms if platforms.empty? - @gem_platforms ||= @platforms.map {|pl| PLATFORM_MAP[pl] }.compact.uniq + valid_platforms.select {|p| expanded_platforms.include?(Gem::Platform.generic(p)) } + end - valid_platforms & @gem_platforms + def expanded_platforms + @expanded_platforms ||= platforms.filter_map {|pl| CurrentRuby::PLATFORM_MAP[pl] }.flatten.uniq end def should_include? - @should_include && current_env? && current_platform? + should_include && current_env? && current_platform? + end + + def gemspec_dev_dep? + @gemspec_dev_dep ||= @options.fetch("gemspec_dev_dep", false) + end + + def gemfile_dep? + !gemspec_dev_dep? end def current_env? - return true unless @env - if @env.is_a?(Hash) - @env.all? do |key, val| + return true unless env + if env.is_a?(Hash) + env.all? do |key, val| ENV[key.to_s] && (val.is_a?(String) ? ENV[key.to_s] == val : ENV[key.to_s] =~ val) end else - ENV[@env.to_s] + ENV[env.to_s] end end def current_platform? - return true if @platforms.empty? - @platforms.any? do |p| + return true if platforms.empty? + platforms.any? do |p| Bundler.current_ruby.send("#{p}?") end end @@ -126,7 +139,7 @@ module Bundler def to_lock out = super out << "!" if source - out << "\n" + out end def specific? diff --git a/lib/bundler/deployment.rb b/lib/bundler/deployment.rb index 291e158ca0..3344449e82 100644 --- a/lib/bundler/deployment.rb +++ b/lib/bundler/deployment.rb @@ -1,69 +1,6 @@ # frozen_string_literal: true -require "bundler/shared_helpers" -Bundler::SharedHelpers.major_deprecation 2, "Bundler no longer integrates with " \ +require_relative "shared_helpers" +Bundler::SharedHelpers.feature_removed! "Bundler no longer integrates with " \ "Capistrano, but Capistrano provides its own integration with " \ "Bundler via the capistrano-bundler gem. Use it instead." - -module Bundler - class Deployment - def self.define_task(context, task_method = :task, opts = {}) - if defined?(Capistrano) && context.is_a?(Capistrano::Configuration) - context_name = "capistrano" - role_default = "{:except => {:no_release => true}}" - error_type = ::Capistrano::CommandError - else - context_name = "vlad" - role_default = "[:app]" - error_type = ::Rake::CommandFailedError - end - - roles = context.fetch(:bundle_roles, false) - opts[:roles] = roles if roles - - context.send :namespace, :bundle do - send :desc, <<-DESC - Install the current Bundler environment. By default, gems will be \ - installed to the shared/bundle path. Gems in the development and \ - test group will not be installed. The install command is executed \ - with the --deployment and --quiet flags. If the bundle cmd cannot \ - be found then you can override the bundle_cmd variable to specify \ - which one it should use. The base path to the app is fetched from \ - the :latest_release variable. Set it for custom deploy layouts. - - You can override any of these defaults by setting the variables shown below. - - N.B. bundle_roles must be defined before you require 'bundler/#{context_name}' \ - in your deploy.rb file. - - set :bundle_gemfile, "Gemfile" - set :bundle_dir, File.join(fetch(:shared_path), 'bundle') - set :bundle_flags, "--deployment --quiet" - set :bundle_without, [:development, :test] - set :bundle_with, [:mysql] - set :bundle_cmd, "bundle" # e.g. "/opt/ruby/bin/bundle" - set :bundle_roles, #{role_default} # e.g. [:app, :batch] - DESC - send task_method, :install, opts do - bundle_cmd = context.fetch(:bundle_cmd, "bundle") - bundle_flags = context.fetch(:bundle_flags, "--deployment --quiet") - bundle_dir = context.fetch(:bundle_dir, File.join(context.fetch(:shared_path), "bundle")) - bundle_gemfile = context.fetch(:bundle_gemfile, "Gemfile") - bundle_without = [*context.fetch(:bundle_without, [:development, :test])].compact - bundle_with = [*context.fetch(:bundle_with, [])].compact - app_path = context.fetch(:latest_release) - if app_path.to_s.empty? - raise error_type.new("Cannot detect current release path - make sure you have deployed at least once.") - end - args = ["--gemfile #{File.join(app_path, bundle_gemfile)}"] - args << "--path #{bundle_dir}" unless bundle_dir.to_s.empty? - args << bundle_flags.to_s - args << "--without #{bundle_without.join(" ")}" unless bundle_without.empty? - args << "--with #{bundle_with.join(" ")}" unless bundle_with.empty? - - run "cd #{app_path} && #{bundle_cmd} install #{args.join(" ")}" - end - end - end - end -end diff --git a/lib/bundler/deprecate.rb b/lib/bundler/deprecate.rb index 387f632a39..f59533630e 100644 --- a/lib/bundler/deprecate.rb +++ b/lib/bundler/deprecate.rb @@ -8,7 +8,8 @@ rescue LoadError end module Bundler - if defined? Bundler::Deprecate + # If Bundler::Deprecate is an autoload constant, we need to define it + if defined?(Bundler::Deprecate) && !autoload?(:Deprecate) # nothing to do! elsif defined? ::Deprecate Deprecate = ::Deprecate diff --git a/lib/bundler/digest.rb b/lib/bundler/digest.rb new file mode 100644 index 0000000000..158803033d --- /dev/null +++ b/lib/bundler/digest.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +# This code was extracted from https://github.com/Solistra/ruby-digest which is under public domain +module Bundler + module Digest + # The initial constant values for the 32-bit constant words A, B, C, D, and + # E, respectively. + SHA1_WORDS = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0].freeze + + # The 8-bit field used for bitwise `AND` masking. Defaults to `0xFFFFFFFF`. + SHA1_MASK = 0xFFFFFFFF + + class << self + def sha1(string) + unless string.is_a?(String) + raise TypeError, "can't convert #{string.class.inspect} into String" + end + + buffer = string.b + + words = SHA1_WORDS.dup + generate_split_buffer(buffer) do |chunk| + w = [] + chunk.each_slice(4) do |a, b, c, d| + w << (((a << 8 | b) << 8 | c) << 8 | d) + end + a, b, c, d, e = *words + (16..79).each do |i| + w[i] = SHA1_MASK & rotate(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1) + end + 0.upto(79) do |i| + case i + when 0..19 + f = ((b & c) | (~b & d)) + k = 0x5A827999 + when 20..39 + f = (b ^ c ^ d) + k = 0x6ED9EBA1 + when 40..59 + f = ((b & c) | (b & d) | (c & d)) + k = 0x8F1BBCDC + when 60..79 + f = (b ^ c ^ d) + k = 0xCA62C1D6 + end + t = SHA1_MASK & rotate(a, 5) + f + e + k + w[i] + a, b, c, d, e = t, a, SHA1_MASK & rotate(b, 30), c, d # rubocop:disable Style/ParallelAssignment + end + mutated = [a, b, c, d, e] + words.map!.with_index {|word, index| SHA1_MASK & (word + mutated[index]) } + end + + words.pack("N*").unpack1("H*") + end + + private + + def generate_split_buffer(string, &block) + size = string.bytesize * 8 + buffer = string.bytes << 128 + buffer << 0 while buffer.size % 64 != 56 + buffer.concat([size].pack("Q>").bytes) + buffer.each_slice(64, &block) + end + + def rotate(value, spaces) + value << spaces | value >> (32 - spaces) + end + end + end +end diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index 8681163277..6e2638a8be 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require "bundler/dependency" -require "bundler/ruby_dsl" +require_relative "dependency" +require_relative "ruby_dsl" module Bundler class Dsl @@ -9,16 +9,20 @@ module Bundler def self.evaluate(gemfile, lockfile, unlock) builder = new + builder.lockfile(lockfile) builder.eval_gemfile(gemfile) - builder.to_definition(lockfile, unlock) + builder.to_definition(builder.lockfile_path, unlock) end - VALID_PLATFORMS = Bundler::Dependency::PLATFORM_MAP.keys.freeze + VALID_PLATFORMS = Bundler::CurrentRuby::PLATFORM_MAP.keys.freeze VALID_KEYS = %w[group groups git path glob name branch ref tag require submodules - platform platforms type source install_if].freeze + platform platforms source install_if force_ruby_platform].freeze - attr_reader :gemspecs + GITHUB_PULL_REQUEST_URL = %r{\Ahttps://github\.com/([A-Za-z0-9_\-\.]+/[A-Za-z0-9_\-\.]+)/pull/(\d+)\z} + GITLAB_MERGE_REQUEST_URL = %r{\Ahttps://gitlab\.com/([A-Za-z0-9_\-\./]+)/-/merge_requests/(\d+)\z} + + attr_reader :gemspecs, :gemfile, :overrides attr_accessor :dependencies def initialize @@ -35,24 +39,26 @@ module Bundler @gemspecs = [] @gemfile = nil @gemfiles = [] + @lockfile = nil + @overrides = [] add_git_sources end def eval_gemfile(gemfile, contents = nil) - expanded_gemfile_path = Pathname.new(gemfile).expand_path(@gemfile && @gemfile.parent) - original_gemfile = @gemfile - @gemfile = expanded_gemfile_path - @gemfiles << expanded_gemfile_path - contents ||= Bundler.read_file(@gemfile.to_s) - instance_eval(contents.dup.untaint, gemfile.to_s, 1) - rescue Exception => e - message = "There was an error " \ - "#{e.is_a?(GemfileEvalError) ? "evaluating" : "parsing"} " \ - "`#{File.basename gemfile.to_s}`: #{e.message}" - - raise DSLError.new(message, gemfile, e.backtrace, contents) - ensure - @gemfile = original_gemfile + with_gemfile(gemfile) do |current_gemfile| + contents ||= Bundler.read_file(current_gemfile) + instance_eval(contents, current_gemfile, 1) + rescue GemfileEvalError => e + message = "There was an error evaluating `#{File.basename current_gemfile}`: #{e.message}" + raise DSLError.new(message, current_gemfile, e.backtrace, contents) + rescue GemfileError, InvalidArgumentError, InvalidOption, DeprecatedError, ScriptError => e + message = "There was an error parsing `#{File.basename current_gemfile}`: #{e.message}" + raise DSLError.new(message, current_gemfile, e.backtrace, contents) + rescue StandardError => e + raise unless e.backtrace_locations.first.path == current_gemfile + message = "There was an error parsing `#{File.basename current_gemfile}`: #{e.message}" + raise DSLError.new(message, current_gemfile, e.backtrace, contents) + end end def gemspec(opts = nil) @@ -63,25 +69,23 @@ module Bundler development_group = opts[:development_group] || :development expanded_path = gemfile_root.join(path) - gemspecs = Dir[File.join(expanded_path, "{,*}.gemspec")].map {|g| Bundler.load_gemspec(g) }.compact + gemspecs = Gem::Util.glob_files_in_dir("{,*}.gemspec", expanded_path).filter_map {|g| Bundler.load_gemspec(g) } gemspecs.reject! {|s| s.name != name } if name - Index.sort_specs(gemspecs) specs_by_name_and_version = gemspecs.group_by {|s| [s.name, s.version] } case specs_by_name_and_version.size when 1 specs = specs_by_name_and_version.values.first - spec = specs.find {|s| s.match_platform(Bundler.local_platform) } || specs.first + spec = specs.find {|s| s.installable_on_platform?(Bundler.local_platform) } || specs.first @gemspecs << spec - gem_platforms = Bundler::Dependency::REVERSE_PLATFORM_MAP[Bundler::GemHelpers.generic_local_platform] - gem spec.name, :name => spec.name, :path => path, :glob => glob, :platforms => gem_platforms + path path, "glob" => glob, "name" => spec.name, "gemspec" => spec do + add_dependency spec.name + end - group(development_group) do - spec.development_dependencies.each do |dep| - gem dep.name, *(dep.requirement.as_list + [:type => :development]) - end + spec.development_dependencies.each do |dep| + add_dependency dep.name, dep.requirement.as_list, "gemspec_dev_dep" => true, "group" => development_group end when 0 raise InvalidOption, "There are no gemspecs at #{expanded_path}" @@ -97,42 +101,26 @@ module Bundler normalize_options(name, version, options) - dep = Dependency.new(name, version, options) - - # if there's already a dependency with this name we try to prefer one - if current = @dependencies.find {|d| d.name == dep.name } - deleted_dep = @dependencies.delete(current) if current.type == :development - - if current.requirement != dep.requirement - unless deleted_dep - return if dep.type == :development - raise GemfileError, "You cannot specify the same gem twice with different version requirements.\n" \ - "You specified: #{current.name} (#{current.requirement}) and #{dep.name} (#{dep.requirement})" - end - - else - Bundler.ui.warn "Your Gemfile lists the gem #{current.name} (#{current.requirement}) more than once.\n" \ - "You should probably keep only one of them.\n" \ - "While it's not a problem now, it could cause errors if you change the version of one of them later." - end + add_dependency(name, version, options) + end - if current.source != dep.source - unless deleted_dep - return if dep.type == :development - raise GemfileError, "You cannot specify the same gem twice coming from different sources.\n" \ - "You specified that #{dep.name} (#{dep.requirement}) should come from " \ - "#{current.source || "an unspecified source"} and #{dep.source}\n" - end - end - end + # For usage in Dsl.evaluate, since lockfile is used as part of the Gemfile. + def lockfile_path + @lockfile + end - @dependencies << dep + def lockfile(file) + @lockfile = file end def source(source, *args, &blk) options = args.last.is_a?(Hash) ? args.pop.dup : {} options = normalize_hash(options) source = normalize_source(source) + cooldown = options["cooldown"] + if cooldown && !(cooldown.is_a?(Integer) && cooldown >= 0) + raise InvalidOption, "Expected `cooldown` to be a non-negative integer, got #{cooldown.inspect}" + end if options.key?("type") options["type"] = options["type"].to_s @@ -147,10 +135,9 @@ module Bundler source_opts = options.merge("uri" => source) with_source(@sources.add_plugin_source(options["type"], source_opts), &blk) elsif block_given? - with_source(@sources.add_rubygems_source("remotes" => source), &blk) + with_source(@sources.add_rubygems_source("remotes" => source, "cooldown" => cooldown), &blk) else - check_primary_source_safety(@sources) - @sources.global_rubygems_source = source + @sources.add_global_rubygems_remote(source, cooldown: cooldown) end end @@ -168,24 +155,13 @@ module Bundler end def path(path, options = {}, &blk) - unless block_given? - msg = "You can no longer specify a path source by itself. Instead, \n" \ - "either use the :path option on a gem, or specify the gems that \n" \ - "bundler should find in the path source by passing a block to \n" \ - "the path method, like: \n\n" \ - " path 'dir/containing/rails' do\n" \ - " gem 'rails'\n" \ - " end\n\n" - - raise DeprecatedError, msg if Bundler.feature_flag.disable_multisource? - SharedHelpers.major_deprecation(2, msg.strip) - end - source_options = normalize_hash(options).merge( "path" => Pathname.new(path), - "root_path" => gemfile_root, - "gemspec" => gemspecs.find {|g| g.name == options["name"] } + "root_path" => gemfile_root ) + + source_options["global"] = true unless block_given? + source = @sources.add_path_source(source_options) with_source(source, &blk) end @@ -206,16 +182,39 @@ module Bundler end def github(repo, options = {}) - raise ArgumentError, "GitHub sources require a block" unless block_given? - raise DeprecatedError, "The #github method has been removed" if Bundler.feature_flag.skip_default_git_sources? + raise InvalidArgumentError, "GitHub sources require a block" unless block_given? github_uri = @git_sources["github"].call(repo) git_options = normalize_hash(options).merge("uri" => github_uri) git_source = @sources.add_git_source(git_options) with_source(git_source) { yield } end + SUPPORTED_OVERRIDE_FIELDS = [:version, :required_ruby_version, :required_rubygems_version].freeze + SUPPORTED_OVERRIDE_SYMBOL_OPERATIONS = [:ignore_upper].freeze + + def override(target, **operations) + validate_override_target!(target) + + if target == :all && operations.key?(:version) + raise ArgumentError, "`override :all, version:` is not allowed; version requirements are per-gem" + end + + operations.each do |field, operation| + validate_override_field!(field) + validate_override_operation!(operation) + validate_override_uniqueness!(target, field) + end + + source_location = caller_locations(1, 1)&.first + operations.each do |field, operation| + @overrides << Override.new(target, field, operation, source_location: source_location) + end + end + def to_definition(lockfile, unlock) - Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles) + check_primary_source_safety + lockfile = @lockfile unless @lockfile.nil? + Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles, @overrides) end def group(*args, &blk) @@ -265,57 +264,168 @@ module Bundler raise GemfileError, "Undefined local variable or method `#{name}' for Gemfile" end - private + def check_primary_source_safety + check_path_source_safety + check_rubygems_source_safety + end - def add_git_sources - return if Bundler.feature_flag.skip_default_git_sources? + private + + def validate_override_target!(target) + return if target == :all + return if target.is_a?(String) + raise ArgumentError, "override target must be :all or a gem name string, got #{target.inspect}" + end + + def validate_override_field!(field) + return if SUPPORTED_OVERRIDE_FIELDS.include?(field) + supported = SUPPORTED_OVERRIDE_FIELDS.map {|f| "`#{f}:`" }.join(", ") + raise ArgumentError, "unsupported override field `#{field}:`; supported fields: #{supported}" + end + + def validate_override_operation!(operation) + case operation + when String + Gem::Requirement.new(operation) + when nil + # ok + when Symbol + return if SUPPORTED_OVERRIDE_SYMBOL_OPERATIONS.include?(operation) + raise ArgumentError, "unsupported override operation: #{operation.inspect}" + else + raise ArgumentError, "override operation must be a String, Symbol, or nil, got #{operation.inspect}" + end + rescue Gem::Requirement::BadRequirementError => e + raise ArgumentError, "invalid override version requirement #{operation.inspect}: #{e.message}" + end + + def validate_override_uniqueness!(target, field) + return unless @overrides.any? {|o| o.target == target && o.field == field } + raise ArgumentError, "duplicate override for #{target.inspect} `#{field}:`" + end + + def add_dependency(name, version = nil, options = {}) + options["gemfile"] = @gemfile + options["source"] ||= @source + options["env"] ||= @env + + dep = Dependency.new(name, version, options) + + # if there's already a dependency with this name we try to prefer one + if current = @dependencies.find {|d| d.name == name } + if current.requirement != dep.requirement + current_requirement_open = current.requirements_list.include?(">= 0") + + gemspec_dep = [dep, current].find(&:gemspec_dev_dep?) + if gemspec_dep + require_relative "vendor/pub_grub/lib/pub_grub/version_range" + require_relative "vendor/pub_grub/lib/pub_grub/version_constraint" + require_relative "vendor/pub_grub/lib/pub_grub/version_union" + require_relative "vendor/pub_grub/lib/pub_grub/rubygems" + + current_gemspec_range = PubGrub::RubyGems.requirement_to_range(current.requirement) + next_gemspec_range = PubGrub::RubyGems.requirement_to_range(dep.requirement) + + if current_gemspec_range.intersects?(next_gemspec_range) + dep = Dependency.new(name, current.requirement.as_list + dep.requirement.as_list, options) + else + gemfile_dep = [dep, current].find(&:gemfile_dep?) + + if gemfile_dep + raise GemfileError, "The #{name} dependency has conflicting requirements in Gemfile (#{gemfile_dep.requirement}) and gemspec (#{gemspec_dep.requirement})" + else + raise GemfileError, "Two gemspec development dependencies have conflicting requirements on the same gem: #{dep} and #{current}" + end + end + else + update_prompt = "" + + if File.basename(@gemfile) == Injector::INJECTED_GEMS + if dep.requirements_list.include?(">= 0") && !current_requirement_open + update_prompt = ". Gem already added" + else + update_prompt = ". If you want to update the gem version, run `bundle update #{name}`" + + update_prompt += ". You may also need to change the version requirement specified in the Gemfile if it's too restrictive." unless current_requirement_open + end + end + + raise GemfileError, "You cannot specify the same gem twice with different version requirements.\n" \ + "You specified: #{name} (#{current.requirement}) and #{name} (#{dep.requirement})" \ + "#{update_prompt}" + end + end + unless current.gemspec_dev_dep? && dep.gemspec_dev_dep? + # Always prefer the dependency from the Gemfile + if current.gemspec_dev_dep? + @dependencies.delete(current) + elsif dep.gemspec_dev_dep? + return + elsif current.source.to_s != dep.source.to_s + raise GemfileError, "You cannot specify the same gem twice coming from different sources.\n" \ + "You specified that #{name} (#{dep.requirement}) should come from " \ + "#{current.source || "an unspecified source"} and #{dep.source}\n" + else + Bundler.ui.warn "Your Gemfile lists the gem #{name} (#{current.requirement}) more than once.\n" \ + "You should probably keep only one of them.\n" \ + "Remove any duplicate entries and specify the gem only once.\n" \ + "While it's not a problem now, it could cause errors if you change the version of one of them later." + end + end + end + + @dependencies << dep + end + + def with_gemfile(gemfile) + expanded_gemfile_path = Pathname.new(gemfile).expand_path(@gemfile&.parent) + original_gemfile = @gemfile + @gemfile = expanded_gemfile_path + @gemfiles << expanded_gemfile_path + yield @gemfile.to_s + ensure + @gemfile = original_gemfile + end + + def add_git_sources git_source(:github) do |repo_name| - warn_deprecated_git_source(:github, <<-'RUBY'.strip, 'Change any "reponame" :github sources to "username/reponame".') -"https://github.com/#{repo_name}.git" - RUBY - # It would be better to use https instead of the git protocol, but this - # can break deployment of existing locked bundles when switching between - # different versions of Bundler. The change will be made in 2.0, which - # does not guarantee compatibility with the 1.x series. - # - # See https://github.com/bundler/bundler/pull/2569 for discussion - # - # This can be overridden by adding this code to your Gemfiles: - # - # git_source(:github) do |repo_name| - # repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") - # "https://github.com/#{repo_name}.git" - # end - repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") - # TODO: 2.0 upgrade this setting to the default - if Bundler.settings["github.https"] - Bundler::SharedHelpers.major_deprecation 2, "The `github.https` setting will be removed" - "https://github.com/#{repo_name}.git" + if repo_name =~ GITHUB_PULL_REQUEST_URL + { + "git" => "https://github.com/#{$1}.git", + "branch" => nil, + "ref" => "refs/pull/#{$2}/head", + "tag" => nil, + } else - "git://github.com/#{repo_name}.git" + repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") + "https://github.com/#{repo_name}.git" end end - # TODO: 2.0 remove this deprecated git source git_source(:gist) do |repo_name| - warn_deprecated_git_source(:gist, '"https://gist.github.com/#{repo_name}.git"') - "https://gist.github.com/#{repo_name}.git" end - # TODO: 2.0 remove this deprecated git source git_source(:bitbucket) do |repo_name| - warn_deprecated_git_source(:bitbucket, <<-'RUBY'.strip) -user_name, repo_name = repo_name.split("/") -repo_name ||= user_name -"https://#{user_name}@bitbucket.org/#{user_name}/#{repo_name}.git" - RUBY - user_name, repo_name = repo_name.split("/") repo_name ||= user_name "https://#{user_name}@bitbucket.org/#{user_name}/#{repo_name}.git" end + + git_source(:gitlab) do |repo_name| + if repo_name =~ GITLAB_MERGE_REQUEST_URL + { + "git" => "https://gitlab.com/#{$1}.git", + "branch" => nil, + "ref" => "refs/merge-requests/#{$2}/head", + "tag" => nil, + } + else + repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") + "https://gitlab.com/#{repo_name}.git" + end + end end def with_source(source) @@ -344,12 +454,10 @@ repo_name ||= user_name if name.is_a?(Symbol) raise GemfileError, %(You need to specify gem names as Strings. Use 'gem "#{name}"' instead) end - if name =~ /\s/ + if /\s/.match?(name) raise GemfileError, %('#{name}' is not a valid gem name because it contains whitespace) end - if name.empty? - raise GemfileError, %(an empty gem name is not valid) - end + raise GemfileError, %(an empty gem name is not valid) if name.empty? normalize_hash(opts) @@ -376,6 +484,13 @@ repo_name ||= user_name raise GemfileError, "`#{p}` is not a valid platform. The available options are: #{VALID_PLATFORMS.inspect}" end + windows_platforms = platforms.select {|pl| pl.to_s.match?(/mingw|mswin/) } + if windows_platforms.any? + windows_platforms = windows_platforms.map! {|pl| ":#{pl}" }.join(", ") + deprecated_message = "Platform #{windows_platforms} will be removed in the future. Please use platform :windows instead." + Bundler::SharedHelpers.feature_deprecated! deprecated_message + end + # Save sources passed in a key if opts.key?("source") source = normalize_source(opts["source"]) @@ -384,7 +499,11 @@ repo_name ||= user_name git_name = (git_names & opts.keys).last if @git_sources[git_name] - opts["git"] = @git_sources[git_name].call(opts[git_name]) + git_opts = @git_sources[git_name].call(opts[git_name]) + git_opts = { "git" => git_opts } if git_opts.is_a?(String) + opts.merge!(git_opts) do |key, _gemfile_value, _git_source_value| + raise GemfileError, %(The :#{key} option can't be used with `#{git_name}: #{opts[git_name].inspect}`) + end end %w[git path].each do |type| @@ -398,8 +517,6 @@ repo_name ||= user_name opts["source"] = source end - opts["source"] ||= @source - opts["env"] ||= @env opts["platforms"] = platforms.dup opts["group"] = groups opts["should_include"] = install_if @@ -415,22 +532,20 @@ repo_name ||= user_name end def validate_keys(command, opts, valid_keys) - invalid_keys = opts.keys - valid_keys - - git_source = opts.keys & @git_sources.keys.map(&:to_s) - if opts["branch"] && !(opts["git"] || opts["github"] || git_source.any?) + if opts["branch"] && !(opts["git"] || opts["github"] || (opts.keys & @git_sources.keys.map(&:to_s)).any?) raise GemfileError, %(The `branch` option for `#{command}` is not allowed. Only gems with a git source can specify a branch) end + invalid_keys = opts.keys - valid_keys return true unless invalid_keys.any? message = String.new message << "You passed #{invalid_keys.map {|k| ":" + k }.join(", ")} " message << if invalid_keys.size > 1 - "as options for #{command}, but they are invalid." - else - "as an option for #{command}, but it is invalid." - end + "as options for #{command}, but they are invalid." + else + "as an option for #{command}, but it is invalid." + end message << " Valid options are: #{valid_keys.join(", ")}." message << " You may be able to resolve this by upgrading Bundler to the newest version." @@ -440,10 +555,10 @@ repo_name ||= user_name def normalize_source(source) case source when :gemcutter, :rubygems, :rubyforge - Bundler::SharedHelpers.major_deprecation 2, "The source :#{source} is deprecated because HTTP " \ - "requests are insecure.\nPlease change your source to 'https://" \ - "rubygems.org' if possible, or 'http://rubygems.org' if not." - "http://rubygems.org" + removed_message = + "The source :#{source} is disallowed because HTTP requests are insecure.\n" \ + "Please change your source to 'https://rubygems.org' if possible, or 'http://rubygems.org' if not." + Bundler::SharedHelpers.feature_removed! removed_message when String source else @@ -451,43 +566,29 @@ repo_name ||= user_name end end - def check_primary_source_safety(source_list) - return if source_list.rubygems_primary_remotes.empty? && source_list.global_rubygems_source.nil? + def check_path_source_safety + return if @sources.global_path_source.nil? - if Bundler.feature_flag.disable_multisource? - msg = "This Gemfile contains multiple primary sources. " \ - "Each source after the first must include a block to indicate which gems " \ - "should come from that source" - unless Bundler.feature_flag.bundler_2_mode? - msg += ". To downgrade this error to a warning, run " \ - "`bundle config --delete disable_multisource`" - end - raise GemfileEvalError, msg - else - Bundler::SharedHelpers.major_deprecation 2, "Your Gemfile contains multiple primary sources. " \ - "Using `source` more than once without a block is a security risk, and " \ - "may result in installing unexpected gems. To resolve this warning, use " \ - "a block to indicate which gems should come from the secondary source. " \ - "To upgrade this warning to an error, run `bundle config " \ - "disable_multisource true`." - end - end - - def warn_deprecated_git_source(name, replacement, additional_message = nil) - # TODO: 2.0 remove deprecation - additional_message &&= " #{additional_message}" - replacement = if replacement.count("\n").zero? - "{|repo_name| #{replacement} }" - else - "do |repo_name|\n#{replacement.to_s.gsub(/^/, " ")}\n end" - end + msg = "You can no longer specify a path source by itself. Instead, \n" \ + "either use the :path option on a gem, or specify the gems that \n" \ + "bundler should find in the path source by passing a block to \n" \ + "the path method, like: \n\n" \ + " path 'dir/containing/rails' do\n" \ + " gem 'rails'\n" \ + " end\n\n" - Bundler::SharedHelpers.major_deprecation 2, <<-EOS -The :#{name} git source is deprecated, and will be removed in Bundler 2.0.#{additional_message} Add this code to the top of your Gemfile to ensure it continues to work: + SharedHelpers.feature_removed! msg.strip + end - git_source(:#{name}) #{replacement} + def check_rubygems_source_safety + multiple_global_source_warning if @sources.aggregate_global_source? + end - EOS + def multiple_global_source_warning + msg = "This Gemfile contains multiple global sources. " \ + "Each source after the first must include a block to indicate which gems " \ + "should come from that source" + raise GemfileEvalError, msg end class DSLError < GemfileError @@ -524,9 +625,7 @@ The :#{name} git source is deprecated, and will be removed in Bundler 2.0.#{addi # be raised. # def contents - @contents ||= begin - dsl_path && File.exist?(dsl_path) && File.read(dsl_path) - end + @contents ||= dsl_path && File.exist?(dsl_path) && File.read(dsl_path) end # The message of the exception reports the content of podspec for the @@ -557,35 +656,35 @@ The :#{name} git source is deprecated, and will be removed in Bundler 2.0.#{addi return m unless backtrace && dsl_path && contents - trace_line = backtrace.find {|l| l.include?(dsl_path.to_s) } || trace_line + trace_line = backtrace.find {|l| l.include?(dsl_path) } || trace_line return m unless trace_line - line_numer = trace_line.split(":")[1].to_i - 1 - return m unless line_numer + line_number = trace_line.split(":")[1].to_i - 1 + return m unless line_number lines = contents.lines.to_a indent = " # " indicator = indent.tr("#", ">") - first_line = line_numer.zero? - last_line = (line_numer == (lines.count - 1)) + first_line = line_number.zero? + last_line = (line_number == (lines.count - 1)) m << "\n" m << "#{indent}from #{trace_line.gsub(/:in.*$/, "")}\n" m << "#{indent}-------------------------------------------\n" - m << "#{indent}#{lines[line_numer - 1]}" unless first_line - m << "#{indicator}#{lines[line_numer]}" - m << "#{indent}#{lines[line_numer + 1]}" unless last_line + m << "#{indent}#{lines[line_number - 1]}" unless first_line + m << "#{indicator}#{lines[line_number]}" + m << "#{indent}#{lines[line_number + 1]}" unless last_line m << "\n" unless m.end_with?("\n") m << "#{indent}-------------------------------------------\n" end end - private + private def parse_line_number_from_description description = self.description - if dsl_path && description =~ /((#{Regexp.quote File.expand_path(dsl_path)}|#{Regexp.quote dsl_path.to_s}):\d+)/ + if dsl_path && description =~ /((#{Regexp.quote File.expand_path(dsl_path)}|#{Regexp.quote dsl_path}):\d+)/ trace_line = Regexp.last_match[1] - description = description.sub(/#{Regexp.quote trace_line}:\s*/, "").sub("\n", " - ") + description = description.sub(/\n.*\n(\.\.\.)? *\^~+$/, "").sub(/#{Regexp.quote trace_line}:\s*/, "").sub("\n", " - ") end [trace_line, description] end diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb index 8668c4ea7f..7c7ce107e2 100644 --- a/lib/bundler/endpoint_specification.rb +++ b/lib/bundler/endpoint_specification.rb @@ -3,29 +3,41 @@ module Bundler # used for Creating Specifications from the Gemcutter Endpoint class EndpointSpecification < Gem::Specification - ILLFORMED_MESSAGE = 'Ill-formed requirement ["#<YAML::Syck::DefaultKey'.freeze - include MatchPlatform + include MatchRemoteMetadata - attr_reader :name, :version, :platform, :required_rubygems_version, :required_ruby_version, :checksum - attr_accessor :source, :remote, :dependencies + attr_reader :name, :version, :platform, :checksum, :created_at + attr_writer :dependencies + attr_accessor :remote, :locked_platform - def initialize(name, version, platform, dependencies, metadata = nil) + def initialize(name, version, platform, spec_fetcher, dependencies, metadata = nil) super() @name = name @version = Gem::Version.create version - @platform = platform - @dependencies = dependencies.map {|dep, reqs| build_dependency(dep, reqs) } + @platform = Gem::Platform.new(platform) + @spec_fetcher = spec_fetcher + @dependencies = nil + @unbuilt_dependencies = dependencies @loaded_from = nil @remote_specification = nil + @locked_platform = nil parse_metadata(metadata) end + def insecurely_materialized? + @locked_platform.to_s != @platform.to_s + end + def fetch_platform @platform end + def dependencies + @dependencies ||= @unbuilt_dependencies.map! {|dep, reqs| build_dependency(dep, reqs) } + end + alias_method :runtime_dependencies, :dependencies + # needed for standalone, load required_paths from local gemspec # after the gem is installed def require_paths @@ -92,9 +104,20 @@ module Bundler end end + # needed for `bundle fund` + def metadata + if @remote_specification + @remote_specification.metadata + elsif _local_specification + _local_specification.metadata + else + super + end + end + def _local_specification return unless @loaded_from && File.exist?(local_specification_path) - eval(File.read(local_specification_path)).tap do |spec| + eval(File.read(local_specification_path), nil, local_specification_path).tap do |spec| spec.loaded_from = @loaded_from end end @@ -104,38 +127,58 @@ module Bundler @remote_specification = spec end - private + def inspect + "#<#{self.class} @name=\"#{name}\" (#{full_name.delete_prefix("#{name}-")})>" + end + + private + + def _remote_specification + @_remote_specification ||= @spec_fetcher.fetch_spec([@name, @version, @platform]) + end def local_specification_path "#{base_dir}/specifications/#{full_name}.gemspec" end def parse_metadata(data) - return unless data + unless data + @required_ruby_version = nil + @required_rubygems_version = nil + @created_at = nil + return + end + data.each do |k, v| next unless v case k.to_s when "checksum" - @checksum = v.last + begin + @checksum = Checksum.from_api(v.last, @spec_fetcher.uri) + rescue ArgumentError => e + raise ArgumentError, "Invalid checksum for #{full_name}: #{e.message}" + end when "rubygems" @required_rubygems_version = Gem::Requirement.new(v) when "ruby" @required_ruby_version = Gem::Requirement.new(v) + when "created_at" + value = v.is_a?(Array) ? v.last : v + if value.is_a?(String) + @created_at = begin + Time.new(value) + rescue ArgumentError + nil + end + end end end - rescue => e + rescue StandardError => e raise GemspecError, "There was an error parsing the metadata for the gem #{name} (#{version}): #{e.class}\n#{e}\nThe metadata was #{data.inspect}" end def build_dependency(name, requirements) - Gem::Dependency.new(name, requirements) - rescue ArgumentError => e - raise unless e.message.include?(ILLFORMED_MESSAGE) - puts # we shouldn't print the error message on the "fetching info" status line - raise GemspecError, - "Unfortunately, the gem #{name} (#{version}) has an invalid " \ - "gemspec.\nPlease ask the gem author to yank the bad version to fix " \ - "this issue. For more information, see http://bit.ly/syck-defaultkey." + Dependency.new(name, requirements) end end end diff --git a/lib/bundler/env.rb b/lib/bundler/env.rb index 58fe20dbe7..2b29705060 100644 --- a/lib/bundler/env.rb +++ b/lib/bundler/env.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require "bundler/rubygems_integration" -require "bundler/source/git/git_proxy" +require_relative "rubygems_integration" +require_relative "source/git/git_proxy" module Bundler class Env @@ -40,11 +40,11 @@ module Bundler out << "\n## Gemfile\n" gemfiles.each do |gemfile| - out << "\n### #{Pathname.new(gemfile).relative_path_from(SharedHelpers.pwd)}\n\n" + out << "\n### #{SharedHelpers.relative_path_to(gemfile)}\n\n" out << "```ruby\n" << read_file(gemfile).chomp << "\n```\n" end - out << "\n### #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}\n\n" + out << "\n### #{SharedHelpers.relative_path_to(Bundler.default_lockfile)}\n\n" out << "```\n" << read_file(Bundler.default_lockfile).chomp << "\n```\n" end @@ -61,42 +61,23 @@ module Bundler end def self.read_file(filename) - File.read(filename.to_s).strip + Bundler.read_file(filename.to_s).strip rescue Errno::ENOENT "<No #{filename} found>" - rescue => e + rescue RuntimeError => e "#{e.class}: #{e.message}" end def self.ruby_version - str = String.new("#{RUBY_VERSION}") - if RUBY_VERSION < "1.9" - str << " (#{RUBY_RELEASE_DATE}" - str << " patchlevel #{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL - str << ") [#{RUBY_PLATFORM}]" - else - str << "p#{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL - str << " (#{RUBY_RELEASE_DATE} revision #{RUBY_REVISION}) [#{RUBY_PLATFORM}]" - end + "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE} revision #{RUBY_REVISION}) [#{Gem::Platform.local}]" end def self.git_version - Bundler::Source::Git::GitProxy.new(nil, nil, nil).full_version + Bundler::Source::Git::GitProxy.new(nil, nil).full_version rescue Bundler::Source::Git::GitNotInstalledError "not installed" end - def self.version_of(script) - return "not installed" unless Bundler.which(script) - `#{script} --version` - end - - def self.chruby_version - return "not installed" unless Bundler.which("chruby-exec") - `chruby-exec -- chruby --version`. - sub(/.*^chruby: (#{Gem::Version::VERSION_PATTERN}).*/m, '\1') - end - def self.environment out = [] @@ -106,26 +87,21 @@ module Bundler out << [" Full Path", Gem.ruby] out << [" Config Dir", Pathname.new(Gem::ConfigFile::SYSTEM_WIDE_CONFIG_FILE).dirname] out << ["RubyGems", Gem::VERSION] - out << [" Gem Home", ENV.fetch("GEM_HOME") { Gem.dir }] - out << [" Gem Path", ENV.fetch("GEM_PATH") { Gem.path.join(File::PATH_SEPARATOR) }] + out << [" Gem Home", Gem.dir] + out << [" Gem Path", Gem.path.join(File::PATH_SEPARATOR)] + out << [" User Home", Gem.user_home] out << [" User Path", Gem.user_dir] out << [" Bin Dir", Gem.bindir] - out << ["OpenSSL"] if defined?(OpenSSL) - out << [" Compiled", OpenSSL::OPENSSL_VERSION] if defined?(OpenSSL::OPENSSL_VERSION) - out << [" Loaded", OpenSSL::OPENSSL_LIBRARY_VERSION] if defined?(OpenSSL::OPENSSL_LIBRARY_VERSION) - out << [" Cert File", OpenSSL::X509::DEFAULT_CERT_FILE] if defined?(OpenSSL::X509::DEFAULT_CERT_FILE) - out << [" Cert Dir", OpenSSL::X509::DEFAULT_CERT_DIR] if defined?(OpenSSL::X509::DEFAULT_CERT_DIR) - out << ["Tools"] - out << [" Git", git_version] - out << [" RVM", ENV.fetch("rvm_version") { version_of("rvm") }] - out << [" rbenv", version_of("rbenv")] - out << [" chruby", chruby_version] - - %w[rubygems-bundler open_gem].each do |name| - specs = Bundler.rubygems.find_name(name) - out << [" #{name}", "(#{specs.map(&:version).join(",")})"] unless specs.empty? + if defined?(OpenSSL::SSL) + out << ["OpenSSL"] + out << [" Compiled", OpenSSL::OPENSSL_VERSION] if defined?(OpenSSL::OPENSSL_VERSION) + out << [" Loaded", OpenSSL::OPENSSL_LIBRARY_VERSION] if defined?(OpenSSL::OPENSSL_LIBRARY_VERSION) + out << [" Cert File", OpenSSL::X509::DEFAULT_CERT_FILE] if defined?(OpenSSL::X509::DEFAULT_CERT_FILE) + out << [" Cert Dir", OpenSSL::X509::DEFAULT_CERT_DIR] if defined?(OpenSSL::X509::DEFAULT_CERT_DIR) end - if (exe = caller.last.split(":").first) && exe =~ %r{(exe|bin)/bundler?\z} + out << ["Git", git_version] + + if (exe = caller_locations.last.absolute_path)&.match? %r{(exe|bin)/bundler?\z} shebang = File.read(exe).lines.first shebang.sub!(/^#!\s*/, "") unless shebang.start_with?(Gem.ruby, "/usr/bin/env ruby") @@ -148,6 +124,6 @@ module Bundler out << "```\n" end - private_class_method :read_file, :ruby_version, :git_version, :append_formatted_table, :version_of, :chruby_version + private_class_method :read_file, :ruby_version, :git_version, :append_formatted_table end end diff --git a/lib/bundler/environment_preserver.rb b/lib/bundler/environment_preserver.rb index af7c1ef0a4..bf9478a299 100644 --- a/lib/bundler/environment_preserver.rb +++ b/lib/bundler/environment_preserver.rb @@ -2,12 +2,13 @@ module Bundler class EnvironmentPreserver - INTENTIONALLY_NIL = "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL".freeze + INTENTIONALLY_NIL = "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL" BUNDLER_KEYS = %w[ BUNDLE_BIN_PATH BUNDLE_GEMFILE - BUNDLER_ORIG_MANPATH + BUNDLE_LOCKFILE BUNDLER_VERSION + BUNDLER_SETUP GEM_HOME GEM_PATH MANPATH @@ -16,24 +17,33 @@ module Bundler RUBYLIB RUBYOPT ].map(&:freeze).freeze - BUNDLER_PREFIX = "BUNDLER_ORIG_".freeze + BUNDLER_PREFIX = "BUNDLER_ORIG_" - # @param env [ENV] + def self.from_env + new(ENV.to_hash, BUNDLER_KEYS) + end + + # @param env [Hash] # @param keys [Array<String>] def initialize(env, keys) - @original = env.to_hash + @original = env @keys = keys @prefix = BUNDLER_PREFIX end + # Replaces `ENV` with the bundler environment variables backed up + def replace_with_backup + ENV.replace(backup) + end + # @return [Hash] def backup env = @original.clone @keys.each do |key| value = env[key] - if !value.nil? && !value.empty? + if !value.nil? env[@prefix + key] ||= value - elsif value.nil? + else env[@prefix + key] ||= INTENTIONALLY_NIL end end @@ -45,7 +55,7 @@ module Bundler env = @original.clone @keys.each do |key| value_original = env[@prefix + key] - next if value_original.nil? || value_original.empty? + next if value_original.nil? if value_original == INTENTIONALLY_NIL env.delete(key) else diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb index e471bce0b6..dff5d93128 100644 --- a/lib/bundler/errors.rb +++ b/lib/bundler/errors.rb @@ -21,19 +21,11 @@ module Bundler class InstallError < BundlerError; status_code(5); end # Internal error, should be rescued - class VersionConflict < BundlerError - attr_reader :conflicts - - def initialize(conflicts, msg = nil) - super(msg) - @conflicts = conflicts - end - - status_code(6) - end + class SolveFailure < BundlerError; status_code(6); end class GemNotFound < BundlerError; status_code(7); end class InstallHookError < BundlerError; status_code(8); end + class RemovedError < BundlerError; status_code(9); end class GemfileNotFound < BundlerError; status_code(10); end class GitError < BundlerError; status_code(11); end class DeprecatedError < BundlerError; status_code(12); end @@ -41,24 +33,64 @@ module Bundler 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) def filter_uri(uri) URICredentialsFilter.credential_filtered_uri(uri) end 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 class GemfileLockNotFound < BundlerError; status_code(22); end class PluginError < BundlerError; status_code(29); end - class SudoNotPermittedError < BundlerError; status_code(30); end class ThreadCreationError < BundlerError; status_code(33); end class APIResponseMismatchError < BundlerError; status_code(34); end + class APIResponseInvalidDependenciesError < BundlerError; status_code(35); end class GemfileEvalError < GemfileError; end class MarshalError < StandardError; end + class ChecksumMismatchError < SecurityError + def initialize(lock_name, existing, checksum) + @lock_name = lock_name + @existing = existing + @checksum = checksum + end + + def message + <<~MESSAGE + Bundler found mismatched checksums. This is a potential security risk. + #{@lock_name} #{@existing.to_lock} + from #{@existing.sources.join("\n and ")} + #{@lock_name} #{@checksum.to_lock} + from #{@checksum.sources.join("\n and ")} + + #{mismatch_resolution_instructions} + To ignore checksum security warnings, disable checksum validation with + `bundle config set --local disable_checksum_validation true` + MESSAGE + end + + def mismatch_resolution_instructions + removable, remote = [@existing, @checksum].partition(&:removable?) + case removable.size + when 1 + msg = +"If you trust #{remote.first.sources.first}, to resolve this issue you can:\n" + msg << removable.first.removal_instructions + when 2 + msg = +"To resolve this issue you can either:\n" + msg << @checksum.removal_instructions + msg << "or if you are sure that the new checksum from #{@checksum.sources.first} is correct:\n" + msg << @existing.removal_instructions + end + end + + status_code(37) + end + class PermissionError < BundlerError def initialize(path, permission_type = :write) @path = path @@ -74,10 +106,22 @@ module Bundler end end + def permission_type + case @permission_type + when :create + "executable permissions for all parent directories and write permissions for `#{parent_folder}`" + else + "#{@permission_type} permissions for that path" + end + end + + def parent_folder + File.dirname(@path) + end + def message "There was an error while trying to #{action} `#{@path}`. " \ - "It is likely that you need to grant #{@permission_type} permissions " \ - "for that path." + "It is likely that you need to grant #{permission_type}." end status_code(23) @@ -87,7 +131,8 @@ module Bundler attr_reader :orig_exception def initialize(orig_exception, msg) - full_message = msg + "\nGem Load Error is: #{orig_exception.message}\n"\ + full_message = msg + "\nGem Load Error is: + #{orig_exception.full_message(highlight: false)}\n"\ "Backtrace for gem load error is:\n"\ "#{orig_exception.backtrace.join("\n")}\n"\ "Bundler Error Backtrace:\n" @@ -121,7 +166,7 @@ module Bundler class VirtualProtocolError < BundlerError def message - "There was an error relating to virtualization and file access." \ + "There was an error relating to virtualization and file access. " \ "It is likely that you need to grant access to or mount some file system correctly." end @@ -145,6 +190,24 @@ module Bundler status_code(31) end + class ReadOnlyFileSystemError < PermissionError + def message + "There was an error while trying to #{action} `#{@path}`. " \ + "File system is read-only." + end + + status_code(42) + end + + class OperationNotPermittedError < PermissionError + def message + "There was an error while trying to #{action} `#{@path}`. " \ + "Underlying OS system call raised an EPERM error." + end + + status_code(43) + end + class GenericSystemCallError < BundlerError attr_reader :underlying_error @@ -155,4 +218,89 @@ module Bundler status_code(32) end + + class DirectoryRemovalError < BundlerError + def initialize(orig_exception, msg) + full_message = "#{msg}.\n" \ + "The underlying error was #{orig_exception.class}: + #{orig_exception.full_message(highlight: false)}, + with backtrace:\n" \ + " #{orig_exception.backtrace.join("\n ")}\n\n" \ + "Bundler Error Backtrace:" + super(full_message) + end + + status_code(36) + end + + class InsecureInstallPathError < BundlerError + def initialize(name, path) + @name = name + @path = path + end + + def message + "Bundler cannot reinstall #{@name} because there's a previous installation of it at #{@path} that is unsafe to remove.\n" \ + "The parent of #{@path} is world-writable and does not have the sticky bit set, making it insecure to remove due to potential vulnerabilities.\n" \ + "Please change the permissions of #{File.dirname(@path)} or choose a different install path." + end + + status_code(38) + end + + class CorruptBundlerInstallError < BundlerError + def initialize(loaded_spec) + @loaded_spec = loaded_spec + end + + def message + "The running version of Bundler (#{Bundler::VERSION}) does not match the version of the specification installed for it (#{@loaded_spec.version}). " \ + "This can be caused by reinstalling Ruby without removing previous installation, leaving around an upgraded default version of Bundler. " \ + "Reinstalling Ruby from scratch should fix the problem." + end + + status_code(39) + end + + class InvalidArgumentError < BundlerError; status_code(40); end + + class IncorrectLockfileDependencies < BundlerError + attr_reader :spec, :actual_dependencies, :lockfile_dependencies + + def initialize(spec, actual_dependencies = nil, lockfile_dependencies = nil) + @spec = spec + @actual_dependencies = actual_dependencies + @lockfile_dependencies = lockfile_dependencies + end + + def message + lines = ["Bundler found incorrect dependencies in the lockfile for #{spec.full_name}", ""] + + if @actual_dependencies && @lockfile_dependencies + actual_by_name = @actual_dependencies.each_with_object({}) {|d, h| h[d.name] = d } + lockfile_by_name = @lockfile_dependencies.each_with_object({}) {|d, h| h[d.name] = d } + + (actual_by_name.keys | lockfile_by_name.keys).sort.each do |name| + actual = actual_by_name[name] + lockfile = lockfile_by_name[name] + next if actual && lockfile && actual.requirement == lockfile.requirement + + if actual && lockfile + lines << " #{name}: gemspec specifies #{actual.requirement}, lockfile has #{lockfile.requirement}" + elsif actual + lines << " #{name}: gemspec specifies #{actual.requirement}, not in lockfile" + else + lines << " #{name}: not in gemspec, lockfile has #{lockfile.requirement}" + end + end + + lines << "" + end + + lines << "Please run `bundle install` to regenerate the lockfile." + lines.join("\n") + end + + status_code(41) + end end diff --git a/lib/bundler/feature_flag.rb b/lib/bundler/feature_flag.rb index 6a1809cd40..dea8abedba 100644 --- a/lib/bundler/feature_flag.rb +++ b/lib/bundler/feature_flag.rb @@ -2,66 +2,19 @@ module Bundler class FeatureFlag - def self.settings_flag(flag, &default) - unless Bundler::Settings::BOOL_KEYS.include?(flag.to_s) - raise "Cannot use `#{flag}` as a settings feature flag since it isn't a bool key" - end + (1..10).each {|v| define_method("bundler_#{v}_mode?") { @major_version >= v } } - settings_method("#{flag}?", flag, &default) + def removed_major?(target_major_version) + @major_version > target_major_version end - private_class_method :settings_flag - def self.settings_option(key, &default) - settings_method(key, key, &default) + def deprecated_major?(target_major_version) + @major_version >= target_major_version end - private_class_method :settings_option - - def self.settings_method(name, key, &default) - define_method(name) do - value = Bundler.settings[key] - value = instance_eval(&default) if value.nil? && !default.nil? - value - end - end - private_class_method :settings_method - - (1..10).each {|v| define_method("bundler_#{v}_mode?") { major_version >= v } } - - settings_flag(:allow_bundler_dependency_conflicts) { bundler_2_mode? } - settings_flag(:allow_offline_install) { bundler_2_mode? } - settings_flag(:auto_clean_without_path) { bundler_2_mode? } - settings_flag(:cache_all) { bundler_2_mode? } - settings_flag(:cache_command_is_package) { bundler_2_mode? } - settings_flag(:console_command) { !bundler_2_mode? } - settings_flag(:default_install_uses_path) { bundler_2_mode? } - settings_flag(:deployment_means_frozen) { bundler_2_mode? } - settings_flag(:disable_multisource) { bundler_2_mode? } - settings_flag(:error_on_stderr) { bundler_2_mode? } - settings_flag(:forget_cli_options) { bundler_2_mode? } - settings_flag(:global_gem_cache) { bundler_2_mode? } - settings_flag(:init_gems_rb) { bundler_2_mode? } - settings_flag(:list_command) { bundler_2_mode? } - settings_flag(:lockfile_uses_separate_rubygems_sources) { bundler_2_mode? } - settings_flag(:only_update_to_newer_versions) { bundler_2_mode? } - settings_flag(:plugins) { @bundler_version >= Gem::Version.new("1.14") } - settings_flag(:prefer_gems_rb) { bundler_2_mode? } - settings_flag(:print_only_version_number) { bundler_2_mode? } - settings_flag(:setup_makes_kernel_gem_public) { !bundler_2_mode? } - settings_flag(:skip_default_git_sources) { bundler_2_mode? } - settings_flag(:specific_platform) { bundler_2_mode? } - settings_flag(:suppress_install_using_messages) { bundler_2_mode? } - settings_flag(:unlock_source_unlocks_spec) { !bundler_2_mode? } - settings_flag(:update_requires_all_flag) { bundler_2_mode? } - - settings_option(:default_cli_command) { bundler_2_mode? ? :cli_help : :install } def initialize(bundler_version) @bundler_version = Gem::Version.create(bundler_version) + @major_version = @bundler_version.segments.first end - - def major_version - @bundler_version.segments.first - end - private :major_version end end diff --git a/lib/bundler/fetcher.rb b/lib/bundler/fetcher.rb index 03ff528826..0b6ced6f39 100644 --- a/lib/bundler/fetcher.rb +++ b/lib/bundler/fetcher.rb @@ -1,22 +1,26 @@ # frozen_string_literal: true -require "bundler/vendored_persistent" -require "cgi" -require "securerandom" +require_relative "vendored_persistent" +require_relative "vendored_timeout" +require_relative "vendored_securerandom" require "zlib" module Bundler # Handles all the fetching with the rubygems server class Fetcher - autoload :CompactIndex, "bundler/fetcher/compact_index" - autoload :Downloader, "bundler/fetcher/downloader" - autoload :Dependency, "bundler/fetcher/dependency" - autoload :Index, "bundler/fetcher/index" + autoload :Base, File.expand_path("fetcher/base", __dir__) + autoload :CompactIndex, File.expand_path("fetcher/compact_index", __dir__) + autoload :Downloader, File.expand_path("fetcher/downloader", __dir__) + autoload :Dependency, File.expand_path("fetcher/dependency", __dir__) + autoload :Index, File.expand_path("fetcher/index", __dir__) # This error is raised when it looks like the network is down class NetworkDownError < HTTPError; end + # This error is raised if we should rate limit our requests to the API + class TooManyRequestsError < HTTPError; end # 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 class CertificateFailureError < HTTPError def initialize(remote_uri) @@ -25,28 +29,30 @@ module Bundler " is a chance you are experiencing a man-in-the-middle attack, but" \ " most likely your system doesn't have the CA certificates needed" \ " for verification. For information about OpenSSL certificates, see" \ - " http://bit.ly/ruby-ssl. To connect without using SSL, edit your Gemfile" \ - " sources and change 'https' to 'http'." + " https://railsapps.github.io/openssl-certificate-verify-failed.html." end end + # This is the error raised when a source is HTTPS and OpenSSL didn't load class SSLError < HTTPError def initialize(msg = nil) - super msg || "Could not load OpenSSL.\n" \ - "You must recompile Ruby with OpenSSL support or change the sources in your " \ - "Gemfile from 'https' to 'http'. Instructions for compiling with OpenSSL " \ - "using RVM are available at rvm.io/packages/openssl." + super "Could not load OpenSSL.\n" \ + "You must recompile Ruby with OpenSSL support.\n" \ + "original error: #{msg}\n" end end + # This error is raised if HTTP authentication is required, but not provided. class AuthenticationRequiredError < HTTPError def initialize(remote_uri) remote_uri = filter_uri(remote_uri) super "Authentication is required for #{remote_uri}.\n" \ "Please supply credentials for this source. You can do this by running:\n" \ - " bundle config #{remote_uri} username:password" + "`bundle config set --global #{remote_uri} username:password`\n" \ + "or by storing the credentials in the `#{Settings.key_for(remote_uri)}` environment variable" end end + # This error is raised if HTTP authentication is provided, but incorrect. class BadAuthenticationError < HTTPError def initialize(remote_uri) @@ -56,19 +62,67 @@ module Bundler end end + # This error is raised if HTTP authentication is correct, but lacks + # necessary permissions. + class AuthenticationForbiddenError < HTTPError + def initialize(remote_uri) + remote_uri = filter_uri(remote_uri) + super "Access token could not be authenticated for #{remote_uri}.\n" \ + "Make sure it's valid and has the necessary scopes configured." + end + end + + HTTP_ERRORS = (Downloader::HTTP_RETRYABLE_ERRORS + Downloader::HTTP_NON_RETRYABLE_ERRORS).freeze + deprecate_constant :HTTP_ERRORS + + NET_ERRORS = [ + :HTTPBadGateway, + :HTTPBadRequest, + :HTTPFailedDependency, + :HTTPForbidden, + :HTTPInsufficientStorage, + :HTTPMethodNotAllowed, + :HTTPMovedPermanently, + :HTTPNoContent, + :HTTPNotFound, + :HTTPNotImplemented, + :HTTPPreconditionFailed, + :HTTPRequestEntityTooLarge, + :HTTPRequestURITooLong, + :HTTPUnauthorized, + :HTTPUnprocessableEntity, + :HTTPUnsupportedMediaType, + :HTTPVersionNotSupported, + ].freeze + deprecate_constant :NET_ERRORS + # Exceptions classes that should bypass retry attempts. If your password didn't work the # first time, it's not going to the third time. - NET_ERRORS = [:HTTPBadGateway, :HTTPBadRequest, :HTTPFailedDependency, - :HTTPForbidden, :HTTPInsufficientStorage, :HTTPMethodNotAllowed, - :HTTPMovedPermanently, :HTTPNoContent, :HTTPNotFound, - :HTTPNotImplemented, :HTTPPreconditionFailed, :HTTPRequestEntityTooLarge, - :HTTPRequestURITooLong, :HTTPUnauthorized, :HTTPUnprocessableEntity, - :HTTPUnsupportedMediaType, :HTTPVersionNotSupported].freeze - FAIL_ERRORS = begin - fail_errors = [AuthenticationRequiredError, BadAuthenticationError, FallbackError] - fail_errors << Gem::Requirement::BadRequirementError if defined?(Gem::Requirement::BadRequirementError) - fail_errors.concat(NET_ERRORS.map {|e| SharedHelpers.const_get_safely(e, Net) }.compact) - end.freeze + FAIL_ERRORS = [ + AuthenticationRequiredError, + BadAuthenticationError, + AuthenticationForbiddenError, + FallbackError, + SecurityError, + Gem::Requirement::BadRequirementError, + Gem::Net::HTTPBadGateway, + Gem::Net::HTTPBadRequest, + Gem::Net::HTTPFailedDependency, + Gem::Net::HTTPForbidden, + Gem::Net::HTTPInsufficientStorage, + Gem::Net::HTTPMethodNotAllowed, + Gem::Net::HTTPMovedPermanently, + Gem::Net::HTTPNoContent, + Gem::Net::HTTPNotFound, + Gem::Net::HTTPNotImplemented, + Gem::Net::HTTPPreconditionFailed, + Gem::Net::HTTPRequestEntityTooLarge, + Gem::Net::HTTPRequestURITooLong, + Gem::Net::HTTPUnauthorized, + Gem::Net::HTTPUnprocessableEntity, + Gem::Net::HTTPUnsupportedMediaType, + Gem::Net::HTTPVersionNotSupported, + ].freeze class << self attr_accessor :disable_endpoint, :api_timeout, :redirect_limit, :max_retries @@ -79,6 +133,7 @@ module Bundler self.max_retries = Bundler.settings[:retry] # How many retries for the API call def initialize(remote) + @cis = nil @remote = remote Socket.do_not_reverse_lookup = true @@ -94,14 +149,17 @@ module Bundler spec -= [nil, "ruby", ""] spec_file_name = "#{spec.join "-"}.gemspec" - 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)) + uri = Gem::URI.parse("#{remote_uri}#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}.rz") + spec = if uri.scheme == "file" + path = Gem::Util.correct_for_windows_path(uri.path) + Bundler.safe_load_marshal Bundler.rubygems.inflate(Gem.read_binary(path)) elsif cached_spec_path = gemspec_cached_path(spec_file_name) Bundler.load_gemspec(cached_spec_path) else - Bundler.load_marshal Gem.inflate(downloader.fetch(uri).body) + Bundler.safe_load_marshal Bundler.rubygems.inflate(downloader.fetch(uri).body) end + raise MarshalError, "is #{spec.inspect}" unless spec.is_a?(Gem::Specification) + spec rescue MarshalError raise HTTPError, "Gemspec #{spec} contained invalid data.\n" \ "Your network or your gem server is probably having issues right now." @@ -116,26 +174,13 @@ module Bundler # return the specs in the bundler format as an index def specs(gem_names, source) - old = Bundler.rubygems.sources index = Bundler::Index.new - if Bundler::Fetcher.disable_endpoint - @use_api = false - specs = fetchers.last.specs(gem_names) - else - specs = [] - fetchers.shift until fetchers.first.available? || fetchers.empty? - fetchers.dup.each do |f| - break unless f.api_fetcher? && !gem_names || !specs = f.specs(gem_names) - fetchers.delete(f) - end - @use_api = false if fetchers.none?(&:api_fetcher?) - end - - specs.each do |name, version, platform, dependencies, metadata| - next if name == "bundler" + fetch_specs(gem_names).each do |name, version, platform, dependencies, metadata| spec = if dependencies - EndpointSpecification.new(name, version, platform, dependencies, metadata) + EndpointSpecification.new(name, version, platform, self, dependencies, metadata).tap do |es| + source.checksum_store.replace(es, es.checksum) + end else RemoteSpecification.new(name, version, platform, self) end @@ -146,22 +191,8 @@ module Bundler index rescue CertificateFailureError - Bundler.ui.info "" if gem_names && use_api # newline after dots + Bundler.ui.info "" if gem_names && api_fetcher? # newline after dots raise - ensure - Bundler.rubygems.sources = old - end - - def use_api - return @use_api if defined?(@use_api) - - fetchers.shift until fetchers.first.available? - - @use_api = if remote_uri.scheme == "file" || Bundler::Fetcher.disable_endpoint - false - else - fetchers.first.api_fetcher? - end end def user_agent @@ -178,7 +209,7 @@ module Bundler # engine_version raises on unknown engines engine_version = begin ruby.engine_versions - rescue + rescue RuntimeError "???" end agent << " #{ruby.engine}/#{ruby.versions_string(engine_version)}" @@ -189,7 +220,7 @@ module Bundler agent << " ci/#{cis.join(",")}" if cis.any? # add a random ID so we can consolidate runs server-side - agent << " " << SecureRandom.hex(8) + agent << " " << Gem::SecureRandom.hex(8) # add any user agent strings set in the config extra_ua = Bundler.settings[:user_agent] @@ -199,10 +230,6 @@ module Bundler end end - def fetchers - @fetchers ||= FETCHERS.map {|f| f.new(downloader, @remote, uri) } - end - def http_proxy return unless uri = connection.proxy_uri uri.to_s @@ -212,35 +239,67 @@ module Bundler "#<#{self.class}:0x#{object_id} uri=#{uri}>" end - private + def api_fetcher? + fetchers.first.api_fetcher? + end + + def gem_remote_fetcher + @gem_remote_fetcher ||= begin + require_relative "fetcher/gem_remote_fetcher" + fetcher = GemRemoteFetcher.new Gem.configuration[:http_proxy] + fetcher.headers["User-Agent"] = user_agent + fetcher.headers["X-Gemfile-Source"] = @remote.original_uri.to_s if @remote.original_uri + fetcher + end + end + + private + + def available_fetchers + if Bundler::Fetcher.disable_endpoint + [Index] + elsif remote_uri.scheme == "file" + Bundler.ui.debug("Using a local server, bundler won't use the CompactIndex API") + [Index] + else + [CompactIndex, Dependency, Index] + end + end + + def fetchers + @fetchers ||= available_fetchers.map {|f| f.new(downloader, @remote, uri, gem_remote_fetcher) }.drop_while {|f| !f.available? } + end - FETCHERS = [CompactIndex, Dependency, Index].freeze + def fetch_specs(gem_names) + fetchers.reject!(&:api_fetcher?) unless gem_names + fetchers.reject! do |f| + specs = f.specs(gem_names) + return specs if specs + true + end + [] + end def cis - env_cis = { - "TRAVIS" => "travis", - "CIRCLECI" => "circle", - "SEMAPHORE" => "semaphore", - "JENKINS_URL" => "jenkins", - "BUILDBOX" => "buildbox", - "GO_SERVER_URL" => "go", - "SNAP_CI" => "snap", - "CI_NAME" => ENV["CI_NAME"], - "CI" => "ci" - } - env_cis.find_all {|env, _| ENV[env] }.map {|_, ci| ci } + @cis ||= Bundler::CIDetector.ci_strings end def connection @connection ||= begin needs_ssl = remote_uri.scheme == "https" || - Bundler.settings[:ssl_verify_mode] || - Bundler.settings[:ssl_client_cert] - raise SSLError if needs_ssl && !defined?(OpenSSL::SSL) + Bundler.settings[:ssl_verify_mode] || + Bundler.settings[:ssl_client_cert] + if needs_ssl + begin + require "openssl" + rescue StandardError, LoadError => e + raise SSLError.new(e.message) + end + end - con = PersistentHTTP.new "bundler", :ENV - if gem_proxy = Bundler.rubygems.configuration[:http_proxy] - con.proxy = URI.parse(gem_proxy) if gem_proxy != :no_proxy + con = Gem::Net::HTTP::Persistent.new name: "bundler", proxy: :ENV + if gem_proxy = Gem.configuration[:http_proxy] + con.proxy = Gem::URI.parse(gem_proxy) if gem_proxy != :no_proxy end if remote_uri.scheme == "https" @@ -250,8 +309,8 @@ module Bundler end ssl_client_cert = Bundler.settings[:ssl_client_cert] || - (Bundler.rubygems.configuration.ssl_client_cert if - Bundler.rubygems.configuration.respond_to?(:ssl_client_cert)) + (Gem.configuration.ssl_client_cert if + Gem.configuration.respond_to?(:ssl_client_cert)) if ssl_client_cert pem = File.read(ssl_client_cert) con.cert = OpenSSL::X509::Certificate.new(pem) @@ -269,22 +328,14 @@ module Bundler # 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 + paths.find {|path| File.file? path } end - HTTP_ERRORS = [ - Timeout::Error, EOFError, SocketError, Errno::ENETDOWN, Errno::ENETUNREACH, - Errno::EINVAL, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EAGAIN, - Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, - PersistentHTTP::Error, Zlib::BufError, Errno::EHOSTUNREACH - ].freeze - def bundler_cert_store store = OpenSSL::X509::Store.new ssl_ca_cert = Bundler.settings[:ssl_ca_cert] || - (Bundler.rubygems.configuration.ssl_ca_cert if - Bundler.rubygems.configuration.respond_to?(:ssl_ca_cert)) + (Gem.configuration.ssl_ca_cert if + Gem.configuration.respond_to?(:ssl_ca_cert)) if ssl_ca_cert if File.directory? ssl_ca_cert store.add_path ssl_ca_cert @@ -293,14 +344,12 @@ module Bundler end else store.set_default_paths - certs = File.expand_path("../ssl_certs/*/*.pem", __FILE__) - Dir.glob(certs).each {|c| store.add_file c } + require "rubygems/request" + Gem::Request.get_cert_files.each {|c| store.add_file c } end store end - private - def remote_uri @remote.uri end diff --git a/lib/bundler/fetcher/base.rb b/lib/bundler/fetcher/base.rb index 27987f670a..cfec2f8e94 100644 --- a/lib/bundler/fetcher/base.rb +++ b/lib/bundler/fetcher/base.rb @@ -6,12 +6,14 @@ module Bundler attr_reader :downloader attr_reader :display_uri attr_reader :remote + attr_reader :gem_remote_fetcher - def initialize(downloader, remote, display_uri) + def initialize(downloader, remote, display_uri, gem_remote_fetcher) raise "Abstract class" if self.class == Base @downloader = downloader @remote = remote @display_uri = display_uri + @gem_remote_fetcher = gem_remote_fetcher end def remote_uri @@ -19,14 +21,12 @@ module Bundler end def fetch_uri - @fetch_uri ||= begin - if remote_uri.host == "rubygems.org" - uri = remote_uri.dup - uri.host = "index.rubygems.org" - uri - else - remote_uri - end + @fetch_uri ||= if remote_uri.host == "rubygems.org" + uri = remote_uri.dup + uri.host = "index.rubygems.org" + uri + else + remote_uri end end @@ -38,11 +38,11 @@ module Bundler false end - private + private - def log_specs(debug_msg) + def log_specs(&block) if Bundler.ui.debug? - Bundler.ui.debug debug_msg + Bundler.ui.debug yield else Bundler.ui.info ".", false end diff --git a/lib/bundler/fetcher/compact_index.rb b/lib/bundler/fetcher/compact_index.rb index cfc74d642c..52168111fe 100644 --- a/lib/bundler/fetcher/compact_index.rb +++ b/lib/bundler/fetcher/compact_index.rb @@ -1,28 +1,24 @@ # frozen_string_literal: true -require "bundler/fetcher/base" -require "bundler/worker" +require_relative "base" +require_relative "../worker" module Bundler - autoload :CompactIndexClient, "bundler/compact_index_client" - class Fetcher class CompactIndex < Base def self.compact_index_request(method_name) method = instance_method(method_name) undef_method(method_name) define_method(method_name) do |*args, &blk| - begin - method.bind(self).call(*args, &blk) - rescue NetworkDownError, CompactIndexClient::Updater::MisMatchedChecksumError => e - raise HTTPError, e.message - rescue AuthenticationRequiredError - # Fail since we got a 401 from the server. - raise - rescue HTTPError => e - Bundler.ui.trace(e) - nil - end + method.bind_call(self, *args, &blk) + rescue NetworkDownError, CompactIndexClient::Updater::MismatchedChecksumError => e + raise HTTPError, e.message + rescue AuthenticationRequiredError, BadAuthenticationError + # Fail since we got a 401 from the server. + raise + rescue HTTPError => e + Bundler.ui.trace(e) + nil end end @@ -37,37 +33,27 @@ module Bundler remaining_gems = gem_names.dup until remaining_gems.empty? - log_specs "Looking up gems #{remaining_gems.inspect}" - - deps = compact_index_client.dependencies(remaining_gems) - next_gems = deps.map {|d| d[3].map(&:first).flatten(1) }.flatten(1).uniq + log_specs { "Looking up gems #{remaining_gems.inspect}" } + deps = fetch_gem_infos(remaining_gems).flatten(1) + next_gems = deps.flat_map {|d| d[CompactIndexClient::INFO_DEPS].flat_map(&:first) }.uniq deps.each {|dep| gem_info << dep } complete_gems.concat(deps.map(&:first)).uniq! remaining_gems = next_gems - complete_gems end - @bundle_worker.stop if @bundle_worker + @bundle_worker&.stop @bundle_worker = nil # reset it. Not sure if necessary gem_info end - def fetch_spec(spec) - spec -= [nil, "ruby", ""] - contents = compact_index_client.spec(*spec) - return nil if contents.nil? - contents.unshift(spec.first) - contents[3].map! {|d| Gem::Dependency.new(*d) } - EndpointSpecification.new(*contents) - end - compact_index_request :fetch_spec - def available? - return nil unless SharedHelpers.md5_available? - user_home = Bundler.user_home - return nil unless user_home.directory? && user_home.writable? + unless SharedHelpers.md5_available? + Bundler.ui.debug("FIPS mode is enabled, bundler can't use the CompactIndex API") + return nil + end # Read info file checksums out of /versions, so we can know if gems are up to date - fetch_uri.scheme != "file" && compact_index_client.update_and_parse_checksums! - rescue CompactIndexClient::Updater::MisMatchedChecksumError => e + compact_index_client.available? + rescue CompactIndexClient::Updater::MismatchedChecksumError => e Bundler.ui.debug(e.message) nil end @@ -77,27 +63,35 @@ module Bundler true end - private + private def compact_index_client - @compact_index_client ||= begin + @compact_index_client ||= SharedHelpers.filesystem_access(cache_path) do CompactIndexClient.new(cache_path, client_fetcher) - end.tap do |client| - client.in_parallel = lambda do |inputs, &blk| - func = lambda {|object, _index| blk.call(object) } - worker = bundle_worker(func) - inputs.each {|input| worker.enq(input) } - inputs.map { worker.deq } - end end - end + end + + def fetch_gem_infos(names) + in_parallel(names) {|name| compact_index_client.info(name) } + rescue TooManyRequestsError # rubygems.org is rate limiting us, slow down. + @bundle_worker&.stop + @bundle_worker = nil # reset it. Not sure if necessary + compact_index_client.reset! + names.map {|name| compact_index_client.info(name) } + end + + def in_parallel(inputs, &blk) + func = lambda {|object, _index| blk.call(object) } + worker = bundle_worker(func) + inputs.each {|input| worker.enq(input) } + inputs.map { worker.deq } end def bundle_worker(func = nil) @bundle_worker ||= begin worker_name = "Compact Index (#{display_uri.host})" - Bundler::Worker.new(Bundler.current_ruby.rbx? ? 1 : 25, worker_name, func) + Bundler::Worker.new(Bundler.settings.processor_count, worker_name, func) end @bundle_worker.tap do |worker| worker.instance_variable_set(:@func, func) if func @@ -116,9 +110,9 @@ module Bundler def call(path, headers) fetcher.downloader.fetch(fetcher.fetch_uri + path, headers) rescue NetworkDownError => e - raise unless Bundler.feature_flag.allow_offline_install? && headers["If-None-Match"] + raise unless headers["If-None-Match"] ui.warn "Using the cached data for the new index because of a network error: #{e}" - Net::HTTPNotModified.new(nil, nil, nil) + Gem::Net::HTTPNotModified.new(nil, nil, nil) end end end diff --git a/lib/bundler/fetcher/dependency.rb b/lib/bundler/fetcher/dependency.rb index 1430d1ebeb..4f2414e33d 100644 --- a/lib/bundler/fetcher/dependency.rb +++ b/lib/bundler/fetcher/dependency.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true -require "bundler/fetcher/base" -require "cgi" +require_relative "base" +require "cgi/escape" +require "cgi/util" unless defined?(CGI::EscapeExt) module Bundler class Fetcher @@ -24,7 +25,7 @@ module Bundler def specs(gem_names, full_dependency_list = [], last_spec_list = []) query_list = gem_names.uniq - full_dependency_list - log_specs "Query List: #{query_list.inspect}" + log_specs { "Query List: #{query_list.inspect}" } return last_spec_list if query_list.empty? @@ -34,14 +35,10 @@ module Bundler returned_gems = spec_list.map(&:first).uniq specs(deps_list, full_dependency_list + returned_gems, spec_list + last_spec_list) - rescue MarshalError + rescue MarshalError, HTTPError, GemspecError Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over Bundler.ui.debug "could not fetch from the dependency API, trying the full index" nil - rescue HTTPError, GemspecError - Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over - Bundler.ui.debug "could not fetch from the dependency API\nit's suggested to retry using the full index via `bundle install --full-index`" - nil end def dependency_specs(gem_names) @@ -53,9 +50,9 @@ module Bundler def unmarshalled_dep_gems(gem_names) gem_list = [] - gem_names.each_slice(Source::Rubygems::API_REQUEST_SIZE) do |names| + gem_names.each_slice(api_request_size) do |names| marshalled_deps = downloader.fetch(dependency_api_uri(names)).body - gem_list.concat(Bundler.load_marshal(marshalled_deps)) + gem_list.concat(Bundler.safe_load_marshal(marshalled_deps)) end gem_list end @@ -77,6 +74,12 @@ module Bundler uri.query = "gems=#{CGI.escape(gem_names.sort.join(","))}" if gem_names.any? uri end + + private + + def api_request_size + Bundler.settings[:api_request_size]&.to_i || Source::Rubygems::API_REQUEST_SIZE + end end end end diff --git a/lib/bundler/fetcher/downloader.rb b/lib/bundler/fetcher/downloader.rb index cbc5e220bd..179eed8340 100644 --- a/lib/bundler/fetcher/downloader.rb +++ b/lib/bundler/fetcher/downloader.rb @@ -3,6 +3,28 @@ module Bundler class Fetcher class Downloader + HTTP_NON_RETRYABLE_ERRORS = [ + SocketError, + Errno::EADDRNOTAVAIL, + Errno::ENETDOWN, + Errno::ENETUNREACH, + Gem::Net::HTTP::Persistent::Error, + Errno::EHOSTUNREACH, + ].freeze + + HTTP_RETRYABLE_ERRORS = [ + Gem::Timeout::Error, + EOFError, + Errno::EINVAL, + Errno::ECONNRESET, + Errno::ETIMEDOUT, + Errno::EAGAIN, + Gem::Net::HTTPBadResponse, + Gem::Net::HTTPHeaderSyntaxError, + Gem::Net::ProtocolError, + Zlib::BufError, + ].freeze + attr_reader :connection attr_reader :redirect_limit @@ -11,65 +33,80 @@ module Bundler @redirect_limit = redirect_limit end - def fetch(uri, options = {}, counter = 0) + def fetch(uri, headers = {}, counter = 0) raise HTTPError, "Too many redirects" if counter >= redirect_limit - response = request(uri, options) - Bundler.ui.debug("HTTP #{response.code} #{response.message} #{uri}") + filtered_uri = URICredentialsFilter.credential_filtered_uri(uri) + + response = request(uri, headers) + Bundler.ui.debug("HTTP #{response.code} #{response.message} #{filtered_uri}") case response - when Net::HTTPSuccess, Net::HTTPNotModified + when Gem::Net::HTTPSuccess, Gem::Net::HTTPNotModified response - when Net::HTTPRedirection - new_uri = URI.parse(response["location"]) + when Gem::Net::HTTPRedirection + new_uri = Gem::URI.parse(response["location"]) if new_uri.host == uri.host new_uri.user = uri.user new_uri.password = uri.password end - fetch(new_uri, options, counter + 1) - when Net::HTTPRequestEntityTooLarge + fetch(new_uri, headers, counter + 1) + when Gem::Net::HTTPRequestedRangeNotSatisfiable + new_headers = headers.dup + new_headers.delete("Range") + fetch(uri, new_headers) + when Gem::Net::HTTPRequestEntityTooLarge raise FallbackError, response.body - when Net::HTTPUnauthorized + when Gem::Net::HTTPTooManyRequests + raise TooManyRequestsError, response.body + when Gem::Net::HTTPUnauthorized + raise BadAuthenticationError, uri.host if uri.userinfo raise AuthenticationRequiredError, uri.host - when Net::HTTPNotFound - raise FallbackError, "Net::HTTPNotFound" + when Gem::Net::HTTPForbidden + raise AuthenticationForbiddenError, uri.host + when Gem::Net::HTTPNotFound + raise FallbackError, "Gem::Net::HTTPNotFound: #{filtered_uri}" else - raise HTTPError, "#{response.class}#{": #{response.body}" unless response.body.empty?}" + message = "Gem::#{response.class.name.gsub(/\AGem::/, "")}" + message += ": #{response.body}" unless response.body.empty? + raise HTTPError, message end end - def request(uri, options) + def request(uri, headers) validate_uri_scheme!(uri) - Bundler.ui.debug "HTTP GET #{uri}" - req = Net::HTTP::Get.new uri.request_uri, options + filtered_uri = URICredentialsFilter.credential_filtered_uri(uri) + + Bundler.ui.debug "HTTP GET #{filtered_uri}" + req = Gem::Net::HTTP::Get.new uri.request_uri, headers if uri.user user = CGI.unescape(uri.user) password = uri.password ? CGI.unescape(uri.password) : nil req.basic_auth(user, password) end connection.request(uri, req) - rescue NoMethodError => e - raise unless ["undefined method", "use_ssl="].all? {|snippet| e.message.include? snippet } - raise LoadError.new("cannot load such file -- openssl") rescue OpenSSL::SSL::SSLError raise CertificateFailureError.new(uri) - rescue *HTTP_ERRORS => e + rescue *HTTP_NON_RETRYABLE_ERRORS => e Bundler.ui.trace e - case e.message - when /host down:/, /getaddrinfo: nodename nor servname provided/ - raise NetworkDownError, "Could not reach host #{uri.host}. Check your network " \ - "connection and try again." - else - raise HTTPError, "Network error while fetching #{URICredentialsFilter.credential_filtered_uri(uri)}" \ + + host = uri.host + host_port = "#{host}:#{uri.port}" + host = host_port if filtered_uri.to_s.include?(host_port) + raise NetworkDownError, "Could not reach host #{host}. Check your network " \ + "connection and try again." + rescue *HTTP_RETRYABLE_ERRORS => e + Bundler.ui.trace e + + raise HTTPError, "Network error while fetching #{filtered_uri}" \ " (#{e})" - end end - private + private def validate_uri_scheme!(uri) - return if uri.scheme =~ /\Ahttps?\z/ + return if /\Ahttps?\z/.match?(uri.scheme) raise InvalidOption, "The request uri `#{uri}` has an invalid scheme (`#{uri.scheme}`). " \ "Did you mean `http` or `https`?" diff --git a/lib/bundler/fetcher/gem_remote_fetcher.rb b/lib/bundler/fetcher/gem_remote_fetcher.rb new file mode 100644 index 0000000000..3159e05688 --- /dev/null +++ b/lib/bundler/fetcher/gem_remote_fetcher.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "rubygems/remote_fetcher" + +module Bundler + class Fetcher + class GemRemoteFetcher < Gem::RemoteFetcher + def initialize(*) + super + + @pool_size = Bundler.settings.installation_parallelization + end + + def request(*args) + super do |req| + req.delete("User-Agent") if headers["User-Agent"] + yield req if block_given? + end + end + end + end +end diff --git a/lib/bundler/fetcher/index.rb b/lib/bundler/fetcher/index.rb index 9529944391..6e37e1e5d1 100644 --- a/lib/bundler/fetcher/index.rb +++ b/lib/bundler/fetcher/index.rb @@ -1,51 +1,24 @@ # frozen_string_literal: true -require "bundler/fetcher/base" -require "rubygems/remote_fetcher" +require_relative "base" module Bundler class Fetcher class Index < Base def specs(_gem_names) - Bundler.rubygems.fetch_all_remote_specs(remote) - rescue Gem::RemoteFetcher::FetchError, OpenSSL::SSL::SSLError, Net::HTTPFatalError => e + Bundler.rubygems.fetch_all_remote_specs(remote, gem_remote_fetcher) + rescue Gem::RemoteFetcher::FetchError => e case e.message when /certificate verify failed/ raise CertificateFailureError.new(display_uri) when /401/ - raise AuthenticationRequiredError, remote_uri - when /403/ raise BadAuthenticationError, remote_uri if remote_uri.userinfo raise AuthenticationRequiredError, remote_uri + when /403/ + raise AuthenticationForbiddenError, remote_uri else - Bundler.ui.trace e - raise HTTPError, "Could not fetch specs from #{display_uri}" - end - end - - def fetch_spec(spec) - spec -= [nil, "ruby", ""] - spec_file_name = "#{spec.join "-"}.gemspec" - - 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(downloader.fetch(uri).body) + raise HTTPError, "Could not fetch specs from #{display_uri} due to underlying error <#{e.message}>" end - rescue MarshalError - raise HTTPError, "Gemspec #{spec} contained invalid data.\n" \ - "Your network or your gem server is probably having issues right now." - end - - private - - # 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.find {|path| File.file? path } end end end diff --git a/lib/bundler/force_platform.rb b/lib/bundler/force_platform.rb new file mode 100644 index 0000000000..7af33218cb --- /dev/null +++ b/lib/bundler/force_platform.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Bundler + module ForcePlatform + # The `:force_ruby_platform` value used by dependencies for resolution, and + # by locked specifications for materialization is `false` by default, except + # for TruffleRuby. TruffleRuby generally needs to force the RUBY platform + # variant unless the name is explicitly allowlisted. + + def default_force_ruby_platform + return false unless RUBY_ENGINE == "truffleruby" + + !Gem::Platform::REUSE_AS_BINARY_ON_TRUFFLERUBY.include?(name) + end + end +end diff --git a/lib/bundler/friendly_errors.rb b/lib/bundler/friendly_errors.rb index f624185773..5e8eaee6bb 100644 --- a/lib/bundler/friendly_errors.rb +++ b/lib/bundler/friendly_errors.rb @@ -1,12 +1,22 @@ -# encoding: utf-8 # frozen_string_literal: true -require "cgi" -require "bundler/vendored_thor" +require_relative "vendored_thor" module Bundler module FriendlyErrors - module_function + module_function + + def enable! + @disabled = false + end + + def disabled? + @disabled + end + + def disable! + @disabled = true + end def log_error(error) case error @@ -17,26 +27,20 @@ module Bundler Bundler.ui.error error.message when GemRequireError Bundler.ui.error error.message - Bundler.ui.trace error.orig_exception, nil, true + Bundler.ui.trace error.orig_exception when BundlerError - Bundler.ui.error error.message, :wrap => true - Bundler.ui.trace error + if Bundler.ui.debug? + Bundler.ui.trace error + else + Bundler.ui.error error.message, wrap: true + end when Thor::Error Bundler.ui.error error.message - when LoadError - raise error unless error.message =~ /cannot load such file -- openssl|openssl.so|libcrypto.so/ - Bundler.ui.error "\nCould not load OpenSSL." - Bundler.ui.warn <<-WARN, :wrap => true - You must recompile Ruby with OpenSSL support or change the sources in your \ - Gemfile from 'https' to 'http'. Instructions for compiling with OpenSSL \ - using RVM are available at http://rvm.io/packages/openssl. - WARN - Bundler.ui.trace error when Interrupt Bundler.ui.error "\nQuitting..." Bundler.ui.trace error when Gem::InvalidSpecificationException - Bundler.ui.error error.message, :wrap => true + Bundler.ui.error error.message, wrap: true when SystemExit when *[defined?(Java::JavaLang::OutOfMemoryError) && Java::JavaLang::OutOfMemoryError].compact Bundler.ui.error "\nYour JVM has run out of memory, and Bundler cannot continue. " \ @@ -57,40 +61,11 @@ module Bundler end def request_issue_report_for(e) - Bundler.ui.info <<-EOS.gsub(/^ {8}/, "") + Bundler.ui.error <<~EOS, nil, nil --- ERROR REPORT TEMPLATE ------------------------------------------------------- - # Error Report - - ## Questions - - Please fill out answers to these questions, it'll help us figure out - why things are going wrong. - - - **What did you do?** - - I ran the command `#{$PROGRAM_NAME} #{ARGV.join(" ")}` - - - **What did you expect to happen?** - - I expected Bundler to... - - - **What happened instead?** - - Instead, what happened was... - - - **Have you tried any solutions posted on similar issues in our issue tracker, stack overflow, or google?** - - I tried... - - - **Have you read our issues document, https://github.com/bundler/bundler/blob/master/doc/contributing/ISSUES.md?** - - ... - - ## Backtrace ``` - #{e.class}: #{e.message} - #{e.backtrace && e.backtrace.join("\n ").chomp} + #{exception_message(e)} ``` #{Bundler::Env.report} @@ -100,29 +75,52 @@ module Bundler Bundler.ui.error "Unfortunately, an unexpected error occurred, and Bundler cannot continue." - Bundler.ui.warn <<-EOS.gsub(/^ {8}/, "") + Bundler.ui.error <<~EOS, nil, :yellow First, try this link to see if there are any existing issue reports for this error: #{issues_url(e)} - If there aren't any reports for this error yet, please create copy and paste the report template above into a new issue. Don't forget to anonymize any private data! The new issue form is located at: - https://github.com/bundler/bundler/issues/new + If there aren't any reports for this error yet, please fill in the new issue form located at #{new_issue_url}. Make sure to copy and paste the full output of this command under the "What happened instead?" section. + EOS + end + + def exception_message(error) + message = serialized_exception_for(error) + cause = error.cause + return message unless cause + + message + serialized_exception_for(cause) + end + + def serialized_exception_for(e) + <<~EOS + #{e.class}: #{e.message} + #{e.backtrace&.join("\n ")&.chomp} EOS end def issues_url(exception) message = exception.message.lines.first.tr(":", " ").chomp message = message.split("-").first if exception.is_a?(Errno) - "https://github.com/bundler/bundler/search?q=" \ + require "cgi/escape" + require "cgi/util" unless defined?(CGI::EscapeExt) + "https://github.com/ruby/rubygems/search?q=" \ "#{CGI.escape(message)}&type=Issues" end + + def new_issue_url + "https://github.com/ruby/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md" + end end def self.with_friendly_errors + FriendlyErrors.enable! yield rescue SignalException raise - rescue Exception => e + rescue Exception => e # rubocop:disable Lint/RescueException + raise if FriendlyErrors.disabled? + FriendlyErrors.log_error(e) exit FriendlyErrors.exit_status(e) end diff --git a/lib/bundler/gem_helper.rb b/lib/bundler/gem_helper.rb index 1d7fc508d5..5ce0ef6280 100644 --- a/lib/bundler/gem_helper.rb +++ b/lib/bundler/gem_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require "bundler/vendored_thor" unless defined?(Thor) -require "bundler" +require_relative "../bundler" +require "shellwords" module Bundler class GemHelper @@ -15,22 +15,28 @@ module Bundler new(opts[:dir], opts[:name]).install end + def tag_prefix=(prefix) + instance.tag_prefix = prefix + end + def gemspec(&block) gemspec = instance.gemspec - block.call(gemspec) if block + block&.call(gemspec) gemspec end end attr_reader :spec_path, :base, :gemspec + attr_writer :tag_prefix + def initialize(base = nil, name = nil) - Bundler.ui = UI::Shell.new - @base = (base ||= SharedHelpers.pwd) - gemspecs = name ? [File.join(base, "#{name}.gemspec")] : Dir[File.join(base, "{,*}.gemspec")] + @base = File.expand_path(base || SharedHelpers.pwd) + gemspecs = name ? [File.join(@base, "#{name}.gemspec")] : Gem::Util.glob_files_in_dir("{,*}.gemspec", @base) 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 @gemspec = Bundler.load_gemspec(@spec_path) + @tag_prefix = "" end def install @@ -41,6 +47,11 @@ module Bundler built_gem_path = build_gem end + desc "Generate SHA512 checksum of #{name}-#{version}.gem into the checksums directory." + task "build:checksum" => "build" do + build_checksum(built_gem_path) + end + desc "Build and install #{name}-#{version}.gem into system gems." task "install" => "build" do install_gem(built_gem_path) @@ -65,7 +76,7 @@ module Bundler tag_version { git_push(args[:remote]) } unless already_tagged? end - task "release:rubygem_push" do + task "release:rubygem_push" => "build" do rubygem_push(built_gem_path) if gem_push? end @@ -74,7 +85,7 @@ module Bundler def build_gem file_name = nil - sh("gem build -V '#{spec_path}'") do + sh([*gem_command, "build", "-V", spec_path]) do file_name = File.basename(built_gem_path) SharedHelpers.filesystem_access(File.join(base, "pkg")) {|p| FileUtils.mkdir_p(p) } FileUtils.mv(built_gem_path, "pkg") @@ -85,32 +96,54 @@ module Bundler def install_gem(built_gem_path = nil, local = false) built_gem_path ||= build_gem - out, _ = sh_with_code("gem install '#{built_gem_path}'#{" --local" if local}") - raise "Couldn't install gem, run `gem install #{built_gem_path}' for more detailed output" unless out[/Successfully installed/] + cmd = [*gem_command, "install", built_gem_path.to_s] + cmd << "--local" if local + sh(cmd) Bundler.ui.confirm "#{name} (#{version}) installed." end - protected + def build_checksum(built_gem_path = nil) + built_gem_path ||= build_gem + SharedHelpers.filesystem_access(File.join(base, "checksums")) {|p| FileUtils.mkdir_p(p) } + file_name = "#{File.basename(built_gem_path)}.sha512" + require "digest/sha2" + checksum = ::Digest::SHA512.file(built_gem_path).hexdigest + target = File.join(base, "checksums", file_name) + File.write(target, checksum + "\n") + Bundler.ui.confirm "#{name} #{version} checksum written to checksums/#{file_name}." + end + + protected def rubygem_push(path) - gem_command = "gem push '#{path}'" - gem_command += " --key #{gem_key}" if gem_key - gem_command += " --host #{allowed_push_host}" if allowed_push_host - unless allowed_push_host || Bundler.user_home.join(".gem/credentials").file? - raise "Your rubygems.org credentials aren't set. Run `gem push` to set them." - end - sh(gem_command) + cmd = [*gem_command, "push", path] + cmd << "--key" << gem_key if gem_key + cmd << "--host" << allowed_push_host if allowed_push_host + sh_with_input(cmd) Bundler.ui.confirm "Pushed #{name} #{version} to #{gem_push_host}" end def built_gem_path - Dir[File.join(base, "#{name}-*.gem")].sort_by {|f| File.mtime(f) }.last + Gem::Util.glob_files_in_dir("#{name}-*.gem", base).sort_by {|f| File.mtime(f) }.last end - def git_push(remote = "") - perform_git_push remote - perform_git_push "#{remote} --tags" - Bundler.ui.confirm "Pushed git commits and tags." + def git_push(remote = nil) + remote ||= default_remote + sh("git push #{remote} refs/heads/#{current_branch}".shellsplit) + sh("git push #{remote} refs/tags/#{version_tag}".shellsplit) + Bundler.ui.confirm "Pushed git commits and release tag." + end + + def default_remote + remote_for_branch, status = sh_with_status(%W[git config --get branch.#{current_branch}.remote]) + return "origin" unless status.success? + + remote_for_branch.strip + end + + def current_branch + # We can replace this with `git branch --show-current` once we drop support for git < 2.22.0 + sh(%w[git rev-parse --abbrev-ref HEAD]).gsub(%r{\Aheads/}, "").strip end def allowed_push_host @@ -119,20 +152,13 @@ module Bundler def gem_push_host env_rubygems_host = ENV["RUBYGEMS_HOST"] - env_rubygems_host = nil if - env_rubygems_host && env_rubygems_host.empty? + env_rubygems_host = nil if env_rubygems_host&.empty? allowed_push_host || env_rubygems_host || "rubygems.org" end - def perform_git_push(options = "") - cmd = "git push #{options}" - out, code = sh_with_code(cmd) - raise "Couldn't git push. `#{cmd}' failed with the following output:\n\n#{out}\n" unless code == 0 - end - def already_tagged? - return false unless sh("git tag").split(/\n/).include?(version_tag) + return false unless sh(%w[git tag]).split(/\n/).include?(version_tag) Bundler.ui.confirm "Tag #{version_tag} has already been created." true end @@ -142,20 +168,20 @@ module Bundler end def clean? - sh_with_code("git diff --exit-code")[1] == 0 + sh_with_status(%w[git diff --exit-code])[1].success? end def committed? - sh_with_code("git diff-index --quiet --cached HEAD")[1] == 0 + sh_with_status(%w[git diff-index --quiet --cached HEAD])[1].success? end def tag_version - sh "git tag -m \"Version #{version}\" #{version_tag}" + sh %W[git tag -m Version\ #{version} #{version_tag}] Bundler.ui.confirm "Tagged #{version_tag}." yield if block_given? - rescue + rescue RuntimeError Bundler.ui.error "Untagging #{version_tag} due to error." - sh_with_code "git tag -d #{version_tag}" + sh_with_status %W[git tag -d #{version_tag}] raise end @@ -164,29 +190,34 @@ module Bundler end def version_tag - "v#{version}" + "#{@tag_prefix}v#{version}" end def name gemspec.name end + def sh_with_input(cmd) + Bundler.ui.debug(cmd) + SharedHelpers.chdir(base) do + abort unless Kernel.system(*cmd) + end + end + def sh(cmd, &block) - out, code = sh_with_code(cmd, &block) - unless code.zero? - raise(out.empty? ? "Running `#{cmd}` failed. Run this command directly for more detailed output." : out) + out, status = sh_with_status(cmd, &block) + unless status.success? + raise("Running `#{cmd.shelljoin}` failed with the following output:\n\n#{out}\n") end out end - def sh_with_code(cmd, &block) - cmd += " 2>&1" - outbuf = String.new + def sh_with_status(cmd, &block) Bundler.ui.debug(cmd) SharedHelpers.chdir(base) do - outbuf = `#{cmd}` - status = $?.exitstatus - block.call(outbuf) if status.zero? && block + outbuf = IO.popen(cmd, err: [:child, :out], &:read) + status = $? + block&.call(outbuf) if status.success? [outbuf, status] end end @@ -198,5 +229,9 @@ module Bundler def gem_push? !%w[n no nil false off 0].include?(ENV["gem_push"].to_s.downcase) end + + def gem_command + ENV["GEM_COMMAND"]&.shellsplit || ["gem"] + end end end diff --git a/lib/bundler/gem_helpers.rb b/lib/bundler/gem_helpers.rb deleted file mode 100644 index 019ae10c66..0000000000 --- a/lib/bundler/gem_helpers.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -module Bundler - module GemHelpers - GENERIC_CACHE = {} # rubocop:disable MutableConstant - GENERICS = [ - [Gem::Platform.new("java"), Gem::Platform.new("java")], - [Gem::Platform.new("mswin32"), Gem::Platform.new("mswin32")], - [Gem::Platform.new("mswin64"), Gem::Platform.new("mswin64")], - [Gem::Platform.new("universal-mingw32"), Gem::Platform.new("universal-mingw32")], - [Gem::Platform.new("x64-mingw32"), Gem::Platform.new("x64-mingw32")], - [Gem::Platform.new("x86_64-mingw32"), Gem::Platform.new("x64-mingw32")], - [Gem::Platform.new("mingw32"), Gem::Platform.new("x86-mingw32")] - ].freeze - - def generic(p) - return p if p == Gem::Platform::RUBY - - GENERIC_CACHE[p] ||= begin - _, found = GENERICS.find do |match, _generic| - p.os == match.os && (!match.cpu || p.cpu == match.cpu) - end - found || Gem::Platform::RUBY - end - end - module_function :generic - - def generic_local_platform - generic(Bundler.local_platform) - end - module_function :generic_local_platform - - def platform_specificity_match(spec_platform, user_platform) - spec_platform = Gem::Platform.new(spec_platform) - return PlatformMatch::EXACT_MATCH if spec_platform == user_platform - return PlatformMatch::WORST_MATCH if spec_platform.nil? || spec_platform == Gem::Platform::RUBY || user_platform == Gem::Platform::RUBY - - PlatformMatch.new( - PlatformMatch.os_match(spec_platform, user_platform), - PlatformMatch.cpu_match(spec_platform, user_platform), - PlatformMatch.platform_version_match(spec_platform, user_platform) - ) - end - module_function :platform_specificity_match - - def select_best_platform_match(specs, platform) - specs.select {|spec| spec.match_platform(platform) }. - min_by {|spec| platform_specificity_match(spec.platform, platform) } - end - module_function :select_best_platform_match - - PlatformMatch = Struct.new(:os_match, :cpu_match, :platform_version_match) - class PlatformMatch - def <=>(other) - return nil unless other.is_a?(PlatformMatch) - - m = os_match <=> other.os_match - return m unless m.zero? - - m = cpu_match <=> other.cpu_match - return m unless m.zero? - - m = platform_version_match <=> other.platform_version_match - m - end - - EXACT_MATCH = new(-1, -1, -1).freeze - WORST_MATCH = new(1_000_000, 1_000_000, 1_000_000).freeze - - def self.os_match(spec_platform, user_platform) - if spec_platform.os == user_platform.os - 0 - else - 1 - end - end - - def self.cpu_match(spec_platform, user_platform) - if spec_platform.cpu == user_platform.cpu - 0 - elsif spec_platform.cpu == "arm" && user_platform.cpu.to_s.start_with?("arm") - 0 - elsif spec_platform.cpu.nil? || spec_platform.cpu == "universal" - 1 - else - 2 - end - end - - def self.platform_version_match(spec_platform, user_platform) - if spec_platform.version == user_platform.version - 0 - elsif spec_platform.version.nil? - 1 - else - 2 - end - end - end - end -end diff --git a/lib/bundler/gem_remote_fetcher.rb b/lib/bundler/gem_remote_fetcher.rb deleted file mode 100644 index 9577535d63..0000000000 --- a/lib/bundler/gem_remote_fetcher.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require "rubygems/remote_fetcher" - -module Bundler - # Adds support for setting custom HTTP headers when fetching gems from the - # server. - # - # TODO: Get rid of this when and if gemstash only supports RubyGems versions - # that contain https://github.com/rubygems/rubygems/commit/3db265cc20b2f813. - class GemRemoteFetcher < Gem::RemoteFetcher - attr_accessor :headers - - # Extracted from RubyGems 2.4. - def fetch_http(uri, last_modified = nil, head = false, depth = 0) - fetch_type = head ? Net::HTTP::Head : Net::HTTP::Get - # beginning of change - response = request uri, fetch_type, last_modified do |req| - headers.each {|k, v| req.add_field(k, v) } if headers - end - # end of change - - case response - when Net::HTTPOK, Net::HTTPNotModified then - response.uri = uri if response.respond_to? :uri - head ? response : response.body - when Net::HTTPMovedPermanently, Net::HTTPFound, Net::HTTPSeeOther, - Net::HTTPTemporaryRedirect then - raise FetchError.new("too many redirects", uri) if depth > 10 - - location = URI.parse response["Location"] - - if https?(uri) && !https?(location) - raise FetchError.new("redirecting to non-https resource: #{location}", uri) - end - - fetch_http(location, last_modified, head, depth + 1) - else - raise FetchError.new("bad response #{response.message} #{response.code}", uri) - end - end - end -end diff --git a/lib/bundler/gem_tasks.rb b/lib/bundler/gem_tasks.rb index f736517bd7..bc725d3602 100644 --- a/lib/bundler/gem_tasks.rb +++ b/lib/bundler/gem_tasks.rb @@ -3,5 +3,5 @@ require "rake/clean" CLOBBER.include "pkg" -require "bundler/gem_helper" +require_relative "gem_helper" Bundler::GemHelper.install_tasks diff --git a/lib/bundler/gem_version_promoter.rb b/lib/bundler/gem_version_promoter.rb index 52b5386045..d64dbacfdb 100644 --- a/lib/bundler/gem_version_promoter.rb +++ b/lib/bundler/gem_version_promoter.rb @@ -7,12 +7,13 @@ module Bundler # available dependency versions as found in its index, before returning it to # to the resolution engine to select the best version. class GemVersionPromoter - attr_reader :level, :locked_specs, :unlock_gems + attr_reader :level + attr_accessor :pre # By default, strict is false, meaning every available version of a gem # is returned from sort_versions. The order gives preference to the # requested level (:patch, :minor, :major) but in complicated requirement - # cases some gems will by necessity by promoted past the requested level, + # cases some gems will by necessity be promoted past the requested level, # or even reverted to older versions. # # If strict is set to true, the results from sort_versions will be @@ -22,21 +23,13 @@ module Bundler # existing in the referenced source. attr_accessor :strict - # Given a list of locked_specs and a list of gems to unlock creates a - # GemVersionPromoter instance. + # Creates a GemVersionPromoter instance. # - # @param locked_specs [SpecSet] All current locked specs. Unlike Definition - # where this list is empty if all gems are being updated, this should - # always be populated for all gems so this class can properly function. - # @param unlock_gems [String] List of gem names being unlocked. If empty, - # all gems will be considered unlocked. # @return [GemVersionPromoter] - def initialize(locked_specs = SpecSet.new([]), unlock_gems = []) + def initialize @level = :major @strict = false - @locked_specs = locked_specs - @unlock_gems = unlock_gems - @sort_versions = {} + @pre = false end # @param value [Symbol] One of three Symbols: :major, :minor or :patch. @@ -50,37 +43,39 @@ module Bundler @level = v end - # Given a Dependency and an Array of SpecGroups of available versions for a - # gem, this method will return the Array of SpecGroups sorted (and possibly - # truncated if strict is true) in an order to give preference to the current - # level (:major, :minor or :patch) when resolution is deciding what versions - # best resolve all dependencies in the bundle. - # @param dep [Dependency] The Dependency of the gem. - # @param spec_groups [SpecGroup] An array of SpecGroups for the same gem - # named in the @dep param. - # @return [SpecGroup] A new instance of the SpecGroup Array sorted and - # possibly filtered. - def sort_versions(dep, spec_groups) - before_result = "before sort_versions: #{debug_format_result(dep, spec_groups).inspect}" if ENV["DEBUG_RESOLVER"] - - @sort_versions[dep] ||= begin - gem_name = dep.name - - # An Array per version returned, different entries for different platforms. - # We only need the version here so it's ok to hard code this to the first instance. - locked_spec = locked_specs[gem_name].first - - if strict - filter_dep_specs(spec_groups, locked_spec) + # Given a Resolver::Package and an Array of Specifications of available + # versions for a gem, this method will return the Array of Specifications + # sorted in an order to give preference to the current level (:major, :minor + # or :patch) when resolution is deciding what versions best resolve all + # dependencies in the bundle. + # @param package [Resolver::Package] The package being resolved. + # @param specs [Specification] An array of Specifications for the package. + # @return [Specification] A new instance of the Specification Array sorted. + def sort_versions(package, specs) + locked_version = package.locked_version + + result = specs.sort do |a, b| + unless package.prerelease_specified? || pre? + a_pre = a.prerelease? + b_pre = b.prerelease? + + next 1 if a_pre && !b_pre + next -1 if b_pre && !a_pre + end + + if major? || locked_version.nil? + b <=> a + elsif either_version_older_than_locked?(a, b, locked_version) + b <=> a + elsif segments_do_not_match?(a, b, :major) + a <=> b + elsif !minor? && segments_do_not_match?(a, b, :minor) + a <=> b else - sort_dep_specs(spec_groups, locked_spec) - end.tap do |specs| - if ENV["DEBUG_RESOLVER"] - STDERR.puts before_result - STDERR.puts " after sort_versions: #{debug_format_result(dep, specs).inspect}" - end + b <=> a end end + post_sort(result, package.unlock?, locked_version) end # @return [bool] Convenience method for testing value of level variable. @@ -93,84 +88,60 @@ module Bundler level == :minor end - private + # @return [bool] Convenience method for testing value of pre variable. + def pre? + pre == true + end - def filter_dep_specs(spec_groups, locked_spec) - res = spec_groups.select do |spec_group| - if locked_spec && !major? - gsv = spec_group.version - lsv = locked_spec.version + # Given a Resolver::Package and an Array of Specifications of available + # versions for a gem, this method will truncate the Array if strict + # is true. That means filtering out downgrades from the version currently + # locked, and filtering out upgrades that go past the selected level (major, + # minor, or patch). + # @param package [Resolver::Package] The package being resolved. + # @param specs [Specification] An array of Specifications for the package. + # @return [Specification] A new instance of the Specification Array + # truncated. + def filter_versions(package, specs) + return specs unless strict - must_match = minor? ? [0] : [0, 1] + locked_version = package.locked_version + return specs if locked_version.nil? || major? - matches = must_match.map {|idx| gsv.segments[idx] == lsv.segments[idx] } - (matches.uniq == [true]) ? (gsv >= lsv) : false - else - true - end - end + specs.select do |spec| + gsv = spec.version - sort_dep_specs(res, locked_spec) - end + must_match = minor? ? [0] : [0, 1] - def sort_dep_specs(spec_groups, locked_spec) - return spec_groups unless locked_spec - @gem_name = locked_spec.name - @locked_version = locked_spec.version - - result = spec_groups.sort do |a, b| - @a_ver = a.version - @b_ver = b.version - if major? - @a_ver <=> @b_ver - elsif either_version_older_than_locked - @a_ver <=> @b_ver - elsif segments_do_not_match(:major) - @b_ver <=> @a_ver - elsif !minor? && segments_do_not_match(:minor) - @b_ver <=> @a_ver - else - @a_ver <=> @b_ver - end + all_match = must_match.all? {|idx| gsv.segments[idx] == locked_version.segments[idx] } + all_match && gsv >= locked_version end - post_sort(result) end - def either_version_older_than_locked - @a_ver < @locked_version || @b_ver < @locked_version - end + private - def segments_do_not_match(level) - index = [:major, :minor].index(level) - @a_ver.segments[index] != @b_ver.segments[index] + def either_version_older_than_locked?(a, b, locked_version) + a.version < locked_version || b.version < locked_version end - def unlocking_gem? - unlock_gems.empty? || unlock_gems.include?(@gem_name) + def segments_do_not_match?(a, b, level) + index = [:major, :minor].index(level) + a.segments[index] != b.segments[index] end # Specific version moves can't always reliably be done during sorting # as not all elements are compared against each other. - def post_sort(result) - # default :major behavior in Bundler does not do this - return result if major? - if unlocking_gem? + def post_sort(result, unlock, locked_version) + if unlock || locked_version.nil? result else - move_version_to_end(result, @locked_version) + move_version_to_beginning(result, locked_version) end end - def move_version_to_end(result, version) + def move_version_to_beginning(result, version) move, keep = result.partition {|s| s.version.to_s == version.to_s } - keep.concat(move) - end - - def debug_format_result(dep, spec_groups) - a = [dep.to_s, - spec_groups.map {|sg| [sg.version, sg.dependencies_for_activated_platforms.map {|dp| [dp.name, dp.requirement.to_s] }] }] - last_map = a.last.map {|sg_data| [sg_data.first.version, sg_data.last.map {|aa| aa.join(" ") }] } - [a.first, last_map, level, strict ? :strict : :not_strict] + move.concat(keep) end end end diff --git a/lib/bundler/gemdeps.rb b/lib/bundler/gemdeps.rb deleted file mode 100644 index cd4b25d0e6..0000000000 --- a/lib/bundler/gemdeps.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module Bundler - class Gemdeps - def initialize(runtime) - @runtime = runtime - end - - def requested_specs - @runtime.requested_specs - end - - def specs - @runtime.specs - end - - def dependencies - @runtime.dependencies - end - - def current_dependencies - @runtime.current_dependencies - end - - def requires - @runtime.requires - end - end -end diff --git a/lib/bundler/graph.rb b/lib/bundler/graph.rb deleted file mode 100644 index de6bba0214..0000000000 --- a/lib/bundler/graph.rb +++ /dev/null @@ -1,152 +0,0 @@ -# frozen_string_literal: true - -require "set" -module Bundler - class Graph - GRAPH_NAME = :Gemfile - - def initialize(env, output_file, show_version = false, show_requirements = false, output_format = "png", without = []) - @env = env - @output_file = output_file - @show_version = show_version - @show_requirements = show_requirements - @output_format = output_format - @without_groups = without.map(&:to_sym) - - @groups = [] - @relations = Hash.new {|h, k| h[k] = Set.new } - @node_options = {} - @edge_options = {} - - _populate_relations - end - - attr_reader :groups, :relations, :node_options, :edge_options, :output_file, :output_format - - def viz - GraphVizClient.new(self).run - end - - private - - def _populate_relations - parent_dependencies = _groups.values.to_set.flatten - loop do - break if parent_dependencies.empty? - - tmp = Set.new - parent_dependencies.each do |dependency| - child_dependencies = spec_for_dependency(dependency).runtime_dependencies.to_set - @relations[dependency.name] += child_dependencies.map(&:name).to_set - tmp += child_dependencies - - @node_options[dependency.name] = _make_label(dependency, :node) - child_dependencies.each do |c_dependency| - @edge_options["#{dependency.name}_#{c_dependency.name}"] = _make_label(c_dependency, :edge) - end - end - parent_dependencies = tmp - end - end - - def _groups - relations = Hash.new {|h, k| h[k] = Set.new } - @env.current_dependencies.each do |dependency| - dependency.groups.each do |group| - next if @without_groups.include?(group) - - relations[group.to_s].add(dependency) - @relations[group.to_s].add(dependency.name) - - @node_options[group.to_s] ||= _make_label(group, :node) - @edge_options["#{group}_#{dependency.name}"] = _make_label(dependency, :edge) - end - end - @groups = relations.keys - relations - end - - def _make_label(symbol_or_string_or_dependency, element_type) - case element_type.to_sym - when :node - if symbol_or_string_or_dependency.is_a?(Gem::Dependency) - label = symbol_or_string_or_dependency.name.dup - label << "\n#{spec_for_dependency(symbol_or_string_or_dependency).version}" if @show_version - else - label = symbol_or_string_or_dependency.to_s - end - when :edge - label = nil - if symbol_or_string_or_dependency.respond_to?(:requirements_list) && @show_requirements - tmp = symbol_or_string_or_dependency.requirements_list.join(", ") - label = tmp if tmp != ">= 0" - end - else - raise ArgumentError, "2nd argument is invalid" - end - label.nil? ? {} : { :label => label } - end - - def spec_for_dependency(dependency) - @env.requested_specs.find {|s| s.name == dependency.name } - end - - class GraphVizClient - def initialize(graph_instance) - @graph_name = graph_instance.class::GRAPH_NAME - @groups = graph_instance.groups - @relations = graph_instance.relations - @node_options = graph_instance.node_options - @edge_options = graph_instance.edge_options - @output_file = graph_instance.output_file - @output_format = graph_instance.output_format - end - - def g - @g ||= ::GraphViz.digraph(@graph_name, :concentrate => true, :normalize => true, :nodesep => 0.55) do |g| - g.edge[:weight] = 2 - g.edge[:fontname] = g.node[:fontname] = "Arial, Helvetica, SansSerif" - g.edge[:fontsize] = 12 - end - end - - def run - @groups.each do |group| - g.add_nodes( - group, { - :style => "filled", - :fillcolor => "#B9B9D5", - :shape => "box3d", - :fontsize => 16 - }.merge(@node_options[group]) - ) - end - - @relations.each do |parent, children| - children.each do |child| - if @groups.include?(parent) - g.add_nodes(child, { :style => "filled", :fillcolor => "#B9B9D5" }.merge(@node_options[child])) - g.add_edges(parent, child, { :constraint => false }.merge(@edge_options["#{parent}_#{child}"])) - else - g.add_nodes(child, @node_options[child]) - g.add_edges(parent, child, @edge_options["#{parent}_#{child}"]) - end - end - end - - if @output_format.to_s == "debug" - $stdout.puts g.output :none => String - Bundler.ui.info "debugging bundle viz..." - else - begin - g.output @output_format.to_sym => "#{@output_file}.#{@output_format}" - Bundler.ui.info "#{@output_file}.#{@output_format}" - rescue ArgumentError => e - $stderr.puts "Unsupported output format. See Ruby-Graphviz/lib/graphviz/constants.rb" - raise e - end - end - end - end - end -end diff --git a/lib/bundler/index.rb b/lib/bundler/index.rb index 9166a92738..9aef2dfa12 100644 --- a/lib/bundler/index.rb +++ b/lib/bundler/index.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "set" - module Bundler class Index include Enumerable @@ -12,30 +10,30 @@ module Bundler i end - attr_reader :specs, :all_specs, :sources - protected :specs, :all_specs + attr_reader :specs, :duplicates, :sources + protected :specs, :duplicates - RUBY = "ruby".freeze - NULL = "\0".freeze + RUBY = "ruby" + NULL = "\0" def initialize @sources = [] @cache = {} - @specs = Hash.new {|h, k| h[k] = {} } - @all_specs = Hash.new {|h, k| h[k] = EMPTY_SEARCH } + @specs = {} + @duplicates = {} end def initialize_copy(o) @sources = o.sources.dup @cache = {} - @specs = Hash.new {|h, k| h[k] = {} } - @all_specs = Hash.new {|h, k| h[k] = EMPTY_SEARCH } + @specs = {} + @duplicates = {} o.specs.each do |name, hash| @specs[name] = hash.dup end - o.all_specs.each do |name, array| - @all_specs[name] = array.dup + o.duplicates.each do |name, array| + @duplicates[name] = array.dup end end @@ -48,63 +46,35 @@ module Bundler true end - def search_all(name) - all_matches = local_search(name) + @all_specs[name] - @sources.each do |source| - all_matches.concat(source.search_all(name)) - end - all_matches - end - # Search this index's specs, and any source indexes that this index knows # about, returning all of the results. - def search(query, base = nil) - sort_specs(unsorted_search(query, base)) - end - - def unsorted_search(query, base) - results = local_search(query, base) - - seen = results.map(&:full_name).to_set unless @sources.empty? + def search(query) + results = local_search(query) + return results unless @sources.any? @sources.each do |source| - source.unsorted_search(query, base).each do |spec| - results << spec if seen.add?(spec.full_name) - end + results = safe_concat(results, source.search(query)) end - + results.uniq!(&:full_name) unless results.empty? # avoid modifying frozen EMPTY_SEARCH results end - protected :unsorted_search - def self.sort_specs(specs) - specs.sort_by do |s| - platform_string = s.platform.to_s - [s.version, platform_string == RUBY ? NULL : platform_string] - end - end - - def sort_specs(specs) - self.class.sort_specs(specs) - end + alias_method :[], :search - def local_search(query, base = nil) + def local_search(query) case query when Gem::Specification, RemoteSpecification, LazySpecification, EndpointSpecification then search_by_spec(query) when String then specs_by_name(query) - when Gem::Dependency then search_by_dependency(query, base) - when DepProxy then search_by_dependency(query.dep, base) + when Array then specs_by_name_and_version(*query) else raise "You can't search for a #{query.inspect}." end end - alias_method :[], :search - - def <<(spec) - @specs[spec.name][spec.full_name] = spec - spec + def add(spec) + (@specs[spec.name] ||= {}).store(spec.full_name, spec) end + alias_method :<<, :add def each(&blk) return enum_for(:each) unless blk @@ -121,10 +91,9 @@ module Bundler names end - # returns a list of the dependencies def unmet_dependency_names dependency_names.select do |name| - name != "bundler" && search(name).empty? + search(name).empty? end end @@ -139,15 +108,30 @@ module Bundler names.uniq end - def use(other, override_dupes = false) + # Combines indexes proritizing existing specs, like `Hash#reverse_merge!` + # Duplicate specs found in `other` are stored in `@duplicates`. + def use(other) return unless other - other.each do |s| - if (dupes = search_by_spec(s)) && !dupes.empty? - # safe to << since it's a new array when it has contents - @all_specs[s.name] = dupes << s - next unless override_dupes + other.each do |spec| + exist?(spec) ? add_duplicate(spec) : add(spec) + end + self + end + + # Combines indexes proritizing specs from `other`, like `Hash#merge!` + # Duplicate specs found in `self` are saved in `@duplicates`. + def merge!(other) + return unless other + other.each do |spec| + if existing = find_by_spec(spec) + unless dependencies_eql?(existing, spec) + Bundler.ui.warn "Local specification for #{spec.full_name} has different dependencies than the remote gem, ignoring it" + next + end + + add_duplicate(existing) end - self << s + add spec end self end @@ -159,8 +143,7 @@ module Bundler end # Whether all the specs in self are in other - # TODO: rename to #include? - def ==(other) + def subset?(other) all? do |spec| other_spec = other[spec].first other_spec && dependencies_eql?(spec, other_spec) && spec.source == other_spec.source @@ -168,9 +151,9 @@ module Bundler end def dependencies_eql?(spec, other_spec) - deps = spec.dependencies.select {|d| d.type != :development } - other_deps = other_spec.dependencies.select {|d| d.type != :development } - Set.new(deps) == Set.new(other_deps) + deps = spec.runtime_dependencies + other_deps = other_spec.runtime_dependencies + deps.sort == other_deps.sort end def add_source(index) @@ -179,35 +162,42 @@ module Bundler @sources.uniq! # need to use uniq! here instead of checking for the item before adding end - private + private - def specs_by_name(name) - @specs[name].values - end - - def search_by_dependency(dependency, base = nil) - @cache[base || false] ||= {} - @cache[base || false][dependency] ||= begin - specs = specs_by_name(dependency.name) - specs += base if base - found = specs.select do |spec| - next true if spec.source.is_a?(Source::Gemspec) - if base # allow all platforms when searching from a lockfile - dependency.matches_spec?(spec) - else - dependency.matches_spec?(spec) && Gem::Platform.match(spec.platform) - end - end + def safe_concat(a, b) + return a if b.empty? + return b if a.empty? + a.concat(b) + end - found - end + def add_duplicate(spec) + (@duplicates[spec.name] ||= []) << spec + end + + def specs_by_name_and_version(name, version) + results = @specs[name]&.values + return EMPTY_SEARCH unless results + results.select! {|spec| spec.version == version } + results + end + + def specs_by_name(name) + @specs[name]&.values || EMPTY_SEARCH end EMPTY_SEARCH = [].freeze def search_by_spec(spec) - spec = @specs[spec.name][spec.full_name] + spec = find_by_spec(spec) spec ? [spec] : EMPTY_SEARCH end + + def find_by_spec(spec) + @specs[spec.name]&.fetch(spec.full_name, nil) + end + + def exist?(spec) + @specs[spec.name]&.key?(spec.full_name) + end end end diff --git a/lib/bundler/injector.rb b/lib/bundler/injector.rb index 7fe6a91ddd..6aa9179024 100644 --- a/lib/bundler/injector.rb +++ b/lib/bundler/injector.rb @@ -2,67 +2,102 @@ module Bundler class Injector + INJECTED_GEMS = "injected gems" + def self.inject(new_deps, options = {}) injector = new(new_deps, options) injector.inject(Bundler.default_gemfile, Bundler.default_lockfile) end - def initialize(new_deps, options = {}) - @new_deps = new_deps + def self.remove(gems, options = {}) + injector = new(gems, options) + injector.remove(Bundler.default_gemfile, Bundler.default_lockfile) + end + + def initialize(deps, options = {}) + @deps = deps @options = options end + # @param [Pathname] gemfile_path The Gemfile in which to inject the new dependency. + # @param [Pathname] lockfile_path The lockfile in which to inject the new dependency. + # @return [Array] def inject(gemfile_path, lockfile_path) - if Bundler.frozen? - # ensure the lock and Gemfile are synced - Bundler.definition.ensure_equivalent_gemfile_and_lockfile(true) - end + Bundler.definition.ensure_equivalent_gemfile_and_lockfile(true) # temporarily unfreeze - Bundler.settings.temporary(:deployment => false, :frozen => false) do + Bundler.settings.temporary(deployment: false, frozen: false) do # evaluate the Gemfile we have now builder = Dsl.new builder.eval_gemfile(gemfile_path) # don't inject any gems that are already in the Gemfile - @new_deps -= builder.dependencies + @deps -= builder.dependencies # add new deps to the end of the in-memory Gemfile - # Set conservative versioning to false because we want to let the resolver resolve the version first - builder.eval_gemfile("injected gems", build_gem_lines(false)) if @new_deps.any? + # Set conservative versioning to false because + # we want to let the resolver resolve the version first + builder.eval_gemfile(INJECTED_GEMS, build_gem_lines(false)) if @deps.any? # resolve to see if the new deps broke anything @definition = builder.to_definition(lockfile_path, {}) - @definition.resolve_remotely! + @definition.remotely! # since nothing broke, we can add those gems to the gemfile - append_to(gemfile_path, build_gem_lines(@options[:conservative_versioning])) if @new_deps.any? + append_to(gemfile_path, build_gem_lines(@options[:conservative_versioning])) if @deps.any? # since we resolved successfully, write out the lockfile - @definition.lock(Bundler.default_lockfile) + @definition.lock # invalidate the cached Bundler.definition Bundler.reset_paths! # return an array of the deps that we added - @new_deps + @deps end end - private + # @param [Pathname] gemfile_path The Gemfile from which to remove dependencies. + # @param [Pathname] lockfile_path The lockfile from which to remove dependencies. + # @return [Array] + def remove(gemfile_path, lockfile_path) + # remove gems from each gemfiles we have + Bundler.definition.gemfiles.each do |path| + deps = remove_deps(path) + + show_warning("No gems were removed from the gemfile.") if deps.empty? + + deps.each {|dep| Bundler.ui.confirm "#{SharedHelpers.pretty_dependency(dep)} was removed." } + end + + # Invalidate the cached Bundler.definition. + # This prevents e.g. `bundle remove ...` from using outdated information. + Bundler.reset_paths! + end + + private def conservative_version(spec) version = spec.version return ">= 0" if version.nil? - segments = version.segments seg_end_index = version >= Gem::Version.new("1.0") ? 1 : 2 - prerelease_suffix = version.to_s.gsub(version.release.to_s, "") if version.prerelease? - "~> #{segments[0..seg_end_index].join(".")}#{prerelease_suffix}" + prerelease_suffix = version.to_s.delete_prefix(version.release.to_s) if version.prerelease? + "#{version_prefix}#{version.segments[0..seg_end_index].join(".")}#{prerelease_suffix}" + end + + def version_prefix + if @options[:strict] + "= " + elsif @options[:pessimistic] + "~> " + else + ">= " + end end def build_gem_lines(conservative_versioning) - @new_deps.map do |d| + @deps.map do |d| name = d.name.dump requirement = if conservative_versioning @@ -72,23 +107,178 @@ module Bundler end if d.groups != Array(:default) - group = d.groups.size == 1 ? ", :group => #{d.groups.inspect}" : ", :groups => #{d.groups.inspect}" + group = d.groups.size == 1 ? ", group: #{d.groups.first.inspect}" : ", groups: #{d.groups.inspect}" end - source = ", :source => \"#{d.source}\"" unless d.source.nil? + source = ", source: \"#{d.source}\"" unless d.source.nil? + path = ", path: \"#{d.path}\"" unless d.path.nil? + git = ", git: \"#{d.git}\"" unless d.git.nil? + github = ", github: \"#{d.github}\"" unless d.github.nil? + branch = ", branch: \"#{d.branch}\"" unless d.branch.nil? + ref = ", ref: \"#{d.ref}\"" unless d.ref.nil? + glob = ", glob: \"#{d.glob}\"" unless d.glob.nil? + require_path = ", require: #{convert_autorequire(d.autorequire)}" unless d.autorequire.nil? - %(gem #{name}#{requirement}#{group}#{source}) + %(gem #{name}#{requirement}#{group}#{source}#{path}#{git}#{github}#{branch}#{ref}#{glob}#{require_path}) end.join("\n") end def append_to(gemfile_path, new_gem_lines) gemfile_path.open("a") do |f| f.puts - if @options["timestamp"] || @options["timestamp"].nil? - f.puts "# Added at #{Time.now} by #{`whoami`.chomp}:" - end f.puts new_gem_lines end end + + # evaluates a gemfile to remove the specified gem + # from it. + def remove_deps(gemfile_path) + initial_gemfile = File.readlines(gemfile_path) + + Bundler.ui.info "Removing gems from #{gemfile_path}" + + # evaluate the Gemfile we have + builder = Dsl.new + builder.eval_gemfile(gemfile_path) + + removed_deps = remove_gems_from_dependencies(builder, @deps, gemfile_path) + + # abort the operation if no gems were removed + # no need to operate on gemfile further + return [] if removed_deps.empty? + + cleaned_gemfile = remove_gems_from_gemfile(@deps, gemfile_path) + + SharedHelpers.write_to_gemfile(gemfile_path, cleaned_gemfile) + + # check for errors + # including extra gems being removed + # or some gems not being removed + # and return the actual removed deps + cross_check_for_errors(gemfile_path, builder.dependencies, removed_deps, initial_gemfile) + end + + # @param [Dsl] builder Dsl object of current Gemfile. + # @param [Array] gems Array of names of gems to be removed. + # @param [Pathname] gemfile_path Path of the Gemfile. + # @return [Array] Array of removed dependencies. + def remove_gems_from_dependencies(builder, gems, gemfile_path) + removed_deps = [] + + gems.each do |gem_name| + deleted_dep = builder.dependencies.find {|d| d.name == gem_name } + + if deleted_dep.nil? + raise GemfileError, "`#{gem_name}` is not specified in #{gemfile_path} so it could not be removed." + end + + builder.dependencies.delete(deleted_dep) + + removed_deps << deleted_dep + end + + removed_deps + end + + # @param [Array] gems Array of names of gems to be removed. + # @param [Pathname] gemfile_path The Gemfile from which to remove dependencies. + def remove_gems_from_gemfile(gems, gemfile_path) + patterns = /gem\s+(['"])#{Regexp.union(gems)}\1|gem\s*\((['"])#{Regexp.union(gems)}\2.*\)/ + new_gemfile = [] + multiline_removal = false + File.readlines(gemfile_path).each do |line| + match_data = line.match(patterns) + if match_data && is_not_within_comment?(line, match_data) + multiline_removal = line.rstrip.end_with?(",") + # skip lines which match the regex + next + end + + # skip followup lines until line does not end with ',' + new_gemfile << line unless multiline_removal + multiline_removal = line.rstrip.end_with?(",") if multiline_removal + end + + # remove line \n and append them with other strings + new_gemfile.each_with_index do |_line, index| + if new_gemfile[index + 1] == "\n" + new_gemfile[index] += new_gemfile[index + 1] + new_gemfile.delete_at(index + 1) + end + end + + %w[group source env install_if].each {|block| remove_nested_blocks(new_gemfile, block) } + + new_gemfile.join.chomp + end + + # @param [String] line Individual line of gemfile content. + # @param [MatchData] match_data Data about Regex match. + def is_not_within_comment?(line, match_data) + match_start_index = match_data.offset(0).first + !line[0..match_start_index].include?("#") + end + + # @param [Array] gemfile Array of gemfile contents. + # @param [String] block_name Name of block name to look for. + def remove_nested_blocks(gemfile, block_name) + nested_blocks = 0 + + # count number of nested blocks + gemfile.each_with_index {|line, index| nested_blocks += 1 if !gemfile[index + 1].nil? && gemfile[index + 1].include?(block_name) && line.include?(block_name) } + + while nested_blocks >= 0 + nested_blocks -= 1 + + gemfile.each_with_index do |line, index| + next unless !line.nil? && line.strip.start_with?(block_name) + if /^\s*end\s*$/.match?(gemfile[index + 1]) + gemfile[index] = nil + gemfile[index + 1] = nil + end + end + + gemfile.compact! + end + end + + # @param [Pathname] gemfile_path The Gemfile from which to remove dependencies. + # @param [Array] original_deps Array of original dependencies. + # @param [Array] removed_deps Array of removed dependencies. + # @param [Array] initial_gemfile Contents of original Gemfile before any operation. + def cross_check_for_errors(gemfile_path, original_deps, removed_deps, initial_gemfile) + # evaluate the new gemfile to look for any failure cases + builder = Dsl.new + builder.eval_gemfile(gemfile_path) + + # record gems which were removed but not requested + extra_removed_gems = original_deps - builder.dependencies + + # if some extra gems were removed then raise error + # and revert Gemfile to original + unless extra_removed_gems.empty? + SharedHelpers.write_to_gemfile(gemfile_path, initial_gemfile.join) + + raise InvalidOption, "Gems could not be removed. #{extra_removed_gems.join(", ")} would also have been removed. Bundler cannot continue." + end + + # record gems which could not be removed due to some reasons + errored_deps = builder.dependencies.select {|d| d.gemfile == gemfile_path } & removed_deps.select {|d| d.gemfile == gemfile_path } + + show_warning "#{errored_deps.map(&:name).join(", ")} could not be removed." unless errored_deps.empty? + + # return actual removed dependencies + removed_deps - errored_deps + end + + def show_warning(message) + Bundler.ui.info Bundler.ui.add_color(message, :yellow) + end + + def convert_autorequire(autorequire) + autorequire = autorequire.first + return autorequire if autorequire == "false" + autorequire.inspect + end end end diff --git a/lib/bundler/inline.rb b/lib/bundler/inline.rb index 9d25f3261a..a1b8e0475e 100644 --- a/lib/bundler/inline.rb +++ b/lib/bundler/inline.rb @@ -1,18 +1,20 @@ # frozen_string_literal: true -require "bundler/compatibility_guard" - -# Allows for declaring a Gemfile inline in a ruby script, optionally installing -# any gems that aren't already installed on the user's system. +# Allows for declaring a Gemfile inline in a ruby script, installing any gems +# that aren't already installed on the user's system. # # @note Every gem that is specified in this 'Gemfile' will be `require`d, as if # the user had manually called `Bundler.require`. To avoid a requested gem # being automatically required, add the `:require => false` option to the # `gem` dependency declaration. # -# @param install [Boolean] whether gems that aren't already installed on the -# user's system should be installed. -# Defaults to `false`. +# @param force_latest_compatible [Boolean] Force installing the *latest* +# compatible versions of the gems, +# even if compatible versions are +# already installed locally. +# This also logs output if the +# `:quiet` option is not set. +# Defaults to `false`. # # @param gemfile [Proc] a block that is evaluated as a `Gemfile`. # @@ -31,44 +33,114 @@ require "bundler/compatibility_guard" # # puts Pod::VERSION # => "0.34.4" # -def gemfile(install = false, options = {}, &gemfile) - require "bundler" +def gemfile(force_latest_compatible = false, options = {}, &gemfile) + require_relative "../bundler" + Bundler.reset! opts = options.dup ui = opts.delete(:ui) { Bundler::UI::Shell.new } + ui.level = "silent" if opts.delete(:quiet) || !force_latest_compatible + Bundler.ui = ui raise ArgumentError, "Unknown options: #{opts.keys.join(", ")}" unless opts.empty? - old_root = Bundler.method(:root) - def Bundler.root - Bundler::SharedHelpers.pwd.expand_path - end - Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", "Gemfile" + old_gemfile = ENV["BUNDLE_GEMFILE"] + old_lockfile = ENV["BUNDLE_LOCKFILE"] - Bundler::Plugin.gemfile_install(&gemfile) if Bundler.feature_flag.plugins? - builder = Bundler::Dsl.new - builder.instance_eval(&gemfile) + Bundler.unbundle_env! - definition = builder.to_definition(nil, true) - def definition.lock(*); end - definition.validate_runtime! + begin + Bundler.instance_variable_set(:@bundle_path, Pathname.new(Gem.dir)) + Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", "Gemfile" + Bundler::SharedHelpers.set_env "BUNDLE_LOCKFILE", "Gemfile.lock" - missing_specs = proc do - definition.missing_specs? - end + Bundler::Plugin.gemfile_install(&gemfile) if Bundler.settings[:plugins] + builder = Bundler::Dsl.new + builder.instance_eval(&gemfile) + + Bundler.settings.temporary(deployment: false, frozen: false) do + definition = builder.to_definition(nil, true) + definition.validate_runtime! + + if force_latest_compatible || definition.missing_specs? + do_install = -> do + Bundler.settings.temporary(inline: true, no_install: false) do + installer = Bundler::Installer.install(Bundler.root, definition, system: true) + installer.post_install_messages.each do |name, message| + Bundler.ui.info "Post-install message from #{name}:\n#{message}" + end + end + end + + # When possible we do the install in a subprocess because to install + # gems we need to require some default gems like `securerandom` etc + # which may later conflict with the Gemfile requirements. + installed_in_fork = false + if Process.respond_to?(:fork) + Gem.load_yaml # Avoid NameError on Ruby 3.2's safe_yaml.rb after Gem::Specification.reset + _, status = Process.waitpid2(Process.fork do + $VERBOSE = nil + do_install.call end) + exit(status.exitstatus || status.to_i) unless status.success? + + installed_in_fork = true + + Bundler.reset! + Gem::Specification.reset + Bundler.instance_variable_set(:@bundle_path, Pathname.new(Gem.dir)) + + builder = Bundler::Dsl.new + builder.instance_eval(&gemfile) + builder.check_primary_source_safety + + definition = builder.to_definition(nil, true) + definition.validate_runtime! + else + do_install.call + end + end - Bundler.ui = ui if install - if install || missing_specs.call - Bundler.settings.temporary(:inline => true) do - installer = Bundler::Installer.install(Bundler.root, definition, :system => true) - installer.post_install_messages.each do |name, message| - Bundler.ui.info "Post-install message from #{name}:\n#{message}" + configure_forked_definition = ->(d) do + d.sources.rubygems_sources.each(&:remote!) + d.sources.git_sources.each do |source| + source.cached! + source.instance_variable_set(:@copied, true) + end + def d.lock(*); end end + configure_forked_definition.call(definition) if installed_in_fork + + begin + runtime = Bundler::Runtime.new(nil, definition).setup + rescue Gem::LoadError => e + name = e.name + version = e.requirement.requirements.first[1] + activated_version = Gem.loaded_specs[name].version + + Bundler.ui.info \ + "The #{name} gem was resolved to #{version}, but #{activated_version} was activated by Bundler while installing it, causing a conflict. " \ + "Bundler will now retry resolving with #{activated_version} instead." + + builder.dependencies.delete_if {|d| d.name == name } + builder.instance_eval { gem name, activated_version } + definition = builder.to_definition(nil, true) + configure_forked_definition.call(definition) if installed_in_fork + + retry + end + + runtime.require + end + ensure + if old_gemfile + ENV["BUNDLE_GEMFILE"] = old_gemfile + else + ENV["BUNDLE_GEMFILE"] = "" end - end - runtime = Bundler::Runtime.new(nil, definition) - runtime.setup.require -ensure - bundler_module = class << Bundler; self; end - bundler_module.send(:define_method, :root, old_root) if old_root + if old_lockfile + ENV["BUNDLE_LOCKFILE"] = old_lockfile + else + ENV["BUNDLE_LOCKFILE"] = "" + end + end end diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb index d1066c9c19..87d9a75627 100644 --- a/lib/bundler/installer.rb +++ b/lib/bundler/installer.rb @@ -1,28 +1,21 @@ # frozen_string_literal: true -require "erb" -require "rubygems/dependency_installer" -require "bundler/worker" -require "bundler/installer/parallel_installer" -require "bundler/installer/standalone" -require "bundler/installer/gem_installer" +require_relative "worker" +require_relative "installer/parallel_installer" +require_relative "installer/standalone" +require_relative "installer/gem_installer" module Bundler class Installer - class << self - attr_accessor :ambiguous_gems - - Installer.ambiguous_gems = [] - end - - attr_reader :post_install_messages + attr_reader :post_install_messages, :definition # Begins the installation process for Bundler. # For more information see the #run method on this class. def self.install(root, definition, options = {}) installer = new(root, definition) - Plugin.hook("before-install-all", definition.dependencies) + Plugin.hook(Plugin::Events::GEM_BEFORE_INSTALL_ALL, definition.dependencies) installer.run(options) + Plugin.hook(Plugin::Events::GEM_AFTER_INSTALL_ALL, definition.dependencies) installer end @@ -67,12 +60,15 @@ module Bundler # require paths and save them in a `setup.rb` file. See `bundle standalone --help` for more # information. def run(options) - create_bundle_path + Bundler.create_bundle_path ProcessLock.lock do - if Bundler.frozen? - @definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment]) - end + # Invalidate any stale gem specification cache from before we acquired the lock. + # Another process may have installed gems while we were waiting. + Gem::Specification.reset + @definition.sources.clear_cache + + @definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment]) if @definition.dependencies.empty? Bundler.ui.warn "The Gemfile specifies no dependencies" @@ -80,22 +76,25 @@ module Bundler return end - if resolve_if_needed(options) + if @definition.setup_domain!(options) ensure_specs_are_compatible! - warn_on_incompatible_bundler_deps load_plugins - options.delete(:jobs) - else - options[:jobs] = 1 # to avoid the overhead of Bundler::Worker end install(options) - lock unless Bundler.frozen? + Gem::Specification.reset # invalidate gem specification cache so that installed gems are immediately available + + lock Standalone.new(options[:standalone], @definition).generate if options[:standalone] end end def generate_bundler_executable_stubs(spec, options = {}) + if spec.name == "bundler" + Bundler.ui.warn "Bundler itself does not use binstubs because its version is selected by RubyGems" + return + end + if options[:binstubs_cmd] && spec.executables.empty? options = {} spec.runtime_dependencies.each do |dep| @@ -119,11 +118,7 @@ module Bundler relative_gemfile_path = relative_gemfile_path ruby_command = Thor::Util.ruby_command ruby_command = ruby_command - template_path = File.expand_path("../templates/Executable", __FILE__) - if spec.name == "bundler" - template_path += ".bundler" - spec.executables = %(bundle) - end + template_path = File.expand_path("templates/Executable", __dir__) template = File.read(template_path) exists = [] @@ -134,8 +129,14 @@ module Bundler next end - File.open(binstub_path, "w", 0o777 & ~File.umask) do |f| - f.puts ERB.new(template, nil, "-").result(binding) + mode = Gem.win_platform? ? "wb:UTF-8" : "w" + require "erb" + content = ERB.new(template, trim_mode: "-").result(binding) + + File.write(binstub_path, content, mode: mode, perm: 0o777 & ~File.umask) + if Gem.win_platform? || options[:all_platforms] + prefix = "@ruby -x \"%~f0\" %*\n@exit /b %ERRORLEVEL%\n\n" + File.write("#{binstub_path}.cmd", prefix + content, mode: mode) end end @@ -154,7 +155,7 @@ module Bundler end end - def generate_standalone_bundler_executable_stubs(spec) + def generate_standalone_bundler_executable_stubs(spec, options = {}) # double-assignment to avoid warnings about variables that will be used by ERB bin_path = Bundler.bin_path unless path = Bundler.settings[:path] @@ -162,7 +163,7 @@ module Bundler end standalone_path = Bundler.root.join(path).relative_path_from(bin_path) standalone_path = standalone_path - template = File.read(File.expand_path("../templates/Executable.standalone", __FILE__)) + template = File.read(File.expand_path("templates/Executable.standalone", __dir__)) ruby_command = Thor::Util.ruby_command ruby_command = ruby_command @@ -170,118 +171,66 @@ module Bundler next if executable == "bundle" executable_path = Pathname(spec.full_gem_path).join(spec.bindir, executable).relative_path_from(bin_path) executable_path = executable_path - File.open "#{bin_path}/#{executable}", "w", 0o755 do |f| - f.puts ERB.new(template, nil, "-").result(binding) + + mode = Gem.win_platform? ? "wb:UTF-8" : "w" + require "erb" + content = ERB.new(template, trim_mode: "-").result(binding) + + File.write("#{bin_path}/#{executable}", content, mode: mode, perm: 0o755) + if Gem.win_platform? || options[:all_platforms] + prefix = "@ruby -x \"%~f0\" %*\n@exit /b %ERRORLEVEL%\n\n" + File.write("#{bin_path}/#{executable}.cmd", prefix + content, mode: mode) end end end - private + private # the order that the resolver provides is significant, since # dependencies might affect the installation of a gem. # that said, it's a rare situation (other than rake), and parallel # installation is SO MUCH FASTER. so we let people opt in. def install(options) - force = options["force"] - jobs = options.delete(:jobs) do - if can_install_in_parallel? - [Bundler.settings[:jobs].to_i - 1, 1].max - else - 1 - end + standalone = options[:standalone] + force = options[:force] + local = options[:local] || options[:"prefer-local"] + jobs = Bundler.settings.installation_parallelization + spec_installations = ParallelInstaller.call(self, @definition.specs, jobs, standalone, force, local: local) + spec_installations.each do |installation| + post_install_messages[installation.name] = installation.post_install_message if installation.has_post_install_message? end - install_in_parallel jobs, options[:standalone], force end def load_plugins - Bundler.rubygems.load_plugins - - requested_path_gems = @definition.requested_specs.select {|s| s.source.is_a?(Source::Path) } - path_plugin_files = requested_path_gems.map do |spec| - begin - Bundler.rubygems.spec_matches_for_glob(spec, "rubygems_plugin#{Bundler.rubygems.suffix_pattern}") - rescue TypeError - error_message = "#{spec.name} #{spec.version} has an invalid gemspec" - raise Gem::InvalidSpecificationException, error_message - end - end.flatten - Bundler.rubygems.load_plugin_files(path_plugin_files) + Gem.load_plugins + + requested_path_gems = @definition.specs.select {|s| s.source.is_a?(Source::Path) } + path_plugin_files = requested_path_gems.flat_map do |spec| + spec.matches_for_glob("rubygems_plugin#{Bundler.rubygems.suffix_pattern}") + rescue TypeError + error_message = "#{spec.name} #{spec.version} has an invalid gemspec" + raise Gem::InvalidSpecificationException, error_message + end + Gem.load_plugin_files(path_plugin_files) + Gem.load_env_plugins end def ensure_specs_are_compatible! - system_ruby = Bundler::RubyVersion.system - rubygems_version = Gem::Version.create(Gem::VERSION) + overrides = @definition.overrides @definition.specs.each do |spec| - if required_ruby_version = spec.required_ruby_version - unless required_ruby_version.satisfied_by?(system_ruby.gem_version) - raise InstallError, "#{spec.full_name} requires ruby version #{required_ruby_version}, " \ - "which is incompatible with the current version, #{system_ruby}" - end + unless spec.matches_current_ruby_with_overrides?(overrides) + raise InstallError, "#{spec.full_name} requires ruby version #{spec.required_ruby_version}, " \ + "which is incompatible with the current version, #{Gem.ruby_version}" end - next unless required_rubygems_version = spec.required_rubygems_version - unless required_rubygems_version.satisfied_by?(rubygems_version) - raise InstallError, "#{spec.full_name} requires rubygems version #{required_rubygems_version}, " \ - "which is incompatible with the current version, #{rubygems_version}" + unless spec.matches_current_rubygems_with_overrides?(overrides) + raise InstallError, "#{spec.full_name} requires rubygems version #{spec.required_rubygems_version}, " \ + "which is incompatible with the current version, #{Gem.rubygems_version}" end end end - def warn_on_incompatible_bundler_deps - bundler_version = Gem::Version.create(Bundler::VERSION) - @definition.specs.each do |spec| - spec.dependencies.each do |dep| - next if dep.type == :development - next unless dep.name == "bundler".freeze - next if dep.requirement.satisfied_by?(bundler_version) - - Bundler.ui.warn "#{spec.name} (#{spec.version}) has dependency" \ - " #{SharedHelpers.pretty_dependency(dep)}" \ - ", which is unsatisfied by the current bundler version #{VERSION}" \ - ", so the dependency is being ignored" - end - end - end - - def can_install_in_parallel? - if Bundler.rubygems.provides?(">= 2.1.0") - true - else - Bundler.ui.warn "RubyGems #{Gem::VERSION} is not threadsafe, so your "\ - "gems will be installed one at a time. Upgrade to RubyGems 2.1.0 " \ - "or higher to enable parallel gem installation." - false - end - end - - def install_in_parallel(size, standalone, force = false) - spec_installations = ParallelInstaller.call(self, @definition.specs, size, standalone, force) - spec_installations.each do |installation| - post_install_messages[installation.name] = installation.post_install_message if installation.has_post_install_message? - end - end - - def create_bundle_path - SharedHelpers.filesystem_access(Bundler.bundle_path.to_s) do |p| - Bundler.mkdir_p(p) - end unless Bundler.bundle_path.exist? - rescue Errno::EEXIST - raise PathError, "Could not install to path `#{Bundler.bundle_path}` " \ - "because a file already exists at that path. Either remove or rename the file so the directory can be created." - end - - # returns whether or not a re-resolve was needed - def resolve_if_needed(options) - if !@definition.unlocking? && !options["force"] && !Bundler.settings[:inline] && Bundler.default_lockfile.file? - return false if @definition.nothing_changed? && !@definition.missing_specs? - end - - options["local"] ? @definition.resolve_with_cache! : @definition.resolve_remotely! - true - end - - def lock(opts = {}) - @definition.lock(Bundler.default_lockfile, opts[:preserve_unknown_sections]) + def lock + @definition.lock end end end diff --git a/lib/bundler/installer/gem_installer.rb b/lib/bundler/installer/gem_installer.rb index 086b763d20..f3b43c31ee 100644 --- a/lib/bundler/installer/gem_installer.rb +++ b/lib/bundler/installer/gem_installer.rb @@ -2,77 +2,87 @@ module Bundler class GemInstaller - attr_reader :spec, :standalone, :worker, :force, :installer + attr_reader :spec, :standalone, :worker, :force, :local, :installer - def initialize(spec, installer, standalone = false, worker = 0, force = false) + def initialize(spec, installer, standalone = false, worker = 0, force = false, local = false) @spec = spec @installer = installer @standalone = standalone @worker = worker @force = force + @local = local end def install_from_spec - post_install_message = spec_settings ? install_with_settings : install + post_install_message = install Bundler.ui.debug "#{worker}: #{spec.name} (#{spec.version}) from #{spec.loaded_from}" - generate_executable_stubs - return true, post_install_message - rescue Bundler::InstallHookError, Bundler::SecurityError, APIResponseMismatchError + [true, post_install_message] + rescue Bundler::InstallHookError, Bundler::SecurityError, Bundler::APIResponseMismatchError, Bundler::InsecureInstallPathError raise rescue Errno::ENOSPC - return false, out_of_space_message - rescue => e - return false, specific_failure_message(e) + [false, out_of_space_message] + rescue Bundler::BundlerError, Gem::InstallError => e + [false, specific_failure_message(e)] end - private + def download + spec.source.download( + spec, + force: force, + local: local, + build_args: Array(spec_settings), + previous_spec: previous_spec, + ) + + [true, nil] + rescue Bundler::BundlerError => e + [false, specific_failure_message(e)] + end + + private def specific_failure_message(e) message = "#{e.class}: #{e.message}\n" - message += " " + e.backtrace.join("\n ") + "\n\n" if Bundler.ui.debug? + message += " " + e.backtrace.join("\n ") + "\n\n" message = message.lines.first + Bundler.ui.add_color(message.lines.drop(1).join, :clear) message + Bundler.ui.add_color(failure_message, :red) end def failure_message - return install_error_message if spec.source.options["git"] - "#{install_error_message}\n#{gem_install_message}" + install_error_message end def install_error_message "An error occurred while installing #{spec.name} (#{spec.version}), and Bundler cannot continue." end - def gem_install_message - "Make sure that `gem install #{spec.name} -v '#{spec.version}'` succeeds before bundling." - end - def spec_settings # Fetch the build settings, if there are any - Bundler.settings["build.#{spec.name}"] + if settings = Bundler.settings["build.#{spec.name}"] + require "shellwords" + Shellwords.shellsplit(settings) + end end def install - spec.source.install(spec, :force => force, :ensure_builtin_gems_cached => standalone, :build_args => Array(spec_settings)) + spec.source.install( + spec, + force: force, + local: local, + build_args: Array(spec_settings), + previous_spec: previous_spec, + ) end - def install_with_settings - # Build arguments are global, so this is mutexed - Bundler.rubygems.install_with_build_args([spec_settings]) { install } + def previous_spec + locked_gems = installer.definition.locked_gems + return unless locked_gems + + locked_gems.specs.find {|s| s.name == spec.name } end def out_of_space_message "#{install_error_message}\nYour disk is out of space. Free some space to be able to install your bundle." end - - def generate_executable_stubs - return if Bundler.feature_flag.forget_cli_options? - return if Bundler.settings[:inline] - if Bundler.settings[:bin] && standalone - installer.generate_standalone_bundler_executable_stubs(spec) - elsif Bundler.settings[:bin] - installer.generate_bundler_executable_stubs(spec, :force => true) - end - end end end diff --git a/lib/bundler/installer/parallel_installer.rb b/lib/bundler/installer/parallel_installer.rb index 95d9575c44..fef326ed0a 100644 --- a/lib/bundler/installer/parallel_installer.rb +++ b/lib/bundler/installer/parallel_installer.rb @@ -1,15 +1,16 @@ # frozen_string_literal: true -require "bundler/worker" -require "bundler/installer/gem_installer" +require_relative "../worker" +require_relative "gem_installer" module Bundler class ParallelInstaller class SpecInstallation - attr_accessor :spec, :name, :post_install_message, :state, :error + attr_accessor :spec, :name, :full_name, :post_install_message, :state, :error, :dependencies def initialize(spec) @spec = spec @name = spec.name + @full_name = spec.full_name @state = :none @post_install_message = "" @error = nil @@ -23,78 +24,72 @@ module Bundler state == :enqueued end - def failed? - state == :failed + def enqueue_with_priority? + state == :installable && spec.extensions.any? end - def installation_attempted? - installed? || failed? + def failed? + state == :failed end - # Only true when spec in neither installed nor already enqueued def ready_to_enqueue? - !enqueued? && !installation_attempted? + state == :none end - def has_post_install_message? - !post_install_message.empty? - end + def ready_to_install?(installed_specs) + return false unless state == :downloaded - def ignorable_dependency?(dep) - dep.type == :development || dep.name == @name + spec.extensions.none? || dependencies_installed?(installed_specs) end - # Checks installed dependencies against spec's dependencies to make - # sure needed dependencies have been installed. - def dependencies_installed?(all_specs) - installed_specs = all_specs.select(&:installed?).map(&:name) - dependencies.all? {|d| installed_specs.include? d.name } + def has_post_install_message? + !post_install_message.empty? end - # Represents only the non-development dependencies, the ones that are - # itself and are in the total list. - def dependencies - @dependencies ||= begin - all_dependencies.reject {|dep| ignorable_dependency? dep } + # Recursively checks that all dependencies (direct and transitive) have been installed. + def dependencies_installed?(installed_specs) + dependencies.all? do |dep| + installed_specs.include?(dep.name) && dep.dependencies_installed?(installed_specs) end end - def missing_lockfile_dependencies(all_spec_names) - deps = all_dependencies.reject {|dep| ignorable_dependency? dep } - deps.reject {|dep| all_spec_names.include? dep.name } - end - - # Represents all dependencies - def all_dependencies - @spec.dependencies - end - def to_s - "#<#{self.class} #{@spec.full_name} (#{state})>" + "#<#{self.class} #{full_name} (#{state})>" end end - def self.call(*args) - new(*args).call + def self.call(*args, **kwargs) + new(*args, **kwargs).call end attr_reader :size - def initialize(installer, all_specs, size, standalone, force) + def initialize(installer, all_specs, size, standalone, force, local: false, skip: nil) @installer = installer @size = size @standalone = standalone @force = force + @local = local @specs = all_specs.map {|s| SpecInstallation.new(s) } + specs_by_name = @specs.to_h {|s| [s.name, s] } + @specs.each do |spec_install| + spec_install.dependencies = spec_install.spec.dependencies.filter_map do |dep| + specs_by_name[dep.name] unless dep.type == :development || dep.name == spec_install.name + end + end + @specs.each do |spec_install| + spec_install.state = :installed if skip.include?(spec_install.name) + end if skip @spec_set = all_specs + @rake = @specs.find {|s| s.name == "rake" unless s.installed? } end def call - # Since `autoload` has the potential for threading issues on 1.8.7 - # TODO: remove in bundler 2.0 - require "bundler/gem_remote_fetcher" if RUBY_VERSION < "1.9" - - check_for_corrupt_lockfile + if @rake + do_download(@rake, 0) + do_install(@rake, 0) + Gem::Specification.reset + end if @size > 1 install_with_worker @@ -102,74 +97,79 @@ module Bundler install_serially end - handle_error if @specs.any?(&:failed?) + handle_error if failed_specs.any? @specs ensure - worker_pool && worker_pool.stop + worker_pool&.stop end - def check_for_corrupt_lockfile - missing_dependencies = @specs.map do |s| - [ - s, - s.missing_lockfile_dependencies(@specs.map(&:name)), - ] - end.reject { |a| a.last.empty? } - return if missing_dependencies.empty? + private - warning = [] - warning << "Your lockfile was created by an old Bundler that left some things out." - if @size != 1 - warning << "Because of the missing DEPENDENCIES, we can only install gems one at a time, instead of installing #{@size} at a time." - @size = 1 - end - warning << "You can fix this by adding the missing gems to your Gemfile, running bundle install, and then removing the gems from your Gemfile." - warning << "The missing gems are:" - - missing_dependencies.each do |spec, missing| - warning << "* #{missing.map(&:name).join(", ")} depended upon by #{spec.name}" - end - - Bundler.ui.warn(warning.join("\n")) + def failed_specs + @specs.select(&:failed?) end - private - def install_with_worker - enqueue_specs - process_specs until finished_installing? + installed_specs = {} + enqueue_specs(installed_specs) + + process_specs(installed_specs) until finished_installing? end def install_serially until finished_installing? raise "failed to find a spec to enqueue while installing serially" unless spec_install = @specs.find(&:ready_to_enqueue?) spec_install.state = :enqueued + do_download(spec_install, 0) do_install(spec_install, 0) end end def worker_pool - @worker_pool ||= Bundler::Worker.new @size, "Parallel Installer", lambda { |spec_install, worker_num| - do_install(spec_install, worker_num) + @worker_pool ||= Bundler::Worker.new @size, "Parallel Installer", lambda {|spec_install, worker_num| + case spec_install.state + when :enqueued + do_download(spec_install, worker_num) + when :installable + do_install(spec_install, worker_num) + else + spec_install + end } end - def do_install(spec_install, worker_num) + def do_download(spec_install, worker_num) + Plugin.hook(Plugin::Events::GEM_BEFORE_INSTALL, spec_install) + gem_installer = Bundler::GemInstaller.new( - spec_install.spec, @installer, @standalone, worker_num, @force + spec_install.spec, @installer, @standalone, worker_num, @force, @local ) - success, message = begin - gem_installer.install_from_spec - rescue => e - raise e, "#{e}\n\n#{require_tree_for_spec(spec_install.spec)}" + + success, message = gem_installer.download + + if success + spec_install.state = :downloaded + else + spec_install.error = "#{message}\n\n#{require_tree_for_spec(spec_install.spec)}" + spec_install.state = :failed end + + spec_install + end + + def do_install(spec_install, worker_num) + gem_installer = Bundler::GemInstaller.new( + spec_install.spec, @installer, @standalone, worker_num, @force, @local + ) + success, message = gem_installer.install_from_spec if success spec_install.state = :installed spec_install.post_install_message = message unless message.nil? else - spec_install.state = :failed spec_install.error = "#{message}\n\n#{require_tree_for_spec(spec_install.spec)}" + spec_install.state = :failed end + Plugin.hook(Plugin::Events::GEM_AFTER_INSTALL, spec_install) spec_install end @@ -178,9 +178,19 @@ module Bundler # Some specs might've had to wait til this spec was installed to be # processed so the call to `enqueue_specs` is important after every # dequeue. - def process_specs - worker_pool.deq - enqueue_specs + def process_specs(installed_specs) + spec = worker_pool.deq + + if spec.installed? + installed_specs[spec.name] = true + return + elsif spec.failed? + return + elsif spec.ready_to_install?(installed_specs) + spec.state = :installable + end + + worker_pool.enq(spec, priority: spec.enqueue_with_priority?) end def finished_installing? @@ -191,11 +201,11 @@ module Bundler end def handle_error - errors = @specs.select(&:failed?).map(&:error) + errors = failed_specs.map(&:error) if exception = errors.find {|e| e.is_a?(Bundler::BundlerError) } raise exception end - raise Bundler::InstallError, errors.map(&:to_s).join("\n\n") + raise Bundler::InstallError, errors.join("\n\n") end def require_tree_for_spec(spec) @@ -216,12 +226,15 @@ module Bundler # Later we call this lambda again to install specs that depended on # previously installed specifications. We continue until all specs # are installed. - def enqueue_specs - @specs.select(&:ready_to_enqueue?).each do |spec| - if spec.dependencies_installed? @specs - spec.state = :enqueued - worker_pool.enq spec + def enqueue_specs(installed_specs) + @specs.each do |spec| + if spec.installed? + installed_specs[spec.name] = true + next end + + spec.state = :enqueued + worker_pool.enq spec end end end diff --git a/lib/bundler/installer/standalone.rb b/lib/bundler/installer/standalone.rb index ce0c9df1eb..8b4de64df5 100644 --- a/lib/bundler/installer/standalone.rb +++ b/lib/bundler/installer/standalone.rb @@ -3,7 +3,7 @@ module Bundler class Standalone def initialize(groups, definition) - @specs = groups.empty? ? definition.requested_specs : definition.specs_for(groups.map(&:to_sym)) + @specs = definition.specs_for(groups) end def generate @@ -12,42 +12,102 @@ module Bundler end File.open File.join(bundler_path, "setup.rb"), "w" do |file| file.puts "require 'rbconfig'" - file.puts "# ruby 1.8.7 doesn't define RUBY_ENGINE" - file.puts "ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'" - file.puts "ruby_version = RbConfig::CONFIG[\"ruby_version\"]" - file.puts "path = File.expand_path('..', __FILE__)" + file.puts prevent_gem_activation + file.puts define_path_helpers + file.puts reverse_rubygems_kernel_mixin paths.each do |path| - file.puts %($:.unshift "\#{path}/#{path}") + if Pathname.new(path).absolute? + file.puts %($:.unshift "#{path}") + else + file.puts %($:.unshift File.expand_path("\#{__dir__}/#{path}")) + end end end end - private + private def paths - @specs.map do |spec| + @specs.flat_map do |spec| next if spec.name == "bundler" Array(spec.require_paths).map do |path| - gem_path(path, spec).sub(version_dir, '#{ruby_engine}/#{ruby_version}') + gem_path(path, spec). + sub(version_dir, '#{RUBY_ENGINE}/#{Gem.ruby_api_version}'). + sub(extensions_dir, 'extensions/\k<platform>/#{Gem.extension_api_version}') # This is a static string intentionally. It's interpolated at a later time. end - end.flatten + end.compact end def version_dir - "#{Bundler::RubyVersion.system.engine}/#{RbConfig::CONFIG["ruby_version"]}" + "#{RUBY_ENGINE}/#{Gem.ruby_api_version}" + end + + def extensions_dir + %r{extensions/(?<platform>[^/]+)/#{Regexp.escape(Gem.extension_api_version)}} end def bundler_path - Bundler.root.join(Bundler.settings[:path], "bundler") + Bundler.root.join(Bundler.settings[:path].to_s, "bundler") end def gem_path(path, spec) full_path = Pathname.new(path).absolute? ? path : File.join(spec.full_gem_path, path) - Pathname.new(full_path).relative_path_from(Bundler.root.join(bundler_path)).to_s - rescue TypeError - error_message = "#{spec.name} #{spec.version} has an invalid gemspec" - raise Gem::InvalidSpecificationException.new(error_message) + if spec.source.instance_of?(Source::Path) && spec.source.path.absolute? + full_path + else + SharedHelpers.relative_path_to(full_path, from: Bundler.root.join(bundler_path)) + end + end + + def prevent_gem_activation + <<~'END' + module Kernel + remove_method(:gem) if private_method_defined?(:gem) + + def gem(*) + end + + private :gem + end + END + end + + def define_path_helpers + <<~'END' + unless defined?(Gem) + module Gem + def self.ruby_api_version + RbConfig::CONFIG["ruby_version"] + end + + def self.extension_api_version + if 'no' == RbConfig::CONFIG['ENABLE_SHARED'] + "#{ruby_api_version}-static" + else + ruby_api_version + end + end + end + end + END + end + + def reverse_rubygems_kernel_mixin + <<~END + if Gem.respond_to?(:discover_gems_on_require=) + Gem.discover_gems_on_require = false + else + [::Kernel.singleton_class, ::Kernel].each do |k| + if k.private_method_defined?(:gem_original_require) + private_require = k.private_method_defined?(:require) + k.send(:remove_method, :require) + k.send(:define_method, :require, k.instance_method(:gem_original_require)) + k.send(:private, :require) if private_require + end + end + end + END end end end diff --git a/lib/bundler/lazy_specification.rb b/lib/bundler/lazy_specification.rb index 993952c23b..0da621d21f 100644 --- a/lib/bundler/lazy_specification.rb +++ b/lib/bundler/lazy_specification.rb @@ -1,64 +1,119 @@ # frozen_string_literal: true -require "uri" -require "bundler/match_platform" +require_relative "force_platform" module Bundler class LazySpecification - Identifier = Struct.new(:name, :version, :source, :platform, :dependencies) - class Identifier - include Comparable - def <=>(other) - return unless other.is_a?(Identifier) - [name, version, platform_string] <=> [other.name, other.version, other.platform_string] - end + include MatchMetadata + include MatchPlatform + include ForcePlatform - protected + attr_reader :name, :version, :platform, :materialization + attr_accessor :source, :remote, :force_ruby_platform, :dependencies, :required_ruby_version, :required_rubygems_version, :overrides - def platform_string - platform_string = platform.to_s - platform_string == Index::RUBY ? Index::NULL : platform_string - end - end + # + # For backwards compatibility with existing lockfiles, if the most specific + # locked platform is not a specific platform like x86_64-linux or + # universal-java-11, then we keep the previous behaviour of resolving the + # best platform variant at materiliazation time. For previous bundler + # versions (before 2.2.0) this was always the case (except when the lockfile + # only included non-ruby platforms), but we're also keeping this behaviour + # on newer bundlers unless users generate the lockfile from scratch or + # explicitly add a more specific platform. + # + attr_accessor :most_specific_locked_platform - include MatchPlatform + alias_method :runtime_dependencies, :dependencies - attr_reader :name, :version, :dependencies, :platform - attr_accessor :source, :remote + def self.from_spec(s) + lazy_spec = new(s.name, s.version, s.platform, s.source) + lazy_spec.dependencies = s.runtime_dependencies + lazy_spec.required_ruby_version = s.required_ruby_version + lazy_spec.required_rubygems_version = s.required_rubygems_version + lazy_spec.overrides = s.overrides if s.is_a?(LazySpecification) + lazy_spec + end - def initialize(name, version, platform, source = nil) + def initialize(name, version, platform, source = nil, **materialization_options) @name = name @version = version @dependencies = [] - @platform = platform || Gem::Platform::RUBY - @source = source - @specification = nil + @required_ruby_version = Gem::Requirement.default + @required_rubygems_version = Gem::Requirement.default + @platform = platform || Gem::Platform::RUBY + + @original_source = source + @source = source + @materialization_options = materialization_options + + @force_ruby_platform = default_force_ruby_platform + @most_specific_locked_platform = nil + @materialization = nil + end + + def missing? + @materialization == self + end + + def incomplete? + @materialization.nil? + end + + def source_changed? + @original_source != source end def full_name - if platform == Gem::Platform::RUBY || platform.nil? + @full_name ||= if platform == Gem::Platform::RUBY "#{@name}-#{@version}" else "#{@name}-#{@version}-#{platform}" end end + def lock_name + @lock_name ||= name_tuple.lock_name + end + + def name_tuple + Gem::NameTuple.new(@name, @version, @platform) + end + def ==(other) - identifier == other.identifier + full_name == other.full_name + end + + def eql?(other) + full_name.eql?(other.full_name) + end + + def hash + full_name.hash end + ## + # Does this locked specification satisfy +dependency+? + # + # NOTE: Rubygems default requirement is ">= 0", which doesn't match + # prereleases of 0 versions, like "0.0.0.dev" or "0.0.0.SNAPSHOT". However, + # bundler users expect those to work. We need to make sure that Gemfile + # dependencies without explicit requirements (which use ">= 0" under the + # hood by default) are still valid for locked specs using this kind of + # versions. The method implements an ad-hoc fix for that. A better solution + # might be to change default rubygems requirement of dependencies to be ">= + # 0.A" but that's a major refactoring likely to break things. Hopefully we + # can attempt it in the future. + # + def satisfies?(dependency) - @name == dependency.name && dependency.requirement.satisfied_by?(Gem::Version.new(@version)) + effective_requirement = dependency.requirement == Gem::Requirement.default ? Gem::Requirement.new(">= 0.A") : dependency.requirement + + @name == dependency.name && effective_requirement.satisfied_by?(Gem::Version.new(@version)) end def to_lock out = String.new - - if platform == Gem::Platform::RUBY || platform.nil? - out << " #{name} (#{version})\n" - else - out << " #{name} (#{version}-#{platform})\n" - end + out << " #{lock_name}\n" dependencies.sort_by(&:to_s).uniq.each do |dep| next if dep.type == :development @@ -68,37 +123,41 @@ module Bundler out end - def __materialize__ - search_object = Bundler.feature_flag.specific_platform? || Bundler.settings[:force_ruby_platform] ? self : Dependency.new(name, version) - @specification = if source.is_a?(Source::Gemspec) && source.gemspec.name == name - source.gemspec.tap {|s| s.source = source } - else - search = source.specs.search(search_object).last - if search && Gem::Platform.new(search.platform) != Gem::Platform.new(platform) && !search.runtime_dependencies.-(dependencies.reject {|d| d.type == :development }).empty? - Bundler.ui.warn "Unable to use the platform-specific (#{search.platform}) version of #{name} (#{version}) " \ - "because it has different dependencies from the #{platform} version. " \ - "To use the platform-specific version of the gem, run `bundle config specific_platform true` and install again." - search = source.specs.search(self).last - end - search.dependencies = dependencies if search.is_a?(RemoteSpecification) || search.is_a?(EndpointSpecification) - search - end + def materialize_for_cache + source.remote! + + materialize(self, &:first) end - def respond_to?(*args) - super || @specification ? @specification.respond_to?(*args) : nil + def materialized_for_installation + @materialization = materialize_for_installation + + self unless incomplete? end - def to_s - @__to_s ||= if platform == Gem::Platform::RUBY || platform.nil? - "#{name} (#{version})" + def materialize_for_installation + source.local! + + if use_exact_resolved_specifications? + spec = materialize(self) {|specs| choose_compatible(specs, fallback_to_non_installable: false) } + return spec if spec + + # Exact spec is incompatible; in frozen mode, try to find a compatible platform variant + # In non-frozen mode, return nil to trigger re-resolution and lockfile update + if Bundler.frozen_bundle? + materialize([name, version]) {|specs| resolve_best_platform(specs) } + end else - "#{name} (#{version}-#{platform})" + materialize([name, version]) {|specs| resolve_best_platform(specs) } end end - def identifier - @__identifier ||= Identifier.new(name, version, source, platform, dependencies) + def inspect + "#<#{self.class} @name=\"#{name}\" (#{full_name.delete_prefix("#{name}-")})>" + end + + def to_s + lock_name end def git_version @@ -106,18 +165,108 @@ module Bundler " #{source.revision[0..6]}" end - private + def force_ruby_platform! + @force_ruby_platform = true + end + + def replace_source_with!(gemfile_source) + return unless gemfile_source.can_lock?(self) - def to_ary + @source = gemfile_source + + true + end + + private + + def use_exact_resolved_specifications? + !source.is_a?(Source::Path) && ruby_platform_materializes_to_ruby_platform? + end + + # Try platforms in order of preference until finding a compatible spec. + # Used for legacy lockfiles and as a fallback when the exact locked spec + # is incompatible. Falls back to frozen bundle behavior if none match. + def resolve_best_platform(specs) + find_compatible_platform_spec(specs) || frozen_bundle_fallback(specs) + end + + def find_compatible_platform_spec(specs) + candidate_platforms.each do |plat| + candidates = MatchPlatform.select_best_platform_match(specs, plat) + spec = choose_compatible(candidates, fallback_to_non_installable: false) + return spec if spec + end nil end - def method_missing(method, *args, &blk) - raise "LazySpecification has not been materialized yet (calling :#{method} #{args.inspect})" unless @specification + # Platforms to try in order of preference. Ruby platform is last since it + # requires compilation, but works when precompiled gems are incompatible. + def candidate_platforms + target = source.is_a?(Source::Path) ? platform : Bundler.local_platform + [target, platform, Gem::Platform::RUBY].uniq + end + + # In frozen mode, accept any candidate. Will error at install time. + # When target differs from locked platform, prefer locked platform's candidates + # to preserve lockfile integrity. + def frozen_bundle_fallback(specs) + target = source.is_a?(Source::Path) ? platform : Bundler.local_platform + fallback_platform = target == platform ? target : platform + candidates = MatchPlatform.select_best_platform_match(specs, fallback_platform) + choose_compatible(candidates) + end + + def ruby_platform_materializes_to_ruby_platform? + generic_platform = Bundler.generic_local_platform == Gem::Platform::JAVA ? Gem::Platform::JAVA : Gem::Platform::RUBY + + (most_specific_locked_platform != generic_platform) || force_ruby_platform || Bundler.settings[:force_ruby_platform] + end - return super unless respond_to?(method) + def materialize(query) + matching_specs = source.specs.search(query) + return self if matching_specs.empty? - @specification.send(method, *args, &blk) + yield matching_specs + end + + # If in frozen mode, we fallback to a non-installable candidate because by + # doing this we avoid re-resolving and potentially end up changing the + # lockfile, which is not allowed. In that case, we will give a proper error + # about the mismatch higher up the stack, right before trying to install the + # bad gem. + def choose_compatible(candidates, fallback_to_non_installable: Bundler.frozen_bundle?) + override_list = overrides || [] + search = candidates.reverse.find do |spec| + spec.is_a?(StubSpecification) || spec.matches_current_metadata_with_overrides?(override_list) + end + if search.nil? && fallback_to_non_installable + search = candidates.last + end + + if search + validate_dependencies(search) if search.platform == platform + + search.locked_platform = platform if search.instance_of?(RemoteSpecification) || search.instance_of?(EndpointSpecification) + end + search + end + + # Validate dependencies of this locked spec are consistent with dependencies + # of the actual spec that was materialized. + # + # Note that unless we are in strict mode (which we set during installation) + # we don't validate dependencies of locally installed gems but + # accept what's in the lockfile instead for performance, since loading + # dependencies of locally installed gems would mean evaluating all gemspecs, + # which would affect `bundler/setup` performance. + def validate_dependencies(spec) + if !@materialization_options[:strict] && spec.is_a?(StubSpecification) + spec.dependencies = dependencies + else + if !source.is_a?(Source::Path) && spec.runtime_dependencies.sort != dependencies.sort + raise IncorrectLockfileDependencies.new(self, spec.runtime_dependencies, dependencies) + end + end end end end diff --git a/lib/bundler/lockfile_generator.rb b/lib/bundler/lockfile_generator.rb index 585077d18d..2a3ad22480 100644 --- a/lib/bundler/lockfile_generator.rb +++ b/lib/bundler/lockfile_generator.rb @@ -19,16 +19,17 @@ module Bundler add_sources add_platforms add_dependencies + add_checksums add_locked_ruby_version add_bundled_with out end - private + private def add_sources - definition.send(:sources).lock_sources.each_with_index do |source, idx| + definition.sources.lock_sources.each_with_index do |source, idx| out << "\n" unless idx.zero? # Add the source header @@ -45,7 +46,7 @@ module Bundler # gems with the same name, but different platform # are ordered consistently specs.sort_by(&:full_name).each do |spec| - next if spec.name == "bundler".freeze + next if spec.name == "bundler" out << spec.to_lock end end @@ -60,18 +61,27 @@ module Bundler handled = [] definition.dependencies.sort_by(&:to_s).each do |dep| next if handled.include?(dep.name) - out << dep.to_lock + out << dep.to_lock << "\n" handled << dep.name end end + def add_checksums + return unless definition.locked_checksums + checksums = definition.resolve.map do |spec| + spec.source.checksum_store.to_lock(spec) + end + + add_section("CHECKSUMS", checksums + bundler_checksum) + end + def add_locked_ruby_version return unless locked_ruby_version = definition.locked_ruby_version add_section("RUBY VERSION", locked_ruby_version.to_s) end def add_bundled_with - add_section("BUNDLED WITH", definition.locked_bundler_version.to_s) + add_section("BUNDLED WITH", definition.bundler_version_to_lock.to_s) end def add_section(name, value) @@ -86,10 +96,24 @@ module Bundler out << " #{key}: #{val}\n" end when String - out << " #{value}\n" + out << " #{value}\n" else raise ArgumentError, "#{value.inspect} can't be serialized in a lockfile" end end + + def bundler_checksum + return [] if Bundler.gem_version.to_s.end_with?(".dev") || ENV["SKIP_BUNDLER_CHECKSUM"] + + bundler_spec = definition.sources.metadata_source.specs.search(["bundler", Bundler.gem_version]).last + return [] unless File.exist?(bundler_spec.cache_file) + + require "rubygems/package" + + package = Gem::Package.new(bundler_spec.cache_file) + definition.sources.metadata_source.checksum_store.register(bundler_spec, Checksum.from_gem_package(package)) + + [definition.sources.metadata_source.checksum_store.to_lock(bundler_spec)] + end end end diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb index ff706fca1d..852fc631f3 100644 --- a/lib/bundler/lockfile_parser.rb +++ b/lib/bundler/lockfile_parser.rb @@ -1,46 +1,73 @@ # frozen_string_literal: true -# Some versions of the Bundler 1.1 RC series introduced corrupted -# lockfiles. There were two major problems: -# -# * multiple copies of the same GIT section appeared in the lockfile -# * when this happened, those sections got multiple copies of gems -# in those sections. -# -# As a result, Bundler 1.1 contains code that fixes the earlier -# corruption. We will remove this fix-up code in Bundler 1.2. +require_relative "shared_helpers" module Bundler class LockfileParser - attr_reader :sources, :dependencies, :specs, :platforms, :bundler_version, :ruby_version - - BUNDLED = "BUNDLED WITH".freeze - DEPENDENCIES = "DEPENDENCIES".freeze - PLATFORMS = "PLATFORMS".freeze - RUBY = "RUBY VERSION".freeze - GIT = "GIT".freeze - GEM = "GEM".freeze - PATH = "PATH".freeze - PLUGIN = "PLUGIN SOURCE".freeze - SPECS = " specs:".freeze + class Position + attr_reader :line, :column + def initialize(line, column) + @line = line + @column = column + end + + def advance!(string) + lines = string.count("\n") + if lines > 0 + @line += lines + @column = string.length - string.rindex("\n") + else + @column += string.length + end + end + + def to_s + "#{line}:#{column}" + end + end + + attr_reader( + :sources, + :metadata_source, + :dependencies, + :specs, + :platforms, + :most_specific_locked_platform, + :bundler_version, + :ruby_version, + :checksums, + ) + + BUNDLED = "BUNDLED WITH" + DEPENDENCIES = "DEPENDENCIES" + CHECKSUMS = "CHECKSUMS" + PLATFORMS = "PLATFORMS" + RUBY = "RUBY VERSION" + GIT = "GIT" + GEM = "GEM" + PATH = "PATH" + PLUGIN = "PLUGIN SOURCE" + SPECS = " specs:" OPTIONS = /^ ([a-z]+): (.*)$/i SOURCE = [GIT, GEM, PATH, PLUGIN].freeze SECTIONS_BY_VERSION_INTRODUCED = { - # The strings have to be dup'ed for old RG on Ruby 2.3+ - # TODO: remove dup in Bundler 2.0 - Gem::Version.create("1.0".dup) => [DEPENDENCIES, PLATFORMS, GIT, GEM, PATH].freeze, - Gem::Version.create("1.10".dup) => [BUNDLED].freeze, - Gem::Version.create("1.12".dup) => [RUBY].freeze, - Gem::Version.create("1.13".dup) => [PLUGIN].freeze, + Gem::Version.create("1.0") => [DEPENDENCIES, PLATFORMS, GIT, GEM, PATH].freeze, + Gem::Version.create("1.10") => [BUNDLED].freeze, + Gem::Version.create("1.12") => [RUBY].freeze, + Gem::Version.create("1.13") => [PLUGIN].freeze, + Gem::Version.create("2.5.0") => [CHECKSUMS].freeze, }.freeze - KNOWN_SECTIONS = SECTIONS_BY_VERSION_INTRODUCED.values.flatten.freeze + KNOWN_SECTIONS = SECTIONS_BY_VERSION_INTRODUCED.values.flatten!.freeze ENVIRONMENT_VERSION_SECTIONS = [BUNDLED, RUBY].freeze + deprecate_constant(:ENVIRONMENT_VERSION_SECTIONS) def self.sections_in_lockfile(lockfile_contents) - lockfile_contents.scan(/^\w[\w ]*$/).uniq + sections = lockfile_contents.scan(/^\w[\w ]*$/) + sections.uniq! + sections end def self.unknown_sections_in_lockfile(lockfile_contents) @@ -49,7 +76,7 @@ module Bundler def self.sections_to_ignore(base_version = nil) base_version &&= base_version.release - base_version ||= Gem::Version.create("1.0".dup) + base_version ||= Gem::Version.create("1.0") attributes = [] SECTIONS_BY_VERSION_INTRODUCED.each do |version, introduced| next if version <= base_version @@ -58,103 +85,115 @@ module Bundler attributes end - def initialize(lockfile) + def self.bundled_with + lockfile = Bundler.default_lockfile + return unless lockfile.file? + + lockfile_contents = Bundler.read_file(lockfile) + return unless lockfile_contents.include?(BUNDLED) + + lockfile_contents.split(BUNDLED).last.strip + end + + def initialize(lockfile, strict: false, lockfile_path: nil) @platforms = [] @sources = [] + @metadata_source = Source::Metadata.new @dependencies = {} - @state = nil + @parse_method = nil @specs = {} + @lockfile_path = lockfile_path || begin + SharedHelpers.relative_lockfile_path + rescue GemfileNotFound + "Gemfile.lock" + end + @pos = Position.new(1, 1) + @strict = strict + + if lockfile.match?(/<<<<<<<|=======|>>>>>>>|\|\|\|\|\|\|\|/) + raise LockfileError, "Your #{@lockfile_path} contains merge conflicts.\n" \ + "Run `git checkout HEAD -- #{@lockfile_path}` first to get a clean lock." + end - @rubygems_aggregate = Source::Rubygems.new + @valid = lockfile.strip.empty? || + lockfile.split(/(?:\r?\n)+/).any? {|l| KNOWN_SECTIONS.include?(l) } - if lockfile.match(/<<<<<<<|=======|>>>>>>>|\|\|\|\|\|\|\|/) - raise LockfileError, "Your #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} contains merge conflicts.\n" \ - "Run `git checkout HEAD -- #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}` first to get a clean lock." + unless @valid + SharedHelpers.feature_deprecated!( + "Your #{@lockfile_path} does not appear to be a valid lockfile. " \ + "Run `rm #{@lockfile_path}` and then `bundle install` to generate a new lockfile. " \ + "This will raise a LockfileError in a future version of Bundler." + ) end - lockfile.split(/(?:\r?\n)+/).each do |line| + lockfile.split(/((?:\r?\n)+)/) do |line| + # split alternates between the line and the following whitespace + next @pos.advance!(line) if line.match?(/^\s*$/) + if SOURCE.include?(line) - @state = :source + @parse_method = :parse_source parse_source(line) elsif line == DEPENDENCIES - @state = :dependency + @parse_method = :parse_dependency + elsif line == CHECKSUMS + # This is a temporary solution to make this feature disabled by default + # for all gemfiles that don't already explicitly include the feature. + @checksums = true + @parse_method = :parse_checksum elsif line == PLATFORMS - @state = :platform + @parse_method = :parse_platform elsif line == RUBY - @state = :ruby + @parse_method = :parse_ruby elsif line == BUNDLED - @state = :bundled_with - elsif line =~ /^[^\s]/ - @state = nil - elsif @state - send("parse_#{@state}", line) + @parse_method = :parse_bundled_with + elsif /^[^\s]/.match?(line) + @parse_method = nil + elsif @parse_method + send(@parse_method, line) end + @pos.advance!(line) + end + + if @platforms.include?(Gem::Platform::X64_MINGW_LEGACY) + SharedHelpers.feature_deprecated!("Found x64-mingw32 in lockfile, which is deprecated and will be removed in the future.") + end + + @most_specific_locked_platform = @platforms.min_by do |bundle_platform| + Gem::Platform.platform_specificity_match(bundle_platform, Bundler.local_platform) + end + @specs = @specs.values.sort_by!(&:full_name).each do |spec| + spec.most_specific_locked_platform = @most_specific_locked_platform end - @sources << @rubygems_aggregate unless Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? - @specs = @specs.values.sort_by(&:identifier) - warn_for_outdated_bundler_version rescue ArgumentError => e Bundler.ui.debug(e) - raise LockfileError, "Your lockfile is unreadable. Run `rm #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}` " \ - "and then `bundle install` to generate a new lockfile." + raise LockfileError, "Your lockfile is unreadable. Run `rm #{@lockfile_path}` " \ + "and then `bundle install` to generate a new lockfile. The error occurred while " \ + "evaluating #{@lockfile_path}:#{@pos}" end - def warn_for_outdated_bundler_version - return unless bundler_version - prerelease_text = bundler_version.prerelease? ? " --pre" : "" - current_version = Gem::Version.create(Bundler::VERSION) - case current_version.segments.first <=> bundler_version.segments.first - when -1 - raise LockfileError, "You must use Bundler #{bundler_version.segments.first} or greater with this lockfile." - when 0 - if current_version < bundler_version - Bundler.ui.warn "Warning: the running version of Bundler (#{current_version}) is older " \ - "than the version that created the lockfile (#{bundler_version}). We suggest you " \ - "upgrade to the latest version of Bundler by running `gem " \ - "install bundler#{prerelease_text}`.\n" - end - end + def may_include_redundant_platform_specific_gems? + bundler_version.nil? || bundler_version < Gem::Version.new("1.16.2") + end + + def valid? + @valid end - private + private TYPES = { - GIT => Bundler::Source::Git, - GEM => Bundler::Source::Rubygems, - PATH => Bundler::Source::Path, + GIT => Bundler::Source::Git, + GEM => Bundler::Source::Rubygems, + PATH => Bundler::Source::Path, PLUGIN => Bundler::Plugin, }.freeze def parse_source(line) case line when SPECS - case @type - when PATH - @current_source = TYPES[@type].from_lock(@opts) - @sources << @current_source - when GIT - @current_source = TYPES[@type].from_lock(@opts) - # Strip out duplicate GIT sections - if @sources.include?(@current_source) - @current_source = @sources.find {|s| s == @current_source } - else - @sources << @current_source - end - when GEM - if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? - @opts["remotes"] = @opts.delete("remote") - @current_source = TYPES[@type].from_lock(@opts) - @sources << @current_source - else - Array(@opts["remote"]).each do |url| - @rubygems_aggregate.add_remote(url) - end - @current_source = @rubygems_aggregate - end - when PLUGIN - @current_source = Plugin.source_from_lock(@opts) - @sources << @current_source - end + return unless TYPES.key?(@type) + @current_source = TYPES[@type].from_lock(@opts) + @sources << @current_source when OPTIONS value = $2 value = true if value == "true" @@ -184,6 +223,7 @@ module Bundler (?:#{space}\(([^-]*) # Space, followed by version (?:-(.*))?\))? # Optional platform (!)? # Optional pinned marker + (?:#{space}([^ ]+))? # Optional checksum $ # Line end /xo @@ -191,11 +231,11 @@ module Bundler return unless line =~ NAME_VERSION spaces = $1 return unless spaces.size == 2 - name = $2 + name = -$2 version = $3 pinned = $5 - version = version.split(",").map(&:strip) if version + version = version.split(",").each(&:strip!) if version dep = Bundler::Dependency.new(name, version) @@ -216,41 +256,73 @@ module Bundler @dependencies[dep.name] = dep end - def parse_spec(line) + def parse_checksum(line) return unless line =~ NAME_VERSION + spaces = $1 + return unless spaces.size == 2 + checksums = $6 name = $2 version = $3 platform = $4 + version = Gem::Version.new(version) + platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY + full_name = Gem::NameTuple.new(name, version, platform).full_name + spec = @specs[full_name] + + if name == "bundler" + spec ||= LazySpecification.new(name, version, platform, @metadata_source) + end + return unless spec + + if checksums + checksums.split(",") do |lock_checksum| + column = line.index(lock_checksum) + 1 + checksum = Checksum.from_lock(lock_checksum, "#{@lockfile_path}:#{@pos.line}:#{column}") + spec.source.checksum_store.register(spec, checksum) + end + else + spec.source.checksum_store.register(spec, nil) + end + end + + def parse_spec(line) + return unless line =~ NAME_VERSION + spaces = $1 + name = -$2 + version = $3 + if spaces.size == 4 + # only load platform for non-dependency (spec) line + platform = $4 + version = Gem::Version.new(version) platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY - @current_spec = LazySpecification.new(name, version, platform) - @current_spec.source = @current_source + @current_spec = LazySpecification.new(name, version, platform, @current_source, strict: @strict) + @current_source.add_dependency_names(name) - # Avoid introducing multiple copies of the same spec (caused by - # duplicate GIT sections) - @specs[@current_spec.identifier] ||= @current_spec + @specs[@current_spec.full_name] = @current_spec elsif spaces.size == 6 - version = version.split(",").map(&:strip) if version + version = version.split(",").each(&:strip!) if version dep = Gem::Dependency.new(name, version) @current_spec.dependencies << dep end end def parse_platform(line) - @platforms << Gem::Platform.new($1) if line =~ /^ (.*)$/ + @platforms << Gem::Platform.new($1.strip) if line =~ /^ (.*)$/ end def parse_bundled_with(line) - line = line.strip + line.strip! return unless Gem::Version.correct?(line) @bundler_version = Gem::Version.create(line) end def parse_ruby(line) - @ruby_version = line.strip + line.strip! + @ruby_version = line end end end diff --git a/lib/bundler/ssl_certs/.document b/lib/bundler/man/.document index fb66f13c33..fb66f13c33 100644 --- a/lib/bundler/ssl_certs/.document +++ b/lib/bundler/man/.document diff --git a/lib/bundler/man/bundle-add.1 b/lib/bundler/man/bundle-add.1 new file mode 100644 index 0000000000..0956aa83f1 --- /dev/null +++ b/lib/bundler/man/bundle-add.1 @@ -0,0 +1,82 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-ADD" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-add\fR \- Add gem to the Gemfile and run bundle install +.SH "SYNOPSIS" +\fBbundle add\fR \fIGEM_NAME\fR [\-\-group=GROUP] [\-\-version=VERSION] [\-\-source=SOURCE] [\-\-path=PATH] [\-\-git=GIT|\-\-github=GITHUB] [\-\-branch=BRANCH] [\-\-ref=REF] [\-\-cooldown=NUMBER] [\-\-quiet] [\-\-skip\-install] [\-\-strict|\-\-optimistic] +.SH "DESCRIPTION" +Adds the named gem to the [\fBGemfile(5)\fR][Gemfile(5)] and run \fBbundle install\fR\. \fBbundle install\fR can be avoided by using the flag \fB\-\-skip\-install\fR\. +.SH "OPTIONS" +.TP +\fB\-\-version=VERSION\fR, \fB\-v=VERSION\fR +Specify version requirements(s) for the added gem\. +.TP +\fB\-\-group=GROUP\fR, \fB\-g=GROUP\fR +Specify the group(s) for the added gem\. Multiple groups should be separated by commas\. +.TP +\fB\-\-source=SOURCE\fR, \fB\-s=SOURCE\fR +Specify the source for the added gem\. +.TP +\fB\-\-require=REQUIRE\fR, \fB\-r=REQUIRE\fR +Adds require path to gem\. Provide false, or a path as a string\. +.TP +\fB\-\-path=PATH\fR +Specify the file system path for the added gem\. +.TP +\fB\-\-git=GIT\fR +Specify the git source for the added gem\. +.TP +\fB\-\-github=GITHUB\fR +Specify the github source for the added gem\. +.TP +\fB\-\-branch=BRANCH\fR +Specify the git branch for the added gem\. +.TP +\fB\-\-ref=REF\fR +Specify the git ref for the added gem\. +.TP +\fB\-\-glob=GLOB\fR +Specify the location of a dependency's \.gemspec, expanded within Ruby (single quotes recommended)\. +.TP +\fB\-\-quiet\fR +Do not print progress information to the standard output\. +.TP +\fB\-\-skip\-install\fR +Adds the gem to the Gemfile but does not install it\. +.TP +\fB\-\-optimistic\fR +Ignored (now default behavior) +.TP +\fB\-\-pessimistic\fR +Adds pessimistic declaration of version\. +.TP +\fB\-\-strict\fR +Adds strict declaration of version\. +.TP +\fB\-\-cooldown=<number>\fR +Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run\. See \fBcooldown\fR in bundle\-config(1) for precedence rules\. +.SH "EXAMPLES" +.IP "1." 4 +You can add the \fBrails\fR gem to the Gemfile without any version restriction\. The source of the gem will be the global source\. +.IP +\fBbundle add rails\fR +.IP "2." 4 +You can add the \fBrails\fR gem with version greater than 1\.1 (not including 1\.1) and less than 3\.0\. +.IP +\fBbundle add rails \-\-version "> 1\.1, < 3\.0"\fR +.IP "3." 4 +You can use the \fBhttps://gems\.example\.com\fR custom source and assign the gem to a group\. +.IP +\fBbundle add rails \-\-version ">= 5\.0\.0" \-\-source "https://gems\.example\.com" \-\-group "development"\fR +.IP "4." 4 +The following adds the \fBgem\fR entry to the Gemfile without installing the gem\. You can install gems later via \fBbundle install\fR\. +.IP +\fBbundle add rails \-\-skip\-install\fR +.IP "5." 4 +You can assign the gem to more than one group\. +.IP +\fBbundle add rails \-\-group "development, test"\fR +.IP "" 0 +.SH "SEE ALSO" +Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR, bundle\-remove(1) \fIbundle\-remove\.1\.html\fR diff --git a/lib/bundler/man/bundle-add.1.ronn b/lib/bundler/man/bundle-add.1.ronn new file mode 100644 index 0000000000..8c65af0cc0 --- /dev/null +++ b/lib/bundler/man/bundle-add.1.ronn @@ -0,0 +1,95 @@ +bundle-add(1) -- Add gem to the Gemfile and run bundle install +============================================================== + +## SYNOPSIS + +`bundle add` <GEM_NAME> [--group=GROUP] [--version=VERSION] [--source=SOURCE] + [--path=PATH] [--git=GIT|--github=GITHUB] [--branch=BRANCH] [--ref=REF] + [--cooldown=NUMBER] [--quiet] [--skip-install] [--strict|--optimistic] + +## DESCRIPTION + +Adds the named gem to the [`Gemfile(5)`][Gemfile(5)] and run `bundle install`. +`bundle install` can be avoided by using the flag `--skip-install`. + +## OPTIONS + +* `--version=VERSION`, `-v=VERSION`: + Specify version requirements(s) for the added gem. + +* `--group=GROUP`, `-g=GROUP`: + Specify the group(s) for the added gem. Multiple groups should be separated by commas. + +* `--source=SOURCE`, `-s=SOURCE`: + Specify the source for the added gem. + +* `--require=REQUIRE`, `-r=REQUIRE`: + Adds require path to gem. Provide false, or a path as a string. + +* `--path=PATH`: + Specify the file system path for the added gem. + +* `--git=GIT`: + Specify the git source for the added gem. + +* `--github=GITHUB`: + Specify the github source for the added gem. + +* `--branch=BRANCH`: + Specify the git branch for the added gem. + +* `--ref=REF`: + Specify the git ref for the added gem. + +* `--glob=GLOB`: + Specify the location of a dependency's .gemspec, expanded within Ruby (single quotes recommended). + +* `--quiet`: + Do not print progress information to the standard output. + +* `--skip-install`: + Adds the gem to the Gemfile but does not install it. + +* `--optimistic`: + Ignored (now default behavior) + +* `--pessimistic`: + Adds pessimistic declaration of version. + +* `--strict`: + Adds strict declaration of version. + +* `--cooldown=<number>`: + Only consider gem versions published at least <number> days ago when + resolving. Pass `0` to disable cooldown for this run. See `cooldown` + in bundle-config(1) for precedence rules. + +## EXAMPLES + +1. You can add the `rails` gem to the Gemfile without any version restriction. + The source of the gem will be the global source. + + `bundle add rails` + +2. You can add the `rails` gem with version greater than 1.1 (not including 1.1) and less than 3.0. + + `bundle add rails --version "> 1.1, < 3.0"` + +3. You can use the `https://gems.example.com` custom source and assign the gem + to a group. + + `bundle add rails --version ">= 5.0.0" --source "https://gems.example.com" --group "development"` + +4. The following adds the `gem` entry to the Gemfile without installing the + gem. You can install gems later via `bundle install`. + + `bundle add rails --skip-install` + +5. You can assign the gem to more than one group. + + `bundle add rails --group "development, test"` + +## SEE ALSO + +[Gemfile(5)](https://bundler.io/man/gemfile.5.html), +[bundle-remove(1)](bundle-remove.1.html) diff --git a/lib/bundler/man/bundle-binstubs.1 b/lib/bundler/man/bundle-binstubs.1 new file mode 100644 index 0000000000..246daeae53 --- /dev/null +++ b/lib/bundler/man/bundle-binstubs.1 @@ -0,0 +1,30 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-BINSTUBS" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-binstubs\fR \- Install the binstubs of the listed gems +.SH "SYNOPSIS" +\fBbundle binstubs\fR \fIGEM_NAME\fR [\-\-force] [\-\-standalone] [\-\-all\-platforms] +.SH "DESCRIPTION" +Binstubs are scripts that wrap around executables\. Bundler creates a small Ruby file (a binstub) that loads Bundler, runs the command, and puts it into \fBbin/\fR\. Binstubs are a shortcut\-or alternative\- to always using \fBbundle exec\fR\. This gives you a file that can be run directly, and one that will always run the correct gem version used by the application\. +.P +For example, if you run \fBbundle binstubs rspec\-core\fR, Bundler will create the file \fBbin/rspec\fR\. That file will contain enough code to load Bundler, tell it to load the bundled gems, and then run rspec\. +.P +This command generates binstubs for executables in \fBGEM_NAME\fR\. Binstubs are put into \fBbin\fR, or the directory specified by \fBbin\fR setting if it has been configured\. Calling binstubs with [GEM [GEM]] will create binstubs for all given gems\. +.SH "OPTIONS" +.TP +\fB\-\-force\fR +Overwrite existing binstubs if they exist\. +.TP +\fB\-\-standalone\fR +Makes binstubs that can work without depending on Rubygems or Bundler at runtime\. +.TP +\fB\-\-shebang=SHEBANG\fR +Specify a different shebang executable name than the default (default 'ruby') +.TP +\fB\-\-all\fR +Create binstubs for all gems in the bundle\. +.TP +\fB\-\-all\-platforms\fR +Install binstubs for all platforms\. + diff --git a/lib/bundler/man/bundle-binstubs.1.ronn b/lib/bundler/man/bundle-binstubs.1.ronn new file mode 100644 index 0000000000..cbe5983f4d --- /dev/null +++ b/lib/bundler/man/bundle-binstubs.1.ronn @@ -0,0 +1,42 @@ +bundle-binstubs(1) -- Install the binstubs of the listed gems +============================================================= + +## SYNOPSIS + +`bundle binstubs` <GEM_NAME> [--force] [--standalone] [--all-platforms] + +## DESCRIPTION + +Binstubs are scripts that wrap around executables. Bundler creates a +small Ruby file (a binstub) that loads Bundler, runs the command, +and puts it into `bin/`. Binstubs are a shortcut-or alternative- +to always using `bundle exec`. This gives you a file that can be run +directly, and one that will always run the correct gem version +used by the application. + +For example, if you run `bundle binstubs rspec-core`, Bundler will create +the file `bin/rspec`. That file will contain enough code to load Bundler, +tell it to load the bundled gems, and then run rspec. + +This command generates binstubs for executables in `GEM_NAME`. +Binstubs are put into `bin`, or the directory specified by `bin` setting if it +has been configured. Calling binstubs with [GEM [GEM]] will create binstubs for +all given gems. + +## OPTIONS + +* `--force`: + Overwrite existing binstubs if they exist. + +* `--standalone`: + Makes binstubs that can work without depending on Rubygems or Bundler at + runtime. + +* `--shebang=SHEBANG`: + Specify a different shebang executable name than the default (default 'ruby') + +* `--all`: + Create binstubs for all gems in the bundle. + +* `--all-platforms`: + Install binstubs for all platforms. diff --git a/lib/bundler/man/bundle-cache.1 b/lib/bundler/man/bundle-cache.1 new file mode 100644 index 0000000000..38ea047961 --- /dev/null +++ b/lib/bundler/man/bundle-cache.1 @@ -0,0 +1,56 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-CACHE" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-cache\fR \- Package your needed \fB\.gem\fR files into your application +.SH "SYNOPSIS" +\fBbundle cache\fR [\fIOPTIONS\fR] +.P +alias: \fBpackage\fR, \fBpack\fR +.SH "DESCRIPTION" +Copy all of the \fB\.gem\fR files needed to run the application into the \fBvendor/cache\fR directory\. In the future, when running \fBbundle install(1)\fR \fIbundle\-install\.1\.html\fR, use the gems in the cache in preference to the ones on \fBrubygems\.org\fR\. +.SH "OPTIONS" +.TP +\fB\-\-all\-platforms\fR +Include gems for all platforms present in the lockfile, not only the current one\. +.TP +\fB\-\-cache\-path=CACHE\-PATH\fR +Specify a different cache path than the default (vendor/cache)\. +.TP +\fB\-\-gemfile=GEMFILE\fR +Use the specified gemfile instead of Gemfile\. +.TP +\fB\-\-no\-install\fR +Don't install the gems, only update the cache\. +.TP +\fB\-\-quiet\fR +Only output warnings and errors\. +.SH "GIT AND PATH GEMS" +The \fBbundle cache\fR command can also package \fB:git\fR and \fB:path\fR dependencies besides \.gem files\. This can be disabled setting \fBcache_all\fR to false\. +.SH "SUPPORT FOR MULTIPLE PLATFORMS" +When using gems that have different packages for different platforms, Bundler supports caching of gems for other platforms where the Gemfile has been resolved (i\.e\. present in the lockfile) in \fBvendor/cache\fR\. This needs to be enabled via the \fB\-\-all\-platforms\fR option\. This setting will be remembered in your local bundler configuration\. +.SH "REMOTE FETCHING" +By default, if you run \fBbundle install(1)\fR \fIbundle\-install\.1\.html\fR after running bundle cache(1) \fIbundle\-cache\.1\.html\fR, bundler will still connect to \fBrubygems\.org\fR to check whether a platform\-specific gem exists for any of the gems in \fBvendor/cache\fR\. +.P +For instance, consider this Gemfile(5): +.IP "" 4 +.nf +source "https://rubygems\.org" + +gem "nokogiri" +.fi +.IP "" 0 +.P +If you run \fBbundle cache\fR under C Ruby, bundler will retrieve the version of \fBnokogiri\fR for the \fB"ruby"\fR platform\. If you deploy to JRuby and run \fBbundle install\fR, bundler is forced to check to see whether a \fB"java"\fR platformed \fBnokogiri\fR exists\. +.P +Even though the \fBnokogiri\fR gem for the Ruby platform is \fItechnically\fR acceptable on JRuby, it has a C extension that does not run on JRuby\. As a result, bundler will, by default, still connect to \fBrubygems\.org\fR to check whether it has a version of one of your gems more specific to your platform\. +.P +This problem is also not limited to the \fB"java"\fR platform\. A similar (common) problem can happen when developing on Windows and deploying to Linux, or even when developing on OSX and deploying to Linux\. +.P +If you know for sure that the gems packaged in \fBvendor/cache\fR are appropriate for the platform you are on, you can run \fBbundle install \-\-local\fR to skip checking for more appropriate gems, and use the ones in \fBvendor/cache\fR\. +.P +One way to be sure that you have the right platformed versions of all your gems is to run \fBbundle cache\fR on an identical machine and check in the gems\. For instance, you can run \fBbundle cache\fR on an identical staging box during your staging process, and check in the \fBvendor/cache\fR before deploying to production\. +.P +By default, bundle cache(1) \fIbundle\-cache\.1\.html\fR fetches and also installs the gems to the default location\. To package the dependencies to \fBvendor/cache\fR without installing them to the local install location, you can run \fBbundle cache \-\-no\-install\fR\. +.SH "HISTORY" +In Bundler 2\.1, \fBcache\fR took in the functionalities of \fBpackage\fR and now \fBpackage\fR and \fBpack\fR are aliases of \fBcache\fR\. diff --git a/lib/bundler/man/bundle-cache.1.ronn b/lib/bundler/man/bundle-cache.1.ronn new file mode 100644 index 0000000000..51846c96b4 --- /dev/null +++ b/lib/bundler/man/bundle-cache.1.ronn @@ -0,0 +1,95 @@ +bundle-cache(1) -- Package your needed `.gem` files into your application +========================================================================= + +## SYNOPSIS + +`bundle cache` [*OPTIONS*] + +alias: `package`, `pack` + +## DESCRIPTION + +Copy all of the `.gem` files needed to run the application into the +`vendor/cache` directory. In the future, when running [`bundle install(1)`](bundle-install.1.html), +use the gems in the cache in preference to the ones on `rubygems.org`. + +## OPTIONS + +* `--all-platforms`: + Include gems for all platforms present in the lockfile, not only the current one. + +* `--cache-path=CACHE-PATH`: + Specify a different cache path than the default (vendor/cache). + +* `--gemfile=GEMFILE`: + Use the specified gemfile instead of Gemfile. + +* `--no-install`: + Don't install the gems, only update the cache. + +* `--quiet`: + Only output warnings and errors. + +## GIT AND PATH GEMS + +The `bundle cache` command can also package `:git` and `:path` dependencies +besides .gem files. This can be disabled setting `cache_all` to false. + +## SUPPORT FOR MULTIPLE PLATFORMS + +When using gems that have different packages for different platforms, Bundler +supports caching of gems for other platforms where the Gemfile has been resolved +(i.e. present in the lockfile) in `vendor/cache`. This needs to be enabled via +the `--all-platforms` option. This setting will be remembered in your local +bundler configuration. + +## REMOTE FETCHING + +By default, if you run [`bundle install(1)`](bundle-install.1.html) after running +[bundle cache(1)](bundle-cache.1.html), bundler will still connect to `rubygems.org` +to check whether a platform-specific gem exists for any of the gems +in `vendor/cache`. + +For instance, consider this Gemfile(5): + + source "https://rubygems.org" + + gem "nokogiri" + +If you run `bundle cache` under C Ruby, bundler will retrieve +the version of `nokogiri` for the `"ruby"` platform. If you deploy +to JRuby and run `bundle install`, bundler is forced to check to +see whether a `"java"` platformed `nokogiri` exists. + +Even though the `nokogiri` gem for the Ruby platform is +_technically_ acceptable on JRuby, it has a C extension +that does not run on JRuby. As a result, bundler will, by default, +still connect to `rubygems.org` to check whether it has a version +of one of your gems more specific to your platform. + +This problem is also not limited to the `"java"` platform. +A similar (common) problem can happen when developing on Windows +and deploying to Linux, or even when developing on OSX and +deploying to Linux. + +If you know for sure that the gems packaged in `vendor/cache` +are appropriate for the platform you are on, you can run +`bundle install --local` to skip checking for more appropriate +gems, and use the ones in `vendor/cache`. + +One way to be sure that you have the right platformed versions +of all your gems is to run `bundle cache` on an identical +machine and check in the gems. For instance, you can run +`bundle cache` on an identical staging box during your +staging process, and check in the `vendor/cache` before +deploying to production. + +By default, [bundle cache(1)](bundle-cache.1.html) fetches and also +installs the gems to the default location. To package the +dependencies to `vendor/cache` without installing them to the +local install location, you can run `bundle cache --no-install`. + +## HISTORY + +In Bundler 2.1, `cache` took in the functionalities of `package` and now +`package` and `pack` are aliases of `cache`. diff --git a/lib/bundler/man/bundle-check.1 b/lib/bundler/man/bundle-check.1 new file mode 100644 index 0000000000..6cd474d90a --- /dev/null +++ b/lib/bundler/man/bundle-check.1 @@ -0,0 +1,21 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-CHECK" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-check\fR \- Verifies if dependencies are satisfied by installed gems +.SH "SYNOPSIS" +\fBbundle check\fR [\-\-dry\-run] [\-\-gemfile=FILE] +.SH "DESCRIPTION" +\fBcheck\fR searches the local machine for each of the gems requested in the Gemfile\. If all gems are found, Bundler prints a success message and exits with a status of 0\. +.P +If not, the first missing gem is listed and Bundler exits status 1\. +.P +If the lockfile needs to be updated then it will be resolved using the gems installed on the local machine, if they satisfy the requirements\. +.SH "OPTIONS" +.TP +\fB\-\-dry\-run\fR +Locks the [\fBGemfile(5)\fR][Gemfile(5)] before running the command\. +.TP +\fB\-\-gemfile=GEMFILE\fR +Use the specified gemfile instead of the [\fBGemfile(5)\fR][Gemfile(5)]\. + diff --git a/lib/bundler/man/bundle-check.1.ronn b/lib/bundler/man/bundle-check.1.ronn new file mode 100644 index 0000000000..92589159c9 --- /dev/null +++ b/lib/bundler/man/bundle-check.1.ronn @@ -0,0 +1,26 @@ +bundle-check(1) -- Verifies if dependencies are satisfied by installed gems +=========================================================================== + +## SYNOPSIS + +`bundle check` [--dry-run] + [--gemfile=FILE] + +## DESCRIPTION + +`check` searches the local machine for each of the gems requested in the +Gemfile. If all gems are found, Bundler prints a success message and exits with +a status of 0. + +If not, the first missing gem is listed and Bundler exits status 1. + +If the lockfile needs to be updated then it will be resolved using the gems +installed on the local machine, if they satisfy the requirements. + +## OPTIONS + +* `--dry-run`: + Locks the [`Gemfile(5)`][Gemfile(5)] before running the command. + +* `--gemfile=GEMFILE`: + Use the specified gemfile instead of the [`Gemfile(5)`][Gemfile(5)]. diff --git a/lib/bundler/man/bundle-clean.1 b/lib/bundler/man/bundle-clean.1 new file mode 100644 index 0000000000..eb90636c17 --- /dev/null +++ b/lib/bundler/man/bundle-clean.1 @@ -0,0 +1,17 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-CLEAN" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-clean\fR \- Cleans up unused gems in your bundler directory +.SH "SYNOPSIS" +\fBbundle clean\fR [\-\-dry\-run] [\-\-force] +.SH "DESCRIPTION" +This command will remove all unused gems in your bundler directory\. This is useful when you have made many changes to your gem dependencies\. +.SH "OPTIONS" +.TP +\fB\-\-dry\-run\fR +Print the changes, but do not clean the unused gems\. +.TP +\fB\-\-force\fR +Forces cleaning up unused gems even if Bundler is configured to use globally installed gems\. As a consequence, removes all system gems except for the ones in the current application\. + diff --git a/lib/bundler/man/bundle-clean.1.ronn b/lib/bundler/man/bundle-clean.1.ronn new file mode 100644 index 0000000000..dae27c21ee --- /dev/null +++ b/lib/bundler/man/bundle-clean.1.ronn @@ -0,0 +1,18 @@ +bundle-clean(1) -- Cleans up unused gems in your bundler directory +================================================================== + +## SYNOPSIS + +`bundle clean` [--dry-run] [--force] + +## DESCRIPTION + +This command will remove all unused gems in your bundler directory. This is +useful when you have made many changes to your gem dependencies. + +## OPTIONS + +* `--dry-run`: + Print the changes, but do not clean the unused gems. +* `--force`: + Forces cleaning up unused gems even if Bundler is configured to use globally installed gems. As a consequence, removes all system gems except for the ones in the current application. diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 new file mode 100644 index 0000000000..c055c8a415 --- /dev/null +++ b/lib/bundler/man/bundle-config.1 @@ -0,0 +1,343 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-CONFIG" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-config\fR \- Set bundler configuration options +.SH "SYNOPSIS" +\fBbundle config\fR [list] +.br +\fBbundle config\fR [get [\-\-local|\-\-global]] NAME +.br +\fBbundle config\fR [set [\-\-local|\-\-global]] NAME VALUE +.br +\fBbundle config\fR unset [\-\-local|\-\-global] NAME +.SH "DESCRIPTION" +This command allows you to interact with Bundler's configuration system\. +.P +Bundler loads configuration settings in this order: +.IP "1." 4 +Local config (\fB<project_root>/\.bundle/config\fR or \fB$BUNDLE_APP_CONFIG/config\fR) +.IP "2." 4 +Environmental variables (\fBENV\fR) +.IP "3." 4 +Global config (\fB~/\.bundle/config\fR) +.IP "4." 4 +Bundler default config +.IP "" 0 +.P +Executing \fBbundle\fR with the \fBBUNDLE_IGNORE_CONFIG\fR environment variable set will cause it to ignore all configuration\. +.SH "SUB\-COMMANDS" +.SS "list (default command)" +Executing \fBbundle config list\fR will print a list of all bundler configuration for the current bundle, and where that configuration was set\. +.SS "get" +Executing \fBbundle config get <name>\fR will print the value of that configuration setting, and all locations where it was set\. +.P +\fBOPTIONS\fR +.TP +\fB\-\-local\fR +Get configuration from configuration file for the local application, namely, \fB<project_root>/\.bundle/config\fR, or \fB$BUNDLE_APP_CONFIG/config\fR if \fBBUNDLE_APP_CONFIG\fR is set\. +.TP +\fB\-\-global\fR +Get configuration from configuration file global to all bundles executed as the current user, namely, from \fB~/\.bundle/config\fR\. +.SS "set" +Executing \fBbundle config set <name> <value>\fR defaults to setting \fBlocal\fR configuration if executing from within a local application, otherwise it will set \fBglobal\fR configuration\. +.P +\fBOPTIONS\fR +.TP +\fB\-\-local\fR +Executing \fBbundle config set \-\-local <name> <value>\fR will set that configuration in the directory for the local application\. The configuration will be stored in \fB<project_root>/\.bundle/config\fR\. If \fBBUNDLE_APP_CONFIG\fR is set, the configuration will be stored in \fB$BUNDLE_APP_CONFIG/config\fR\. +.TP +\fB\-\-global\fR +Executing \fBbundle config set \-\-global <name> <value>\fR will set that configuration to the value specified for all bundles executed as the current user\. The configuration will be stored in \fB~/\.bundle/config\fR\. If \fIname\fR already is set, \fIname\fR will be overridden and user will be warned\. +.SS "unset" +Executing \fBbundle config unset <name>\fR will delete the configuration in both local and global sources\. +.P +\fBOPTIONS\fR +.TP +\fB\-\-local\fR +Executing \fBbundle config unset \-\-local <name>\fR will delete the configuration only from the local application\. +.TP +\fB\-\-global\fR +Executing \fBbundle config unset \-\-global <name>\fR will delete the configuration only from the user configuration\. +.SH "CONFIGURATION KEYS" +Configuration keys in bundler have two forms: the canonical form and the environment variable form\. +.P +For instance, passing the \fB\-\-without\fR flag to bundle install(1) \fIbundle\-install\.1\.html\fR prevents Bundler from installing certain groups specified in the Gemfile(5)\. Bundler persists this value in \fBapp/\.bundle/config\fR so that calls to \fBBundler\.setup\fR do not try to find gems from the \fBGemfile\fR that you didn't install\. Additionally, subsequent calls to bundle install(1) \fIbundle\-install\.1\.html\fR remember this setting and skip those groups\. +.P +The canonical form of this configuration is \fB"without"\fR\. To convert the canonical form to the environment variable form, capitalize it, and prepend \fBBUNDLE_\fR\. The environment variable form of \fB"without"\fR is \fBBUNDLE_WITHOUT\fR\. +.P +Any periods in the configuration keys must be replaced with two underscores when setting it via environment variables\. The configuration key \fBlocal\.rack\fR becomes the environment variable \fBBUNDLE_LOCAL__RACK\fR\. +.SH "LIST OF AVAILABLE KEYS" +The following is a list of all configuration keys and their purpose\. You can learn more about their operation in bundle install(1) \fIbundle\-install\.1\.html\fR\. +.IP "\(bu" 4 +\fBapi_request_size\fR (\fBBUNDLE_API_REQUEST_SIZE\fR): Configure how many dependencies to fetch when resolving the specifications\. This configuration is only used when fetching specifications from RubyGems servers that didn't implement the Compact Index API\. Defaults to 100\. +.IP "\(bu" 4 +\fBauto_install\fR (\fBBUNDLE_AUTO_INSTALL\fR): Automatically run \fBbundle install\fR when gems are missing\. +.IP "\(bu" 4 +\fBbin\fR (\fBBUNDLE_BIN\fR): If configured, \fBbundle binstubs\fR will install executables from gems in the bundle to the specified directory\. Otherwise it will create them in a \fBbin\fR directory relative to the Gemfile directory\. These executables run in Bundler's context\. If used, you might add this directory to your environment's \fBPATH\fR variable\. For instance, if the \fBrails\fR gem comes with a \fBrails\fR executable, \fBbundle binstubs\fR will create a \fBbin/rails\fR executable that ensures that all referred dependencies will be resolved using the bundled gems\. +.IP "\(bu" 4 +\fBcache_all\fR (\fBBUNDLE_CACHE_ALL\fR): Cache all gems, including path and git gems\. This needs to be explicitly before bundler 4, but will be the default on bundler 4\. +.IP "\(bu" 4 +\fBcache_all_platforms\fR (\fBBUNDLE_CACHE_ALL_PLATFORMS\fR): Cache gems for all platforms\. +.IP "\(bu" 4 +\fBcache_path\fR (\fBBUNDLE_CACHE_PATH\fR): The directory that bundler will place cached gems in when running \fBbundle package\fR, and that bundler will look in when installing gems\. Defaults to \fBvendor/cache\fR\. +.IP "\(bu" 4 +\fBclean\fR (\fBBUNDLE_CLEAN\fR): Whether Bundler should run \fBbundle clean\fR automatically after \fBbundle install\fR\. Defaults to \fBtrue\fR in Bundler 4, as long as \fBpath\fR is not explicitly configured\. +.IP "\(bu" 4 +\fBconsole\fR (\fBBUNDLE_CONSOLE\fR): The console that \fBbundle console\fR starts\. Defaults to \fBirb\fR\. +.IP "\(bu" 4 +\fBcooldown\fR (\fBBUNDLE_COOLDOWN\fR): Number of days a published gem version must age before bundler will resolve to it\. Defaults to unset (no cooldown)\. Pass \fB0\fR to disable cooldown for an individual run\. +.IP +The effective cooldown for any given gem is resolved from three layers, highest precedence first: +.IP "1." 4 +CLI flag \fB\-\-cooldown N\fR on \fBinstall\fR, \fBupdate\fR, \fBadd\fR, and \fBoutdated\fR\. +.IP "2." 4 +This setting (\fBbundle config set cooldown N\fR or \fBBUNDLE_COOLDOWN=N\fR)\. +.IP "3." 4 +The per\-source \fBcooldown:\fR keyword in the Gemfile, such as \fBsource "https://rubygems\.org", cooldown: 7\fR\. +.IP "" 0 +.IP +The CLI flag and this setting apply uniformly to every source, including ones declared with their own \fBcooldown:\fR value\. To keep a private registry permanently exempt while still cooling down public gems, declare \fBsource "https://internal", cooldown: 0\fR in the Gemfile; remember that \fB\-\-cooldown N\fR on the command line will still override it for that single run\. +.IP +Cooldown filtering depends on the gem server providing a per\-version \fBcreated_at\fR timestamp in the v2 compact\-index format\. Versions without that metadata \- older gem servers, historical entries that predate the v2 cutover on \fBrubygems\.org\fR, or private registries that still emit the v1 format \- are treated as outside the cooldown window and remain resolvable\. If you rely on cooldown for supply\-chain protection, confirm that the gem server emits \fBcreated_at\fR in its \fB/info/<gem>\fR responses\. +.IP "\(bu" 4 +\fBdefault_cli_command\fR (\fBBUNDLE_DEFAULT_CLI_COMMAND\fR): The command that running \fBbundle\fR without arguments should run\. Defaults to \fBcli_help\fR since Bundler 4, but can also be \fBinstall\fR which was the previous default\. +.IP "\(bu" 4 +\fBdeployment\fR (\fBBUNDLE_DEPLOYMENT\fR): Equivalent to setting \fBfrozen\fR to \fBtrue\fR and \fBpath\fR to \fBvendor/bundle\fR\. +.IP "\(bu" 4 +\fBdisable_checksum_validation\fR (\fBBUNDLE_DISABLE_CHECKSUM_VALIDATION\fR): Allow installing gems even if they do not match the checksum provided by RubyGems\. +.IP "\(bu" 4 +\fBdisable_exec_load\fR (\fBBUNDLE_DISABLE_EXEC_LOAD\fR): Stop Bundler from using \fBload\fR to launch an executable in\-process in \fBbundle exec\fR\. +.IP "\(bu" 4 +\fBdisable_local_branch_check\fR (\fBBUNDLE_DISABLE_LOCAL_BRANCH_CHECK\fR): Allow Bundler to use a local git override without a branch specified in the Gemfile\. +.IP "\(bu" 4 +\fBdisable_local_revision_check\fR (\fBBUNDLE_DISABLE_LOCAL_REVISION_CHECK\fR): Allow Bundler to use a local git override without checking if the revision present in the lockfile is present in the repository\. +.IP "\(bu" 4 +\fBdisable_shared_gems\fR (\fBBUNDLE_DISABLE_SHARED_GEMS\fR): Stop Bundler from accessing gems installed to RubyGems' normal location\. +.IP "\(bu" 4 +\fBdisable_version_check\fR (\fBBUNDLE_DISABLE_VERSION_CHECK\fR): Stop Bundler from checking if a newer Bundler version is available on rubygems\.org\. +.IP "\(bu" 4 +\fBforce_ruby_platform\fR (\fBBUNDLE_FORCE_RUBY_PLATFORM\fR): Ignore the current machine's platform and install only \fBruby\fR platform gems\. As a result, gems with native extensions will be compiled from source\. +.IP "\(bu" 4 +\fBfrozen\fR (\fBBUNDLE_FROZEN\fR): Disallow any automatic changes to \fBGemfile\.lock\fR\. Bundler commands will be blocked unless the lockfile can be installed exactly as written\. Usually this will happen when changing the \fBGemfile\fR manually and forgetting to update the lockfile through \fBbundle lock\fR or \fBbundle install\fR\. +.IP "\(bu" 4 +\fBgem\.github_username\fR (\fBBUNDLE_GEM__GITHUB_USERNAME\fR): Sets a GitHub username or organization to be used in the \fBREADME\fR and \fB\.gemspec\fR files when you create a new gem via \fBbundle gem\fR command\. It can be overridden by passing an explicit \fB\-\-github\-username\fR flag to \fBbundle gem\fR\. +.IP "\(bu" 4 +\fBgem\.push_key\fR (\fBBUNDLE_GEM__PUSH_KEY\fR): Sets the \fB\-\-key\fR parameter for \fBgem push\fR when using the \fBrake release\fR command with a private gemstash server\. +.IP "\(bu" 4 +\fBgemfile\fR (\fBBUNDLE_GEMFILE\fR): The name of the file that bundler should use as the \fBGemfile\fR\. This location of this file also sets the root of the project, which is used to resolve relative paths in the \fBGemfile\fR, among other things\. By default, bundler will search up from the current working directory until it finds a \fBGemfile\fR\. +.IP "\(bu" 4 +\fBglobal_gem_cache\fR (\fBBUNDLE_GLOBAL_GEM_CACHE\fR): Whether Bundler should cache all gems and compiled extensions globally, rather than locally to the configured installation path\. +.IP "\(bu" 4 +\fBignore_funding_requests\fR (\fBBUNDLE_IGNORE_FUNDING_REQUESTS\fR): When set, no funding requests will be printed\. +.IP "\(bu" 4 +\fBignore_messages\fR (\fBBUNDLE_IGNORE_MESSAGES\fR): When set, no post install messages will be printed\. To silence a single gem, use dot notation like \fBignore_messages\.httparty true\fR\. +.IP "\(bu" 4 +\fBinit_gems_rb\fR (\fBBUNDLE_INIT_GEMS_RB\fR): Generate a \fBgems\.rb\fR instead of a \fBGemfile\fR when running \fBbundle init\fR\. +.IP "\(bu" 4 +\fBjobs\fR (\fBBUNDLE_JOBS\fR): The number of gems Bundler can download and install in parallel\. Defaults to the number of available processors\. +.IP "\(bu" 4 +\fBlockfile\fR (\fBBUNDLE_LOCKFILE\fR): The path to the lockfile that bundler should use\. By default, Bundler adds \fB\.lock\fR to the end of the \fBgemfile\fR entry\. Can be set to \fBfalse\fR in the Gemfile to disable lockfile creation entirely (see gemfile(5))\. +.IP "\(bu" 4 +\fBlockfile_checksums\fR (\fBBUNDLE_LOCKFILE_CHECKSUMS\fR): Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources\. Defaults to true\. +.IP "\(bu" 4 +\fBno_build_extension\fR (\fBBUNDLE_NO_BUILD_EXTENSION\fR): Whether Bundler should skip building native extensions during installation\. When set, gems are installed without compiling their C extensions\. To build extensions later, unset this setting and run \fBbundle pristine <gem>\fR\. +.IP "\(bu" 4 +\fBno_install\fR (\fBBUNDLE_NO_INSTALL\fR): Whether \fBbundle package\fR should skip installing gems\. +.IP "\(bu" 4 +\fBno_install_plugin\fR (\fBBUNDLE_NO_INSTALL_PLUGIN\fR): Whether Bundler should skip installing RubyGems plugins during installation\. When set, plugin files are not written to the plugins directory\. To install plugins later, unset this setting and run \fBbundle pristine <gem>\fR\. +.IP "\(bu" 4 +\fBno_prune\fR (\fBBUNDLE_NO_PRUNE\fR): Whether Bundler should leave outdated gems unpruned when caching\. +.IP "\(bu" 4 +\fBonly\fR (\fBBUNDLE_ONLY\fR): A space\-separated list of groups to install only gems of the specified groups\. Please check carefully if you want to install also gems without a group, because they get put inside \fBdefault\fR group\. For example \fBonly test:default\fR will install all gems specified in test group and without one\. +.IP "\(bu" 4 +\fBpath\fR (\fBBUNDLE_PATH\fR): The location on disk where all gems in your bundle will be located regardless of \fB$GEM_HOME\fR or \fB$GEM_PATH\fR values\. Bundle gems not found in this location will be installed by \fBbundle install\fR\. When not set, Bundler install by default to a \fB\.bundle\fR directory relative to repository root in Bundler 4, and to the default system path (\fBGem\.dir\fR) before Bundler 4\. That means that before Bundler 4, Bundler shares this location with Rubygems, and \fBgem install \|\.\|\.\|\.\fR will have gems installed in the same location and therefore, gems installed without \fBpath\fR set will show up by calling \fBgem list\fR\. This will not be the case in Bundler 4\. +.IP "\(bu" 4 +\fBpath\.system\fR (\fBBUNDLE_PATH__SYSTEM\fR): Whether Bundler will install gems into the default system path (\fBGem\.dir\fR)\. +.IP "\(bu" 4 +\fBplugins\fR (\fBBUNDLE_PLUGINS\fR): Enable Bundler's experimental plugin system\. +.IP "\(bu" 4 +\fBprefer_patch\fR (\fBBUNDLE_PREFER_PATCH\fR): Prefer updating only to next patch version during updates\. Makes \fBbundle update\fR calls equivalent to \fBbundler update \-\-patch\fR\. +.IP "\(bu" 4 +\fBredirect\fR (\fBBUNDLE_REDIRECT\fR): The number of redirects allowed for network requests\. Defaults to \fB5\fR\. +.IP "\(bu" 4 +\fBretry\fR (\fBBUNDLE_RETRY\fR): The number of times to retry failed network requests\. Defaults to \fB3\fR\. +.IP "\(bu" 4 +\fBshebang\fR (\fBBUNDLE_SHEBANG\fR): The program name that should be invoked for generated binstubs\. Defaults to the ruby install name used to generate the binstub\. +.IP "\(bu" 4 +\fBsilence_deprecations\fR (\fBBUNDLE_SILENCE_DEPRECATIONS\fR): Whether Bundler should silence deprecation warnings for behavior that will be changed in the next major version\. +.IP "\(bu" 4 +\fBsilence_root_warning\fR (\fBBUNDLE_SILENCE_ROOT_WARNING\fR): Silence the warning Bundler prints when installing gems as root\. +.IP "\(bu" 4 +\fBsimulate_version\fR (\fBBUNDLE_SIMULATE_VERSION\fR): The virtual version Bundler should use for activating feature flags\. Can be used to simulate all the new functionality that will be enabled in a future major version\. +.IP "\(bu" 4 +\fBssl_ca_cert\fR (\fBBUNDLE_SSL_CA_CERT\fR): Path to a designated CA certificate file or folder containing multiple certificates for trusted CAs in PEM format\. +.IP "\(bu" 4 +\fBssl_client_cert\fR (\fBBUNDLE_SSL_CLIENT_CERT\fR): Path to a designated file containing a X\.509 client certificate and key in PEM format\. +.IP "\(bu" 4 +\fBssl_verify_mode\fR (\fBBUNDLE_SSL_VERIFY_MODE\fR): The SSL verification mode Bundler uses when making HTTPS requests\. Defaults to verify peer\. +.IP "\(bu" 4 +\fBsystem_bindir\fR (\fBBUNDLE_SYSTEM_BINDIR\fR): The location where RubyGems installs binstubs\. Defaults to \fBGem\.bindir\fR\. +.IP "\(bu" 4 +\fBtimeout\fR (\fBBUNDLE_TIMEOUT\fR): The seconds allowed before timing out for network requests\. Defaults to \fB10\fR\. +.IP "\(bu" 4 +\fBupdate_requires_all_flag\fR (\fBBUNDLE_UPDATE_REQUIRES_ALL_FLAG\fR): Require passing \fB\-\-all\fR to \fBbundle update\fR when everything should be updated, and disallow passing no options to \fBbundle update\fR\. +.IP "\(bu" 4 +\fBuser_agent\fR (\fBBUNDLE_USER_AGENT\fR): The custom user agent fragment Bundler includes in API requests\. +.IP "\(bu" 4 +\fBverbose\fR (\fBBUNDLE_VERBOSE\fR): Whether Bundler should print verbose output\. Defaults to \fBfalse\fR, unless the \fB\-\-verbose\fR CLI flag is used\. +.IP "\(bu" 4 +\fBversion\fR (\fBBUNDLE_VERSION\fR): The version of Bundler to use when running under Bundler environment\. Defaults to \fBlockfile\fR\. You can also specify \fBsystem\fR or \fBx\.y\.z\fR\. \fBlockfile\fR will use the Bundler version specified in the \fBGemfile\.lock\fR, \fBsystem\fR will use the system version of Bundler, and \fBx\.y\.z\fR will use the specified version of Bundler\. +.IP "\(bu" 4 +\fBwith\fR (\fBBUNDLE_WITH\fR): A space\-separated or \fB:\fR\-separated list of groups whose gems bundler should install\. +.IP "\(bu" 4 +\fBwithout\fR (\fBBUNDLE_WITHOUT\fR): A space\-separated or \fB:\fR\-separated list of groups whose gems bundler should not install\. +.IP "" 0 +.SH "BUILD OPTIONS" +You can use \fBbundle config\fR to give Bundler the flags to pass to the gem installer every time bundler tries to install a particular gem\. +.P +A very common example, the \fBmysql\fR gem, requires Snow Leopard users to pass configuration flags to \fBgem install\fR to specify where to find the \fBmysql_config\fR executable\. +.IP "" 4 +.nf +gem install mysql \-\- \-\-with\-mysql\-config=/usr/local/mysql/bin/mysql_config +.fi +.IP "" 0 +.P +Since the specific location of that executable can change from machine to machine, you can specify these flags on a per\-machine basis\. +.IP "" 4 +.nf +bundle config set \-\-global build\.mysql \-\-with\-mysql\-config=/usr/local/mysql/bin/mysql_config +.fi +.IP "" 0 +.P +After running this command, every time bundler needs to install the \fBmysql\fR gem, it will pass along the flags you specified\. +.SH "LOCAL GIT REPOS" +Bundler also allows you to work against a git repository locally instead of using the remote version\. This can be achieved by setting up a local override: +.IP "" 4 +.nf +bundle config set \-\-local local\.GEM_NAME /path/to/local/git/repository +.fi +.IP "" 0 +.P +Important: This feature only works for gems that are specified with a git source in your Gemfile\. It does not work for gems installed from RubyGems or other sources\. The gem must be defined with \fBgit:\fR option pointing to a remote repository\. +.P +For example, if your Gemfile contains: +.IP "" 4 +.nf +gem "rack", git: "https://github\.com/rack/rack\.git", branch: "main" +.fi +.IP "" 0 +.P +Then you can use a local Rack repository by running: +.IP "" 4 +.nf +bundle config set \-\-local local\.rack ~/Work/git/rack +.fi +.IP "" 0 +.P +Now instead of checking out the remote git repository, the local override will be used\. Similar to a path source, every time the local git repository change, changes will be automatically picked up by Bundler\. This means a commit in the local git repo will update the revision in the \fBGemfile\.lock\fR to the local git repo revision\. This requires the same attention as git submodules\. Before pushing to the remote, you need to ensure the local override was pushed, otherwise you may point to a commit that only exists in your local machine\. You'll also need to CGI escape your usernames and passwords as well\. +.P +Bundler does many checks to ensure a developer won't work with invalid references\. Particularly, we force a developer to specify a branch in the \fBGemfile\fR in order to use this feature\. If the branch specified in the \fBGemfile\fR and the current branch in the local git repository do not match, Bundler will abort\. This ensures that a developer is always working against the correct branches, and prevents accidental locking to a different branch\. +.P +Finally, Bundler also ensures that the current revision in the \fBGemfile\.lock\fR exists in the local git repository\. By doing this, Bundler forces you to fetch the latest changes in the remotes\. +.P +If you need to temporarily use a local version of a gem that is normally installed from RubyGems (not from git), use a path source instead: +.IP "" 4 +.nf +gem "rack", path: "~/Work/git/rack" +.fi +.IP "" 0 +.SH "MIRRORS OF GEM SOURCES" +Bundler supports overriding gem sources with mirrors\. This allows you to configure rubygems\.org as the gem source in your Gemfile while still using your mirror to fetch gems\. +.IP "" 4 +.nf +bundle config set \-\-global mirror\.SOURCE_URL MIRROR_URL +.fi +.IP "" 0 +.P +For example, to use a mirror of https://rubygems\.org hosted at https://example\.org: +.IP "" 4 +.nf +bundle config set \-\-global mirror\.https://rubygems\.org https://example\.org +.fi +.IP "" 0 +.P +Each mirror also provides a fallback timeout setting\. If the mirror does not respond within the fallback timeout, Bundler will try to use the original server instead of the mirror\. +.IP "" 4 +.nf +bundle config set \-\-global mirror\.SOURCE_URL\.fallback_timeout TIMEOUT +.fi +.IP "" 0 +.P +For example, to fall back to rubygems\.org after 3 seconds: +.IP "" 4 +.nf +bundle config set \-\-global mirror\.https://rubygems\.org\.fallback_timeout 3 +.fi +.IP "" 0 +.P +The default fallback timeout is 0\.1 seconds, but the setting can currently only accept whole seconds (for example, 1, 15, or 30)\. +.SH "CREDENTIALS FOR GEM SOURCES" +Bundler allows you to configure credentials for any gem source, which allows you to avoid putting secrets into your Gemfile\. +.IP "" 4 +.nf +bundle config set \-\-global SOURCE_HOSTNAME USERNAME:PASSWORD +.fi +.IP "" 0 +.P +For example, to save the credentials of user \fBclaudette\fR for the gem source at \fBgems\.longerous\.com\fR, you would run: +.IP "" 4 +.nf +bundle config set \-\-global gems\.longerous\.com claudette:s00pers3krit +.fi +.IP "" 0 +.P +Or you can set the credentials as an environment variable like this: +.IP "" 4 +.nf +export BUNDLE_GEMS__LONGEROUS__COM="claudette:s00pers3krit" +.fi +.IP "" 0 +.P +For gems with a git source with HTTP(S) URL you can specify credentials like so: +.IP "" 4 +.nf +bundle config set \-\-global https://github\.com/ruby/rubygems\.git username:password +.fi +.IP "" 0 +.P +Or you can set the credentials as an environment variable like so: +.IP "" 4 +.nf +export BUNDLE_GITHUB__COM=username:password +.fi +.IP "" 0 +.P +This is especially useful for private repositories on hosts such as GitHub, where you can use personal OAuth tokens: +.IP "" 4 +.nf +export BUNDLE_GITHUB__COM=abcd0123generatedtoken:x\-oauth\-basic +.fi +.IP "" 0 +.P +Note that any configured credentials will be redacted by informative commands such as \fBbundle config list\fR or \fBbundle config get\fR, unless you use the \fB\-\-parseable\fR flag\. This is to avoid unintentionally leaking credentials when copy\-pasting bundler output\. +.P +Also note that to guarantee a sane mapping between valid environment variable names and valid host names, bundler makes the following transformations: +.IP "\(bu" 4 +Any \fB\-\fR characters in a host name are mapped to a triple underscore (\fB___\fR) in the corresponding environment variable\. +.IP "\(bu" 4 +Any \fB\.\fR characters in a host name are mapped to a double underscore (\fB__\fR) in the corresponding environment variable\. +.IP "" 0 +.P +This means that if you have a gem server named \fBmy\.gem\-host\.com\fR, you'll need to use the \fBBUNDLE_MY__GEM___HOST__COM\fR variable to configure credentials for it through ENV\. +.SH "CONFIGURE BUNDLER DIRECTORIES" +Bundler's home, cache and plugin directories and config file can be configured through environment variables\. The default location for Bundler's home directory is \fB~/\.bundle\fR, which all directories inherit from by default\. The following outlines the available environment variables and their default values +.IP "" 4 +.nf +BUNDLE_USER_HOME : $HOME/\.bundle +BUNDLE_USER_CACHE : $BUNDLE_USER_HOME/cache +BUNDLE_USER_CONFIG : $BUNDLE_USER_HOME/config +BUNDLE_USER_PLUGIN : $BUNDLE_USER_HOME/plugin +.fi +.IP "" 0 + diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn new file mode 100644 index 0000000000..72f891b428 --- /dev/null +++ b/lib/bundler/man/bundle-config.1.ronn @@ -0,0 +1,463 @@ +bundle-config(1) -- Set bundler configuration options +===================================================== + +## SYNOPSIS + +`bundle config` [list]<br> +`bundle config` [get [--local|--global]] NAME<br> +`bundle config` [set [--local|--global]] NAME VALUE<br> +`bundle config` unset [--local|--global] NAME + +## DESCRIPTION + +This command allows you to interact with Bundler's configuration system. + +Bundler loads configuration settings in this order: + +1. Local config (`<project_root>/.bundle/config` or `$BUNDLE_APP_CONFIG/config`) +2. Environmental variables (`ENV`) +3. Global config (`~/.bundle/config`) +4. Bundler default config + +Executing `bundle` with the `BUNDLE_IGNORE_CONFIG` environment variable set will +cause it to ignore all configuration. + +## SUB-COMMANDS + +### list (default command) + +Executing `bundle config list` will print a list of all bundler +configuration for the current bundle, and where that configuration +was set. + +### get + +Executing `bundle config get <name>` will print the value of that configuration +setting, and all locations where it was set. + +**OPTIONS** + +* `--local`: + Get configuration from configuration file for the local application, namely, + `<project_root>/.bundle/config`, or `$BUNDLE_APP_CONFIG/config` if + `BUNDLE_APP_CONFIG` is set. + +* `--global`: + Get configuration from configuration file global to all bundles executed as + the current user, namely, from `~/.bundle/config`. + +### set + +Executing `bundle config set <name> <value>` defaults to setting `local` +configuration if executing from within a local application, otherwise it will +set `global` configuration. + +**OPTIONS** + +* `--local`: + Executing `bundle config set --local <name> <value>` will set that configuration + in the directory for the local application. The configuration will be stored in + `<project_root>/.bundle/config`. If `BUNDLE_APP_CONFIG` is set, the configuration + will be stored in `$BUNDLE_APP_CONFIG/config`. + +* `--global`: + Executing `bundle config set --global <name> <value>` will set that + configuration to the value specified for all bundles executed as the current + user. The configuration will be stored in `~/.bundle/config`. If <name> already + is set, <name> will be overridden and user will be warned. + +### unset + +Executing `bundle config unset <name>` will delete the configuration in both +local and global sources. + +**OPTIONS** + +* `--local`: + Executing `bundle config unset --local <name>` will delete the configuration + only from the local application. + +* `--global`: + Executing `bundle config unset --global <name>` will delete the configuration + only from the user configuration. + +## CONFIGURATION KEYS + +Configuration keys in bundler have two forms: the canonical form and the +environment variable form. + +For instance, passing the `--without` flag to [bundle install(1)](bundle-install.1.html) +prevents Bundler from installing certain groups specified in the Gemfile(5). Bundler +persists this value in `app/.bundle/config` so that calls to `Bundler.setup` +do not try to find gems from the `Gemfile` that you didn't install. Additionally, +subsequent calls to [bundle install(1)](bundle-install.1.html) remember this setting +and skip those groups. + +The canonical form of this configuration is `"without"`. To convert the canonical +form to the environment variable form, capitalize it, and prepend `BUNDLE_`. The +environment variable form of `"without"` is `BUNDLE_WITHOUT`. + +Any periods in the configuration keys must be replaced with two underscores when +setting it via environment variables. The configuration key `local.rack` becomes +the environment variable `BUNDLE_LOCAL__RACK`. + +## LIST OF AVAILABLE KEYS + +The following is a list of all configuration keys and their purpose. You can +learn more about their operation in [bundle install(1)](bundle-install.1.html). + +* `api_request_size` (`BUNDLE_API_REQUEST_SIZE`): + Configure how many dependencies to fetch when resolving the specifications. + This configuration is only used when fetching specifications from RubyGems + servers that didn't implement the Compact Index API. + Defaults to 100. +* `auto_install` (`BUNDLE_AUTO_INSTALL`): + Automatically run `bundle install` when gems are missing. +* `bin` (`BUNDLE_BIN`): + If configured, `bundle binstubs` will install executables from gems in the + bundle to the specified directory. Otherwise it will create them in a `bin` + directory relative to the Gemfile directory. These executables run in + Bundler's context. If used, you might add this directory to your + environment's `PATH` variable. For instance, if the `rails` gem comes with a + `rails` executable, `bundle binstubs` will create a `bin/rails` executable + that ensures that all referred dependencies will be resolved using the + bundled gems. +* `cache_all` (`BUNDLE_CACHE_ALL`): + Cache all gems, including path and git gems. This needs to be explicitly + before bundler 4, but will be the default on bundler 4. +* `cache_all_platforms` (`BUNDLE_CACHE_ALL_PLATFORMS`): + Cache gems for all platforms. +* `cache_path` (`BUNDLE_CACHE_PATH`): + The directory that bundler will place cached gems in when running + <code>bundle package</code>, and that bundler will look in when installing gems. + Defaults to `vendor/cache`. +* `clean` (`BUNDLE_CLEAN`): + Whether Bundler should run `bundle clean` automatically after + `bundle install`. Defaults to `true` in Bundler 4, as long as `path` is not + explicitly configured. +* `console` (`BUNDLE_CONSOLE`): + The console that `bundle console` starts. Defaults to `irb`. +* `cooldown` (`BUNDLE_COOLDOWN`): + Number of days a published gem version must age before bundler will + resolve to it. Defaults to unset (no cooldown). Pass `0` to disable + cooldown for an individual run. + + The effective cooldown for any given gem is resolved from three + layers, highest precedence first: + + 1. CLI flag `--cooldown N` on `install`, `update`, `add`, and + `outdated`. + 2. This setting (`bundle config set cooldown N` or + `BUNDLE_COOLDOWN=N`). + 3. The per-source `cooldown:` keyword in the Gemfile, such as + `source "https://rubygems.org", cooldown: 7`. + + The CLI flag and this setting apply uniformly to every source, + including ones declared with their own `cooldown:` value. To keep a + private registry permanently exempt while still cooling down public + gems, declare `source "https://internal", cooldown: 0` in the + Gemfile; remember that `--cooldown N` on the command line will + still override it for that single run. + + Cooldown filtering depends on the gem server providing a per-version + `created_at` timestamp in the v2 compact-index format. Versions + without that metadata - older gem servers, historical entries that + predate the v2 cutover on `rubygems.org`, or private registries that + still emit the v1 format - are treated as outside the cooldown + window and remain resolvable. If you rely on cooldown for + supply-chain protection, confirm that the gem server emits + `created_at` in its `/info/<gem>` responses. +* `default_cli_command` (`BUNDLE_DEFAULT_CLI_COMMAND`): + The command that running `bundle` without arguments should run. Defaults to + `cli_help` since Bundler 4, but can also be `install` which was the previous + default. +* `deployment` (`BUNDLE_DEPLOYMENT`): + Equivalent to setting `frozen` to `true` and `path` to `vendor/bundle`. +* `disable_checksum_validation` (`BUNDLE_DISABLE_CHECKSUM_VALIDATION`): + Allow installing gems even if they do not match the checksum provided by + RubyGems. +* `disable_exec_load` (`BUNDLE_DISABLE_EXEC_LOAD`): + Stop Bundler from using `load` to launch an executable in-process in + `bundle exec`. +* `disable_local_branch_check` (`BUNDLE_DISABLE_LOCAL_BRANCH_CHECK`): + Allow Bundler to use a local git override without a branch specified in the + Gemfile. +* `disable_local_revision_check` (`BUNDLE_DISABLE_LOCAL_REVISION_CHECK`): + Allow Bundler to use a local git override without checking if the revision + present in the lockfile is present in the repository. +* `disable_shared_gems` (`BUNDLE_DISABLE_SHARED_GEMS`): + Stop Bundler from accessing gems installed to RubyGems' normal location. +* `disable_version_check` (`BUNDLE_DISABLE_VERSION_CHECK`): + Stop Bundler from checking if a newer Bundler version is available on + rubygems.org. +* `force_ruby_platform` (`BUNDLE_FORCE_RUBY_PLATFORM`): + Ignore the current machine's platform and install only `ruby` platform gems. + As a result, gems with native extensions will be compiled from source. +* `frozen` (`BUNDLE_FROZEN`): + Disallow any automatic changes to `Gemfile.lock`. Bundler commands will + be blocked unless the lockfile can be installed exactly as written. + Usually this will happen when changing the `Gemfile` manually and forgetting + to update the lockfile through `bundle lock` or `bundle install`. +* `gem.github_username` (`BUNDLE_GEM__GITHUB_USERNAME`): + Sets a GitHub username or organization to be used in the `README` and `.gemspec` files + when you create a new gem via `bundle gem` command. It can be overridden by passing an + explicit `--github-username` flag to `bundle gem`. +* `gem.push_key` (`BUNDLE_GEM__PUSH_KEY`): + Sets the `--key` parameter for `gem push` when using the `rake release` + command with a private gemstash server. +* `gemfile` (`BUNDLE_GEMFILE`): + The name of the file that bundler should use as the `Gemfile`. This location + of this file also sets the root of the project, which is used to resolve + relative paths in the `Gemfile`, among other things. By default, bundler + will search up from the current working directory until it finds a + `Gemfile`. +* `global_gem_cache` (`BUNDLE_GLOBAL_GEM_CACHE`): + Whether Bundler should cache all gems and compiled extensions globally, + rather than locally to the configured installation path. +* `ignore_funding_requests` (`BUNDLE_IGNORE_FUNDING_REQUESTS`): + When set, no funding requests will be printed. +* `ignore_messages` (`BUNDLE_IGNORE_MESSAGES`): + When set, no post install messages will be printed. To silence a single gem, + use dot notation like `ignore_messages.httparty true`. +* `init_gems_rb` (`BUNDLE_INIT_GEMS_RB`): + Generate a `gems.rb` instead of a `Gemfile` when running `bundle init`. +* `jobs` (`BUNDLE_JOBS`): + The number of gems Bundler can download and install in parallel. + Defaults to the number of available processors. +* `lockfile` (`BUNDLE_LOCKFILE`): + The path to the lockfile that bundler should use. By default, Bundler adds + `.lock` to the end of the `gemfile` entry. Can be set to `false` in the + Gemfile to disable lockfile creation entirely (see gemfile(5)). +* `lockfile_checksums` (`BUNDLE_LOCKFILE_CHECKSUMS`): + Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources. Defaults to true. +* `no_build_extension` (`BUNDLE_NO_BUILD_EXTENSION`): + Whether Bundler should skip building native extensions during installation. + When set, gems are installed without compiling their C extensions. + To build extensions later, unset this setting and run `bundle pristine <gem>`. +* `no_install` (`BUNDLE_NO_INSTALL`): + Whether `bundle package` should skip installing gems. +* `no_install_plugin` (`BUNDLE_NO_INSTALL_PLUGIN`): + Whether Bundler should skip installing RubyGems plugins during installation. + When set, plugin files are not written to the plugins directory. + To install plugins later, unset this setting and run `bundle pristine <gem>`. +* `no_prune` (`BUNDLE_NO_PRUNE`): + Whether Bundler should leave outdated gems unpruned when caching. +* `only` (`BUNDLE_ONLY`): + A space-separated list of groups to install only gems of the specified groups. + Please check carefully if you want to install also gems without a group, because + they get put inside `default` group. For example `only test:default` will install + all gems specified in test group and without one. +* `path` (`BUNDLE_PATH`): + The location on disk where all gems in your bundle will be located regardless + of `$GEM_HOME` or `$GEM_PATH` values. Bundle gems not found in this location + will be installed by `bundle install`. When not set, Bundler install by + default to a `.bundle` directory relative to repository root in Bundler 4, + and to the default system path (`Gem.dir`) before Bundler 4. That means that + before Bundler 4, Bundler shares this location with Rubygems, and `gem + install ...` will have gems installed in the same location and therefore, + gems installed without `path` set will show up by calling `gem list`. This + will not be the case in Bundler 4. +* `path.system` (`BUNDLE_PATH__SYSTEM`): + Whether Bundler will install gems into the default system path (`Gem.dir`). +* `plugins` (`BUNDLE_PLUGINS`): + Enable Bundler's experimental plugin system. +* `prefer_patch` (`BUNDLE_PREFER_PATCH`): + Prefer updating only to next patch version during updates. Makes `bundle update` calls equivalent to `bundler update --patch`. +* `redirect` (`BUNDLE_REDIRECT`): + The number of redirects allowed for network requests. Defaults to `5`. +* `retry` (`BUNDLE_RETRY`): + The number of times to retry failed network requests. Defaults to `3`. +* `shebang` (`BUNDLE_SHEBANG`): + The program name that should be invoked for generated binstubs. Defaults to + the ruby install name used to generate the binstub. +* `silence_deprecations` (`BUNDLE_SILENCE_DEPRECATIONS`): + Whether Bundler should silence deprecation warnings for behavior that will + be changed in the next major version. +* `silence_root_warning` (`BUNDLE_SILENCE_ROOT_WARNING`): + Silence the warning Bundler prints when installing gems as root. +* `simulate_version` (`BUNDLE_SIMULATE_VERSION`): + The virtual version Bundler should use for activating feature flags. Can be + used to simulate all the new functionality that will be enabled in a future + major version. +* `ssl_ca_cert` (`BUNDLE_SSL_CA_CERT`): + Path to a designated CA certificate file or folder containing multiple + certificates for trusted CAs in PEM format. +* `ssl_client_cert` (`BUNDLE_SSL_CLIENT_CERT`): + Path to a designated file containing a X.509 client certificate + and key in PEM format. +* `ssl_verify_mode` (`BUNDLE_SSL_VERIFY_MODE`): + The SSL verification mode Bundler uses when making HTTPS requests. + Defaults to verify peer. +* `system_bindir` (`BUNDLE_SYSTEM_BINDIR`): + The location where RubyGems installs binstubs. Defaults to `Gem.bindir`. +* `timeout` (`BUNDLE_TIMEOUT`): + The seconds allowed before timing out for network requests. Defaults to `10`. +* `update_requires_all_flag` (`BUNDLE_UPDATE_REQUIRES_ALL_FLAG`): + Require passing `--all` to `bundle update` when everything should be updated, + and disallow passing no options to `bundle update`. +* `user_agent` (`BUNDLE_USER_AGENT`): + The custom user agent fragment Bundler includes in API requests. +* `verbose` (`BUNDLE_VERBOSE`): + Whether Bundler should print verbose output. Defaults to `false`, unless the + `--verbose` CLI flag is used. +* `version` (`BUNDLE_VERSION`): + The version of Bundler to use when running under Bundler environment. + Defaults to `lockfile`. You can also specify `system` or `x.y.z`. + `lockfile` will use the Bundler version specified in the `Gemfile.lock`, + `system` will use the system version of Bundler, and `x.y.z` will use + the specified version of Bundler. +* `with` (`BUNDLE_WITH`): + A space-separated or `:`-separated list of groups whose gems bundler should install. +* `without` (`BUNDLE_WITHOUT`): + A space-separated or `:`-separated list of groups whose gems bundler should not install. + +## BUILD OPTIONS + +You can use `bundle config` to give Bundler the flags to pass to the gem +installer every time bundler tries to install a particular gem. + +A very common example, the `mysql` gem, requires Snow Leopard users to +pass configuration flags to `gem install` to specify where to find the +`mysql_config` executable. + + gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config + +Since the specific location of that executable can change from machine +to machine, you can specify these flags on a per-machine basis. + + bundle config set --global build.mysql --with-mysql-config=/usr/local/mysql/bin/mysql_config + +After running this command, every time bundler needs to install the +`mysql` gem, it will pass along the flags you specified. + +## LOCAL GIT REPOS + +Bundler also allows you to work against a git repository locally +instead of using the remote version. This can be achieved by setting +up a local override: + + bundle config set --local local.GEM_NAME /path/to/local/git/repository + +Important: This feature only works for gems that are specified with a git +source in your Gemfile. It does not work for gems installed from RubyGems +or other sources. The gem must be defined with `git:` option pointing to a +remote repository. + +For example, if your Gemfile contains: + + gem "rack", git: "https://github.com/rack/rack.git", branch: "main" + +Then you can use a local Rack repository by running: + + bundle config set --local local.rack ~/Work/git/rack + +Now instead of checking out the remote git repository, the local +override will be used. Similar to a path source, every time the local +git repository change, changes will be automatically picked up by +Bundler. This means a commit in the local git repo will update the +revision in the `Gemfile.lock` to the local git repo revision. This +requires the same attention as git submodules. Before pushing to +the remote, you need to ensure the local override was pushed, otherwise +you may point to a commit that only exists in your local machine. +You'll also need to CGI escape your usernames and passwords as well. + +Bundler does many checks to ensure a developer won't work with +invalid references. Particularly, we force a developer to specify +a branch in the `Gemfile` in order to use this feature. If the branch +specified in the `Gemfile` and the current branch in the local git +repository do not match, Bundler will abort. This ensures that +a developer is always working against the correct branches, and prevents +accidental locking to a different branch. + +Finally, Bundler also ensures that the current revision in the +`Gemfile.lock` exists in the local git repository. By doing this, Bundler +forces you to fetch the latest changes in the remotes. + +If you need to temporarily use a local version of a gem that is normally +installed from RubyGems (not from git), use a path source instead: + + gem "rack", path: "~/Work/git/rack" + +## MIRRORS OF GEM SOURCES + +Bundler supports overriding gem sources with mirrors. This allows you to +configure rubygems.org as the gem source in your Gemfile while still using your +mirror to fetch gems. + + bundle config set --global mirror.SOURCE_URL MIRROR_URL + +For example, to use a mirror of https://rubygems.org hosted at https://example.org: + + bundle config set --global mirror.https://rubygems.org https://example.org + +Each mirror also provides a fallback timeout setting. If the mirror does not +respond within the fallback timeout, Bundler will try to use the original +server instead of the mirror. + + bundle config set --global mirror.SOURCE_URL.fallback_timeout TIMEOUT + +For example, to fall back to rubygems.org after 3 seconds: + + bundle config set --global mirror.https://rubygems.org.fallback_timeout 3 + +The default fallback timeout is 0.1 seconds, but the setting can currently +only accept whole seconds (for example, 1, 15, or 30). + +## CREDENTIALS FOR GEM SOURCES + +Bundler allows you to configure credentials for any gem source, which allows +you to avoid putting secrets into your Gemfile. + + bundle config set --global SOURCE_HOSTNAME USERNAME:PASSWORD + +For example, to save the credentials of user `claudette` for the gem source at +`gems.longerous.com`, you would run: + + bundle config set --global gems.longerous.com claudette:s00pers3krit + +Or you can set the credentials as an environment variable like this: + + export BUNDLE_GEMS__LONGEROUS__COM="claudette:s00pers3krit" + +For gems with a git source with HTTP(S) URL you can specify credentials like so: + + bundle config set --global https://github.com/ruby/rubygems.git username:password + +Or you can set the credentials as an environment variable like so: + + export BUNDLE_GITHUB__COM=username:password + +This is especially useful for private repositories on hosts such as GitHub, +where you can use personal OAuth tokens: + + export BUNDLE_GITHUB__COM=abcd0123generatedtoken:x-oauth-basic + +Note that any configured credentials will be redacted by informative commands +such as `bundle config list` or `bundle config get`, unless you use the +`--parseable` flag. This is to avoid unintentionally leaking credentials when +copy-pasting bundler output. + +Also note that to guarantee a sane mapping between valid environment variable +names and valid host names, bundler makes the following transformations: + +* Any `-` characters in a host name are mapped to a triple underscore (`___`) in the + corresponding environment variable. + +* Any `.` characters in a host name are mapped to a double underscore (`__`) in the + corresponding environment variable. + +This means that if you have a gem server named `my.gem-host.com`, you'll need to +use the `BUNDLE_MY__GEM___HOST__COM` variable to configure credentials for it +through ENV. + +## CONFIGURE BUNDLER DIRECTORIES + +Bundler's home, cache and plugin directories and config file can be configured +through environment variables. The default location for Bundler's home directory is +`~/.bundle`, which all directories inherit from by default. The following +outlines the available environment variables and their default values + + BUNDLE_USER_HOME : $HOME/.bundle + BUNDLE_USER_CACHE : $BUNDLE_USER_HOME/cache + BUNDLE_USER_CONFIG : $BUNDLE_USER_HOME/config + BUNDLE_USER_PLUGIN : $BUNDLE_USER_HOME/plugin diff --git a/lib/bundler/man/bundle-console.1 b/lib/bundler/man/bundle-console.1 new file mode 100644 index 0000000000..5d3f65365f --- /dev/null +++ b/lib/bundler/man/bundle-console.1 @@ -0,0 +1,33 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-CONSOLE" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-console\fR \- Open an IRB session with the bundle pre\-loaded +.SH "SYNOPSIS" +\fBbundle console\fR [GROUP] +.SH "DESCRIPTION" +Starts an interactive Ruby console session in the context of the current bundle\. +.P +If no \fBGROUP\fR is specified, all gems in the \fBdefault\fR group in the Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR are preliminarily loaded\. +.P +If \fBGROUP\fR is specified, all gems in the given group in the Gemfile in addition to the gems in \fBdefault\fR group are loaded\. Even if the given group does not exist in the Gemfile, IRB console starts without any warning or error\. +.P +The environment variable \fBBUNDLE_CONSOLE\fR or \fBbundle config set console\fR can be used to change the shell from the following: +.IP "\(bu" 4 +\fBirb\fR (default) +.IP "\(bu" 4 +\fBpry\fR (https://github\.com/pry/pry) +.IP "\(bu" 4 +\fBripl\fR (https://github\.com/cldwalker/ripl) +.IP "" 0 +.P +\fBbundle console\fR uses irb by default\. An alternative Pry or Ripl can be used with \fBbundle console\fR by adjusting the \fBconsole\fR Bundler setting\. Also make sure that \fBpry\fR or \fBripl\fR is in your Gemfile\. +.SH "EXAMPLE" +.nf +$ bundle config set console pry +$ bundle console +Resolving dependencies\|\.\|\.\|\. +[1] pry(main)> +.fi +.SH "SEE ALSO" +Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR diff --git a/lib/bundler/man/bundle-console.1.ronn b/lib/bundler/man/bundle-console.1.ronn new file mode 100644 index 0000000000..ed842ae1c3 --- /dev/null +++ b/lib/bundler/man/bundle-console.1.ronn @@ -0,0 +1,39 @@ +bundle-console(1) -- Open an IRB session with the bundle pre-loaded +=================================================================== + +## SYNOPSIS + +`bundle console` [GROUP] + +## DESCRIPTION + +Starts an interactive Ruby console session in the context of the current bundle. + +If no `GROUP` is specified, all gems in the `default` group in the [Gemfile(5)](https://bundler.io/man/gemfile.5.html) are +preliminarily loaded. + +If `GROUP` is specified, all gems in the given group in the Gemfile in addition +to the gems in `default` group are loaded. Even if the given group does not +exist in the Gemfile, IRB console starts without any warning or error. + +The environment variable `BUNDLE_CONSOLE` or `bundle config set console` can be used to change +the shell from the following: + +* `irb` (default) +* `pry` (https://github.com/pry/pry) +* `ripl` (https://github.com/cldwalker/ripl) + +`bundle console` uses irb by default. An alternative Pry or Ripl can be used with +`bundle console` by adjusting the `console` Bundler setting. Also make sure that +`pry` or `ripl` is in your Gemfile. + +## EXAMPLE + + $ bundle config set console pry + $ bundle console + Resolving dependencies... + [1] pry(main)> + +## SEE ALSO + +[Gemfile(5)](https://bundler.io/man/gemfile.5.html) diff --git a/lib/bundler/man/bundle-doctor.1 b/lib/bundler/man/bundle-doctor.1 new file mode 100644 index 0000000000..4c59871b66 --- /dev/null +++ b/lib/bundler/man/bundle-doctor.1 @@ -0,0 +1,69 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-DOCTOR" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-doctor\fR \- Checks the bundle for common problems +.SH "SYNOPSIS" +\fBbundle doctor [diagnose]\fR [\-\-quiet] [\-\-gemfile=GEMFILE] [\-\-ssl] +.br +\fBbundle doctor ssl\fR [\-\-host=HOST] [\-\-tls\-version=TLS\-VERSION] [\-\-verify\-mode=VERIFY\-MODE] +.br +\fBbundle doctor\fR help [COMMAND] +.SH "DESCRIPTION" +You can diagnose common Bundler problems with this command such as checking gem environment or SSL/TLS issue\. +.SH "SUB\-COMMANDS" +.SS "diagnose (default command)" +Checks your Gemfile and gem environment for common problems\. If issues are detected, Bundler prints them and exits status 1\. Otherwise, Bundler prints a success message and exits status 0\. +.P +Examples of common problems caught include: +.IP "\(bu" 4 +Invalid Bundler settings +.IP "\(bu" 4 +Mismatched Ruby versions +.IP "\(bu" 4 +Mismatched platforms +.IP "\(bu" 4 +Uninstalled gems +.IP "\(bu" 4 +Missing dependencies +.IP "" 0 +.P +\fBOPTIONS\fR +.TP +\fB\-\-quiet\fR +Only output warnings and errors\. +.TP +\fB\-\-gemfile=GEMFILE\fR +The location of the Gemfile(5) which Bundler should use\. This defaults to a Gemfile(5) in the current working directory\. In general, Bundler will assume that the location of the Gemfile(5) is also the project's root and will try to find \fBGemfile\.lock\fR and \fBvendor/cache\fR relative to this location\. +.TP +\fB\-\-ssl\fR +Diagnose common SSL problems when connecting to https://rubygems\.org\. +.IP +This flag runs the \fBbundle doctor ssl\fR subcommand with default values underneath\. +.SS "ssl" +If you've experienced issues related to SSL certificates and/or TLS versions while connecting to https://rubygems\.org, this command can help troubleshoot common problems\. The diagnostic will perform a few checks such as: +.IP "\(bu" 4 +Verify the Ruby OpenSSL version installed on your system\. +.IP "\(bu" 4 +Check the OpenSSL library version used for compilation\. +.IP "\(bu" 4 +Ensure CA certificates are correctly setup on your machine\. +.IP "\(bu" 4 +Open a TLS connection and verify the outcome\. +.IP "" 0 +.P +\fBOPTIONS\fR +.TP +\fB\-\-host=HOST\fR +Perform the diagnostic on HOST\. Defaults to \fBrubygems\.org\fR\. +.TP +\fB\-\-tls\-version=TLS\-VERSION\fR +Specify the TLS version when opening the connection to HOST\. +.IP +Accepted values are: \fB1\.1\fR or \fB1\.2\fR\. +.TP +\fB\-\-verify\-mode=VERIFY\-MODE\fR +Specify the TLS verify mode when opening the connection to HOST\. Defaults to \fBSSL_VERIFY_PEER\fR\. +.IP +Accepted values are: \fBCLIENT_ONCE\fR, \fBFAIL_IF_NO_PEER_CERT\fR, \fBNONE\fR, \fBPEER\fR\. + diff --git a/lib/bundler/man/bundle-doctor.1.ronn b/lib/bundler/man/bundle-doctor.1.ronn new file mode 100644 index 0000000000..7495099ff5 --- /dev/null +++ b/lib/bundler/man/bundle-doctor.1.ronn @@ -0,0 +1,77 @@ +bundle-doctor(1) -- Checks the bundle for common problems +========================================================= + +## SYNOPSIS + +`bundle doctor [diagnose]` [--quiet] + [--gemfile=GEMFILE] + [--ssl]<br> +`bundle doctor ssl` [--host=HOST] + [--tls-version=TLS-VERSION] + [--verify-mode=VERIFY-MODE]<br> +`bundle doctor` help [COMMAND] + +## DESCRIPTION + +You can diagnose common Bundler problems with this command such as checking gem environment or SSL/TLS issue. + +## SUB-COMMANDS + +### diagnose (default command) + +Checks your Gemfile and gem environment for common problems. If issues +are detected, Bundler prints them and exits status 1. Otherwise, +Bundler prints a success message and exits status 0. + +Examples of common problems caught include: + +* Invalid Bundler settings +* Mismatched Ruby versions +* Mismatched platforms +* Uninstalled gems +* Missing dependencies + +**OPTIONS** + +* `--quiet`: + Only output warnings and errors. + +* `--gemfile=GEMFILE`: + The location of the Gemfile(5) which Bundler should use. This defaults + to a Gemfile(5) in the current working directory. In general, Bundler + will assume that the location of the Gemfile(5) is also the project's + root and will try to find `Gemfile.lock` and `vendor/cache` relative + to this location. + +* `--ssl`: + Diagnose common SSL problems when connecting to https://rubygems.org. + + This flag runs the `bundle doctor ssl` subcommand with default values + underneath. + +### ssl + +If you've experienced issues related to SSL certificates and/or TLS versions while connecting +to https://rubygems.org, this command can help troubleshoot common problems. +The diagnostic will perform a few checks such as: + +* Verify the Ruby OpenSSL version installed on your system. +* Check the OpenSSL library version used for compilation. +* Ensure CA certificates are correctly setup on your machine. +* Open a TLS connection and verify the outcome. + +**OPTIONS** + +* `--host=HOST`: + Perform the diagnostic on HOST. Defaults to `rubygems.org`. + +* `--tls-version=TLS-VERSION`: + Specify the TLS version when opening the connection to HOST. + + Accepted values are: `1.1` or `1.2`. + +* `--verify-mode=VERIFY-MODE`: + Specify the TLS verify mode when opening the connection to HOST. + Defaults to `SSL_VERIFY_PEER`. + + Accepted values are: `CLIENT_ONCE`, `FAIL_IF_NO_PEER_CERT`, `NONE`, `PEER`. diff --git a/lib/bundler/man/bundle-env.1 b/lib/bundler/man/bundle-env.1 new file mode 100644 index 0000000000..25fcb64891 --- /dev/null +++ b/lib/bundler/man/bundle-env.1 @@ -0,0 +1,9 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-ENV" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-env\fR \- Print information about the environment Bundler is running under +.SH "SYNOPSIS" +\fBbundle env\fR +.SH "DESCRIPTION" +Prints information about the environment Bundler is running under\. diff --git a/lib/bundler/man/bundle-env.1.ronn b/lib/bundler/man/bundle-env.1.ronn new file mode 100644 index 0000000000..c2df9c29c2 --- /dev/null +++ b/lib/bundler/man/bundle-env.1.ronn @@ -0,0 +1,10 @@ +bundle-env(1) -- Print information about the environment Bundler is running under +================================================================================= + +## SYNOPSIS + +`bundle env` + +## DESCRIPTION + +Prints information about the environment Bundler is running under. diff --git a/lib/bundler/man/bundle-exec.1 b/lib/bundler/man/bundle-exec.1 new file mode 100644 index 0000000000..c3a6a09d57 --- /dev/null +++ b/lib/bundler/man/bundle-exec.1 @@ -0,0 +1,104 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-EXEC" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-exec\fR \- Execute a command in the context of the bundle +.SH "SYNOPSIS" +\fBbundle exec\fR [\-\-gemfile=GEMFILE] \fIcommand\fR +.SH "DESCRIPTION" +This command executes the command, making all gems specified in the [\fBGemfile(5)\fR][Gemfile(5)] available to \fBrequire\fR in Ruby programs\. +.P +Essentially, if you would normally have run something like \fBrspec spec/my_spec\.rb\fR, and you want to use the gems specified in the [\fBGemfile(5)\fR][Gemfile(5)] and installed via bundle install(1) \fIbundle\-install\.1\.html\fR, you should run \fBbundle exec rspec spec/my_spec\.rb\fR\. +.P +Note that \fBbundle exec\fR does not require that an executable is available on your shell's \fB$PATH\fR\. +.SH "OPTIONS" +.TP +\fB\-\-gemfile=GEMFILE\fR +Use the specified gemfile instead of [\fBGemfile(5)\fR][Gemfile(5)]\. +.SH "BUNDLE INSTALL \-\-BINSTUBS" +If you use the \fB\-\-binstubs\fR flag in bundle install(1) \fIbundle\-install\.1\.html\fR, Bundler will automatically create a directory (which defaults to \fBapp_root/bin\fR) containing all of the executables available from gems in the bundle\. +.P +After using \fB\-\-binstubs\fR, \fBbin/rspec spec/my_spec\.rb\fR is identical to \fBbundle exec rspec spec/my_spec\.rb\fR\. +.SH "ENVIRONMENT MODIFICATIONS" +\fBbundle exec\fR makes a number of changes to the shell environment, then executes the command you specify in full\. +.IP "\(bu" 4 +make sure that it's still possible to shell out to \fBbundle\fR from inside a command invoked by \fBbundle exec\fR (using \fB$BUNDLE_BIN_PATH\fR) +.IP "\(bu" 4 +put the directory containing executables (like \fBrails\fR, \fBrspec\fR, \fBrackup\fR) for your bundle on \fB$PATH\fR +.IP "\(bu" 4 +make sure that if bundler is invoked in the subshell, it uses the same \fBGemfile\fR (by setting \fBBUNDLE_GEMFILE\fR) +.IP "\(bu" 4 +add \fB\-rbundler/setup\fR to \fB$RUBYOPT\fR, which makes sure that Ruby programs invoked in the subshell can see the gems in the bundle +.IP "" 0 +.P +It also modifies Rubygems: +.IP "\(bu" 4 +disallow loading additional gems not in the bundle +.IP "\(bu" 4 +modify the \fBgem\fR method to be a no\-op if a gem matching the requirements is in the bundle, and to raise a \fBGem::LoadError\fR if it's not +.IP "\(bu" 4 +Define \fBGem\.refresh\fR to be a no\-op, since the source index is always frozen when using bundler, and to prevent gems from the system leaking into the environment +.IP "\(bu" 4 +Override \fBGem\.bin_path\fR to use the gems in the bundle, making system executables work +.IP "\(bu" 4 +Add all gems in the bundle into Gem\.loaded_specs +.IP "" 0 +.P +Finally, \fBbundle exec\fR also implicitly modifies \fBGemfile\.lock\fR if the lockfile and the Gemfile do not match\. Bundler needs the Gemfile to determine things such as a gem's groups, \fBautorequire\fR, and platforms, etc\., and that information isn't stored in the lockfile\. The Gemfile and lockfile must be synced in order to \fBbundle exec\fR successfully, so \fBbundle exec\fR updates the lockfile beforehand\. +.SS "Loading" +By default, when attempting to \fBbundle exec\fR to a file with a ruby shebang, Bundler will \fBKernel\.load\fR that file instead of using \fBKernel\.exec\fR\. For the vast majority of cases, this is a performance improvement\. In a rare few cases, this could cause some subtle side\-effects (such as dependence on the exact contents of \fB$0\fR or \fB__FILE__\fR) and the optimization can be disabled by enabling the \fBdisable_exec_load\fR setting\. +.SS "Shelling out" +Any Ruby code that opens a subshell (like \fBsystem\fR, backticks, or \fB%x{}\fR) will automatically use the current Bundler environment\. If you need to shell out to a Ruby command that is not part of your current bundle, use the \fBwith_unbundled_env\fR method with a block\. Any subshells created inside the block will be given the environment present before Bundler was activated\. For example, Homebrew commands run Ruby, but don't work inside a bundle: +.IP "" 4 +.nf +Bundler\.with_unbundled_env do + `brew install wget` +end +.fi +.IP "" 0 +.P +Using \fBwith_unbundled_env\fR is also necessary if you are shelling out to a different bundle\. Any Bundler commands run in a subshell will inherit the current Gemfile, so commands that need to run in the context of a different bundle also need to use \fBwith_unbundled_env\fR\. +.IP "" 4 +.nf +Bundler\.with_unbundled_env do + Dir\.chdir "/other/bundler/project" do + `bundle exec \./script` + end +end +.fi +.IP "" 0 +.P +Bundler provides convenience helpers that wrap \fBsystem\fR and \fBexec\fR, and they can be used like this: +.IP "" 4 +.nf +Bundler\.unbundled_system('brew install wget') +Bundler\.unbundled_exec('brew install wget') +.fi +.IP "" 0 +.SH "RUBYGEMS PLUGINS" +At present, the Rubygems plugin system requires all files named \fBrubygems_plugin\.rb\fR on the load path of \fIany\fR installed gem when any Ruby code requires \fBrubygems\.rb\fR\. This includes executables installed into the system, like \fBrails\fR, \fBrackup\fR, and \fBrspec\fR\. +.P +Since Rubygems plugins can contain arbitrary Ruby code, they commonly end up activating themselves or their dependencies\. +.P +For instance, the \fBgemcutter 0\.5\fR gem depended on \fBjson_pure\fR\. If you had that version of gemcutter installed (even if you \fIalso\fR had a newer version without this problem), Rubygems would activate \fBgemcutter 0\.5\fR and \fBjson_pure <latest>\fR\. +.P +If your Gemfile(5) also contained \fBjson_pure\fR (or a gem with a dependency on \fBjson_pure\fR), the latest version on your system might conflict with the version in your Gemfile(5), or the snapshot version in your \fBGemfile\.lock\fR\. +.P +If this happens, bundler will say: +.IP "" 4 +.nf +You have already activated json_pure 1\.4\.6 but your Gemfile +requires json_pure 1\.4\.3\. Consider using bundle exec\. +.fi +.IP "" 0 +.P +In this situation, you almost certainly want to remove the underlying gem with the problematic gem plugin\. In general, the authors of these plugins (in this case, the \fBgemcutter\fR gem) have released newer versions that are more careful in their plugins\. +.P +You can find a list of all the gems containing gem plugins by running +.IP "" 4 +.nf +ruby \-e "puts Gem\.find_files('rubygems_plugin\.rb')" +.fi +.IP "" 0 +.P +At the very least, you should remove all but the newest version of each gem plugin, and also remove all gem plugins that you aren't using (\fBgem uninstall gem_name\fR)\. diff --git a/lib/bundler/man/bundle-exec.1.ronn b/lib/bundler/man/bundle-exec.1.ronn new file mode 100644 index 0000000000..e51a66a084 --- /dev/null +++ b/lib/bundler/man/bundle-exec.1.ronn @@ -0,0 +1,150 @@ +bundle-exec(1) -- Execute a command in the context of the bundle +================================================================ + +## SYNOPSIS + +`bundle exec` [--gemfile=GEMFILE] <command> + +## DESCRIPTION + +This command executes the command, making all gems specified in the +[`Gemfile(5)`][Gemfile(5)] available to `require` in Ruby programs. + +Essentially, if you would normally have run something like +`rspec spec/my_spec.rb`, and you want to use the gems specified +in the [`Gemfile(5)`][Gemfile(5)] and installed via [bundle install(1)](bundle-install.1.html), you +should run `bundle exec rspec spec/my_spec.rb`. + +Note that `bundle exec` does not require that an executable is +available on your shell's `$PATH`. + +## OPTIONS + +* `--gemfile=GEMFILE`: + Use the specified gemfile instead of [`Gemfile(5)`][Gemfile(5)]. + +## BUNDLE INSTALL --BINSTUBS + +If you use the `--binstubs` flag in [bundle install(1)](bundle-install.1.html), Bundler will +automatically create a directory (which defaults to `app_root/bin`) +containing all of the executables available from gems in the bundle. + +After using `--binstubs`, `bin/rspec spec/my_spec.rb` is identical +to `bundle exec rspec spec/my_spec.rb`. + +## ENVIRONMENT MODIFICATIONS + +`bundle exec` makes a number of changes to the shell environment, +then executes the command you specify in full. + +* make sure that it's still possible to shell out to `bundle` + from inside a command invoked by `bundle exec` (using + `$BUNDLE_BIN_PATH`) +* put the directory containing executables (like `rails`, `rspec`, + `rackup`) for your bundle on `$PATH` +* make sure that if bundler is invoked in the subshell, it uses + the same `Gemfile` (by setting `BUNDLE_GEMFILE`) +* add `-rbundler/setup` to `$RUBYOPT`, which makes sure that + Ruby programs invoked in the subshell can see the gems in + the bundle + +It also modifies Rubygems: + +* disallow loading additional gems not in the bundle +* modify the `gem` method to be a no-op if a gem matching + the requirements is in the bundle, and to raise a + `Gem::LoadError` if it's not +* Define `Gem.refresh` to be a no-op, since the source + index is always frozen when using bundler, and to + prevent gems from the system leaking into the environment +* Override `Gem.bin_path` to use the gems in the bundle, + making system executables work +* Add all gems in the bundle into Gem.loaded_specs + +Finally, `bundle exec` also implicitly modifies `Gemfile.lock` if the lockfile +and the Gemfile do not match. Bundler needs the Gemfile to determine things +such as a gem's groups, `autorequire`, and platforms, etc., and that +information isn't stored in the lockfile. The Gemfile and lockfile must be +synced in order to `bundle exec` successfully, so `bundle exec` +updates the lockfile beforehand. + +### Loading + +By default, when attempting to `bundle exec` to a file with a ruby shebang, +Bundler will `Kernel.load` that file instead of using `Kernel.exec`. For the +vast majority of cases, this is a performance improvement. In a rare few cases, +this could cause some subtle side-effects (such as dependence on the exact +contents of `$0` or `__FILE__`) and the optimization can be disabled by enabling +the `disable_exec_load` setting. + +### Shelling out + +Any Ruby code that opens a subshell (like `system`, backticks, or `%x{}`) will +automatically use the current Bundler environment. If you need to shell out to +a Ruby command that is not part of your current bundle, use the +`with_unbundled_env` method with a block. Any subshells created inside the block +will be given the environment present before Bundler was activated. For +example, Homebrew commands run Ruby, but don't work inside a bundle: + + Bundler.with_unbundled_env do + `brew install wget` + end + +Using `with_unbundled_env` is also necessary if you are shelling out to a different +bundle. Any Bundler commands run in a subshell will inherit the current +Gemfile, so commands that need to run in the context of a different bundle also +need to use `with_unbundled_env`. + + Bundler.with_unbundled_env do + Dir.chdir "/other/bundler/project" do + `bundle exec ./script` + end + end + +Bundler provides convenience helpers that wrap `system` and `exec`, and they +can be used like this: + + Bundler.unbundled_system('brew install wget') + Bundler.unbundled_exec('brew install wget') + + +## RUBYGEMS PLUGINS + +At present, the Rubygems plugin system requires all files +named `rubygems_plugin.rb` on the load path of _any_ installed +gem when any Ruby code requires `rubygems.rb`. This includes +executables installed into the system, like `rails`, `rackup`, +and `rspec`. + +Since Rubygems plugins can contain arbitrary Ruby code, they +commonly end up activating themselves or their dependencies. + +For instance, the `gemcutter 0.5` gem depended on `json_pure`. +If you had that version of gemcutter installed (even if +you _also_ had a newer version without this problem), Rubygems +would activate `gemcutter 0.5` and `json_pure <latest>`. + +If your Gemfile(5) also contained `json_pure` (or a gem +with a dependency on `json_pure`), the latest version on +your system might conflict with the version in your +Gemfile(5), or the snapshot version in your `Gemfile.lock`. + +If this happens, bundler will say: + + You have already activated json_pure 1.4.6 but your Gemfile + requires json_pure 1.4.3. Consider using bundle exec. + +In this situation, you almost certainly want to remove the +underlying gem with the problematic gem plugin. In general, +the authors of these plugins (in this case, the `gemcutter` +gem) have released newer versions that are more careful in +their plugins. + +You can find a list of all the gems containing gem plugins +by running + + ruby -e "puts Gem.find_files('rubygems_plugin.rb')" + +At the very least, you should remove all but the newest +version of each gem plugin, and also remove all gem plugins +that you aren't using (`gem uninstall gem_name`). diff --git a/lib/bundler/man/bundle-fund.1 b/lib/bundler/man/bundle-fund.1 new file mode 100644 index 0000000000..caee1f81dd --- /dev/null +++ b/lib/bundler/man/bundle-fund.1 @@ -0,0 +1,22 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-FUND" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-fund\fR \- Lists information about gems seeking funding assistance +.SH "SYNOPSIS" +\fBbundle fund\fR [\fIOPTIONS\fR] +.SH "DESCRIPTION" +\fBbundle fund\fR lists information about gems seeking funding assistance\. +.SH "OPTIONS" +.TP +\fB\-\-group=<list>\fR, \fB\-g=<list>\fR +Fetch funding information for a specific group\. +.SH "EXAMPLES" +.nf +# Lists funding information for all gems +bundle fund + +# Lists funding information for a specific group +bundle fund \-\-group=security +.fi + diff --git a/lib/bundler/man/bundle-fund.1.ronn b/lib/bundler/man/bundle-fund.1.ronn new file mode 100644 index 0000000000..faf8b9c4a7 --- /dev/null +++ b/lib/bundler/man/bundle-fund.1.ronn @@ -0,0 +1,25 @@ +bundle-fund(1) -- Lists information about gems seeking funding assistance +========================================================================= + +## SYNOPSIS + +`bundle fund` [*OPTIONS*] + +## DESCRIPTION + +**bundle fund** lists information about gems seeking funding assistance. + +## OPTIONS + +* `--group=<list>`, `-g=<list>`: + Fetch funding information for a specific group. + +## EXAMPLES + +``` +# Lists funding information for all gems +bundle fund + +# Lists funding information for a specific group +bundle fund --group=security +``` diff --git a/lib/bundler/man/bundle-gem.1 b/lib/bundler/man/bundle-gem.1 new file mode 100644 index 0000000000..87d7568246 --- /dev/null +++ b/lib/bundler/man/bundle-gem.1 @@ -0,0 +1,107 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-GEM" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-gem\fR \- Generate a project skeleton for creating a rubygem +.SH "SYNOPSIS" +\fBbundle gem\fR \fIGEM_NAME\fR \fIOPTIONS\fR +.SH "DESCRIPTION" +Generates a directory named \fBGEM_NAME\fR with a \fBRakefile\fR, \fBGEM_NAME\.gemspec\fR, and other supporting files and directories that can be used to develop a rubygem with that name\. +.P +Run \fBrake \-T\fR in the resulting project for a list of Rake tasks that can be used to test and publish the gem to rubygems\.org\. +.P +The generated project skeleton can be customized with OPTIONS, as explained below\. Note that these options can also be specified via Bundler's global configuration file using the following names: +.IP "\(bu" 4 +\fBgem\.coc\fR +.IP "\(bu" 4 +\fBgem\.mit\fR +.IP "\(bu" 4 +\fBgem\.test\fR +.IP "" 0 +.SH "OPTIONS" +.TP +\fB\-\-exe\fR, \fB\-\-bin\fR, \fB\-b\fR +Specify that Bundler should create a binary executable (as \fBexe/GEM_NAME\fR) in the generated rubygem project\. This binary will also be added to the \fBGEM_NAME\.gemspec\fR manifest\. This behavior is disabled by default\. +.TP +\fB\-\-no\-exe\fR +Do not create a binary (overrides \fB\-\-exe\fR specified in the global config)\. +.TP +\fB\-\-coc\fR +Add a \fBCODE_OF_CONDUCT\.md\fR file to the root of the generated project\. If this option is unspecified, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\. +.TP +\fB\-\-no\-coc\fR +Do not create a \fBCODE_OF_CONDUCT\.md\fR (overrides \fB\-\-coc\fR specified in the global config)\. +.TP +\fB\-\-changelog\fR +Add a \fBCHANGELOG\.md\fR file to the root of the generated project\. If this option is unspecified, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\. Update the default with \fBbundle config set \-\-global gem\.changelog <true|false>\fR\. +.TP +\fB\-\-no\-changelog\fR +Do not create a \fBCHANGELOG\.md\fR (overrides \fB\-\-changelog\fR specified in the global config)\. +.TP +\fB\-\-ext=c\fR, \fB\-\-ext=go\fR, \fB\-\-ext=rust\fR +Add boilerplate for C, Go (currently go\-gem\-wrapper \fIhttps://github\.com/ruby\-go\-gem/go\-gem\-wrapper\fR based) or Rust (currently magnus \fIhttps://docs\.rs/magnus\fR based) extension code to the generated project\. This behavior is disabled by default\. +.TP +\fB\-\-no\-ext\fR +Do not add extension code (overrides \fB\-\-ext\fR specified in the global config)\. +.TP +\fB\-\-git\fR +Initialize a git repo inside your library\. +.TP +\fB\-\-github\-username=GITHUB_USERNAME\fR +Fill in GitHub username on README so that you don't have to do it manually\. Set a default with \fBbundle config set \-\-global gem\.github_username <your_username>\fR\. +.TP +\fB\-\-mit\fR +Add an MIT license to a \fBLICENSE\.txt\fR file in the root of the generated project\. Your name from the global git config is used for the copyright statement\. If this option is unspecified, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\. +.TP +\fB\-\-no\-mit\fR +Do not create a \fBLICENSE\.txt\fR (overrides \fB\-\-mit\fR specified in the global config)\. +.TP +\fB\-t\fR, \fB\-\-test=minitest\fR, \fB\-\-test=rspec\fR, \fB\-\-test=test\-unit\fR +Specify the test framework that Bundler should use when generating the project\. Acceptable values are \fBminitest\fR, \fBrspec\fR and \fBtest\-unit\fR\. The \fBGEM_NAME\.gemspec\fR will be configured and a skeleton test/spec directory will be created based on this option\. Given no option is specified: +.IP +When Bundler is configured to generate tests, this defaults to Bundler's global config setting \fBgem\.test\fR\. +.IP +When Bundler is configured to not generate tests, an interactive prompt will be displayed and the answer will be used for the current rubygem project\. +.IP +When Bundler is unconfigured, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\. +.TP +\fB\-\-no\-test\fR +Do not use a test framework (overrides \fB\-\-test\fR specified in the global config)\. +.TP +\fB\-\-ci\fR, \fB\-\-ci=circle\fR, \fB\-\-ci=github\fR, \fB\-\-ci=gitlab\fR +Specify the continuous integration service that Bundler should use when generating the project\. Acceptable values are \fBgithub\fR, \fBgitlab\fR and \fBcircle\fR\. A configuration file will be generated in the project directory\. Given no option is specified: +.IP +When Bundler is configured to generate CI files, this defaults to Bundler's global config setting \fBgem\.ci\fR\. +.IP +When Bundler is configured to not generate CI files, an interactive prompt will be displayed and the answer will be used for the current rubygem project\. +.IP +When Bundler is unconfigured, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\. +.TP +\fB\-\-no\-ci\fR +Do not use a continuous integration service (overrides \fB\-\-ci\fR specified in the global config)\. +.TP +\fB\-\-linter\fR, \fB\-\-linter=rubocop\fR, \fB\-\-linter=standard\fR +Specify the linter and code formatter that Bundler should add to the project's development dependencies\. Acceptable values are \fBrubocop\fR and \fBstandard\fR\. A configuration file will be generated in the project directory\. Given no option is specified: +.IP +When Bundler is configured to add a linter, this defaults to Bundler's global config setting \fBgem\.linter\fR\. +.IP +When Bundler is configured not to add a linter, an interactive prompt will be displayed and the answer will be used for the current rubygem project\. +.IP +When Bundler is unconfigured, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\. +.TP +\fB\-\-no\-linter\fR +Do not add a linter (overrides \fB\-\-linter\fR specified in the global config)\. +.TP +\fB\-\-edit=EDIT\fR, \fB\-e=EDIT\fR +Open the resulting GEM_NAME\.gemspec in EDIT, or the default editor if not specified\. The default is \fB$BUNDLER_EDITOR\fR, \fB$VISUAL\fR, or \fB$EDITOR\fR\. +.TP +\fB\-\-bundle\fR +Run \fBbundle install\fR after creating the gem\. +.TP +\fB\-\-no\-bundle\fR +Do not run \fBbundle install\fR after creating the gem\. +.SH "SEE ALSO" +.IP "\(bu" 4 +bundle config(1) \fIbundle\-config\.1\.html\fR +.IP "" 0 + diff --git a/lib/bundler/man/bundle-gem.1.ronn b/lib/bundler/man/bundle-gem.1.ronn new file mode 100644 index 0000000000..488c8113e4 --- /dev/null +++ b/lib/bundler/man/bundle-gem.1.ronn @@ -0,0 +1,150 @@ +bundle-gem(1) -- Generate a project skeleton for creating a rubygem +=================================================================== + +## SYNOPSIS + +`bundle gem` <GEM_NAME> [OPTIONS] + +## DESCRIPTION + +Generates a directory named `GEM_NAME` with a `Rakefile`, `GEM_NAME.gemspec`, +and other supporting files and directories that can be used to develop a +rubygem with that name. + +Run `rake -T` in the resulting project for a list of Rake tasks that can be used +to test and publish the gem to rubygems.org. + +The generated project skeleton can be customized with OPTIONS, as explained +below. Note that these options can also be specified via Bundler's global +configuration file using the following names: + +* `gem.coc` +* `gem.mit` +* `gem.test` + +## OPTIONS + +* `--exe`, `--bin`, `-b`: + Specify that Bundler should create a binary executable (as `exe/GEM_NAME`) + in the generated rubygem project. This binary will also be added to the + `GEM_NAME.gemspec` manifest. This behavior is disabled by default. + +* `--no-exe`: + Do not create a binary (overrides `--exe` specified in the global config). + +* `--coc`: + Add a `CODE_OF_CONDUCT.md` file to the root of the generated project. If + this option is unspecified, an interactive prompt will be displayed and the + answer will be saved in Bundler's global config for future `bundle gem` use. + +* `--no-coc`: + Do not create a `CODE_OF_CONDUCT.md` (overrides `--coc` specified in the + global config). + +* `--changelog`: + Add a `CHANGELOG.md` file to the root of the generated project. If + this option is unspecified, an interactive prompt will be displayed and the + answer will be saved in Bundler's global config for future `bundle gem` use. + Update the default with `bundle config set --global gem.changelog <true|false>`. + +* `--no-changelog`: + Do not create a `CHANGELOG.md` (overrides `--changelog` specified in the + global config). + +* `--ext=c`, `--ext=go`, `--ext=rust`: + Add boilerplate for C, Go (currently [go-gem-wrapper](https://github.com/ruby-go-gem/go-gem-wrapper) based) or Rust (currently [magnus](https://docs.rs/magnus) based) extension code to the generated project. This behavior + is disabled by default. + +* `--no-ext`: + Do not add extension code (overrides `--ext` specified in the global + config). + +* `--git`: + Initialize a git repo inside your library. + +* `--github-username=GITHUB_USERNAME`: + Fill in GitHub username on README so that you don't have to do it manually. Set a default with `bundle config set --global gem.github_username <your_username>`. + +* `--mit`: + Add an MIT license to a `LICENSE.txt` file in the root of the generated + project. Your name from the global git config is used for the copyright + statement. If this option is unspecified, an interactive prompt will be + displayed and the answer will be saved in Bundler's global config for future + `bundle gem` use. + +* `--no-mit`: + Do not create a `LICENSE.txt` (overrides `--mit` specified in the global + config). + +* `-t`, `--test=minitest`, `--test=rspec`, `--test=test-unit`: + Specify the test framework that Bundler should use when generating the + project. Acceptable values are `minitest`, `rspec` and `test-unit`. The + `GEM_NAME.gemspec` will be configured and a skeleton test/spec directory will + be created based on this option. Given no option is specified: + + When Bundler is configured to generate tests, this defaults to Bundler's + global config setting `gem.test`. + + When Bundler is configured to not generate tests, an interactive prompt will + be displayed and the answer will be used for the current rubygem project. + + When Bundler is unconfigured, an interactive prompt will be displayed and + the answer will be saved in Bundler's global config for future `bundle gem` + use. + +* `--no-test`: + Do not use a test framework (overrides `--test` specified in the global + config). + +* `--ci`, `--ci=circle`, `--ci=github`, `--ci=gitlab`: + Specify the continuous integration service that Bundler should use when + generating the project. Acceptable values are `github`, `gitlab` + and `circle`. A configuration file will be generated in the project directory. + Given no option is specified: + + When Bundler is configured to generate CI files, this defaults to Bundler's + global config setting `gem.ci`. + + When Bundler is configured to not generate CI files, an interactive prompt + will be displayed and the answer will be used for the current rubygem project. + + When Bundler is unconfigured, an interactive prompt will be displayed and + the answer will be saved in Bundler's global config for future `bundle gem` + use. + +* `--no-ci`: + Do not use a continuous integration service (overrides `--ci` specified in + the global config). + +* `--linter`, `--linter=rubocop`, `--linter=standard`: + Specify the linter and code formatter that Bundler should add to the + project's development dependencies. Acceptable values are `rubocop` and + `standard`. A configuration file will be generated in the project directory. + Given no option is specified: + + When Bundler is configured to add a linter, this defaults to Bundler's + global config setting `gem.linter`. + + When Bundler is configured not to add a linter, an interactive prompt + will be displayed and the answer will be used for the current rubygem project. + + When Bundler is unconfigured, an interactive prompt will be displayed and + the answer will be saved in Bundler's global config for future `bundle gem` + use. + +* `--no-linter`: + Do not add a linter (overrides `--linter` specified in the global config). + +* `--edit=EDIT`, `-e=EDIT`: + Open the resulting GEM_NAME.gemspec in EDIT, or the default editor if not + specified. The default is `$BUNDLER_EDITOR`, `$VISUAL`, or `$EDITOR`. + +* `--bundle`: + Run `bundle install` after creating the gem. + +* `--no-bundle`: + Do not run `bundle install` after creating the gem. + +## SEE ALSO + +* [bundle config(1)](bundle-config.1.html) diff --git a/lib/bundler/man/bundle-help.1 b/lib/bundler/man/bundle-help.1 new file mode 100644 index 0000000000..3bcfd047e5 --- /dev/null +++ b/lib/bundler/man/bundle-help.1 @@ -0,0 +1,9 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-HELP" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-help\fR \- Displays detailed help for each subcommand +.SH "SYNOPSIS" +\fBbundle help\fR [COMMAND] +.SH "DESCRIPTION" +Displays detailed help for the given subcommand\. You can specify a single \fBCOMMAND\fR at the same time\. When \fBCOMMAND\fR is omitted, help for \fBhelp\fR command will be displayed\. diff --git a/lib/bundler/man/bundle-help.1.ronn b/lib/bundler/man/bundle-help.1.ronn new file mode 100644 index 0000000000..0e144aead7 --- /dev/null +++ b/lib/bundler/man/bundle-help.1.ronn @@ -0,0 +1,12 @@ +bundle-help(1) -- Displays detailed help for each subcommand +============================================================ + +## SYNOPSIS + +`bundle help` [COMMAND] + +## DESCRIPTION + +Displays detailed help for the given subcommand. +You can specify a single `COMMAND` at the same time. +When `COMMAND` is omitted, help for `help` command will be displayed. diff --git a/lib/bundler/man/bundle-info.1 b/lib/bundler/man/bundle-info.1 new file mode 100644 index 0000000000..49c2295f8c --- /dev/null +++ b/lib/bundler/man/bundle-info.1 @@ -0,0 +1,17 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-INFO" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-info\fR \- Show information for the given gem in your bundle +.SH "SYNOPSIS" +\fBbundle info\fR [GEM_NAME] [\-\-path] [\-\-version] +.SH "DESCRIPTION" +Given a gem name present in your bundle, print the basic information about it such as homepage, version, path and summary\. +.SH "OPTIONS" +.TP +\fB\-\-path\fR +Print the path of the given gem +.TP +\fB\-\-version\fR +Print gem version + diff --git a/lib/bundler/man/bundle-info.1.ronn b/lib/bundler/man/bundle-info.1.ronn new file mode 100644 index 0000000000..e99db8c614 --- /dev/null +++ b/lib/bundler/man/bundle-info.1.ronn @@ -0,0 +1,21 @@ +bundle-info(1) -- Show information for the given gem in your bundle +=================================================================== + +## SYNOPSIS + +`bundle info` [GEM_NAME] + [--path] + [--version] + +## DESCRIPTION + +Given a gem name present in your bundle, print the basic information about it + such as homepage, version, path and summary. + +## OPTIONS + +* `--path`: + Print the path of the given gem + +* `--version`: + Print gem version diff --git a/lib/bundler/man/bundle-init.1 b/lib/bundler/man/bundle-init.1 new file mode 100644 index 0000000000..63e2376c3f --- /dev/null +++ b/lib/bundler/man/bundle-init.1 @@ -0,0 +1,20 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-INIT" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-init\fR \- Generates a Gemfile into the current working directory +.SH "SYNOPSIS" +\fBbundle init\fR [\-\-gemspec=FILE] +.SH "DESCRIPTION" +Init generates a default [\fBGemfile(5)\fR][Gemfile(5)] in the current working directory\. When adding a [\fBGemfile(5)\fR][Gemfile(5)] to a gem with a gemspec, the \fB\-\-gemspec\fR option will automatically add each dependency listed in the gemspec file to the newly created [\fBGemfile(5)\fR][Gemfile(5)]\. +.SH "OPTIONS" +.TP +\fB\-\-gemspec=GEMSPEC\fR +Use the specified \.gemspec to create the [\fBGemfile(5)\fR][Gemfile(5)] +.TP +\fB\-\-gemfile=GEMFILE\fR +Use the specified name for the gemfile instead of \fBGemfile\fR +.SH "FILES" +Included in the default [\fBGemfile(5)\fR][Gemfile(5)] generated is the line \fB# frozen_string_literal: true\fR\. This is a magic comment supported for the first time in Ruby 2\.3\. The presence of this line results in all string literals in the file being implicitly frozen\. +.SH "SEE ALSO" +Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR diff --git a/lib/bundler/man/bundle-init.1.ronn b/lib/bundler/man/bundle-init.1.ronn new file mode 100644 index 0000000000..ab3c427b52 --- /dev/null +++ b/lib/bundler/man/bundle-init.1.ronn @@ -0,0 +1,32 @@ +bundle-init(1) -- Generates a Gemfile into the current working directory +======================================================================== + +## SYNOPSIS + +`bundle init` [--gemspec=FILE] + +## DESCRIPTION + +Init generates a default [`Gemfile(5)`][Gemfile(5)] in the current working directory. When +adding a [`Gemfile(5)`][Gemfile(5)] to a gem with a gemspec, the `--gemspec` option will +automatically add each dependency listed in the gemspec file to the newly +created [`Gemfile(5)`][Gemfile(5)]. + +## OPTIONS + +* `--gemspec=GEMSPEC`: + Use the specified .gemspec to create the [`Gemfile(5)`][Gemfile(5)] + +* `--gemfile=GEMFILE`: + Use the specified name for the gemfile instead of `Gemfile` + +## FILES + +Included in the default [`Gemfile(5)`][Gemfile(5)] +generated is the line `# frozen_string_literal: true`. This is a magic comment +supported for the first time in Ruby 2.3. The presence of this line +results in all string literals in the file being implicitly frozen. + +## SEE ALSO + +[Gemfile(5)](https://bundler.io/man/gemfile.5.html) diff --git a/lib/bundler/man/bundle-install.1 b/lib/bundler/man/bundle-install.1 new file mode 100644 index 0000000000..801768c7ec --- /dev/null +++ b/lib/bundler/man/bundle-install.1 @@ -0,0 +1,178 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-INSTALL" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-install\fR \- Install the dependencies specified in your Gemfile +.SH "SYNOPSIS" +\fBbundle install\fR [\-\-cooldown=NUMBER] [\-\-force] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-local] [\-\-lockfile=LOCKFILE] [\-\-no\-cache] [\-\-no\-lock] [\-\-prefer\-local] [\-\-quiet] [\-\-retry=NUMBER] [\-\-standalone[=GROUP[ GROUP\|\.\|\.\|\.]]] [\-\-trust\-policy=TRUST\-POLICY] [\-\-target\-rbconfig=TARGET\-RBCONFIG] +.SH "DESCRIPTION" +Install the gems specified in your Gemfile(5)\. If this is the first time you run bundle install (and a \fBGemfile\.lock\fR does not exist), Bundler will fetch all remote sources, resolve dependencies and install all needed gems\. +.P +If a \fBGemfile\.lock\fR does exist, and you have not updated your Gemfile(5), Bundler will fetch all remote sources, but use the dependencies specified in the \fBGemfile\.lock\fR instead of resolving dependencies\. +.P +If a \fBGemfile\.lock\fR does exist, and you have updated your Gemfile(5), Bundler will use the dependencies in the \fBGemfile\.lock\fR for all gems that you did not update, but will re\-resolve the dependencies of gems that you did update\. You can find more information about this update process below under \fICONSERVATIVE UPDATING\fR\. +.SH "OPTIONS" +.TP +\fB\-\-cooldown=<number>\fR +Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. See \fBcooldown\fR in bundle\-config(1) for details on the precedence between the CLI flag, Bundler config, and Gemfile per\-source settings\. +.TP +\fB\-\-force\fR, \fB\-\-redownload\fR +Force reinstalling every gem, even if already installed\. +.TP +\fB\-\-full\-index\fR +Bundler will not call Rubygems' API endpoint (default) but download and cache a (currently big) index file of all gems\. Performance can be improved for large bundles that seldom change by enabling this option\. +.TP +\fB\-\-gemfile=GEMFILE\fR +The location of the Gemfile(5) which Bundler should use\. This defaults to a Gemfile(5) in the current working directory\. In general, Bundler will assume that the location of the Gemfile(5) is also the project's root and will try to find \fBGemfile\.lock\fR and \fBvendor/cache\fR relative to this location\. +.TP +\fB\-\-jobs=<number>\fR, \fB\-j=<number>\fR +The maximum number of parallel download and install jobs\. The default is the number of available processors\. +.TP +\fB\-\-local\fR +Do not attempt to connect to \fBrubygems\.org\fR\. Instead, Bundler will use the gems already present in Rubygems' cache or in \fBvendor/cache\fR\. Note that if an appropriate platform\-specific gem exists on \fBrubygems\.org\fR it will not be found\. +.TP +\fB\-\-lockfile=LOCKFILE\fR +The location of the lockfile which Bundler should use\. This defaults to the Gemfile location with \fB\.lock\fR appended\. +.TP +\fB\-\-prefer\-local\fR +Force using locally installed gems, or gems already present in Rubygems' cache or in \fBvendor/cache\fR, when resolving, even if newer versions are available remotely\. Only attempt to connect to \fBrubygems\.org\fR for gems that are not present locally\. +.TP +\fB\-\-no\-cache\fR +Do not update the cache in \fBvendor/cache\fR with the newly bundled gems\. This does not remove any gems in the cache but keeps the newly bundled gems from being cached during the install\. +.TP +\fB\-\-no\-lock\fR +Do not create a lockfile\. Useful if you want to install dependencies but not lock versions of gems\. Recommended for library development, and other situations where the code is expected to work with a range of dependency versions\. +.IP +This has the same effect as using \fBlockfile false\fR in the Gemfile\. See gemfile(5) for more information\. +.TP +\fB\-\-quiet\fR +Do not print progress information to the standard output\. +.TP +\fB\-\-retry=[<number>]\fR +Retry failed network or git requests for \fInumber\fR times\. +.TP +\fB\-\-standalone[=<list>]\fR +Makes a bundle that can work without depending on Rubygems or Bundler at runtime\. A space separated list of groups to install can be specified\. Bundler creates a directory named \fBbundle\fR and installs the bundle there\. It also generates a \fBbundle/bundler/setup\.rb\fR file to replace Bundler's own setup in the manner required\. +.TP +\fB\-\-trust\-policy=TRUST\-POLICY\fR +Apply the Rubygems security policy \fIpolicy\fR, where policy is one of \fBHighSecurity\fR, \fBMediumSecurity\fR, \fBLowSecurity\fR, \fBAlmostNoSecurity\fR, or \fBNoSecurity\fR\. For more details, please see the Rubygems signing documentation linked below in \fISEE ALSO\fR\. +.TP +\fB\-\-target\-rbconfig=TARGET\-RBCONFIG\fR +Path to rbconfig\.rb for the deployment target platform\. +.SH "DEPLOYMENT MODE" +Bundler's defaults are optimized for development\. To switch to defaults optimized for deployment and for CI, use the \fBdeployment\fR setting\. Do not activate deployment mode on development machines, as it will cause an error when the Gemfile(5) is modified\. +.IP "1." 4 +A \fBGemfile\.lock\fR is required\. +.IP +To ensure that the same versions of the gems you developed with and tested with are also used in deployments, a \fBGemfile\.lock\fR is required\. +.IP +This is mainly to ensure that you remember to check your \fBGemfile\.lock\fR into version control\. +.IP "2." 4 +The \fBGemfile\.lock\fR must be up to date +.IP +In development, you can modify your Gemfile(5) and re\-run \fBbundle install\fR to \fIconservatively update\fR your \fBGemfile\.lock\fR snapshot\. +.IP +In deployment, your \fBGemfile\.lock\fR should be up\-to\-date with changes made in your Gemfile(5)\. +.IP "3." 4 +Gems are installed to \fBvendor/bundle\fR not your default system location +.IP +In development, it's convenient to share the gems used in your application with other applications and other scripts that run on the system\. +.IP +In deployment, isolation is a more important default\. In addition, the user deploying the application may not have permission to install gems to the system, or the web server may not have permission to read them\. +.IP +As a result, when \fBdeployment\fR is configured, \fBbundle install\fR installs gems to the \fBvendor/bundle\fR directory in the application\. This may be overridden using the \fBpath\fR setting\. +.IP "" 0 +.SH "INSTALLING GROUPS" +By default, \fBbundle install\fR will install all gems in all groups in your Gemfile(5), except those declared for a different platform\. +.P +However, you can explicitly tell Bundler to skip installing certain groups with the \fBwithout\fR setting\. This setting takes a space\-separated list of groups\. +.P +While the \fBwithout\fR setting will skip \fIinstalling\fR the gems in the specified groups, \fBbundle install\fR will still \fIdownload\fR those gems and use them to resolve the dependencies of every gem in your Gemfile(5)\. +.P +This is so that installing a different set of groups on another machine (such as a production server) will not change the gems and versions that you have already developed and tested against\. +.P +\fBBundler offers a rock\-solid guarantee that the third\-party code you are running in development and testing is also the third\-party code you are running in production\. You can choose to exclude some of that code in different environments, but you will never be caught flat\-footed by different versions of third\-party code being used in different environments\.\fR +.P +For a simple illustration, consider the following Gemfile(5): +.IP "" 4 +.nf +source 'https://rubygems\.org' + +gem 'sinatra' + +group :production do + gem 'rack\-perftools\-profiler' +end +.fi +.IP "" 0 +.P +In this case, \fBsinatra\fR depends on any version of Rack (\fB>= 1\.0\fR), while \fBrack\-perftools\-profiler\fR depends on 1\.x (\fB~> 1\.0\fR)\. +.P +When you configure \fBbundle config without production\fR in development, we look at the dependencies of \fBrack\-perftools\-profiler\fR as well\. That way, you do not spend all your time developing against Rack 2\.0, using new APIs unavailable in Rack 1\.x, only to have Bundler switch to Rack 1\.2 when the \fBproduction\fR group \fIis\fR used\. +.P +This should not cause any problems in practice, because we do not attempt to \fBinstall\fR the gems in the excluded groups, and only evaluate as part of the dependency resolution process\. +.P +This also means that you cannot include different versions of the same gem in different groups, because doing so would result in different sets of dependencies used in development and production\. Because of the vagaries of the dependency resolution process, this usually affects more than the gems you list in your Gemfile(5), and can (surprisingly) radically change the gems you are using\. +.SH "THE GEMFILE\.LOCK" +When you run \fBbundle install\fR, Bundler will persist the full names and versions of all gems that you used (including dependencies of the gems specified in the Gemfile(5)) into a file called \fBGemfile\.lock\fR\. +.P +Bundler uses this file in all subsequent calls to \fBbundle install\fR, which guarantees that you always use the same exact code, even as your application moves across machines\. +.P +Because of the way dependency resolution works, even a seemingly small change (for instance, an update to a point\-release of a dependency of a gem in your Gemfile(5)) can result in radically different gems being needed to satisfy all dependencies\. +.P +As a result, you \fBSHOULD\fR check your \fBGemfile\.lock\fR into version control, in both applications and gems\. If you do not, every machine that checks out your repository (including your production server) will resolve all dependencies again, which will result in different versions of third\-party code being used if \fBany\fR of the gems in the Gemfile(5) or any of their dependencies have been updated\. +.P +When Bundler first shipped, the \fBGemfile\.lock\fR was included in the \fB\.gitignore\fR file included with generated gems\. Over time, however, it became clear that this practice forces the pain of broken dependencies onto new contributors, while leaving existing contributors potentially unaware of the problem\. Since \fBbundle install\fR is usually the first step towards a contribution, the pain of broken dependencies would discourage new contributors from contributing\. As a result, we have revised our guidance for gem authors to now recommend checking in the lock for gems\. +.SH "CONSERVATIVE UPDATING" +When you make a change to the Gemfile(5) and then run \fBbundle install\fR, Bundler will update only the gems that you modified\. +.P +In other words, if a gem that you \fBdid not modify\fR worked before you called \fBbundle install\fR, it will continue to use the exact same versions of all dependencies as it used before the update\. +.P +Let's take a look at an example\. Here's your original Gemfile(5): +.IP "" 4 +.nf +source 'https://rubygems\.org' + +gem 'actionpack', '2\.3\.8' +gem 'activemerchant' +.fi +.IP "" 0 +.P +In this case, both \fBactionpack\fR and \fBactivemerchant\fR depend on \fBactivesupport\fR\. The \fBactionpack\fR gem depends on \fBactivesupport 2\.3\.8\fR and \fBrack ~> 1\.1\.0\fR, while the \fBactivemerchant\fR gem depends on \fBactivesupport >= 2\.3\.2\fR, \fBbraintree >= 2\.0\.0\fR, and \fBbuilder >= 2\.0\.0\fR\. +.P +When the dependencies are first resolved, Bundler will select \fBactivesupport 2\.3\.8\fR, which satisfies the requirements of both gems in your Gemfile(5)\. +.P +Next, you modify your Gemfile(5) to: +.IP "" 4 +.nf +source 'https://rubygems\.org' + +gem 'actionpack', '3\.0\.0\.rc' +gem 'activemerchant' +.fi +.IP "" 0 +.P +The \fBactionpack 3\.0\.0\.rc\fR gem has a number of new dependencies, and updates the \fBactivesupport\fR dependency to \fB= 3\.0\.0\.rc\fR and the \fBrack\fR dependency to \fB~> 1\.2\.1\fR\. +.P +When you run \fBbundle install\fR, Bundler notices that you changed the \fBactionpack\fR gem, but not the \fBactivemerchant\fR gem\. It evaluates the gems currently being used to satisfy its requirements: +.TP +\fBactivesupport 2\.3\.8\fR +also used to satisfy a dependency in \fBactivemerchant\fR, which is not being updated +.TP +\fBrack ~> 1\.1\.0\fR +not currently being used to satisfy another dependency +.P +Because you did not explicitly ask to update \fBactivemerchant\fR, you would not expect it to suddenly stop working after updating \fBactionpack\fR\. However, satisfying the new \fBactivesupport 3\.0\.0\.rc\fR dependency of actionpack requires updating one of its dependencies\. +.P +Even though \fBactivemerchant\fR declares a very loose dependency that theoretically matches \fBactivesupport 3\.0\.0\.rc\fR, Bundler treats gems in your Gemfile(5) that have not changed as an atomic unit together with their dependencies\. In this case, the \fBactivemerchant\fR dependency is treated as \fBactivemerchant 1\.7\.1 + activesupport 2\.3\.8\fR, so \fBbundle install\fR will report that it cannot update \fBactionpack\fR\. +.P +To explicitly update \fBactionpack\fR, including its dependencies which other gems in the Gemfile(5) still depend on, run \fBbundle update actionpack\fR (see \fBbundle update(1)\fR)\. +.P +\fBSummary\fR: In general, after making a change to the Gemfile(5) , you should first try to run \fBbundle install\fR, which will guarantee that no other gem in the Gemfile(5) is impacted by the change\. If that does not work, run bundle update(1) \fIbundle\-update\.1\.html\fR\. +.SH "SEE ALSO" +.IP "\(bu" 4 +Gem install docs \fIhttps://guides\.rubygems\.org/rubygems\-basics/#installing\-gems\fR +.IP "\(bu" 4 +Rubygems signing docs \fIhttps://guides\.rubygems\.org/security/\fR +.IP "" 0 + diff --git a/lib/bundler/man/bundle-install.1.ronn b/lib/bundler/man/bundle-install.1.ronn new file mode 100644 index 0000000000..56fd8bdf42 --- /dev/null +++ b/lib/bundler/man/bundle-install.1.ronn @@ -0,0 +1,314 @@ +bundle-install(1) -- Install the dependencies specified in your Gemfile +======================================================================= + +## SYNOPSIS + +`bundle install` [--cooldown=NUMBER] + [--force] + [--full-index] + [--gemfile=GEMFILE] + [--jobs=NUMBER] + [--local] + [--lockfile=LOCKFILE] + [--no-cache] + [--no-lock] + [--prefer-local] + [--quiet] + [--retry=NUMBER] + [--standalone[=GROUP[ GROUP...]]] + [--trust-policy=TRUST-POLICY] + [--target-rbconfig=TARGET-RBCONFIG] + +## DESCRIPTION + +Install the gems specified in your Gemfile(5). If this is the first +time you run bundle install (and a `Gemfile.lock` does not exist), +Bundler will fetch all remote sources, resolve dependencies and +install all needed gems. + +If a `Gemfile.lock` does exist, and you have not updated your Gemfile(5), +Bundler will fetch all remote sources, but use the dependencies +specified in the `Gemfile.lock` instead of resolving dependencies. + +If a `Gemfile.lock` does exist, and you have updated your Gemfile(5), +Bundler will use the dependencies in the `Gemfile.lock` for all gems +that you did not update, but will re-resolve the dependencies of +gems that you did update. You can find more information about this +update process below under [CONSERVATIVE UPDATING][]. + +## OPTIONS + +* `--cooldown=<number>`: + Only consider gem versions published at least <number> days ago when + resolving. Pass `0` to disable cooldown for this run, overriding any + per-source or global configuration. See `cooldown` in bundle-config(1) + for details on the precedence between the CLI flag, Bundler config, + and Gemfile per-source settings. + +* `--force`, `--redownload`: + Force reinstalling every gem, even if already installed. + +* `--full-index`: + Bundler will not call Rubygems' API endpoint (default) but download and cache + a (currently big) index file of all gems. Performance can be improved for + large bundles that seldom change by enabling this option. + +* `--gemfile=GEMFILE`: + The location of the Gemfile(5) which Bundler should use. This defaults + to a Gemfile(5) in the current working directory. In general, Bundler + will assume that the location of the Gemfile(5) is also the project's + root and will try to find `Gemfile.lock` and `vendor/cache` relative + to this location. + +* `--jobs=<number>`, `-j=<number>`: + The maximum number of parallel download and install jobs. The default is the + number of available processors. + +* `--local`: + Do not attempt to connect to `rubygems.org`. Instead, Bundler will use the + gems already present in Rubygems' cache or in `vendor/cache`. Note that if an + appropriate platform-specific gem exists on `rubygems.org` it will not be + found. + +* `--lockfile=LOCKFILE`: + The location of the lockfile which Bundler should use. This defaults + to the Gemfile location with `.lock` appended. + +* `--prefer-local`: + Force using locally installed gems, or gems already present in Rubygems' cache + or in `vendor/cache`, when resolving, even if newer versions are available + remotely. Only attempt to connect to `rubygems.org` for gems that are not + present locally. + +* `--no-cache`: + Do not update the cache in `vendor/cache` with the newly bundled gems. This + does not remove any gems in the cache but keeps the newly bundled gems from + being cached during the install. + +* `--no-lock`: + Do not create a lockfile. Useful if you want to install dependencies but not + lock versions of gems. Recommended for library development, and other + situations where the code is expected to work with a range of dependency + versions. + + This has the same effect as using `lockfile false` in the Gemfile. + See gemfile(5) for more information. + +* `--quiet`: + Do not print progress information to the standard output. + +* `--retry=[<number>]`: + Retry failed network or git requests for <number> times. + +* `--standalone[=<list>]`: + Makes a bundle that can work without depending on Rubygems or Bundler at + runtime. A space separated list of groups to install can be specified. + Bundler creates a directory named `bundle` and installs the bundle there. It + also generates a `bundle/bundler/setup.rb` file to replace Bundler's own setup + in the manner required. + +* `--trust-policy=TRUST-POLICY`: + Apply the Rubygems security policy <policy>, where policy is one of + `HighSecurity`, `MediumSecurity`, `LowSecurity`, `AlmostNoSecurity`, or + `NoSecurity`. For more details, please see the Rubygems signing documentation + linked below in [SEE ALSO][]. + +* `--target-rbconfig=TARGET-RBCONFIG`: + Path to rbconfig.rb for the deployment target platform. + +## DEPLOYMENT MODE + +Bundler's defaults are optimized for development. To switch to +defaults optimized for deployment and for CI, use the `deployment` +setting. Do not activate deployment mode on development machines, as it +will cause an error when the Gemfile(5) is modified. + +1. A `Gemfile.lock` is required. + + To ensure that the same versions of the gems you developed with + and tested with are also used in deployments, a `Gemfile.lock` + is required. + + This is mainly to ensure that you remember to check your + `Gemfile.lock` into version control. + +2. The `Gemfile.lock` must be up to date + + In development, you can modify your Gemfile(5) and re-run + `bundle install` to [conservatively update][CONSERVATIVE UPDATING] + your `Gemfile.lock` snapshot. + + In deployment, your `Gemfile.lock` should be up-to-date with + changes made in your Gemfile(5). + +3. Gems are installed to `vendor/bundle` not your default system location + + In development, it's convenient to share the gems used in your + application with other applications and other scripts that run on + the system. + + In deployment, isolation is a more important default. In addition, + the user deploying the application may not have permission to install + gems to the system, or the web server may not have permission to + read them. + + As a result, when `deployment` is configured, `bundle install` installs gems + to the `vendor/bundle` directory in the application. This may be + overridden using the `path` setting. + +## INSTALLING GROUPS + +By default, `bundle install` will install all gems in all groups +in your Gemfile(5), except those declared for a different platform. + +However, you can explicitly tell Bundler to skip installing +certain groups with the `without` setting. This setting takes +a space-separated list of groups. + +While the `without` setting will skip _installing_ the gems in the +specified groups, `bundle install` will still _download_ those gems and use them +to resolve the dependencies of every gem in your Gemfile(5). + +This is so that installing a different set of groups on another + machine (such as a production server) will not change the +gems and versions that you have already developed and tested against. + +`Bundler offers a rock-solid guarantee that the third-party +code you are running in development and testing is also the +third-party code you are running in production. You can choose +to exclude some of that code in different environments, but you +will never be caught flat-footed by different versions of +third-party code being used in different environments.` + +For a simple illustration, consider the following Gemfile(5): + + source 'https://rubygems.org' + + gem 'sinatra' + + group :production do + gem 'rack-perftools-profiler' + end + +In this case, `sinatra` depends on any version of Rack (`>= 1.0`), while +`rack-perftools-profiler` depends on 1.x (`~> 1.0`). + +When you configure `bundle config without production` in development, we +look at the dependencies of `rack-perftools-profiler` as well. That way, +you do not spend all your time developing against Rack 2.0, using new +APIs unavailable in Rack 1.x, only to have Bundler switch to Rack 1.2 +when the `production` group _is_ used. + +This should not cause any problems in practice, because we do not +attempt to `install` the gems in the excluded groups, and only evaluate +as part of the dependency resolution process. + +This also means that you cannot include different versions of the same +gem in different groups, because doing so would result in different +sets of dependencies used in development and production. Because of +the vagaries of the dependency resolution process, this usually +affects more than the gems you list in your Gemfile(5), and can +(surprisingly) radically change the gems you are using. + +## THE GEMFILE.LOCK + +When you run `bundle install`, Bundler will persist the full names +and versions of all gems that you used (including dependencies of +the gems specified in the Gemfile(5)) into a file called `Gemfile.lock`. + +Bundler uses this file in all subsequent calls to `bundle install`, +which guarantees that you always use the same exact code, even +as your application moves across machines. + +Because of the way dependency resolution works, even a +seemingly small change (for instance, an update to a point-release +of a dependency of a gem in your Gemfile(5)) can result in radically +different gems being needed to satisfy all dependencies. + +As a result, you `SHOULD` check your `Gemfile.lock` into version +control, in both applications and gems. If you do not, every machine that +checks out your repository (including your production server) will resolve all +dependencies again, which will result in different versions of +third-party code being used if `any` of the gems in the Gemfile(5) +or any of their dependencies have been updated. + +When Bundler first shipped, the `Gemfile.lock` was included in the `.gitignore` +file included with generated gems. Over time, however, it became clear that +this practice forces the pain of broken dependencies onto new contributors, +while leaving existing contributors potentially unaware of the problem. Since +`bundle install` is usually the first step towards a contribution, the pain of +broken dependencies would discourage new contributors from contributing. As a +result, we have revised our guidance for gem authors to now recommend checking +in the lock for gems. + +## CONSERVATIVE UPDATING + +When you make a change to the Gemfile(5) and then run `bundle install`, +Bundler will update only the gems that you modified. + +In other words, if a gem that you `did not modify` worked before +you called `bundle install`, it will continue to use the exact +same versions of all dependencies as it used before the update. + +Let's take a look at an example. Here's your original Gemfile(5): + + source 'https://rubygems.org' + + gem 'actionpack', '2.3.8' + gem 'activemerchant' + +In this case, both `actionpack` and `activemerchant` depend on +`activesupport`. The `actionpack` gem depends on `activesupport 2.3.8` +and `rack ~> 1.1.0`, while the `activemerchant` gem depends on +`activesupport >= 2.3.2`, `braintree >= 2.0.0`, and `builder >= 2.0.0`. + +When the dependencies are first resolved, Bundler will select +`activesupport 2.3.8`, which satisfies the requirements of both +gems in your Gemfile(5). + +Next, you modify your Gemfile(5) to: + + source 'https://rubygems.org' + + gem 'actionpack', '3.0.0.rc' + gem 'activemerchant' + +The `actionpack 3.0.0.rc` gem has a number of new dependencies, +and updates the `activesupport` dependency to `= 3.0.0.rc` and +the `rack` dependency to `~> 1.2.1`. + +When you run `bundle install`, Bundler notices that you changed +the `actionpack` gem, but not the `activemerchant` gem. It +evaluates the gems currently being used to satisfy its requirements: + + * `activesupport 2.3.8`: + also used to satisfy a dependency in `activemerchant`, + which is not being updated + * `rack ~> 1.1.0`: + not currently being used to satisfy another dependency + +Because you did not explicitly ask to update `activemerchant`, +you would not expect it to suddenly stop working after updating +`actionpack`. However, satisfying the new `activesupport 3.0.0.rc` +dependency of actionpack requires updating one of its dependencies. + +Even though `activemerchant` declares a very loose dependency +that theoretically matches `activesupport 3.0.0.rc`, Bundler treats +gems in your Gemfile(5) that have not changed as an atomic unit +together with their dependencies. In this case, the `activemerchant` +dependency is treated as `activemerchant 1.7.1 + activesupport 2.3.8`, +so `bundle install` will report that it cannot update `actionpack`. + +To explicitly update `actionpack`, including its dependencies +which other gems in the Gemfile(5) still depend on, run +`bundle update actionpack` (see `bundle update(1)`). + +`Summary`: In general, after making a change to the Gemfile(5) , you +should first try to run `bundle install`, which will guarantee that no +other gem in the Gemfile(5) is impacted by the change. If that +does not work, run [bundle update(1)](bundle-update.1.html). + +## SEE ALSO + +* [Gem install docs](https://guides.rubygems.org/rubygems-basics/#installing-gems) +* [Rubygems signing docs](https://guides.rubygems.org/security/) diff --git a/lib/bundler/man/bundle-issue.1 b/lib/bundler/man/bundle-issue.1 new file mode 100644 index 0000000000..3af277ef86 --- /dev/null +++ b/lib/bundler/man/bundle-issue.1 @@ -0,0 +1,45 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-ISSUE" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-issue\fR \- Get help reporting Bundler issues +.SH "SYNOPSIS" +\fBbundle issue\fR +.SH "DESCRIPTION" +Provides guidance on reporting Bundler issues and outputs detailed system information that should be included when filing a bug report\. This command: +.IP "1." 4 +Displays links to troubleshooting resources +.IP "2." 4 +Shows instructions for reporting issues +.IP "3." 4 +Outputs comprehensive environment information needed for debugging +.IP "" 0 +.P +The command helps ensure that bug reports include all necessary system details for effective troubleshooting\. +.SH "OUTPUT" +The command outputs several sections: +.IP "\(bu" 4 +Troubleshooting links and resources +.IP "\(bu" 4 +Link to the GitHub issue template +.IP "\(bu" 4 +Environment information including: Bundler version and platforms, Ruby version and configuration, RubyGems version and paths, Development tool versions (Git, RVM, rbenv, chruby) +.IP "\(bu" 4 +Bundler build metadata +.IP "\(bu" 4 +Current Bundler settings +.IP "\(bu" 4 +Bundle Doctor output +.IP "" 0 +.SH "EXAMPLES" +Get issue reporting information: +.IP "" 4 +.nf +$ bundle issue +.fi +.IP "" 0 +.SH "SEE ALSO" +.IP "\(bu" 4 +bundle\-doctor(1) +.IP "" 0 + diff --git a/lib/bundler/man/bundle-issue.1.ronn b/lib/bundler/man/bundle-issue.1.ronn new file mode 100644 index 0000000000..37f676a354 --- /dev/null +++ b/lib/bundler/man/bundle-issue.1.ronn @@ -0,0 +1,37 @@ +bundle-issue(1) -- Get help reporting Bundler issues +==================================================== + +## SYNOPSIS + +`bundle issue` + +## DESCRIPTION + +Provides guidance on reporting Bundler issues and outputs detailed system information that should be included when filing a bug report. This command: + +1. Displays links to troubleshooting resources +2. Shows instructions for reporting issues +3. Outputs comprehensive environment information needed for debugging + +The command helps ensure that bug reports include all necessary system details for effective troubleshooting. + +## OUTPUT + +The command outputs several sections: + +* Troubleshooting links and resources +* Link to the GitHub issue template +* Environment information including: Bundler version and platforms, Ruby version and configuration, RubyGems version and paths, Development tool versions (Git, RVM, rbenv, chruby) +* Bundler build metadata +* Current Bundler settings +* Bundle Doctor output + +## EXAMPLES + +Get issue reporting information: + + $ bundle issue + +## SEE ALSO + +* bundle-doctor(1) diff --git a/lib/bundler/man/bundle-licenses.1 b/lib/bundler/man/bundle-licenses.1 new file mode 100644 index 0000000000..ab5996d2be --- /dev/null +++ b/lib/bundler/man/bundle-licenses.1 @@ -0,0 +1,9 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-LICENSES" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-licenses\fR \- Print the license of all gems in the bundle +.SH "SYNOPSIS" +\fBbundle licenses\fR +.SH "DESCRIPTION" +Prints the license of all gems in the bundle\. diff --git a/lib/bundler/man/bundle-licenses.1.ronn b/lib/bundler/man/bundle-licenses.1.ronn new file mode 100644 index 0000000000..91caba6c2a --- /dev/null +++ b/lib/bundler/man/bundle-licenses.1.ronn @@ -0,0 +1,10 @@ +bundle-licenses(1) -- Print the license of all gems in the bundle +================================================================= + +## SYNOPSIS + +`bundle licenses` + +## DESCRIPTION + +Prints the license of all gems in the bundle. diff --git a/lib/bundler/man/bundle-list.1 b/lib/bundler/man/bundle-list.1 new file mode 100644 index 0000000000..e759e0d449 --- /dev/null +++ b/lib/bundler/man/bundle-list.1 @@ -0,0 +1,40 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-LIST" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-list\fR \- List all the gems in the bundle +.SH "SYNOPSIS" +\fBbundle list\fR [\-\-name\-only] [\-\-paths] [\-\-without\-group=GROUP[ GROUP\|\.\|\.\|\.]] [\-\-only\-group=GROUP[ GROUP\|\.\|\.\|\.]] +.SH "DESCRIPTION" +Prints a list of all the gems in the bundle including their version\. +.P +Example: +.P +bundle list \-\-name\-only +.P +bundle list \-\-paths +.P +bundle list \-\-without\-group test +.P +bundle list \-\-only\-group dev +.P +bundle list \-\-only\-group dev test \-\-paths +.P +bundle list \-\-format json +.SH "OPTIONS" +.TP +\fB\-\-name\-only\fR +Print only the name of each gem\. +.TP +\fB\-\-paths\fR +Print the path to each gem in the bundle\. +.TP +\fB\-\-without\-group=<list>\fR +A space\-separated list of groups of gems to skip during printing\. +.TP +\fB\-\-only\-group=<list>\fR +A space\-separated list of groups of gems to print\. +.TP +\fB\-\-format=FORMAT\fR +Format output ('json' is the only supported format) + diff --git a/lib/bundler/man/bundle-list.1.ronn b/lib/bundler/man/bundle-list.1.ronn new file mode 100644 index 0000000000..9ec2b13282 --- /dev/null +++ b/lib/bundler/man/bundle-list.1.ronn @@ -0,0 +1,41 @@ +bundle-list(1) -- List all the gems in the bundle +================================================= + +## SYNOPSIS + +`bundle list` [--name-only] [--paths] [--without-group=GROUP[ GROUP...]] [--only-group=GROUP[ GROUP...]] + +## DESCRIPTION + +Prints a list of all the gems in the bundle including their version. + +Example: + +bundle list --name-only + +bundle list --paths + +bundle list --without-group test + +bundle list --only-group dev + +bundle list --only-group dev test --paths + +bundle list --format json + +## OPTIONS + +* `--name-only`: + Print only the name of each gem. + +* `--paths`: + Print the path to each gem in the bundle. + +* `--without-group=<list>`: + A space-separated list of groups of gems to skip during printing. + +* `--only-group=<list>`: + A space-separated list of groups of gems to print. + +* `--format=FORMAT`: + Format output ('json' is the only supported format) diff --git a/lib/bundler/man/bundle-lock.1 b/lib/bundler/man/bundle-lock.1 new file mode 100644 index 0000000000..396c8ff6ca --- /dev/null +++ b/lib/bundler/man/bundle-lock.1 @@ -0,0 +1,75 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-LOCK" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-lock\fR \- Creates / Updates a lockfile without installing +.SH "SYNOPSIS" +\fBbundle lock\fR [\-\-update] [\-\-bundler[=BUNDLER]] [\-\-local] [\-\-print] [\-\-lockfile=PATH] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-add\-checksums] [\-\-add\-platform] [\-\-remove\-platform] [\-\-normalize\-platforms] [\-\-patch] [\-\-minor] [\-\-major] [\-\-pre] [\-\-strict] [\-\-conservative] +.SH "DESCRIPTION" +Lock the gems specified in Gemfile\. +.SH "OPTIONS" +.TP +\fB\-\-update[=<list>]\fR +Ignores the existing lockfile\. Resolve then updates lockfile\. Taking a list of gems or updating all gems if no list is given\. +.TP +\fB\-\-bundler[=BUNDLER]\fR +Update the locked version of bundler to the given version or the latest version if no version is given\. +.TP +\fB\-\-local\fR +Do not attempt to connect to \fBrubygems\.org\fR\. Instead, Bundler will use the gems already present in Rubygems' cache or in \fBvendor/cache\fR\. Note that if a appropriate platform\-specific gem exists on \fBrubygems\.org\fR it will not be found\. +.TP +\fB\-\-print\fR +Prints the lockfile to STDOUT instead of writing to the file system\. +.TP +\fB\-\-lockfile=LOCKFILE\fR +The path where the lockfile should be written to\. +.TP +\fB\-\-full\-index\fR +Fall back to using the single\-file index of all gems\. +.TP +\fB\-\-gemfile=GEMFILE\fR +Use the specified gemfile instead of [\fBGemfile(5)\fR][Gemfile(5)]\. +.TP +\fB\-\-add\-checksums\fR +Add checksums to the lockfile\. +.TP +\fB\-\-add\-platform=<list>\fR +Add a new platform to the lockfile, re\-resolving for the addition of that platform\. +.TP +\fB\-\-remove\-platform=<list>\fR +Remove a platform from the lockfile\. +.TP +\fB\-\-normalize\-platforms\fR +Normalize lockfile platforms\. +.TP +\fB\-\-patch\fR +If updating, prefer updating only to next patch version\. +.TP +\fB\-\-minor\fR +If updating, prefer updating only to next minor version\. +.TP +\fB\-\-major\fR +If updating, prefer updating to next major version (default)\. +.TP +\fB\-\-pre\fR +If updating, always choose the highest allowed version, regardless of prerelease status\. +.TP +\fB\-\-strict\fR +If updating, do not allow any gem to be updated past latest \-\-patch | \-\-minor | \-\-major\. +.TP +\fB\-\-conservative\fR +If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated\. +.SH "UPDATING ALL GEMS" +If you run \fBbundle lock\fR with \fB\-\-update\fR option without list of gems, bundler will ignore any previously installed gems and resolve all dependencies again based on the latest versions of all gems available in the sources\. +.SH "UPDATING A LIST OF GEMS" +Sometimes, you want to update a single gem in the Gemfile(5), and leave the rest of the gems that you specified locked to the versions in the \fBGemfile\.lock\fR\. +.P +For instance, you only want to update \fBnokogiri\fR, run \fBbundle lock \-\-update nokogiri\fR\. +.P +Bundler will update \fBnokogiri\fR and any of its dependencies, but leave the rest of the gems that you specified locked to the versions in the \fBGemfile\.lock\fR\. +.SH "SUPPORTING OTHER PLATFORMS" +If you want your bundle to support platforms other than the one you're running locally, you can run \fBbundle lock \-\-add\-platform PLATFORM\fR to add PLATFORM to the lockfile, force bundler to re\-resolve and consider the new platform when picking gems, all without needing to have a machine that matches PLATFORM handy to install those platform\-specific gems on\. +.P +For a full explanation of gem platforms, see \fBgem help platform\fR\. +.SH "PATCH LEVEL OPTIONS" +See bundle update(1) \fIbundle\-update\.1\.html\fR for details\. diff --git a/lib/bundler/man/bundle-lock.1.ronn b/lib/bundler/man/bundle-lock.1.ronn new file mode 100644 index 0000000000..6d3e63c982 --- /dev/null +++ b/lib/bundler/man/bundle-lock.1.ronn @@ -0,0 +1,115 @@ +bundle-lock(1) -- Creates / Updates a lockfile without installing +================================================================= + +## SYNOPSIS + +`bundle lock` [--update] + [--bundler[=BUNDLER]] + [--local] + [--print] + [--lockfile=PATH] + [--full-index] + [--gemfile=GEMFILE] + [--add-checksums] + [--add-platform] + [--remove-platform] + [--normalize-platforms] + [--patch] + [--minor] + [--major] + [--pre] + [--strict] + [--conservative] + +## DESCRIPTION + +Lock the gems specified in Gemfile. + +## OPTIONS + +* `--update[=<list>]`: + Ignores the existing lockfile. Resolve then updates lockfile. Taking a list + of gems or updating all gems if no list is given. + +* `--bundler[=BUNDLER]`: + Update the locked version of bundler to the given version or the latest + version if no version is given. + +* `--local`: + Do not attempt to connect to `rubygems.org`. Instead, Bundler will use the + gems already present in Rubygems' cache or in `vendor/cache`. Note that if a + appropriate platform-specific gem exists on `rubygems.org` it will not be + found. + +* `--print`: + Prints the lockfile to STDOUT instead of writing to the file system. + +* `--lockfile=LOCKFILE`: + The path where the lockfile should be written to. + +* `--full-index`: + Fall back to using the single-file index of all gems. + +* `--gemfile=GEMFILE`: + Use the specified gemfile instead of [`Gemfile(5)`][Gemfile(5)]. + +* `--add-checksums`: + Add checksums to the lockfile. + +* `--add-platform=<list>`: + Add a new platform to the lockfile, re-resolving for the addition of that + platform. + +* `--remove-platform=<list>`: + Remove a platform from the lockfile. + +* `--normalize-platforms`: + Normalize lockfile platforms. + +* `--patch`: + If updating, prefer updating only to next patch version. + +* `--minor`: + If updating, prefer updating only to next minor version. + +* `--major`: + If updating, prefer updating to next major version (default). + +* `--pre`: + If updating, always choose the highest allowed version, regardless of prerelease status. + +* `--strict`: + If updating, do not allow any gem to be updated past latest --patch | --minor | --major. + +* `--conservative`: + If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated. + +## UPDATING ALL GEMS + +If you run `bundle lock` with `--update` option without list of gems, bundler will +ignore any previously installed gems and resolve all dependencies again based +on the latest versions of all gems available in the sources. + +## UPDATING A LIST OF GEMS + +Sometimes, you want to update a single gem in the Gemfile(5), and leave the rest of +the gems that you specified locked to the versions in the `Gemfile.lock`. + +For instance, you only want to update `nokogiri`, run `bundle lock --update nokogiri`. + +Bundler will update `nokogiri` and any of its dependencies, but leave the rest of the +gems that you specified locked to the versions in the `Gemfile.lock`. + +## SUPPORTING OTHER PLATFORMS + +If you want your bundle to support platforms other than the one you're running +locally, you can run `bundle lock --add-platform PLATFORM` to add PLATFORM to +the lockfile, force bundler to re-resolve and consider the new platform when +picking gems, all without needing to have a machine that matches PLATFORM handy +to install those platform-specific gems on. + +For a full explanation of gem platforms, see `gem help platform`. + +## PATCH LEVEL OPTIONS + +See [bundle update(1)](bundle-update.1.html) for details. diff --git a/lib/bundler/man/bundle-open.1 b/lib/bundler/man/bundle-open.1 new file mode 100644 index 0000000000..2aab59f14b --- /dev/null +++ b/lib/bundler/man/bundle-open.1 @@ -0,0 +1,32 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-OPEN" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-open\fR \- Opens the source directory for a gem in your bundle +.SH "SYNOPSIS" +\fBbundle open\fR [GEM] [\-\-path=PATH] +.SH "DESCRIPTION" +Opens the source directory of the provided GEM in your editor\. +.P +For this to work the \fBEDITOR\fR or \fBBUNDLER_EDITOR\fR environment variable has to be set\. +.P +Example: +.IP "" 4 +.nf +bundle open 'rack' +.fi +.IP "" 0 +.P +Will open the source directory for the 'rack' gem in your bundle\. +.IP "" 4 +.nf +bundle open 'rack' \-\-path 'README\.md' +.fi +.IP "" 0 +.P +Will open the README\.md file of the 'rack' gem source in your bundle\. +.SH "OPTIONS" +.TP +\fB\-\-path[=PATH]\fR +Specify GEM source relative path to open\. + diff --git a/lib/bundler/man/bundle-open.1.ronn b/lib/bundler/man/bundle-open.1.ronn new file mode 100644 index 0000000000..24dbe97e44 --- /dev/null +++ b/lib/bundler/man/bundle-open.1.ronn @@ -0,0 +1,28 @@ +bundle-open(1) -- Opens the source directory for a gem in your bundle +===================================================================== + +## SYNOPSIS + +`bundle open` [GEM] [--path=PATH] + +## DESCRIPTION + +Opens the source directory of the provided GEM in your editor. + +For this to work the `EDITOR` or `BUNDLER_EDITOR` environment variable has to +be set. + +Example: + + bundle open 'rack' + +Will open the source directory for the 'rack' gem in your bundle. + + bundle open 'rack' --path 'README.md' + +Will open the README.md file of the 'rack' gem source in your bundle. + +## OPTIONS + +* `--path[=PATH]`: + Specify GEM source relative path to open. diff --git a/lib/bundler/man/bundle-outdated.1 b/lib/bundler/man/bundle-outdated.1 new file mode 100644 index 0000000000..c2f8086e24 --- /dev/null +++ b/lib/bundler/man/bundle-outdated.1 @@ -0,0 +1,106 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-OUTDATED" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-outdated\fR \- List installed gems with newer versions available +.SH "SYNOPSIS" +\fBbundle outdated\fR [GEM] [\-\-local] [\-\-pre] [\-\-source] [\-\-filter\-strict | \-\-strict] [\-\-update\-strict] [\-\-parseable | \-\-porcelain] [\-\-group=GROUP] [\-\-groups] [\-\-patch|\-\-minor|\-\-major] [\-\-filter\-major] [\-\-filter\-minor] [\-\-filter\-patch] [\-\-only\-explicit] [\-\-cooldown=NUMBER] +.SH "DESCRIPTION" +Outdated lists the names and versions of gems that have a newer version available in the given source\. Calling outdated with [GEM [GEM]] will only check for newer versions of the given gems\. Prerelease gems are ignored by default\. If your gems are up to date, Bundler will exit with a status of 0\. Otherwise, it will exit 1\. +.SH "OPTIONS" +.TP +\fB\-\-local\fR +Do not attempt to fetch gems remotely and use the gem cache instead\. +.TP +\fB\-\-pre\fR +Check for newer pre\-release gems\. +.TP +\fB\-\-source=<list>\fR +Check against a specific source\. +.TP +\fB\-\-filter\-strict\fR, \fB\-\-strict\fR +Only list newer versions allowed by your Gemfile requirements, also respecting conservative update flags (\-\-patch, \-\-minor, \-\-major)\. +.TP +\fB\-\-update\-strict\fR +Strict conservative resolution, do not allow any gem to be updated past latest \-\-patch | \-\-minor | \-\-major\. +.TP +\fB\-\-parseable\fR, \fB\-\-porcelain\fR +Use minimal formatting for more parseable output\. +.TP +\fB\-\-group=GROUP\fR +List gems from a specific group\. +.TP +\fB\-\-groups\fR +List gems organized by groups\. +.TP +\fB\-\-minor\fR +Prefer updating only to next minor version\. +.TP +\fB\-\-major\fR +Prefer updating to next major version (default)\. +.TP +\fB\-\-patch\fR +Prefer updating only to next patch version\. +.TP +\fB\-\-filter\-major\fR +Only list major newer versions\. +.TP +\fB\-\-filter\-minor\fR +Only list minor newer versions\. +.TP +\fB\-\-filter\-patch\fR +Only list patch newer versions\. +.TP +\fB\-\-only\-explicit\fR +Only list gems specified in your Gemfile, not their dependencies\. +.TP +\fB\-\-cooldown=<number>\fR +Annotate (rather than hide) versions that are still inside the cooldown window of \fInumber\fR days\. The prose output appends "in cooldown for Nd more days" and the table form adds "(cooldown Nd)" to the Latest column\. See \fBcooldown\fR in bundle\-config(1)\. +.SH "PATCH LEVEL OPTIONS" +See bundle update(1) \fIbundle\-update\.1\.html\fR for details\. +.SH "FILTERING OUTPUT" +The 3 filtering options do not affect the resolution of versions, merely what versions are shown in the output\. +.P +If the regular output shows the following: +.IP "" 4 +.nf +* Gem Current Latest Requested Groups Release Date +* faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test 2024\-02\-05 +* hashie 1\.2\.0 3\.4\.6 = 1\.2\.0 default 2023\-11\-10 +* headless 2\.2\.3 2\.3\.1 = 2\.2\.3 test 2022\-08\-19 +.fi +.IP "" 0 +.P +\fB\-\-filter\-major\fR would only show: +.IP "" 4 +.nf +* Gem Current Latest Requested Groups Release Date +* hashie 1\.2\.0 3\.4\.6 = 1\.2\.0 default 2023\-11\-10 +.fi +.IP "" 0 +.P +\fB\-\-filter\-minor\fR would only show: +.IP "" 4 +.nf +* Gem Current Latest Requested Groups Release Date +* headless 2\.2\.3 2\.3\.1 = 2\.2\.3 test 2022\-08\-19 +.fi +.IP "" 0 +.P +\fB\-\-filter\-patch\fR would only show: +.IP "" 4 +.nf +* Gem Current Latest Requested Groups Release Date +* faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test 2024\-02\-05 +.fi +.IP "" 0 +.P +Filter options can be combined\. \fB\-\-filter\-minor\fR and \fB\-\-filter\-patch\fR would show: +.IP "" 4 +.nf +* Gem Current Latest Requested Groups Release Date +* faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test 2024\-02\-05 +.fi +.IP "" 0 +.P +Combining all three \fBfilter\fR options would be the same result as providing none of them\. diff --git a/lib/bundler/man/bundle-outdated.1.ronn b/lib/bundler/man/bundle-outdated.1.ronn new file mode 100644 index 0000000000..e5badac2e9 --- /dev/null +++ b/lib/bundler/man/bundle-outdated.1.ronn @@ -0,0 +1,117 @@ +bundle-outdated(1) -- List installed gems with newer versions available +======================================================================= + +## SYNOPSIS + +`bundle outdated` [GEM] [--local] + [--pre] + [--source] + [--filter-strict | --strict] + [--update-strict] + [--parseable | --porcelain] + [--group=GROUP] + [--groups] + [--patch|--minor|--major] + [--filter-major] + [--filter-minor] + [--filter-patch] + [--only-explicit] + [--cooldown=NUMBER] + +## DESCRIPTION + +Outdated lists the names and versions of gems that have a newer version available +in the given source. Calling outdated with [GEM [GEM]] will only check for newer +versions of the given gems. Prerelease gems are ignored by default. If your gems +are up to date, Bundler will exit with a status of 0. Otherwise, it will exit 1. + +## OPTIONS + +* `--local`: + Do not attempt to fetch gems remotely and use the gem cache instead. + +* `--pre`: + Check for newer pre-release gems. + +* `--source=<list>`: + Check against a specific source. + +* `--filter-strict`, `--strict`: + Only list newer versions allowed by your Gemfile requirements, also respecting conservative update flags (--patch, --minor, --major). + +* `--update-strict`: + Strict conservative resolution, do not allow any gem to be updated past latest --patch | --minor | --major. + +* `--parseable`, `--porcelain`: + Use minimal formatting for more parseable output. + +* `--group=GROUP`: + List gems from a specific group. + +* `--groups`: + List gems organized by groups. + +* `--minor`: + Prefer updating only to next minor version. + +* `--major`: + Prefer updating to next major version (default). + +* `--patch`: + Prefer updating only to next patch version. + +* `--filter-major`: + Only list major newer versions. + +* `--filter-minor`: + Only list minor newer versions. + +* `--filter-patch`: + Only list patch newer versions. + +* `--only-explicit`: + Only list gems specified in your Gemfile, not their dependencies. + +* `--cooldown=<number>`: + Annotate (rather than hide) versions that are still inside the + cooldown window of <number> days. The prose output appends "in + cooldown for Nd more days" and the table form adds "(cooldown Nd)" to + the Latest column. See `cooldown` in bundle-config(1). + +## PATCH LEVEL OPTIONS + +See [bundle update(1)](bundle-update.1.html) for details. + +## FILTERING OUTPUT + +The 3 filtering options do not affect the resolution of versions, merely what versions are shown +in the output. + +If the regular output shows the following: + + * Gem Current Latest Requested Groups Release Date + * faker 1.6.5 1.6.6 ~> 1.4 development, test 2024-02-05 + * hashie 1.2.0 3.4.6 = 1.2.0 default 2023-11-10 + * headless 2.2.3 2.3.1 = 2.2.3 test 2022-08-19 + +`--filter-major` would only show: + + * Gem Current Latest Requested Groups Release Date + * hashie 1.2.0 3.4.6 = 1.2.0 default 2023-11-10 + +`--filter-minor` would only show: + + * Gem Current Latest Requested Groups Release Date + * headless 2.2.3 2.3.1 = 2.2.3 test 2022-08-19 + +`--filter-patch` would only show: + + * Gem Current Latest Requested Groups Release Date + * faker 1.6.5 1.6.6 ~> 1.4 development, test 2024-02-05 + +Filter options can be combined. `--filter-minor` and `--filter-patch` would show: + + * Gem Current Latest Requested Groups Release Date + * faker 1.6.5 1.6.6 ~> 1.4 development, test 2024-02-05 + +Combining all three `filter` options would be the same result as providing none of them. diff --git a/lib/bundler/man/bundle-platform.1 b/lib/bundler/man/bundle-platform.1 new file mode 100644 index 0000000000..39b7111263 --- /dev/null +++ b/lib/bundler/man/bundle-platform.1 @@ -0,0 +1,49 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-PLATFORM" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-platform\fR \- Displays platform compatibility information +.SH "SYNOPSIS" +\fBbundle platform\fR [\-\-ruby] +.SH "DESCRIPTION" +\fBplatform\fR displays information from your Gemfile, Gemfile\.lock, and Ruby VM about your platform\. +.P +For instance, using this Gemfile(5): +.IP "" 4 +.nf +source "https://rubygems\.org" + +ruby "3\.1\.2" + +gem "rack" +.fi +.IP "" 0 +.P +If you run \fBbundle platform\fR on Ruby 3\.1\.2, it displays the following output: +.IP "" 4 +.nf +Your platform is: x86_64\-linux + +Your app has gems that work on these platforms: +* arm64\-darwin\-21 +* ruby +* x64\-mingw\-ucrt +* x86_64\-linux + +Your Gemfile specifies a Ruby version requirement: +* ruby 3\.1\.2 + +Your current platform satisfies the Ruby version requirement\. +.fi +.IP "" 0 +.P +\fBplatform\fR lists all the platforms in your \fBGemfile\.lock\fR as well as the \fBruby\fR directive if applicable from your Gemfile(5)\. It also lets you know if the \fBruby\fR directive requirement has been met\. If \fBruby\fR directive doesn't match the running Ruby VM, it tells you what part does not\. +.SH "OPTIONS" +.TP +\fB\-\-ruby\fR +It will display the ruby directive information, so you don't have to parse it from the Gemfile(5)\. +.SH "SEE ALSO" +.IP "\(bu" 4 +bundle\-lock(1) \fIbundle\-lock\.1\.html\fR +.IP "" 0 + diff --git a/lib/bundler/man/bundle-platform.1.ronn b/lib/bundler/man/bundle-platform.1.ronn new file mode 100644 index 0000000000..744acd1b43 --- /dev/null +++ b/lib/bundler/man/bundle-platform.1.ronn @@ -0,0 +1,49 @@ +bundle-platform(1) -- Displays platform compatibility information +================================================================= + +## SYNOPSIS + +`bundle platform` [--ruby] + +## DESCRIPTION + +`platform` displays information from your Gemfile, Gemfile.lock, and Ruby +VM about your platform. + +For instance, using this Gemfile(5): + + source "https://rubygems.org" + + ruby "3.1.2" + + gem "rack" + +If you run `bundle platform` on Ruby 3.1.2, it displays the following output: + + Your platform is: x86_64-linux + + Your app has gems that work on these platforms: + * arm64-darwin-21 + * ruby + * x64-mingw-ucrt + * x86_64-linux + + Your Gemfile specifies a Ruby version requirement: + * ruby 3.1.2 + + Your current platform satisfies the Ruby version requirement. + +`platform` lists all the platforms in your `Gemfile.lock` as well as the +`ruby` directive if applicable from your Gemfile(5). It also lets you know +if the `ruby` directive requirement has been met. If `ruby` directive doesn't +match the running Ruby VM, it tells you what part does not. + +## OPTIONS + +* `--ruby`: + It will display the ruby directive information, so you don't have to + parse it from the Gemfile(5). + +## SEE ALSO + +* [bundle-lock(1)](bundle-lock.1.html) diff --git a/lib/bundler/man/bundle-plugin.1 b/lib/bundler/man/bundle-plugin.1 new file mode 100644 index 0000000000..d182c7789b --- /dev/null +++ b/lib/bundler/man/bundle-plugin.1 @@ -0,0 +1,76 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-PLUGIN" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-plugin\fR \- Manage Bundler plugins +.SH "SYNOPSIS" +\fBbundle plugin\fR install PLUGINS [\-\-source=SOURCE] [\-\-version=VERSION] [\-\-git=GIT] [\-\-branch=BRANCH|\-\-ref=REF] [\-\-path=PATH] +.br +\fBbundle plugin\fR uninstall PLUGINS [\-\-all] +.br +\fBbundle plugin\fR list +.br +\fBbundle plugin\fR help [COMMAND] +.SH "DESCRIPTION" +You can install, uninstall, and list plugin(s) with this command to extend functionalities of Bundler\. +.SH "SUB\-COMMANDS" +.SS "install" +Install the given plugin(s)\. +.P +For example, \fBbundle plugin install bundler\-graph\fR will install bundler\-graph gem from globally configured sources (defaults to RubyGems\.org)\. Note that the global source specified in Gemfile is ignored\. +.P +\fBOPTIONS\fR +.TP +\fB\-\-source=SOURCE\fR +Install the plugin gem from a specific source, rather than from globally configured sources\. +.IP +Example: \fBbundle plugin install bundler\-graph \-\-source https://example\.com\fR +.TP +\fB\-\-version=VERSION\fR +Specify a version of the plugin gem to install via \fB\-\-version\fR\. +.IP +Example: \fBbundle plugin install bundler\-graph \-\-version 0\.2\.1\fR +.TP +\fB\-\-git=GIT\fR +Install the plugin gem from a Git repository\. You can use standard Git URLs like: +.IP +\fBssh://[user@]host\.xz[:port]/path/to/repo\.git\fR +.br +\fBhttp[s]://host\.xz[:port]/path/to/repo\.git\fR +.br +\fB/path/to/repo\fR +.br +\fBfile:///path/to/repo\fR +.IP +Example: \fBbundle plugin install bundler\-graph \-\-git https://github\.com/rubygems/bundler\-graph\fR +.TP +\fB\-\-branch=BRANCH\fR +When you specify \fB\-\-git\fR, you can use \fB\-\-branch\fR to use\. +.TP +\fB\-\-ref=REF\fR +When you specify \fB\-\-git\fR, you can use \fB\-\-ref\fR to specify any tag, or commit hash (revision) to use\. +.TP +\fB\-\-path=PATH\fR +Install the plugin gem from a local path\. +.IP +Example: \fBbundle plugin install bundler\-graph \-\-path \.\./bundler\-graph\fR +.SS "uninstall" +Uninstall the plugin(s) specified in PLUGINS\. +.P +\fBOPTIONS\fR +.TP +\fB\-\-all\fR +Uninstall all the installed plugins\. If no plugin is installed, then it does nothing\. +.SS "list" +List the installed plugins and available commands\. +.P +No options\. +.SS "help" +Describe subcommands or one specific subcommand\. +.P +No options\. +.SH "SEE ALSO" +.IP "\(bu" 4 +How to write a Bundler plugin \fIhttps://bundler\.io/guides/bundler_plugins\.html\fR +.IP "" 0 + diff --git a/lib/bundler/man/bundle-plugin.1.ronn b/lib/bundler/man/bundle-plugin.1.ronn new file mode 100644 index 0000000000..b54e0c08b4 --- /dev/null +++ b/lib/bundler/man/bundle-plugin.1.ronn @@ -0,0 +1,84 @@ +bundle-plugin(1) -- Manage Bundler plugins +========================================== + +## SYNOPSIS + +`bundle plugin` install PLUGINS [--source=SOURCE] [--version=VERSION] + [--git=GIT] [--branch=BRANCH|--ref=REF] + [--path=PATH]<br> +`bundle plugin` uninstall PLUGINS [--all]<br> +`bundle plugin` list<br> +`bundle plugin` help [COMMAND] + +## DESCRIPTION + +You can install, uninstall, and list plugin(s) with this command to extend functionalities of Bundler. + +## SUB-COMMANDS + +### install + +Install the given plugin(s). + +For example, `bundle plugin install bundler-graph` will install bundler-graph +gem from globally configured sources (defaults to RubyGems.org). Note that the +global source specified in Gemfile is ignored. + +**OPTIONS** + +* `--source=SOURCE`: + Install the plugin gem from a specific source, rather than from globally configured sources. + + Example: `bundle plugin install bundler-graph --source https://example.com` + +* `--version=VERSION`: + Specify a version of the plugin gem to install via `--version`. + + Example: `bundle plugin install bundler-graph --version 0.2.1` + +* `--git=GIT`: + Install the plugin gem from a Git repository. You can use standard Git URLs like: + + `ssh://[user@]host.xz[:port]/path/to/repo.git`<br> + `http[s]://host.xz[:port]/path/to/repo.git`<br> + `/path/to/repo`<br> + `file:///path/to/repo` + + Example: `bundle plugin install bundler-graph --git https://github.com/rubygems/bundler-graph` + +* `--branch=BRANCH`: + When you specify `--git`, you can use `--branch` to use. + +* `--ref=REF`: + When you specify `--git`, you can use `--ref` to specify any tag, or commit + hash (revision) to use. + +* `--path=PATH`: + Install the plugin gem from a local path. + + Example: `bundle plugin install bundler-graph --path ../bundler-graph` + +### uninstall + +Uninstall the plugin(s) specified in PLUGINS. + +**OPTIONS** + +* `--all`: + Uninstall all the installed plugins. If no plugin is installed, then it does nothing. + +### list + +List the installed plugins and available commands. + +No options. + +### help + +Describe subcommands or one specific subcommand. + +No options. + +## SEE ALSO + +* [How to write a Bundler plugin](https://bundler.io/guides/bundler_plugins.html) diff --git a/lib/bundler/man/bundle-pristine.1 b/lib/bundler/man/bundle-pristine.1 new file mode 100644 index 0000000000..f6cc066571 --- /dev/null +++ b/lib/bundler/man/bundle-pristine.1 @@ -0,0 +1,23 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-PRISTINE" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-pristine\fR \- Restores installed gems to their pristine condition +.SH "SYNOPSIS" +\fBbundle pristine\fR +.SH "DESCRIPTION" +\fBpristine\fR restores the installed gems in the bundle to their pristine condition using the local gem cache from RubyGems\. For git gems, a forced checkout will be performed\. +.P +For further explanation, \fBbundle pristine\fR ignores unpacked files on disk\. In other words, this command utilizes the local \fB\.gem\fR cache or the gem's git repository as if one were installing from scratch\. +.P +Note: the Bundler gem cannot be restored to its original state with \fBpristine\fR\. One also cannot use \fBbundle pristine\fR on gems with a 'path' option in the Gemfile, because bundler has no original copy it can restore from\. +.P +When is it practical to use \fBbundle pristine\fR? +.P +It comes in handy when a developer is debugging a gem\. \fBbundle pristine\fR is a great way to get rid of experimental changes to a gem that one may not want\. +.P +Why use \fBbundle pristine\fR over \fBgem pristine \-\-all\fR? +.P +Both commands are very similar\. For context: \fBbundle pristine\fR, without arguments, cleans all gems from the lockfile\. Meanwhile, \fBgem pristine \-\-all\fR cleans all installed gems for that Ruby version\. +.P +If a developer forgets which gems in their project they might have been debugging, the Rubygems \fBgem pristine [GEMNAME]\fR command may be inconvenient\. One can avoid waiting for \fBgem pristine \-\-all\fR, and instead run \fBbundle pristine\fR\. diff --git a/lib/bundler/man/bundle-pristine.1.ronn b/lib/bundler/man/bundle-pristine.1.ronn new file mode 100644 index 0000000000..984debeb3d --- /dev/null +++ b/lib/bundler/man/bundle-pristine.1.ronn @@ -0,0 +1,34 @@ +bundle-pristine(1) -- Restores installed gems to their pristine condition +========================================================================= + +## SYNOPSIS + +`bundle pristine` + +## DESCRIPTION + +`pristine` restores the installed gems in the bundle to their pristine condition +using the local gem cache from RubyGems. For git gems, a forced checkout will be performed. + +For further explanation, `bundle pristine` ignores unpacked files on disk. In other +words, this command utilizes the local `.gem` cache or the gem's git repository +as if one were installing from scratch. + +Note: the Bundler gem cannot be restored to its original state with `pristine`. +One also cannot use `bundle pristine` on gems with a 'path' option in the Gemfile, +because bundler has no original copy it can restore from. + +When is it practical to use `bundle pristine`? + +It comes in handy when a developer is debugging a gem. `bundle pristine` is a +great way to get rid of experimental changes to a gem that one may not want. + +Why use `bundle pristine` over `gem pristine --all`? + +Both commands are very similar. +For context: `bundle pristine`, without arguments, cleans all gems from the lockfile. +Meanwhile, `gem pristine --all` cleans all installed gems for that Ruby version. + +If a developer forgets which gems in their project they might +have been debugging, the Rubygems `gem pristine [GEMNAME]` command may be inconvenient. +One can avoid waiting for `gem pristine --all`, and instead run `bundle pristine`. diff --git a/lib/bundler/man/bundle-remove.1 b/lib/bundler/man/bundle-remove.1 new file mode 100644 index 0000000000..2ca40e74db --- /dev/null +++ b/lib/bundler/man/bundle-remove.1 @@ -0,0 +1,15 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-REMOVE" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-remove\fR \- Removes gems from the Gemfile +.SH "SYNOPSIS" +`bundle remove [GEM [GEM \|\.\|\.\|\.]] +.SH "DESCRIPTION" +Removes the given gems from the Gemfile while ensuring that the resulting Gemfile is still valid\. If a gem cannot be removed, a warning is printed\. If a gem is already absent from the Gemfile, and error is raised\. +.P +Example: +.P +bundle remove rails +.P +bundle remove rails rack diff --git a/lib/bundler/man/bundle-remove.1.ronn b/lib/bundler/man/bundle-remove.1.ronn new file mode 100644 index 0000000000..49cb4dc1fd --- /dev/null +++ b/lib/bundler/man/bundle-remove.1.ronn @@ -0,0 +1,16 @@ +bundle-remove(1) -- Removes gems from the Gemfile +================================================= + +## SYNOPSIS + +`bundle remove [GEM [GEM ...]] + +## DESCRIPTION + +Removes the given gems from the Gemfile while ensuring that the resulting Gemfile is still valid. If a gem cannot be removed, a warning is printed. If a gem is already absent from the Gemfile, and error is raised. + +Example: + +bundle remove rails + +bundle remove rails rack diff --git a/lib/bundler/man/bundle-show.1 b/lib/bundler/man/bundle-show.1 new file mode 100644 index 0000000000..a2142694b8 --- /dev/null +++ b/lib/bundler/man/bundle-show.1 @@ -0,0 +1,16 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-SHOW" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-show\fR \- Shows all the gems in your bundle, or the path to a gem +.SH "SYNOPSIS" +\fBbundle show\fR [GEM] [\-\-paths] +.SH "DESCRIPTION" +Without the [GEM] option, \fBshow\fR will print a list of the names and versions of all gems that are required by your [\fBGemfile(5)\fR][Gemfile(5)], sorted by name\. +.P +Calling show with [GEM] will list the exact location of that gem on your machine\. +.SH "OPTIONS" +.TP +\fB\-\-paths\fR +List the paths of all gems that are required by your [\fBGemfile(5)\fR][Gemfile(5)], sorted by gem name\. + diff --git a/lib/bundler/man/bundle-show.1.ronn b/lib/bundler/man/bundle-show.1.ronn new file mode 100644 index 0000000000..a6a59a1445 --- /dev/null +++ b/lib/bundler/man/bundle-show.1.ronn @@ -0,0 +1,21 @@ +bundle-show(1) -- Shows all the gems in your bundle, or the path to a gem +========================================================================= + +## SYNOPSIS + +`bundle show` [GEM] + [--paths] + +## DESCRIPTION + +Without the [GEM] option, `show` will print a list of the names and versions of +all gems that are required by your [`Gemfile(5)`][Gemfile(5)], sorted by name. + +Calling show with [GEM] will list the exact location of that gem on your +machine. + +## OPTIONS + +* `--paths`: + List the paths of all gems that are required by your [`Gemfile(5)`][Gemfile(5)], + sorted by gem name. diff --git a/lib/bundler/man/bundle-update.1 b/lib/bundler/man/bundle-update.1 new file mode 100644 index 0000000000..94161083fc --- /dev/null +++ b/lib/bundler/man/bundle-update.1 @@ -0,0 +1,284 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-UPDATE" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-update\fR \- Update your gems to the latest available versions +.SH "SYNOPSIS" +\fBbundle update\fR \fI*gems\fR [\-\-all] [\-\-group=NAME] [\-\-source=NAME] [\-\-local] [\-\-ruby] [\-\-bundler[=VERSION]] [\-\-cooldown=NUMBER] [\-\-force] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-quiet] [\-\-patch|\-\-minor|\-\-major] [\-\-pre] [\-\-strict] [\-\-conservative] +.SH "DESCRIPTION" +Update the gems specified (all gems, if \fB\-\-all\fR flag is used), ignoring the previously installed gems specified in the \fBGemfile\.lock\fR\. In general, you should use bundle install(1) \fIbundle\-install\.1\.html\fR to install the same exact gems and versions across machines\. +.P +You would use \fBbundle update\fR to explicitly update the version of a gem\. +.SH "OPTIONS" +.TP +\fB\-\-all\fR +Update all gems specified in Gemfile\. +.TP +\fB\-\-group=<list>\fR, \fB\-g=<list>\fR +Only update the gems in the specified group\. For instance, you can update all gems in the development group with \fBbundle update \-\-group development\fR\. You can also call \fBbundle update rails \-\-group test\fR to update the rails gem and all gems in the test group, for example\. +.TP +\fB\-\-source=<list>\fR +The name of a \fB:git\fR or \fB:path\fR source used in the Gemfile(5)\. For instance, with a \fB:git\fR source of \fBhttp://github\.com/rails/rails\.git\fR, you would call \fBbundle update \-\-source rails\fR +.TP +\fB\-\-local\fR +Do not attempt to fetch gems remotely and use the gem cache instead\. +.TP +\fB\-\-ruby\fR +Update the locked version of Ruby to the current version of Ruby\. +.TP +\fB\-\-bundler[=BUNDLER]\fR +Update the locked version of bundler to the invoked bundler version\. +.TP +\fB\-\-force\fR, \fB\-\-redownload\fR +Force reinstalling every gem, even if already installed\. +.TP +\fB\-\-full\-index\fR +Fall back to using the single\-file index of all gems\. +.TP +\fB\-\-gemfile=GEMFILE\fR +Use the specified gemfile instead of [\fBGemfile(5)\fR][Gemfile(5)]\. +.TP +\fB\-\-jobs=<number>\fR, \fB\-j=<number>\fR +Specify the number of jobs to run in parallel\. The default is the number of available processors\. +.TP +\fB\-\-retry=[<number>]\fR +Retry failed network or git requests for \fInumber\fR times\. +.TP +\fB\-\-quiet\fR +Only output warnings and errors\. +.TP +\fB\-\-patch\fR +Prefer updating only to next patch version\. +.TP +\fB\-\-minor\fR +Prefer updating only to next minor version\. +.TP +\fB\-\-major\fR +Prefer updating to next major version (default)\. +.TP +\fB\-\-pre\fR +Always choose the highest allowed version, regardless of prerelease status\. +.TP +\fB\-\-strict\fR +Do not allow any gem to be updated past latest \fB\-\-patch\fR | \fB\-\-minor\fR | \fB\-\-major\fR\. +.TP +\fB\-\-conservative\fR +Use bundle install conservative update behavior and do not allow indirect dependencies to be updated\. +.TP +\fB\-\-cooldown=<number>\fR +Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. Combine with \fB\-\-conservative\fR to minimize transitive churn when bypassing cooldown for an urgent update\. See \fBcooldown\fR in bundle\-config(1)\. +.SH "UPDATING ALL GEMS" +If you run \fBbundle update \-\-all\fR, bundler will ignore any previously installed gems and resolve all dependencies again based on the latest versions of all gems available in the sources\. +.P +Consider the following Gemfile(5): +.IP "" 4 +.nf +source "https://rubygems\.org" + +gem "rails", "3\.0\.0\.rc" +gem "nokogiri" +.fi +.IP "" 0 +.P +When you run bundle install(1) \fIbundle\-install\.1\.html\fR the first time, bundler will resolve all of the dependencies, all the way down, and install what you need: +.IP "" 4 +.nf +Fetching gem metadata from https://rubygems\.org/\|\.\|\.\|\.\|\.\|\.\|\.\|\.\|\.\|\. +Resolving dependencies\|\.\|\.\|\. +Installing builder 2\.1\.2 +Installing abstract 1\.0\.0 +Installing rack 1\.2\.8 +Using bundler 1\.7\.6 +Installing rake 10\.4\.0 +Installing polyglot 0\.3\.5 +Installing mime\-types 1\.25\.1 +Installing i18n 0\.4\.2 +Installing mini_portile 0\.6\.1 +Installing tzinfo 0\.3\.42 +Installing rack\-mount 0\.6\.14 +Installing rack\-test 0\.5\.7 +Installing treetop 1\.4\.15 +Installing thor 0\.14\.6 +Installing activesupport 3\.0\.0\.rc +Installing erubis 2\.6\.6 +Installing activemodel 3\.0\.0\.rc +Installing arel 0\.4\.0 +Installing mail 2\.2\.20 +Installing activeresource 3\.0\.0\.rc +Installing actionpack 3\.0\.0\.rc +Installing activerecord 3\.0\.0\.rc +Installing actionmailer 3\.0\.0\.rc +Installing railties 3\.0\.0\.rc +Installing rails 3\.0\.0\.rc +Installing nokogiri 1\.6\.5 + +Bundle complete! 2 Gemfile dependencies, 26 gems total\. +Use `bundle show [gemname]` to see where a bundled gem is installed\. +.fi +.IP "" 0 +.P +As you can see, even though you have two gems in the Gemfile(5), your application needs 26 different gems in order to run\. Bundler remembers the exact versions it installed in \fBGemfile\.lock\fR\. The next time you run bundle install(1) \fIbundle\-install\.1\.html\fR, bundler skips the dependency resolution and installs the same gems as it installed last time\. +.P +After checking in the \fBGemfile\.lock\fR into version control and cloning it on another machine, running bundle install(1) \fIbundle\-install\.1\.html\fR will \fIstill\fR install the gems that you installed last time\. You don't need to worry that a new release of \fBerubis\fR or \fBmail\fR changes the gems you use\. +.P +However, from time to time, you might want to update the gems you are using to the newest versions that still match the gems in your Gemfile(5)\. +.P +To do this, run \fBbundle update \-\-all\fR, which will ignore the \fBGemfile\.lock\fR, and resolve all the dependencies again\. Keep in mind that this process can result in a significantly different set of the 25 gems, based on the requirements of new gems that the gem authors released since the last time you ran \fBbundle update \-\-all\fR\. +.SH "UPDATING A LIST OF GEMS" +Sometimes, you want to update a single gem in the Gemfile(5), and leave the rest of the gems that you specified locked to the versions in the \fBGemfile\.lock\fR\. +.P +For instance, in the scenario above, imagine that \fBnokogiri\fR releases version \fB1\.4\.4\fR, and you want to update it \fIwithout\fR updating Rails and all of its dependencies\. To do this, run \fBbundle update nokogiri\fR\. +.P +Bundler will update \fBnokogiri\fR and any of its dependencies, but leave alone Rails and its dependencies\. +.SH "OVERLAPPING DEPENDENCIES" +Sometimes, multiple gems declared in your Gemfile(5) are satisfied by the same second\-level dependency\. For instance, consider the case of \fBthin\fR and \fBrack\-perftools\-profiler\fR\. +.IP "" 4 +.nf +source "https://rubygems\.org" + +gem "thin" +gem "rack\-perftools\-profiler" +.fi +.IP "" 0 +.P +The \fBthin\fR gem depends on \fBrack >= 1\.0\fR, while \fBrack\-perftools\-profiler\fR depends on \fBrack ~> 1\.0\fR\. If you run bundle install, you get: +.IP "" 4 +.nf +Fetching source index for https://rubygems\.org/ +Installing daemons (1\.1\.0) +Installing eventmachine (0\.12\.10) with native extensions +Installing open4 (1\.0\.1) +Installing perftools\.rb (0\.4\.7) with native extensions +Installing rack (1\.2\.1) +Installing rack\-perftools_profiler (0\.0\.2) +Installing thin (1\.2\.7) with native extensions +Using bundler (1\.0\.0\.rc\.3) +.fi +.IP "" 0 +.P +In this case, the two gems have their own set of dependencies, but they share \fBrack\fR in common\. If you run \fBbundle update thin\fR, bundler will update \fBdaemons\fR, \fBeventmachine\fR and \fBrack\fR, which are dependencies of \fBthin\fR, but not \fBopen4\fR or \fBperftools\.rb\fR, which are dependencies of \fBrack\-perftools_profiler\fR\. Note that \fBbundle update thin\fR will update \fBrack\fR even though it's \fIalso\fR a dependency of \fBrack\-perftools_profiler\fR\. +.P +In short, by default, when you update a gem using \fBbundle update\fR, bundler will update all dependencies of that gem, including those that are also dependencies of another gem\. +.P +To prevent updating indirect dependencies, prior to version 1\.14 the only option was the \fBCONSERVATIVE UPDATING\fR behavior in bundle install(1) \fIbundle\-install\.1\.html\fR: +.P +In this scenario, updating the \fBthin\fR version manually in the Gemfile(5), and then running bundle install(1) \fIbundle\-install\.1\.html\fR will only update \fBdaemons\fR and \fBeventmachine\fR, but not \fBrack\fR\. For more information, see the \fBCONSERVATIVE UPDATING\fR section of bundle install(1) \fIbundle\-install\.1\.html\fR\. +.P +Starting with 1\.14, specifying the \fB\-\-conservative\fR option will also prevent indirect dependencies from being updated\. +.SH "PATCH LEVEL OPTIONS" +Version 1\.14 introduced 4 patch\-level options that will influence how gem versions are resolved\. One of the following options can be used: \fB\-\-patch\fR, \fB\-\-minor\fR or \fB\-\-major\fR\. \fB\-\-strict\fR can be added to further influence resolution\. +.TP +\fB\-\-patch\fR +Prefer updating only to next patch version\. +.TP +\fB\-\-minor\fR +Prefer updating only to next minor version\. +.TP +\fB\-\-major\fR +Prefer updating to next major version (default)\. +.TP +\fB\-\-strict\fR +Do not allow any gem to be updated past latest \fB\-\-patch\fR | \fB\-\-minor\fR | \fB\-\-major\fR\. +.P +When Bundler is resolving what versions to use to satisfy declared requirements in the Gemfile or in parent gems, it looks up all available versions, filters out any versions that don't satisfy the requirement, and then, by default, sorts them from newest to oldest, considering them in that order\. +.P +Providing one of the patch level options (e\.g\. \fB\-\-patch\fR) changes the sort order of the satisfying versions, causing Bundler to consider the latest \fB\-\-patch\fR or \fB\-\-minor\fR version available before other versions\. Note that versions outside the stated patch level could still be resolved to if necessary to find a suitable dependency graph\. +.P +For example, if gem 'foo' is locked at 1\.0\.2, with no gem requirement defined in the Gemfile, and versions 1\.0\.3, 1\.0\.4, 1\.1\.0, 1\.1\.1, 2\.0\.0 all exist, the default order of preference by default (\fB\-\-major\fR) will be "2\.0\.0, 1\.1\.1, 1\.1\.0, 1\.0\.4, 1\.0\.3, 1\.0\.2"\. +.P +If the \fB\-\-patch\fR option is used, the order of preference will change to "1\.0\.4, 1\.0\.3, 1\.0\.2, 1\.1\.1, 1\.1\.0, 2\.0\.0"\. +.P +If the \fB\-\-minor\fR option is used, the order of preference will change to "1\.1\.1, 1\.1\.0, 1\.0\.4, 1\.0\.3, 1\.0\.2, 2\.0\.0"\. +.P +Combining the \fB\-\-strict\fR option with any of the patch level options will remove any versions beyond the scope of the patch level option, to ensure that no gem is updated that far\. +.P +To continue the previous example, if both \fB\-\-patch\fR and \fB\-\-strict\fR options are used, the available versions for resolution would be "1\.0\.4, 1\.0\.3, 1\.0\.2"\. If \fB\-\-minor\fR and \fB\-\-strict\fR are used, it would be "1\.1\.1, 1\.1\.0, 1\.0\.4, 1\.0\.3, 1\.0\.2"\. +.P +Gem requirements as defined in the Gemfile will still be the first determining factor for what versions are available\. If the gem requirement for \fBfoo\fR in the Gemfile is '~> 1\.0', that will accomplish the same thing as providing the \fB\-\-minor\fR and \fB\-\-strict\fR options\. +.SH "PATCH LEVEL EXAMPLES" +Given the following gem specifications: +.IP "" 4 +.nf +foo 1\.4\.3, requires: ~> bar 2\.0 +foo 1\.4\.4, requires: ~> bar 2\.0 +foo 1\.4\.5, requires: ~> bar 2\.1 +foo 1\.5\.0, requires: ~> bar 2\.1 +foo 1\.5\.1, requires: ~> bar 3\.0 +bar with versions 2\.0\.3, 2\.0\.4, 2\.1\.0, 2\.1\.1, 3\.0\.0 +.fi +.IP "" 0 +.P +Gemfile: +.IP "" 4 +.nf +gem 'foo' +.fi +.IP "" 0 +.P +Gemfile\.lock: +.IP "" 4 +.nf +foo (1\.4\.3) + bar (~> 2\.0) +bar (2\.0\.3) +.fi +.IP "" 0 +.P +Cases: +.IP "" 4 +.nf +# Command Line Result +\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\- +1 bundle update \-\-patch 'foo 1\.4\.5', 'bar 2\.1\.1' +2 bundle update \-\-patch foo 'foo 1\.4\.5', 'bar 2\.1\.1' +3 bundle update \-\-minor 'foo 1\.5\.1', 'bar 3\.0\.0' +4 bundle update \-\-minor \-\-strict 'foo 1\.5\.0', 'bar 2\.1\.1' +5 bundle update \-\-patch \-\-strict 'foo 1\.4\.4', 'bar 2\.0\.4' +.fi +.IP "" 0 +.P +In case 1, bar is upgraded to 2\.1\.1, a minor version increase, because the dependency from foo 1\.4\.5 required it\. +.P +In case 2, only foo is requested to be unlocked, but bar is also allowed to move because it's not a declared dependency in the Gemfile\. +.P +In case 3, bar goes up a whole major release, because a minor increase is preferred now for foo, and when it goes to 1\.5\.1, it requires 3\.0\.0 of bar\. +.P +In case 4, foo is preferred up to a minor version, but 1\.5\.1 won't work because the \-\-strict flag removes bar 3\.0\.0 from consideration since it's a major increment\. +.P +In case 5, both foo and bar have any minor or major increments removed from consideration because of the \-\-strict flag, so the most they can move is up to 1\.4\.4 and 2\.0\.4\. +.SH "RECOMMENDED WORKFLOW" +In general, when working with an application managed with bundler, you should use the following workflow: +.IP "\(bu" 4 +After you create your Gemfile(5) for the first time, run +.IP +$ bundle install +.IP "\(bu" 4 +Check the resulting \fBGemfile\.lock\fR into version control +.IP +$ git add Gemfile\.lock +.IP "\(bu" 4 +When checking out this repository on another development machine, run +.IP +$ bundle install +.IP "\(bu" 4 +When checking out this repository on a deployment machine, run +.IP +$ bundle install \-\-deployment +.IP "\(bu" 4 +After changing the Gemfile(5) to reflect a new or update dependency, run +.IP +$ bundle install +.IP "\(bu" 4 +Make sure to check the updated \fBGemfile\.lock\fR into version control +.IP +$ git add Gemfile\.lock +.IP "\(bu" 4 +If bundle install(1) \fIbundle\-install\.1\.html\fR reports a conflict, manually update the specific gems that you changed in the Gemfile(5) +.IP +$ bundle update rails thin +.IP "\(bu" 4 +If you want to update all the gems to the latest possible versions that still match the gems listed in the Gemfile(5), run +.IP +$ bundle update \-\-all +.IP "" 0 + diff --git a/lib/bundler/man/bundle-update.1.ronn b/lib/bundler/man/bundle-update.1.ronn new file mode 100644 index 0000000000..72fbf054d1 --- /dev/null +++ b/lib/bundler/man/bundle-update.1.ronn @@ -0,0 +1,367 @@ +bundle-update(1) -- Update your gems to the latest available versions +===================================================================== + +## SYNOPSIS + +`bundle update` <*gems> [--all] + [--group=NAME] + [--source=NAME] + [--local] + [--ruby] + [--bundler[=VERSION]] + [--cooldown=NUMBER] + [--force] + [--full-index] + [--gemfile=GEMFILE] + [--jobs=NUMBER] + [--quiet] + [--patch|--minor|--major] + [--pre] + [--strict] + [--conservative] + +## DESCRIPTION + +Update the gems specified (all gems, if `--all` flag is used), ignoring +the previously installed gems specified in the `Gemfile.lock`. In +general, you should use [bundle install(1)](bundle-install.1.html) to install the same exact +gems and versions across machines. + +You would use `bundle update` to explicitly update the version of a +gem. + +## OPTIONS + +* `--all`: + Update all gems specified in Gemfile. + +* `--group=<list>`, `-g=<list>`: + Only update the gems in the specified group. For instance, you can update all gems + in the development group with `bundle update --group development`. You can also + call `bundle update rails --group test` to update the rails gem and all gems in + the test group, for example. + +* `--source=<list>`: + The name of a `:git` or `:path` source used in the Gemfile(5). For + instance, with a `:git` source of `http://github.com/rails/rails.git`, + you would call `bundle update --source rails` + +* `--local`: + Do not attempt to fetch gems remotely and use the gem cache instead. + +* `--ruby`: + Update the locked version of Ruby to the current version of Ruby. + +* `--bundler[=BUNDLER]`: + Update the locked version of bundler to the invoked bundler version. + +* `--force`, `--redownload`: + Force reinstalling every gem, even if already installed. + +* `--full-index`: + Fall back to using the single-file index of all gems. + +* `--gemfile=GEMFILE`: + Use the specified gemfile instead of [`Gemfile(5)`][Gemfile(5)]. + +* `--jobs=<number>`, `-j=<number>`: + Specify the number of jobs to run in parallel. The default is the number of + available processors. + +* `--retry=[<number>]`: + Retry failed network or git requests for <number> times. + +* `--quiet`: + Only output warnings and errors. + +* `--patch`: + Prefer updating only to next patch version. + +* `--minor`: + Prefer updating only to next minor version. + +* `--major`: + Prefer updating to next major version (default). + +* `--pre`: + Always choose the highest allowed version, regardless of prerelease status. + +* `--strict`: + Do not allow any gem to be updated past latest `--patch` | `--minor` | `--major`. + +* `--conservative`: + Use bundle install conservative update behavior and do not allow indirect dependencies to be updated. + +* `--cooldown=<number>`: + Only consider gem versions published at least <number> days ago when + resolving. Pass `0` to disable cooldown for this run, overriding any + per-source or global configuration. Combine with `--conservative` to + minimize transitive churn when bypassing cooldown for an urgent + update. See `cooldown` in bundle-config(1). + +## UPDATING ALL GEMS + +If you run `bundle update --all`, bundler will ignore +any previously installed gems and resolve all dependencies again +based on the latest versions of all gems available in the sources. + +Consider the following Gemfile(5): + + source "https://rubygems.org" + + gem "rails", "3.0.0.rc" + gem "nokogiri" + +When you run [bundle install(1)](bundle-install.1.html) the first time, bundler will resolve +all of the dependencies, all the way down, and install what you need: + + Fetching gem metadata from https://rubygems.org/......... + Resolving dependencies... + Installing builder 2.1.2 + Installing abstract 1.0.0 + Installing rack 1.2.8 + Using bundler 1.7.6 + Installing rake 10.4.0 + Installing polyglot 0.3.5 + Installing mime-types 1.25.1 + Installing i18n 0.4.2 + Installing mini_portile 0.6.1 + Installing tzinfo 0.3.42 + Installing rack-mount 0.6.14 + Installing rack-test 0.5.7 + Installing treetop 1.4.15 + Installing thor 0.14.6 + Installing activesupport 3.0.0.rc + Installing erubis 2.6.6 + Installing activemodel 3.0.0.rc + Installing arel 0.4.0 + Installing mail 2.2.20 + Installing activeresource 3.0.0.rc + Installing actionpack 3.0.0.rc + Installing activerecord 3.0.0.rc + Installing actionmailer 3.0.0.rc + Installing railties 3.0.0.rc + Installing rails 3.0.0.rc + Installing nokogiri 1.6.5 + + Bundle complete! 2 Gemfile dependencies, 26 gems total. + Use `bundle show [gemname]` to see where a bundled gem is installed. + +As you can see, even though you have two gems in the Gemfile(5), your application +needs 26 different gems in order to run. Bundler remembers the exact versions +it installed in `Gemfile.lock`. The next time you run [bundle install(1)](bundle-install.1.html), bundler skips +the dependency resolution and installs the same gems as it installed last time. + +After checking in the `Gemfile.lock` into version control and cloning it on another +machine, running [bundle install(1)](bundle-install.1.html) will _still_ install the gems that you installed +last time. You don't need to worry that a new release of `erubis` or `mail` changes +the gems you use. + +However, from time to time, you might want to update the gems you are using to the +newest versions that still match the gems in your Gemfile(5). + +To do this, run `bundle update --all`, which will ignore the `Gemfile.lock`, and resolve +all the dependencies again. Keep in mind that this process can result in a significantly +different set of the 25 gems, based on the requirements of new gems that the gem +authors released since the last time you ran `bundle update --all`. + +## UPDATING A LIST OF GEMS + +Sometimes, you want to update a single gem in the Gemfile(5), and leave the rest of the +gems that you specified locked to the versions in the `Gemfile.lock`. + +For instance, in the scenario above, imagine that `nokogiri` releases version `1.4.4`, and +you want to update it _without_ updating Rails and all of its dependencies. To do this, +run `bundle update nokogiri`. + +Bundler will update `nokogiri` and any of its dependencies, but leave alone Rails and +its dependencies. + +## OVERLAPPING DEPENDENCIES + +Sometimes, multiple gems declared in your Gemfile(5) are satisfied by the same +second-level dependency. For instance, consider the case of `thin` and +`rack-perftools-profiler`. + + source "https://rubygems.org" + + gem "thin" + gem "rack-perftools-profiler" + +The `thin` gem depends on `rack >= 1.0`, while `rack-perftools-profiler` depends +on `rack ~> 1.0`. If you run bundle install, you get: + + Fetching source index for https://rubygems.org/ + Installing daemons (1.1.0) + Installing eventmachine (0.12.10) with native extensions + Installing open4 (1.0.1) + Installing perftools.rb (0.4.7) with native extensions + Installing rack (1.2.1) + Installing rack-perftools_profiler (0.0.2) + Installing thin (1.2.7) with native extensions + Using bundler (1.0.0.rc.3) + +In this case, the two gems have their own set of dependencies, but they share +`rack` in common. If you run `bundle update thin`, bundler will update `daemons`, +`eventmachine` and `rack`, which are dependencies of `thin`, but not `open4` or +`perftools.rb`, which are dependencies of `rack-perftools_profiler`. Note that +`bundle update thin` will update `rack` even though it's _also_ a dependency of +`rack-perftools_profiler`. + +In short, by default, when you update a gem using `bundle update`, bundler will +update all dependencies of that gem, including those that are also dependencies +of another gem. + +To prevent updating indirect dependencies, prior to version 1.14 the only option +was the `CONSERVATIVE UPDATING` behavior in [bundle install(1)](bundle-install.1.html): + +In this scenario, updating the `thin` version manually in the Gemfile(5), +and then running [bundle install(1)](bundle-install.1.html) will only update `daemons` and `eventmachine`, +but not `rack`. For more information, see the `CONSERVATIVE UPDATING` section +of [bundle install(1)](bundle-install.1.html). + +Starting with 1.14, specifying the `--conservative` option will also prevent indirect +dependencies from being updated. + +## PATCH LEVEL OPTIONS + +Version 1.14 introduced 4 patch-level options that will influence how gem +versions are resolved. One of the following options can be used: `--patch`, +`--minor` or `--major`. `--strict` can be added to further influence resolution. + +* `--patch`: + Prefer updating only to next patch version. + +* `--minor`: + Prefer updating only to next minor version. + +* `--major`: + Prefer updating to next major version (default). + +* `--strict`: + Do not allow any gem to be updated past latest `--patch` | `--minor` | `--major`. + +When Bundler is resolving what versions to use to satisfy declared +requirements in the Gemfile or in parent gems, it looks up all +available versions, filters out any versions that don't satisfy +the requirement, and then, by default, sorts them from newest to +oldest, considering them in that order. + +Providing one of the patch level options (e.g. `--patch`) changes the +sort order of the satisfying versions, causing Bundler to consider the +latest `--patch` or `--minor` version available before other versions. +Note that versions outside the stated patch level could still be +resolved to if necessary to find a suitable dependency graph. + +For example, if gem 'foo' is locked at 1.0.2, with no gem requirement +defined in the Gemfile, and versions 1.0.3, 1.0.4, 1.1.0, 1.1.1, 2.0.0 +all exist, the default order of preference by default (`--major`) will +be "2.0.0, 1.1.1, 1.1.0, 1.0.4, 1.0.3, 1.0.2". + +If the `--patch` option is used, the order of preference will change to +"1.0.4, 1.0.3, 1.0.2, 1.1.1, 1.1.0, 2.0.0". + +If the `--minor` option is used, the order of preference will change to +"1.1.1, 1.1.0, 1.0.4, 1.0.3, 1.0.2, 2.0.0". + +Combining the `--strict` option with any of the patch level options +will remove any versions beyond the scope of the patch level option, +to ensure that no gem is updated that far. + +To continue the previous example, if both `--patch` and `--strict` +options are used, the available versions for resolution would be +"1.0.4, 1.0.3, 1.0.2". If `--minor` and `--strict` are used, it would +be "1.1.1, 1.1.0, 1.0.4, 1.0.3, 1.0.2". + +Gem requirements as defined in the Gemfile will still be the first +determining factor for what versions are available. If the gem +requirement for `foo` in the Gemfile is '~> 1.0', that will accomplish +the same thing as providing the `--minor` and `--strict` options. + +## PATCH LEVEL EXAMPLES + +Given the following gem specifications: + + foo 1.4.3, requires: ~> bar 2.0 + foo 1.4.4, requires: ~> bar 2.0 + foo 1.4.5, requires: ~> bar 2.1 + foo 1.5.0, requires: ~> bar 2.1 + foo 1.5.1, requires: ~> bar 3.0 + bar with versions 2.0.3, 2.0.4, 2.1.0, 2.1.1, 3.0.0 + +Gemfile: + + gem 'foo' + +Gemfile.lock: + + foo (1.4.3) + bar (~> 2.0) + bar (2.0.3) + +Cases: + + # Command Line Result + ------------------------------------------------------------ + 1 bundle update --patch 'foo 1.4.5', 'bar 2.1.1' + 2 bundle update --patch foo 'foo 1.4.5', 'bar 2.1.1' + 3 bundle update --minor 'foo 1.5.1', 'bar 3.0.0' + 4 bundle update --minor --strict 'foo 1.5.0', 'bar 2.1.1' + 5 bundle update --patch --strict 'foo 1.4.4', 'bar 2.0.4' + +In case 1, bar is upgraded to 2.1.1, a minor version increase, because +the dependency from foo 1.4.5 required it. + +In case 2, only foo is requested to be unlocked, but bar is also +allowed to move because it's not a declared dependency in the Gemfile. + +In case 3, bar goes up a whole major release, because a minor increase +is preferred now for foo, and when it goes to 1.5.1, it requires 3.0.0 +of bar. + +In case 4, foo is preferred up to a minor version, but 1.5.1 won't work +because the --strict flag removes bar 3.0.0 from consideration since +it's a major increment. + +In case 5, both foo and bar have any minor or major increments removed +from consideration because of the --strict flag, so the most they can +move is up to 1.4.4 and 2.0.4. + +## RECOMMENDED WORKFLOW + +In general, when working with an application managed with bundler, you should +use the following workflow: + +* After you create your Gemfile(5) for the first time, run + + $ bundle install + +* Check the resulting `Gemfile.lock` into version control + + $ git add Gemfile.lock + +* When checking out this repository on another development machine, run + + $ bundle install + +* When checking out this repository on a deployment machine, run + + $ bundle install --deployment + +* After changing the Gemfile(5) to reflect a new or update dependency, run + + $ bundle install + +* Make sure to check the updated `Gemfile.lock` into version control + + $ git add Gemfile.lock + +* If [bundle install(1)](bundle-install.1.html) reports a conflict, manually update the specific + gems that you changed in the Gemfile(5) + + $ bundle update rails thin + +* If you want to update all the gems to the latest possible versions that + still match the gems listed in the Gemfile(5), run + + $ bundle update --all diff --git a/lib/bundler/man/bundle-version.1 b/lib/bundler/man/bundle-version.1 new file mode 100644 index 0000000000..751a408312 --- /dev/null +++ b/lib/bundler/man/bundle-version.1 @@ -0,0 +1,22 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE\-VERSION" "1" "May 2026" "" +.SH "NAME" +\fBbundle\-version\fR \- Prints Bundler version information +.SH "SYNOPSIS" +\fBbundle version\fR +.SH "DESCRIPTION" +Prints Bundler version information\. +.SH "OPTIONS" +No options\. +.SH "EXAMPLE" +Print the version of Bundler with build date and commit hash of the in the Git source\. +.IP "" 4 +.nf +bundle version +.fi +.IP "" 0 +.P +shows \fBBundler version 2\.3\.21 (2022\-08\-24 commit d54be5fdd8)\fR for example\. +.P +cf\. \fBbundle \-\-version\fR shows \fBBundler version 2\.3\.21\fR\. diff --git a/lib/bundler/man/bundle-version.1.ronn b/lib/bundler/man/bundle-version.1.ronn new file mode 100644 index 0000000000..46c6f0b30a --- /dev/null +++ b/lib/bundler/man/bundle-version.1.ronn @@ -0,0 +1,24 @@ +bundle-version(1) -- Prints Bundler version information +======================================================= + +## SYNOPSIS + +`bundle version` + +## DESCRIPTION + +Prints Bundler version information. + +## OPTIONS + +No options. + +## EXAMPLE + +Print the version of Bundler with build date and commit hash of the in the Git source. + + bundle version + +shows `Bundler version 2.3.21 (2022-08-24 commit d54be5fdd8)` for example. + +cf. `bundle --version` shows `Bundler version 2.3.21`. diff --git a/lib/bundler/man/bundle.1 b/lib/bundler/man/bundle.1 new file mode 100644 index 0000000000..167815631a --- /dev/null +++ b/lib/bundler/man/bundle.1 @@ -0,0 +1,93 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "BUNDLE" "1" "May 2026" "" +.SH "NAME" +\fBbundle\fR \- Ruby Dependency Management +.SH "SYNOPSIS" +\fBbundle\fR COMMAND [\-\-no\-color] [\-\-verbose] [ARGS] +.SH "DESCRIPTION" +Bundler manages an \fBapplication's dependencies\fR through its entire life across many machines systematically and repeatably\. +.P +See the bundler website \fIhttps://bundler\.io\fR for information on getting started, and Gemfile(5) for more information on the \fBGemfile\fR format\. +.SH "OPTIONS" +.TP +\fB\-\-no\-color\fR +Print all output without color +.TP +\fB\-\-retry\fR, \fB\-r\fR +Specify the number of times you wish to attempt network commands +.TP +\fB\-\-verbose\fR, \fB\-V\fR +Print out additional logging information +.SH "BUNDLE COMMANDS" +We divide \fBbundle\fR subcommands into primary commands and utilities: +.SH "PRIMARY COMMANDS" +.TP +\fBbundle install(1)\fR \fIbundle\-install\.1\.html\fR +Install the gems specified by the \fBGemfile\fR or \fBGemfile\.lock\fR +.TP +\fBbundle update(1)\fR \fIbundle\-update\.1\.html\fR +Update dependencies to their latest versions +.TP +\fBbundle cache(1)\fR \fIbundle\-cache\.1\.html\fR +Package the \.gem files required by your application into the \fBvendor/cache\fR directory (aliases: \fBbundle package\fR, \fBbundle pack\fR) +.TP +\fBbundle exec(1)\fR \fIbundle\-exec\.1\.html\fR +Execute a script in the current bundle +.TP +\fBbundle config(1)\fR \fIbundle\-config\.1\.html\fR +Specify and read configuration options for Bundler +.TP +\fBbundle help(1)\fR \fIbundle\-help\.1\.html\fR +Display detailed help for each subcommand +.SH "UTILITIES" +.TP +\fBbundle add(1)\fR \fIbundle\-add\.1\.html\fR +Add the named gem to the Gemfile and run \fBbundle install\fR +.TP +\fBbundle binstubs(1)\fR \fIbundle\-binstubs\.1\.html\fR +Generate binstubs for executables in a gem +.TP +\fBbundle check(1)\fR \fIbundle\-check\.1\.html\fR +Determine whether the requirements for your application are installed and available to Bundler +.TP +\fBbundle show(1)\fR \fIbundle\-show\.1\.html\fR +Show the source location of a particular gem in the bundle +.TP +\fBbundle outdated(1)\fR \fIbundle\-outdated\.1\.html\fR +Show all of the outdated gems in the current bundle +.TP +\fBbundle console(1)\fR (deprecated) +Start an IRB session in the current bundle +.TP +\fBbundle open(1)\fR \fIbundle\-open\.1\.html\fR +Open an installed gem in the editor +.TP +\fBbundle lock(1)\fR \fIbundle\-lock\.1\.html\fR +Generate a lockfile for your dependencies +.TP +\fBbundle init(1)\fR \fIbundle\-init\.1\.html\fR +Generate a simple \fBGemfile\fR, placed in the current directory +.TP +\fBbundle gem(1)\fR \fIbundle\-gem\.1\.html\fR +Create a simple gem, suitable for development with Bundler +.TP +\fBbundle platform(1)\fR \fIbundle\-platform\.1\.html\fR +Display platform compatibility information +.TP +\fBbundle clean(1)\fR \fIbundle\-clean\.1\.html\fR +Clean up unused gems in your Bundler directory +.TP +\fBbundle doctor(1)\fR \fIbundle\-doctor\.1\.html\fR +Display warnings about common problems +.TP +\fBbundle remove(1)\fR \fIbundle\-remove\.1\.html\fR +Removes gems from the Gemfile +.TP +\fBbundle plugin(1)\fR \fIbundle\-plugin\.1\.html\fR +Manage Bundler plugins +.TP +\fBbundle version(1)\fR \fIbundle\-version\.1\.html\fR +Prints Bundler version information +.SH "PLUGINS" +When running a command that isn't listed in PRIMARY COMMANDS or UTILITIES, Bundler will try to find an executable on your path named \fBbundler\-<command>\fR and execute it, passing down any extra arguments to it\. diff --git a/lib/bundler/man/bundle.1.ronn b/lib/bundler/man/bundle.1.ronn new file mode 100644 index 0000000000..1c2b3df7af --- /dev/null +++ b/lib/bundler/man/bundle.1.ronn @@ -0,0 +1,107 @@ +bundle(1) -- Ruby Dependency Management +======================================= + +## SYNOPSIS + +`bundle` COMMAND [--no-color] [--verbose] [ARGS] + +## DESCRIPTION + +Bundler manages an `application's dependencies` through its entire life +across many machines systematically and repeatably. + +See [the bundler website](https://bundler.io) for information on getting +started, and Gemfile(5) for more information on the `Gemfile` format. + +## OPTIONS + +* `--no-color`: + Print all output without color + +* `--retry`, `-r`: + Specify the number of times you wish to attempt network commands + +* `--verbose`, `-V`: + Print out additional logging information + +## BUNDLE COMMANDS + +We divide `bundle` subcommands into primary commands and utilities: + +## PRIMARY COMMANDS + +* [`bundle install(1)`](bundle-install.1.html): + Install the gems specified by the `Gemfile` or `Gemfile.lock` + +* [`bundle update(1)`](bundle-update.1.html): + Update dependencies to their latest versions + +* [`bundle cache(1)`](bundle-cache.1.html): + Package the .gem files required by your application into the + `vendor/cache` directory (aliases: `bundle package`, `bundle pack`) + +* [`bundle exec(1)`](bundle-exec.1.html): + Execute a script in the current bundle + +* [`bundle config(1)`](bundle-config.1.html): + Specify and read configuration options for Bundler + +* [`bundle help(1)`](bundle-help.1.html): + Display detailed help for each subcommand + +## UTILITIES + +* [`bundle add(1)`](bundle-add.1.html): + Add the named gem to the Gemfile and run `bundle install` + +* [`bundle binstubs(1)`](bundle-binstubs.1.html): + Generate binstubs for executables in a gem + +* [`bundle check(1)`](bundle-check.1.html): + Determine whether the requirements for your application are installed + and available to Bundler + +* [`bundle show(1)`](bundle-show.1.html): + Show the source location of a particular gem in the bundle + +* [`bundle outdated(1)`](bundle-outdated.1.html): + Show all of the outdated gems in the current bundle + +* `bundle console(1)` (deprecated): + Start an IRB session in the current bundle + +* [`bundle open(1)`](bundle-open.1.html): + Open an installed gem in the editor + +* [`bundle lock(1)`](bundle-lock.1.html): + Generate a lockfile for your dependencies + +* [`bundle init(1)`](bundle-init.1.html): + Generate a simple `Gemfile`, placed in the current directory + +* [`bundle gem(1)`](bundle-gem.1.html): + Create a simple gem, suitable for development with Bundler + +* [`bundle platform(1)`](bundle-platform.1.html): + Display platform compatibility information + +* [`bundle clean(1)`](bundle-clean.1.html): + Clean up unused gems in your Bundler directory + +* [`bundle doctor(1)`](bundle-doctor.1.html): + Display warnings about common problems + +* [`bundle remove(1)`](bundle-remove.1.html): + Removes gems from the Gemfile + +* [`bundle plugin(1)`](bundle-plugin.1.html): + Manage Bundler plugins + +* [`bundle version(1)`](bundle-version.1.html): + Prints Bundler version information + +## PLUGINS + +When running a command that isn't listed in PRIMARY COMMANDS or UTILITIES, +Bundler will try to find an executable on your path named `bundler-<command>` +and execute it, passing down any extra arguments to it. diff --git a/lib/bundler/man/gemfile.5 b/lib/bundler/man/gemfile.5 new file mode 100644 index 0000000000..64e93c4b15 --- /dev/null +++ b/lib/bundler/man/gemfile.5 @@ -0,0 +1,547 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "GEMFILE" "5" "May 2026" "" +.SH "NAME" +\fBGemfile\fR \- A format for describing gem dependencies for Ruby programs +.SH "SYNOPSIS" +A \fBGemfile\fR describes the gem dependencies required to execute associated Ruby code\. +.P +Place the \fBGemfile\fR in the root of the directory containing the associated code\. For instance, in a Rails application, place the \fBGemfile\fR in the same directory as the \fBRakefile\fR\. +.SH "SYNTAX" +A \fBGemfile\fR is evaluated as Ruby code, in a context which makes available a number of methods used to describe the gem requirements\. +.SH "GLOBAL SOURCE" +At the top of the \fBGemfile\fR, add a single line for the \fBRubyGems\fR source that contains the gems listed in the \fBGemfile\fR\. +.IP "" 4 +.nf +source "https://rubygems\.org" +.fi +.IP "" 0 +.P +You can add only one global source\. In Bundler 1\.13, adding multiple global sources was deprecated\. The \fBsource\fR \fBMUST\fR be a valid RubyGems repository\. +.P +To use more than one source of RubyGems, you should use \fI\fBsource\fR block\fR\. +.P +A source is checked for gems following the heuristics described in \fISOURCE PRIORITY\fR\. +.P +\fBNote about a behavior of the feature deprecated in Bundler 1\.13\fR: If a gem is found in more than one global source, Bundler will print a warning after installing the gem indicating which source was used, and listing the other sources where the gem is available\. A specific source can be selected for gems that need to use a non\-standard repository, suppressing this warning, by using the \fI\fB:source\fR option\fR or \fBsource\fR block\. +.SS "CREDENTIALS" +Some gem sources require a username and password\. Use bundle config(1) \fIbundle\-config\.1\.html\fR to set the username and password for any of the sources that need it\. The command must be run once on each computer that will install the Gemfile, but this keeps the credentials from being stored in plain text in version control\. +.IP "" 4 +.nf +bundle config gems\.example\.com user:password +.fi +.IP "" 0 +.P +For some sources, like a company Gemfury account, it may be easier to include the credentials in the Gemfile as part of the source URL\. +.IP "" 4 +.nf +source "https://user:password@gems\.example\.com" +.fi +.IP "" 0 +.P +Credentials in the source URL will take precedence over credentials set using \fBconfig\fR\. +.SH "RUBY" +If your application requires a specific Ruby version or engine, specify your requirements using the \fBruby\fR method, with the following arguments\. All parameters are \fBOPTIONAL\fR unless otherwise specified\. +.SS "VERSION (required)" +The version of Ruby that your application requires\. If your application requires an alternate Ruby engine, such as JRuby, TruffleRuby, etc\., this should be the Ruby version that the engine is compatible with\. +.IP "" 4 +.nf +ruby "3\.1\.2" +.fi +.IP "" 0 +.P +If you wish to derive your Ruby version from a version file (ie \.ruby\-version), you can use the \fBfile\fR option instead\. +.IP "" 4 +.nf +ruby file: "\.ruby\-version" +.fi +.IP "" 0 +.P +The version file should conform to any of the following formats: +.IP "\(bu" 4 +\fB3\.1\.2\fR (\.ruby\-version) +.IP "\(bu" 4 +\fBruby 3\.1\.2\fR (\.tool\-versions, read: https://asdf\-vm\.com/manage/configuration\.html#tool\-versions) +.IP "" 0 +.SS "ENGINE" +Each application \fImay\fR specify a Ruby engine\. If an engine is specified, an engine version \fImust\fR also be specified\. +.P +What exactly is an Engine? +.IP "\(bu" 4 +A Ruby engine is an implementation of the Ruby language\. +.IP "\(bu" 4 +For background: the reference or original implementation of the Ruby programming language is called Matz's Ruby Interpreter \fIhttps://en\.wikipedia\.org/wiki/Ruby_MRI\fR, or MRI for short\. This is named after Ruby creator Yukihiro Matsumoto, also known as Matz\. MRI is also known as CRuby, because it is written in C\. MRI is the most widely used Ruby engine\. +.IP "\(bu" 4 +Other implementations \fIhttps://www\.ruby\-lang\.org/en/about/\fR of Ruby exist\. Some of the more well\-known implementations include JRuby \fIhttps://www\.jruby\.org/\fR and TruffleRuby \fIhttps://www\.graalvm\.org/ruby/\fR\. Rubinius is an alternative implementation of Ruby written in Ruby\. JRuby is an implementation of Ruby on the JVM, short for Java Virtual Machine\. TruffleRuby is a Ruby implementation on the GraalVM, a language toolkit built on the JVM\. +.IP "" 0 +.SS "ENGINE VERSION" +Each application \fImay\fR specify a Ruby engine version\. If an engine version is specified, an engine \fImust\fR also be specified\. If the engine is "ruby" the engine version specified \fImust\fR match the Ruby version\. +.IP "" 4 +.nf +ruby "2\.6\.8", engine: "jruby", engine_version: "9\.3\.8\.0" +.fi +.IP "" 0 +.SS "PATCHLEVEL" +Each application \fImay\fR specify a Ruby patchlevel\. Specifying the patchlevel has been meaningless since Ruby 2\.1\.0 was released as the patchlevel is now uniquely determined by a combination of major, minor, and teeny version numbers\. +.P +This option was implemented in Bundler 1\.4\.0 for Ruby 2\.0 or earlier\. +.IP "" 4 +.nf +ruby "3\.1\.2", patchlevel: "20" +.fi +.IP "" 0 +.SH "GEMS" +Specify gem requirements using the \fBgem\fR method, with the following arguments\. All parameters are \fBOPTIONAL\fR unless otherwise specified\. +.SS "NAME (required)" +For each gem requirement, list a single \fIgem\fR line\. +.IP "" 4 +.nf +gem "nokogiri" +.fi +.IP "" 0 +.SS "VERSION" +Each \fIgem\fR \fBMAY\fR have one or more version specifiers\. +.IP "" 4 +.nf +gem "nokogiri", ">= 1\.4\.2" +gem "RedCloth", ">= 4\.1\.0", "< 4\.2\.0" +.fi +.IP "" 0 +.SS "REQUIRE AS" +Each \fIgem\fR \fBMAY\fR specify files that should be used when autorequiring via \fBBundler\.require\fR\. You may pass an array with multiple files or \fBtrue\fR if the file you want \fBrequired\fR has the same name as \fIgem\fR or \fBfalse\fR to prevent any file from being autorequired\. +.IP "" 4 +.nf +gem "redis", require: ["redis/connection/hiredis", "redis"] +gem "webmock", require: false +gem "byebug", require: true +.fi +.IP "" 0 +.P +The argument defaults to the name of the gem\. For example, these are identical: +.IP "" 4 +.nf +gem "nokogiri" +gem "nokogiri", require: "nokogiri" +gem "nokogiri", require: true +.fi +.IP "" 0 +.SS "GROUPS" +Each \fIgem\fR \fBMAY\fR specify membership in one or more groups\. Any \fIgem\fR that does not specify membership in any group is placed in the \fBdefault\fR group\. +.IP "" 4 +.nf +gem "rspec", group: :test +gem "wirble", groups: [:development, :test] +.fi +.IP "" 0 +.P +The Bundler runtime allows its two main methods, \fBBundler\.setup\fR and \fBBundler\.require\fR, to limit their impact to particular groups\. +.IP "" 4 +.nf +# setup adds gems to Ruby's load path +Bundler\.setup # defaults to all groups +require "bundler/setup" # same as Bundler\.setup +Bundler\.setup(:default) # only set up the _default_ group +Bundler\.setup(:test) # only set up the _test_ group (but `not` _default_) +Bundler\.setup(:default, :test) # set up the _default_ and _test_ groups, but no others + +# require requires all of the gems in the specified groups +Bundler\.require # defaults to the _default_ group +Bundler\.require(:default) # identical +Bundler\.require(:default, :test) # requires the _default_ and _test_ groups +Bundler\.require(:test) # requires the _test_ group +.fi +.IP "" 0 +.P +The Bundler CLI allows you to specify a list of groups whose gems \fBbundle install\fR should not install with the \fBwithout\fR configuration\. +.P +To specify multiple groups to ignore, specify a list of groups separated by spaces\. +.IP "" 4 +.nf +bundle config set \-\-local without test +bundle config set \-\-local without development test +.fi +.IP "" 0 +.P +Also, calling \fBBundler\.setup\fR with no parameters, or calling \fBrequire "bundler/setup"\fR will setup all groups except for the ones you excluded via \fB\-\-without\fR (since they are not available)\. +.P +Note that on \fBbundle install\fR, bundler downloads and evaluates all gems, in order to create a single canonical list of all of the required gems and their dependencies\. This means that you cannot list different versions of the same gems in different groups\. For more details, see Understanding Bundler \fIhttps://bundler\.io/rationale\.html\fR\. +.SS "PLATFORMS" +If a gem should only be used in a particular platform or set of platforms, you can specify them\. Platforms are essentially identical to groups, except that you do not need to use the \fB\-\-without\fR install\-time flag to exclude groups of gems for other platforms\. +.P +There are a number of \fBGemfile\fR platforms: +.TP +\fBruby\fR +C Ruby (MRI), Rubinius, or TruffleRuby, but not Windows +.TP +\fBmri\fR +C Ruby (MRI) only, but not Windows +.TP +\fBwindows\fR +Windows C Ruby (MRI), including RubyInstaller 32\-bit and 64\-bit versions +.TP +\fBmswin\fR +Windows C Ruby (MRI), including RubyInstaller 32\-bit versions +.TP +\fBmswin64\fR +Windows C Ruby (MRI), including RubyInstaller 64\-bit versions +.TP +\fBrbx\fR +Rubinius +.TP +\fBjruby\fR +JRuby +.TP +\fBtruffleruby\fR +TruffleRuby +.P +On platforms \fBruby\fR, \fBmri\fR, \fBmswin\fR, \fBmswin64\fR, and \fBwindows\fR, you may additionally specify a version by appending the major and minor version numbers without a delimiter\. For example, to specify that a gem should only be used on platform \fBruby\fR version 3\.1, use: +.IP "" 4 +.nf +ruby_31 +.fi +.IP "" 0 +.P +As with groups (above), you may specify one or more platforms: +.IP "" 4 +.nf +gem "weakling", platforms: :jruby +gem "ruby\-debug", platforms: :mri_31 +gem "nokogiri", platforms: [:windows_31, :jruby] +.fi +.IP "" 0 +.P +All operations involving groups (\fBbundle install\fR \fIbundle\-install\.1\.html\fR, \fBBundler\.setup\fR, \fBBundler\.require\fR) behave exactly the same as if any groups not matching the current platform were explicitly excluded\. +.P +The following platform values are deprecated and should be replaced with \fBwindows\fR: +.IP "\(bu" 4 +\fBmswin\fR, \fBmswin64\fR, \fBmingw32\fR, \fBx64_mingw\fR +.IP "" 0 +.P +Note that, while unfortunately using the same terminology, the values of this option are different from the values that \fBbundle lock \-\-add\-platform\fR can take\. The values of this option are more closer to "Ruby Implementation" while the values that \fBbundle lock \-\-add\-platform\fR understands are more related to OS and architecture of the different systems where your lockfile will be used\. +.SS "FORCE_RUBY_PLATFORM" +If you always want the pure ruby variant of a gem to be chosen over platform specific variants, you can use the \fBforce_ruby_platform\fR option: +.IP "" 4 +.nf +gem "ffi", force_ruby_platform: true +.fi +.IP "" 0 +.P +This can be handy (assuming the pure ruby variant works fine) when: +.IP "\(bu" 4 +You're having issues with the platform specific variant\. +.IP "\(bu" 4 +The platform specific variant does not yet support a newer ruby (and thus has a \fBrequired_ruby_version\fR upper bound), but you still want your Gemfile{\.lock} files to resolve under that ruby\. +.IP "" 0 +.SS "SOURCE" +You can select an alternate RubyGems repository for a gem using the ':source' option\. +.IP "" 4 +.nf +gem "some_internal_gem", source: "https://gems\.example\.com" +.fi +.IP "" 0 +.P +This forces the gem to be loaded from this source and ignores the global source declared at the top level of the file\. If the gem does not exist in this source, it will not be installed\. +.P +Bundler will search for child dependencies of this gem by first looking in the source selected for the parent, but if they are not found there, it will fall back on the global source\. +.P +\fBNote about a behavior of the feature deprecated in Bundler 1\.13\fR: Selecting a specific source repository this way also suppresses the ambiguous gem warning described above in \fIGLOBAL SOURCE\fR\. +.P +Using the \fB:source\fR option for an individual gem will also make that source available as a possible global source for any other gems which do not specify explicit sources\. Thus, when adding gems with explicit sources, it is recommended that you also ensure all other gems in the Gemfile are using explicit sources\. +.SS "GIT" +If necessary, you can specify that a gem is located at a particular git repository using the \fB:git\fR parameter\. The repository can be accessed via several protocols: +.TP +\fBHTTP(S)\fR +gem "rails", git: "https://github\.com/rails/rails\.git" +.TP +\fBSSH\fR +gem "rails", git: "git@github\.com:rails/rails\.git" +.TP +\fBgit\fR +gem "rails", git: "git://github\.com/rails/rails\.git" +.P +If using SSH, the user that you use to run \fBbundle install\fR \fBMUST\fR have the appropriate keys available in their \fB$HOME/\.ssh\fR\. +.P +\fBNOTE\fR: \fBhttp://\fR and \fBgit://\fR URLs should be avoided if at all possible\. These protocols are unauthenticated, so a man\-in\-the\-middle attacker can deliver malicious code and compromise your system\. HTTPS and SSH are strongly preferred\. +.P +The \fBgroup\fR, \fBplatforms\fR, and \fBrequire\fR options are available and behave exactly the same as they would for a normal gem\. +.P +A git repository \fBSHOULD\fR have at least one file, at the root of the directory containing the gem, with the extension \fB\.gemspec\fR\. This file \fBMUST\fR contain a valid gem specification, as expected by the \fBgem build\fR command\. +.P +If a git repository does not have a \fB\.gemspec\fR, bundler will attempt to create one, but it will not contain any dependencies, executables, or C extension compilation instructions\. As a result, it may fail to properly integrate into your application\. +.P +If a git repository does have a \fB\.gemspec\fR for the gem you attached it to, a version specifier, if provided, means that the git repository is only valid if the \fB\.gemspec\fR specifies a version matching the version specifier\. If not, bundler will print a warning\. +.IP "" 4 +.nf +gem "rails", "2\.3\.8", git: "https://github\.com/rails/rails\.git" +# bundle install will fail, because the \.gemspec in the rails +# repository's master branch specifies version 3\.0\.0 +.fi +.IP "" 0 +.P +If a git repository does \fBnot\fR have a \fB\.gemspec\fR for the gem you attached it to, a version specifier \fBMUST\fR be provided\. Bundler will use this version in the simple \fB\.gemspec\fR it creates\. +.P +Git repositories support a number of additional options\. +.TP +\fBbranch\fR, \fBtag\fR, and \fBref\fR +You \fBMUST\fR only specify at most one of these options\. The default is \fBbranch: "master"\fR\. For example: +.IP +gem "rails", git: "https://github\.com/rails/rails\.git", branch: "5\-0\-stable" +.IP +gem "rails", git: "https://github\.com/rails/rails\.git", tag: "v5\.0\.0" +.IP +gem "rails", git: "https://github\.com/rails/rails\.git", ref: "4aded" +.TP +\fBsubmodules\fR +For reference, a git submodule \fIhttps://git\-scm\.com/book/en/v2/Git\-Tools\-Submodules\fR lets you have another git repository within a subfolder of your repository\. Specify \fBsubmodules: true\fR to cause bundler to expand any submodules included in the git repository +.P +If a git repository contains multiple \fB\.gemspecs\fR, each \fB\.gemspec\fR represents a gem located at the same place in the file system as the \fB\.gemspec\fR\. +.IP "" 4 +.nf +|~rails [git root] +| |\-rails\.gemspec [rails gem located here] +|~actionpack +| |\-actionpack\.gemspec [actionpack gem located here] +|~activesupport +| |\-activesupport\.gemspec [activesupport gem located here] +|\|\.\|\.\|\. +.fi +.IP "" 0 +.P +To install a gem located in a git repository, bundler changes to the directory containing the gemspec, runs \fBgem build name\.gemspec\fR and then installs the resulting gem\. The \fBgem build\fR command, which comes standard with Rubygems, evaluates the \fB\.gemspec\fR in the context of the directory in which it is located\. +.SS "GIT SOURCE" +A custom git source can be defined via the \fBgit_source\fR method\. Provide the source's name as an argument, and a block which receives a single argument and interpolates it into a string to return the full repo address: +.IP "" 4 +.nf +git_source(:stash){ |repo_name| "https://stash\.corp\.acme\.pl/#{repo_name}\.git" } +gem 'rails', stash: 'forks/rails' +.fi +.IP "" 0 +.P +In addition, if you wish to choose a specific branch: +.IP "" 4 +.nf +gem "rails", stash: "forks/rails", branch: "branch_name" +.fi +.IP "" 0 +.SS "GITHUB" +\fBNOTE\fR: This shorthand should be avoided until Bundler 2\.0, since it currently expands to an insecure \fBgit://\fR URL\. This allows a man\-in\-the\-middle attacker to compromise your system\. +.P +If the git repository you want to use is hosted on GitHub and is public, you can use the :github shorthand to specify the github username and repository name (without the trailing "\.git"), separated by a slash\. If both the username and repository name are the same, you can omit one\. +.IP "" 4 +.nf +gem "rails", github: "rails/rails" +gem "rails", github: "rails" +.fi +.IP "" 0 +.P +Are both equivalent to +.IP "" 4 +.nf +gem "rails", git: "https://github\.com/rails/rails\.git" +.fi +.IP "" 0 +.P +Since the \fBgithub\fR method is a specialization of \fBgit_source\fR, it accepts a \fB:branch\fR named argument\. +.P +You can also directly pass a pull request URL: +.IP "" 4 +.nf +gem "rails", github: "https://github\.com/rails/rails/pull/43753" +.fi +.IP "" 0 +.P +Which is equivalent to: +.IP "" 4 +.nf +gem "rails", github: "rails/rails", branch: "refs/pull/43753/head" +.fi +.IP "" 0 +.SS "GIST" +If the git repository you want to use is hosted as a GitHub Gist and is public, you can use the :gist shorthand to specify the gist identifier (without the trailing "\.git")\. +.IP "" 4 +.nf +gem "the_hatch", gist: "4815162342" +.fi +.IP "" 0 +.P +Is equivalent to: +.IP "" 4 +.nf +gem "the_hatch", git: "https://gist\.github\.com/4815162342\.git" +.fi +.IP "" 0 +.P +Since the \fBgist\fR method is a specialization of \fBgit_source\fR, it accepts a \fB:branch\fR named argument\. +.SS "BITBUCKET" +If the git repository you want to use is hosted on Bitbucket and is public, you can use the :bitbucket shorthand to specify the bitbucket username and repository name (without the trailing "\.git"), separated by a slash\. If both the username and repository name are the same, you can omit one\. +.IP "" 4 +.nf +gem "rails", bitbucket: "rails/rails" +gem "rails", bitbucket: "rails" +.fi +.IP "" 0 +.P +Are both equivalent to +.IP "" 4 +.nf +gem "rails", git: "https://rails@bitbucket\.org/rails/rails\.git" +.fi +.IP "" 0 +.P +Since the \fBbitbucket\fR method is a specialization of \fBgit_source\fR, it accepts a \fB:branch\fR named argument\. +.SS "PATH" +You can specify that a gem is located in a particular location on the file system\. Relative paths are resolved relative to the directory containing the \fBGemfile\fR\. +.P +Similar to the semantics of the \fB:git\fR option, the \fB:path\fR option requires that the directory in question either contains a \fB\.gemspec\fR for the gem, or that you specify an explicit version that bundler should use\. +.P +Unlike \fB:git\fR, bundler does not compile C extensions for gems specified as paths\. +.IP "" 4 +.nf +gem "rails", path: "vendor/rails" +.fi +.IP "" 0 +.P +If you would like to use multiple local gems directly from the filesystem, you can set a global \fBpath\fR option to the path containing the gem's files\. This will automatically load gemspec files from subdirectories\. +.IP "" 4 +.nf +path 'components' do + gem 'admin_ui' + gem 'public_ui' +end +.fi +.IP "" 0 +.SH "BLOCK FORM OF SOURCE, GIT, PATH, GROUP and PLATFORMS" +The \fB:source\fR, \fB:git\fR, \fB:path\fR, \fB:group\fR, and \fB:platforms\fR options may be applied to a group of gems by using block form\. +.IP "" 4 +.nf +source "https://gems\.example\.com" do + gem "some_internal_gem" + gem "another_internal_gem" +end + +git "https://github\.com/rails/rails\.git" do + gem "activesupport" + gem "actionpack" +end + +platforms :ruby do + gem "ruby\-debug" + gem "sqlite3" +end + +group :development, optional: true do + gem "wirble" + gem "faker" +end +.fi +.IP "" 0 +.P +In the case of the group block form the :optional option can be given to prevent a group from being installed unless listed in the \fB\-\-with\fR option given to the \fBbundle install\fR command\. +.P +In the case of the \fBgit\fR block form, the \fB:ref\fR, \fB:branch\fR, \fB:tag\fR, and \fB:submodules\fR options may be passed to the \fBgit\fR method, and all gems in the block will inherit those options\. +.P +The presence of a \fBsource\fR block in a Gemfile also makes that source available as a possible global source for any other gems which do not specify explicit sources\. Thus, when defining source blocks, it is recommended that you also ensure all other gems in the Gemfile are using explicit sources, either via source blocks or \fB:source\fR directives on individual gems\. +.SH "INSTALL_IF" +The \fBinstall_if\fR method allows gems to be installed based on a proc or lambda\. This is especially useful for optional gems that can only be used if certain software is installed or some other conditions are met\. +.IP "" 4 +.nf +install_if \-> { RUBY_PLATFORM =~ /darwin/ } do + gem "pasteboard" +end +.fi +.IP "" 0 +.SH "GEMSPEC" +The \fB\.gemspec\fR \fIhttps://guides\.rubygems\.org/specification\-reference/\fR file is where you provide metadata about your gem to Rubygems\. Some required Gemspec attributes include the name, description, and homepage of your gem\. This is also where you specify the dependencies your gem needs to run\. +.P +If you wish to use Bundler to help install dependencies for a gem while it is being developed, use the \fBgemspec\fR method to pull in the dependencies listed in the \fB\.gemspec\fR file\. +.P +The \fBgemspec\fR method adds any runtime dependencies as gem requirements in the default group\. It also adds development dependencies as gem requirements in the \fBdevelopment\fR group\. Finally, it adds a gem requirement on your project (\fBpath: '\.'\fR)\. In conjunction with \fBBundler\.setup\fR, this allows you to require project files in your test code as you would if the project were installed as a gem; you need not manipulate the load path manually or require project files via relative paths\. +.P +The \fBgemspec\fR method supports optional \fB:path\fR, \fB:glob\fR, \fB:name\fR, and \fB:development_group\fR options, which control where bundler looks for the \fB\.gemspec\fR, the glob it uses to look for the gemspec (defaults to: \fB{,*,*/*}\.gemspec\fR), what named \fB\.gemspec\fR it uses (if more than one is present), and which group development dependencies are included in\. +.P +When a \fBgemspec\fR dependency encounters version conflicts during resolution, the local version under development will always be selected \-\- even if there are remote versions that better match other requirements for the \fBgemspec\fR gem\. +.SH "OVERRIDE" +The \fBoverride\fR directive rewrites a constraint on another gem before resolution runs\. It targets the common case where an upstream gem's published metadata is too narrow on the current project's machine \-\- a stale upper bound, an unwanted floor, or a transitive pin that has to be lifted\. +.IP "" 4 +.nf +override <target>, <field>: <operation> +.fi +.IP "" 0 +.P +\fB<target>\fR is a gem name string or \fB:all\fR\. \fB<field>\fR is one of \fBversion:\fR, \fBrequired_ruby_version:\fR, or \fBrequired_rubygems_version:\fR\. \fB<operation>\fR is one of: +.IP "\(bu" 4 +a version requirement string (e\.g\. \fB">= 8\.0"\fR), which \fBreplaces\fR the target's requirement absolutely\. The original requirement, both direct and transitive, is discarded in favour of the override\. +.IP "\(bu" 4 +\fB:ignore_upper\fR, which removes upper\-bound operators (\fB<\fR and \fB<=\fR) from the existing requirement and folds \fB~>\fR into its lower bound (\fB~> 1\.5\fR becomes \fB>= 1\.5\fR)\. Other operators, including \fB!=\fR, are preserved\. +.IP "\(bu" 4 +\fBnil\fR, which collapses the requirement to \fB>= 0\fR (no constraint at all)\. +.IP "" 0 +.P +\fB:all\fR only applies to \fBrequired_ruby_version:\fR and \fBrequired_rubygems_version:\fR\. A per\-gem override on the same field takes precedence over an \fB:all\fR override\. \fB:all + version:\fR is rejected: version requirements only make sense per gem\. +.P +Multiple \fBoverride\fR calls for distinct targets are allowed; declaring the same \fBtarget\fR and \fBfield\fR twice is an error\. +.IP "" 4 +.nf +source "https://rubygems\.org" + +# Force every reference to "rails" \-\- direct or transitive \-\- to >= 8\.0\. +override "rails", version: ">= 8\.0" + +# Strip the upper bound on nokogiri\. +override "nokogiri", version: :ignore_upper + +# Drop the version pin on legacy entirely\. +override "legacy", version: nil + +# Loosen every gem's required_ruby_version upper bound\. +override :all, required_ruby_version: :ignore_upper + +# Override one specific gem's required_rubygems_version\. +override "tricky", required_rubygems_version: nil + +gem "rails", "~> 7\.0" +.fi +.IP "" 0 +.P +The override only affects resolution and the install\-time Ruby/RubyGems compatibility checks; \fBGemfile\.lock\fR continues to reflect the resolved versions, not the rewritten requirements\. When resolution still fails, Bundler appends the active overrides (with their Gemfile location) to the error message so it is clear which override shaped the constraint set\. +.SH "SOURCE PRIORITY" +When attempting to locate a gem to satisfy a gem requirement, bundler uses the following priority order: +.IP "1." 4 +The source explicitly attached to the gem (using \fB:source\fR, \fB:path\fR, or \fB:git\fR) +.IP "2." 4 +For implicit gems (dependencies of explicit gems), any source, git, or path repository declared on the parent\. This results in bundler prioritizing the ActiveSupport gem from the Rails git repository over ones from \fBrubygems\.org\fR +.IP "3." 4 +If neither of the above conditions are met, the global source will be used\. If multiple global sources are specified, they will be prioritized from last to first, but this is deprecated since Bundler 1\.13, so Bundler prints a warning and will abort with an error in the future\. +.IP "" 0 +.SH "LOCKFILE" +By default, Bundler will create a lockfile by adding \fB\.lock\fR to the end of the Gemfile name\. To change this, use the \fBlockfile\fR method: +.IP "" 4 +.nf +lockfile "/path/to/lockfile\.lock" +.fi +.IP "" 0 +.P +This is useful when you want to use different lockfiles per ruby version or platform\. +.P +To avoid writing a lock file, use \fBfalse\fR as the argument: +.IP "" 4 +.nf +lockfile false +.fi +.IP "" 0 +.P +This is useful for library development and other situations where the code is expected to work with a range of dependency versions\. +.SS "LOCKFILE PRECEDENCE" +When determining path to the lockfile or whether to create a lockfile, the following precedence is used: +.IP "1." 4 +The \fBbundle install\fR \fB\-\-no\-lock\fR option (which disables lockfile creation)\. +.IP "2." 4 +The \fBbundle install\fR \fB\-\-lockfile\fR option\. +.IP "3." 4 +The \fBBUNDLE_LOCKFILE\fR environment variable\. +.IP "4." 4 +The \fBlockfile\fR method in the Gemfile\. +.IP "5." 4 +The default behavior of adding \fB\.lock\fR to the end of the Gemfile name\. +.IP "" 0 + diff --git a/lib/bundler/man/gemfile.5.ronn b/lib/bundler/man/gemfile.5.ronn new file mode 100644 index 0000000000..69fef90654 --- /dev/null +++ b/lib/bundler/man/gemfile.5.ronn @@ -0,0 +1,639 @@ +Gemfile(5) -- A format for describing gem dependencies for Ruby programs +======================================================================== + +## SYNOPSIS + +A `Gemfile` describes the gem dependencies required to execute associated +Ruby code. + +Place the `Gemfile` in the root of the directory containing the associated +code. For instance, in a Rails application, place the `Gemfile` in the same +directory as the `Rakefile`. + +## SYNTAX + +A `Gemfile` is evaluated as Ruby code, in a context which makes available +a number of methods used to describe the gem requirements. + +## GLOBAL SOURCE + +At the top of the `Gemfile`, add a single line for the `RubyGems` source that +contains the gems listed in the `Gemfile`. + + source "https://rubygems.org" + +You can add only one global source. In Bundler 1.13, adding multiple global +sources was deprecated. The `source` `MUST` be a valid RubyGems repository. + +To use more than one source of RubyGems, you should use [`source` block +](#BLOCK-FORM-OF-SOURCE-GIT-PATH-GROUP-and-PLATFORMS). + +A source is checked for gems following the heuristics described in +[SOURCE PRIORITY][]. + +**Note about a behavior of the feature deprecated in Bundler 1.13**: +If a gem is found in more than one global source, Bundler +will print a warning after installing the gem indicating which source was used, +and listing the other sources where the gem is available. A specific source can +be selected for gems that need to use a non-standard repository, suppressing +this warning, by using the [`:source` option](#SOURCE) or `source` block. + +### CREDENTIALS + +Some gem sources require a username and password. Use [bundle config(1)](bundle-config.1.html) to set +the username and password for any of the sources that need it. The command must +be run once on each computer that will install the Gemfile, but this keeps the +credentials from being stored in plain text in version control. + + bundle config gems.example.com user:password + +For some sources, like a company Gemfury account, it may be easier to +include the credentials in the Gemfile as part of the source URL. + + source "https://user:password@gems.example.com" + +Credentials in the source URL will take precedence over credentials set using +`config`. + +## RUBY + +If your application requires a specific Ruby version or engine, specify your +requirements using the `ruby` method, with the following arguments. +All parameters are `OPTIONAL` unless otherwise specified. + +### VERSION (required) + +The version of Ruby that your application requires. If your application +requires an alternate Ruby engine, such as JRuby, TruffleRuby, etc., this +should be the Ruby version that the engine is compatible with. + + ruby "3.1.2" + +If you wish to derive your Ruby version from a version file (ie .ruby-version), +you can use the `file` option instead. + + ruby file: ".ruby-version" + +The version file should conform to any of the following formats: + + - `3.1.2` (.ruby-version) + - `ruby 3.1.2` (.tool-versions, read: https://asdf-vm.com/manage/configuration.html#tool-versions) + +### ENGINE + +Each application _may_ specify a Ruby engine. If an engine is specified, an +engine version _must_ also be specified. + +What exactly is an Engine? + - A Ruby engine is an implementation of the Ruby language. + + - For background: the reference or original implementation of the Ruby + programming language is called + [Matz's Ruby Interpreter](https://en.wikipedia.org/wiki/Ruby_MRI), or MRI + for short. This is named after Ruby creator Yukihiro Matsumoto, + also known as Matz. MRI is also known as CRuby, because it is written in C. + MRI is the most widely used Ruby engine. + + - [Other implementations](https://www.ruby-lang.org/en/about/) of Ruby exist. + Some of the more well-known implementations include + [JRuby](https://www.jruby.org/) and [TruffleRuby](https://www.graalvm.org/ruby/). + Rubinius is an alternative implementation of Ruby written in Ruby. + JRuby is an implementation of Ruby on the JVM, short for Java Virtual Machine. + TruffleRuby is a Ruby implementation on the GraalVM, a language toolkit built on the JVM. + +### ENGINE VERSION + +Each application _may_ specify a Ruby engine version. If an engine version is +specified, an engine _must_ also be specified. If the engine is "ruby" the +engine version specified _must_ match the Ruby version. + + ruby "2.6.8", engine: "jruby", engine_version: "9.3.8.0" + +### PATCHLEVEL + +Each application _may_ specify a Ruby patchlevel. Specifying the patchlevel has +been meaningless since Ruby 2.1.0 was released as the patchlevel is now +uniquely determined by a combination of major, minor, and teeny version numbers. + +This option was implemented in Bundler 1.4.0 for Ruby 2.0 or earlier. + + ruby "3.1.2", patchlevel: "20" + +## GEMS + +Specify gem requirements using the `gem` method, with the following arguments. +All parameters are `OPTIONAL` unless otherwise specified. + +### NAME (required) + +For each gem requirement, list a single _gem_ line. + + gem "nokogiri" + +### VERSION + +Each _gem_ `MAY` have one or more version specifiers. + + gem "nokogiri", ">= 1.4.2" + gem "RedCloth", ">= 4.1.0", "< 4.2.0" + +### REQUIRE AS + +Each _gem_ `MAY` specify files that should be used when autorequiring via +`Bundler.require`. You may pass an array with multiple files or `true` if the file +you want `required` has the 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 "byebug", 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 + +Each _gem_ `MAY` specify membership in one or more groups. Any _gem_ that does +not specify membership in any group is placed in the `default` group. + + gem "rspec", group: :test + gem "wirble", groups: [:development, :test] + +The Bundler runtime allows its two main methods, `Bundler.setup` and +`Bundler.require`, to limit their impact to particular groups. + + # setup adds gems to Ruby's load path + Bundler.setup # defaults to all groups + require "bundler/setup" # same as Bundler.setup + Bundler.setup(:default) # only set up the _default_ group + Bundler.setup(:test) # only set up the _test_ group (but `not` _default_) + Bundler.setup(:default, :test) # set up the _default_ and _test_ groups, but no others + + # require requires all of the gems in the specified groups + Bundler.require # defaults to the _default_ group + Bundler.require(:default) # identical + Bundler.require(:default, :test) # requires the _default_ and _test_ groups + Bundler.require(:test) # requires the _test_ group + +The Bundler CLI allows you to specify a list of groups whose gems `bundle install` should +not install with the `without` configuration. + +To specify multiple groups to ignore, specify a list of groups separated by spaces. + + bundle config set --local without test + bundle config set --local without development test + +Also, calling `Bundler.setup` with no parameters, or calling `require "bundler/setup"` +will setup all groups except for the ones you excluded via `--without` (since they +are not available). + +Note that on `bundle install`, bundler downloads and evaluates all gems, in order to +create a single canonical list of all of the required gems and their dependencies. +This means that you cannot list different versions of the same gems in different +groups. For more details, see [Understanding Bundler](https://bundler.io/rationale.html). + +### PLATFORMS + +If a gem should only be used in a particular platform or set of platforms, you can +specify them. Platforms are essentially identical to groups, except that you do not +need to use the `--without` install-time flag to exclude groups of gems for other +platforms. + +There are a number of `Gemfile` platforms: + + * `ruby`: + C Ruby (MRI), Rubinius, or TruffleRuby, but not Windows + * `mri`: + C Ruby (MRI) only, but not Windows + * `windows`: + Windows C Ruby (MRI), including RubyInstaller 32-bit and 64-bit versions + * `mswin`: + Windows C Ruby (MRI), including RubyInstaller 32-bit versions + * `mswin64`: + Windows C Ruby (MRI), including RubyInstaller 64-bit versions + * `rbx`: + Rubinius + * `jruby`: + JRuby + * `truffleruby`: + TruffleRuby + +On platforms `ruby`, `mri`, `mswin`, `mswin64`, and `windows`, you may +additionally specify a version by appending the major and minor version numbers +without a delimiter. For example, to specify that a gem should only be used on +platform `ruby` version 3.1, use: + + ruby_31 + +As with groups (above), you may specify one or more platforms: + + gem "weakling", platforms: :jruby + gem "ruby-debug", platforms: :mri_31 + gem "nokogiri", platforms: [:windows_31, :jruby] + +All operations involving groups ([`bundle install`](bundle-install.1.html), `Bundler.setup`, +`Bundler.require`) behave exactly the same as if any groups not +matching the current platform were explicitly excluded. + +The following platform values are deprecated and should be replaced with `windows`: + + * `mswin`, `mswin64`, `mingw32`, `x64_mingw` + +Note that, while unfortunately using the same terminology, the values of this +option are different from the values that `bundle lock --add-platform` can take. +The values of this option are more closer to "Ruby Implementation" while the +values that `bundle lock --add-platform` understands are more related to OS and +architecture of the different systems where your lockfile will be used. + +### FORCE_RUBY_PLATFORM + +If you always want the pure ruby variant of a gem to be chosen over platform +specific variants, you can use the `force_ruby_platform` option: + + gem "ffi", force_ruby_platform: true + +This can be handy (assuming the pure ruby variant works fine) when: + +* You're having issues with the platform specific variant. +* The platform specific variant does not yet support a newer ruby (and thus has + a `required_ruby_version` upper bound), but you still want your Gemfile{.lock} + files to resolve under that ruby. + +### SOURCE + +You can select an alternate RubyGems repository for a gem using the ':source' +option. + + gem "some_internal_gem", source: "https://gems.example.com" + +This forces the gem to be loaded from this source and ignores the global source +declared at the top level of the file. If the gem does not exist in this source, +it will not be installed. + +Bundler will search for child dependencies of this gem by first looking in the +source selected for the parent, but if they are not found there, it will fall +back on the global source. + +**Note about a behavior of the feature deprecated in Bundler 1.13**: +Selecting a specific source repository this way also suppresses the ambiguous +gem warning described above in [GLOBAL SOURCE](#GLOBAL-SOURCE). + +Using the `:source` option for an individual gem will also make that source +available as a possible global source for any other gems which do not specify +explicit sources. Thus, when adding gems with explicit sources, it is +recommended that you also ensure all other gems in the Gemfile are using +explicit sources. + +### GIT + +If necessary, you can specify that a gem is located at a particular +git repository using the `:git` parameter. The repository can be accessed via +several protocols: + + * `HTTP(S)`: + gem "rails", git: "https://github.com/rails/rails.git" + * `SSH`: + gem "rails", git: "git@github.com:rails/rails.git" + * `git`: + gem "rails", git: "git://github.com/rails/rails.git" + +If using SSH, the user that you use to run `bundle install` `MUST` have the +appropriate keys available in their `$HOME/.ssh`. + +`NOTE`: `http://` and `git://` URLs should be avoided if at all possible. These +protocols are unauthenticated, so a man-in-the-middle attacker can deliver +malicious code and compromise your system. HTTPS and SSH are strongly +preferred. + +The `group`, `platforms`, and `require` options are available and behave +exactly the same as they would for a normal gem. + +A git repository `SHOULD` have at least one file, at the root of the +directory containing the gem, with the extension `.gemspec`. This file +`MUST` contain a valid gem specification, as expected by the `gem build` +command. + +If a git repository does not have a `.gemspec`, bundler will attempt to +create one, but it will not contain any dependencies, executables, or +C extension compilation instructions. As a result, it may fail to properly +integrate into your application. + +If a git repository does have a `.gemspec` for the gem you attached it +to, a version specifier, if provided, means that the git repository is +only valid if the `.gemspec` specifies a version matching the version +specifier. If not, bundler will print a warning. + + gem "rails", "2.3.8", git: "https://github.com/rails/rails.git" + # bundle install will fail, because the .gemspec in the rails + # repository's master branch specifies version 3.0.0 + +If a git repository does `not` have a `.gemspec` for the gem you attached +it to, a version specifier `MUST` be provided. Bundler will use this +version in the simple `.gemspec` it creates. + +Git repositories support a number of additional options. + + * `branch`, `tag`, and `ref`: + You `MUST` only specify at most one of these options. The default + is `branch: "master"`. For example: + + gem "rails", git: "https://github.com/rails/rails.git", branch: "5-0-stable" + + gem "rails", git: "https://github.com/rails/rails.git", tag: "v5.0.0" + + gem "rails", git: "https://github.com/rails/rails.git", ref: "4aded" + + * `submodules`: + For reference, a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) + lets you have another git repository within a subfolder of your repository. + Specify `submodules: true` to cause bundler to expand any + submodules included in the git repository + +If a git repository contains multiple `.gemspecs`, each `.gemspec` +represents a gem located at the same place in the file system as +the `.gemspec`. + + |~rails [git root] + | |-rails.gemspec [rails gem located here] + |~actionpack + | |-actionpack.gemspec [actionpack gem located here] + |~activesupport + | |-activesupport.gemspec [activesupport gem located here] + |... + +To install a gem located in a git repository, bundler changes to +the directory containing the gemspec, runs `gem build name.gemspec` +and then installs the resulting gem. The `gem build` command, +which comes standard with Rubygems, evaluates the `.gemspec` in +the context of the directory in which it is located. + +### GIT SOURCE + +A custom git source can be defined via the `git_source` method. Provide the source's name +as an argument, and a block which receives a single argument and interpolates it into a +string to return the full repo address: + + git_source(:stash){ |repo_name| "https://stash.corp.acme.pl/#{repo_name}.git" } + gem 'rails', stash: 'forks/rails' + +In addition, if you wish to choose a specific branch: + + gem "rails", stash: "forks/rails", branch: "branch_name" + +### GITHUB + +`NOTE`: This shorthand should be avoided until Bundler 2.0, since it +currently expands to an insecure `git://` URL. This allows a +man-in-the-middle attacker to compromise your system. + +If the git repository you want to use is hosted on GitHub and is public, you can use the +:github shorthand to specify the github username and repository name (without the +trailing ".git"), separated by a slash. If both the username and repository name are the +same, you can omit one. + + gem "rails", github: "rails/rails" + gem "rails", github: "rails" + +Are both equivalent to + + gem "rails", git: "https://github.com/rails/rails.git" + +Since the `github` method is a specialization of `git_source`, it accepts a `:branch` named argument. + +You can also directly pass a pull request URL: + + gem "rails", github: "https://github.com/rails/rails/pull/43753" + +Which is equivalent to: + + gem "rails", github: "rails/rails", branch: "refs/pull/43753/head" + +### GIST + +If the git repository you want to use is hosted as a GitHub Gist and is public, you can use +the :gist shorthand to specify the gist identifier (without the trailing ".git"). + + gem "the_hatch", gist: "4815162342" + +Is equivalent to: + + gem "the_hatch", git: "https://gist.github.com/4815162342.git" + +Since the `gist` method is a specialization of `git_source`, it accepts a `:branch` named argument. + +### BITBUCKET + +If the git repository you want to use is hosted on Bitbucket and is public, you can use the +:bitbucket shorthand to specify the bitbucket username and repository name (without the +trailing ".git"), separated by a slash. If both the username and repository name are the +same, you can omit one. + + gem "rails", bitbucket: "rails/rails" + gem "rails", bitbucket: "rails" + +Are both equivalent to + + gem "rails", git: "https://rails@bitbucket.org/rails/rails.git" + +Since the `bitbucket` method is a specialization of `git_source`, it accepts a `:branch` named argument. + +### PATH + +You can specify that a gem is located in a particular location +on the file system. Relative paths are resolved relative to the +directory containing the `Gemfile`. + +Similar to the semantics of the `:git` option, the `:path` +option requires that the directory in question either contains +a `.gemspec` for the gem, or that you specify an explicit +version that bundler should use. + +Unlike `:git`, bundler does not compile C extensions for +gems specified as paths. + + gem "rails", path: "vendor/rails" + +If you would like to use multiple local gems directly from the filesystem, you can set a global `path` option to the path containing the gem's files. This will automatically load gemspec files from subdirectories. + + path 'components' do + gem 'admin_ui' + gem 'public_ui' + end + +## BLOCK FORM OF SOURCE, GIT, PATH, GROUP and PLATFORMS + +The `:source`, `:git`, `:path`, `:group`, and `:platforms` options may be +applied to a group of gems by using block form. + + source "https://gems.example.com" do + gem "some_internal_gem" + gem "another_internal_gem" + end + + git "https://github.com/rails/rails.git" do + gem "activesupport" + gem "actionpack" + end + + platforms :ruby do + gem "ruby-debug" + gem "sqlite3" + end + + group :development, optional: true do + gem "wirble" + gem "faker" + end + +In the case of the group block form the :optional option can be given +to prevent a group from being installed unless listed in the `--with` +option given to the `bundle install` command. + +In the case of the `git` block form, the `:ref`, `:branch`, `:tag`, +and `:submodules` options may be passed to the `git` method, and +all gems in the block will inherit those options. + +The presence of a `source` block in a Gemfile also makes that source +available as a possible global source for any other gems which do not specify +explicit sources. Thus, when defining source blocks, it is +recommended that you also ensure all other gems in the Gemfile are using +explicit sources, either via source blocks or `:source` directives on +individual gems. + +## INSTALL_IF + +The `install_if` method allows gems to be installed based on a proc or lambda. +This is especially useful for optional gems that can only be used if certain +software is installed or some other conditions are met. + + install_if -> { RUBY_PLATFORM =~ /darwin/ } do + gem "pasteboard" + end + +## GEMSPEC + +The [`.gemspec`](https://guides.rubygems.org/specification-reference/) file is where + you provide metadata about your gem to Rubygems. Some required Gemspec + attributes include the name, description, and homepage of your gem. This is + also where you specify the dependencies your gem needs to run. + +If you wish to use Bundler to help install dependencies for a gem while it is +being developed, use the `gemspec` method to pull in the dependencies listed in +the `.gemspec` file. + +The `gemspec` method adds any runtime dependencies as gem requirements in the +default group. It also adds development dependencies as gem requirements in the +`development` group. Finally, it adds a gem requirement on your project (`path: +'.'`). In conjunction with `Bundler.setup`, this allows you to require project +files in your test code as you would if the project were installed as a gem; you +need not manipulate the load path manually or require project files via relative +paths. + +The `gemspec` method supports optional `:path`, `:glob`, `:name`, and `:development_group` +options, which control where bundler looks for the `.gemspec`, the glob it uses to look +for the gemspec (defaults to: `{,*,*/*}.gemspec`), what named `.gemspec` it uses +(if more than one is present), and which group development dependencies are included in. + +When a `gemspec` dependency encounters version conflicts during resolution, the +local version under development will always be selected -- even if there are +remote versions that better match other requirements for the `gemspec` gem. + +## OVERRIDE + +The `override` directive rewrites a constraint on another gem before +resolution runs. It targets the common case where an upstream gem's +published metadata is too narrow on the current project's machine -- a stale +upper bound, an unwanted floor, or a transitive pin that has to be lifted. + + override <target>, <field>: <operation> + +`<target>` is a gem name string or `:all`. `<field>` is one of `version:`, +`required_ruby_version:`, or `required_rubygems_version:`. `<operation>` is +one of: + + * a version requirement string (e.g. `">= 8.0"`), which **replaces** the + target's requirement absolutely. The original requirement, both direct + and transitive, is discarded in favour of the override. + * `:ignore_upper`, which removes upper-bound operators (`<` and `<=`) from + the existing requirement and folds `~>` into its lower bound (`~> 1.5` + becomes `>= 1.5`). Other operators, including `!=`, are preserved. + * `nil`, which collapses the requirement to `>= 0` (no constraint at all). + +`:all` only applies to `required_ruby_version:` and `required_rubygems_version:`. +A per-gem override on the same field takes precedence over an `:all` override. +`:all + version:` is rejected: version requirements only make sense per gem. + +Multiple `override` calls for distinct targets are allowed; declaring the +same `target` and `field` twice is an error. + + source "https://rubygems.org" + + # Force every reference to "rails" -- direct or transitive -- to >= 8.0. + override "rails", version: ">= 8.0" + + # Strip the upper bound on nokogiri. + override "nokogiri", version: :ignore_upper + + # Drop the version pin on legacy entirely. + override "legacy", version: nil + + # Loosen every gem's required_ruby_version upper bound. + override :all, required_ruby_version: :ignore_upper + + # Override one specific gem's required_rubygems_version. + override "tricky", required_rubygems_version: nil + + gem "rails", "~> 7.0" + +The override only affects resolution and the install-time Ruby/RubyGems +compatibility checks; `Gemfile.lock` continues to reflect the resolved +versions, not the rewritten requirements. When resolution still fails, +Bundler appends the active overrides (with their Gemfile location) to the +error message so it is clear which override shaped the constraint set. + +## SOURCE PRIORITY + +When attempting to locate a gem to satisfy a gem requirement, +bundler uses the following priority order: + + 1. The source explicitly attached to the gem (using `:source`, `:path`, or + `:git`) + 2. For implicit gems (dependencies of explicit gems), any source, git, or path + repository declared on the parent. This results in bundler prioritizing the + ActiveSupport gem from the Rails git repository over ones from + `rubygems.org` + 3. If neither of the above conditions are met, the global source will be used. + If multiple global sources are specified, they will be prioritized from + last to first, but this is deprecated since Bundler 1.13, so Bundler prints + a warning and will abort with an error in the future. + +## LOCKFILE + +By default, Bundler will create a lockfile by adding `.lock` to the end of the +Gemfile name. To change this, use the `lockfile` method: + + lockfile "/path/to/lockfile.lock" + +This is useful when you want to use different lockfiles per ruby version or +platform. + +To avoid writing a lock file, use `false` as the argument: + + lockfile false + +This is useful for library development and other situations where the code is +expected to work with a range of dependency versions. + +### LOCKFILE PRECEDENCE + +When determining path to the lockfile or whether to create a lockfile, the +following precedence is used: + +1. The `bundle install` `--no-lock` option (which disables lockfile creation). +1. The `bundle install` `--lockfile` option. +1. The `BUNDLE_LOCKFILE` environment variable. +1. The `lockfile` method in the Gemfile. +1. The default behavior of adding `.lock` to the end of the Gemfile name. diff --git a/lib/bundler/man/index.txt b/lib/bundler/man/index.txt new file mode 100644 index 0000000000..f610ba852a --- /dev/null +++ b/lib/bundler/man/index.txt @@ -0,0 +1,31 @@ +Gemfile(5) gemfile.5 +bundle(1) bundle.1 +bundle-add(1) bundle-add.1 +bundle-binstubs(1) bundle-binstubs.1 +bundle-cache(1) bundle-cache.1 +bundle-check(1) bundle-check.1 +bundle-clean(1) bundle-clean.1 +bundle-config(1) bundle-config.1 +bundle-console(1) bundle-console.1 +bundle-doctor(1) bundle-doctor.1 +bundle-env(1) bundle-env.1 +bundle-exec(1) bundle-exec.1 +bundle-fund(1) bundle-fund.1 +bundle-gem(1) bundle-gem.1 +bundle-help(1) bundle-help.1 +bundle-info(1) bundle-info.1 +bundle-init(1) bundle-init.1 +bundle-install(1) bundle-install.1 +bundle-issue(1) bundle-issue.1 +bundle-licenses(1) bundle-licenses.1 +bundle-list(1) bundle-list.1 +bundle-lock(1) bundle-lock.1 +bundle-open(1) bundle-open.1 +bundle-outdated(1) bundle-outdated.1 +bundle-platform(1) bundle-platform.1 +bundle-plugin(1) bundle-plugin.1 +bundle-pristine(1) bundle-pristine.1 +bundle-remove(1) bundle-remove.1 +bundle-show(1) bundle-show.1 +bundle-update(1) bundle-update.1 +bundle-version(1) bundle-version.1 diff --git a/lib/bundler/match_metadata.rb b/lib/bundler/match_metadata.rb new file mode 100644 index 0000000000..92aeb2e893 --- /dev/null +++ b/lib/bundler/match_metadata.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Bundler + module MatchMetadata + def matches_current_metadata? + matches_current_ruby? && matches_current_rubygems? + end + + def matches_current_ruby? + @required_ruby_version.satisfied_by?(Gem.ruby_version) + end + + def matches_current_rubygems? + @required_rubygems_version.satisfied_by?(Gem.rubygems_version) + end + + def matches_current_metadata_with_overrides?(overrides) + matches_current_ruby_with_overrides?(overrides) && matches_current_rubygems_with_overrides?(overrides) + end + + def matches_current_ruby_with_overrides?(overrides) + effective_required_version(@required_ruby_version, :required_ruby_version, overrides).satisfied_by?(Gem.ruby_version) + end + + def matches_current_rubygems_with_overrides?(overrides) + effective_required_version(@required_rubygems_version, :required_rubygems_version, overrides).satisfied_by?(Gem.rubygems_version) + end + + def expanded_dependencies + runtime_dependencies + [ + metadata_dependency("Ruby", @required_ruby_version), + metadata_dependency("RubyGems", @required_rubygems_version), + ].compact + end + + def metadata_dependency(name, requirement) + return if requirement.nil? || requirement.none? + + Gem::Dependency.new("#{name}\0", requirement) + end + + private + + def effective_required_version(requirement, field, overrides) + return requirement if overrides.nil? || overrides.empty? + override = Override.find_for(overrides, name, field) + override ? override.apply_to(requirement) : requirement + end + end +end diff --git a/lib/bundler/match_platform.rb b/lib/bundler/match_platform.rb index 56cbbfb95d..479818e5ec 100644 --- a/lib/bundler/match_platform.rb +++ b/lib/bundler/match_platform.rb @@ -1,24 +1,42 @@ # frozen_string_literal: true -require "bundler/gem_helpers" - module Bundler module MatchPlatform - include GemHelpers + def installable_on_platform?(target_platform) # :nodoc: + return true if [Gem::Platform::RUBY, nil, target_platform].include?(platform) + return true if Gem::Platform.new(platform) === target_platform - def match_platform(p) - MatchPlatform.platforms_match?(platform, p) + false end - def self.platforms_match?(gemspec_platform, local_platform) - return true if gemspec_platform.nil? - return true if Gem::Platform::RUBY == gemspec_platform - return true if local_platform == gemspec_platform - gemspec_platform = Gem::Platform.new(gemspec_platform) - return true if GemHelpers.generic(gemspec_platform) === local_platform - return true if gemspec_platform === local_platform + def self.select_best_platform_match(specs, platform, force_ruby: false, prefer_locked: false) + matching = select_all_platform_match(specs, platform, force_ruby: force_ruby, prefer_locked: prefer_locked) - false + Gem::Platform.sort_and_filter_best_platform_match(matching, platform) + end + + def self.select_best_local_platform_match(specs, force_ruby: false) + local = Bundler.local_platform + matching = select_all_platform_match(specs, local, force_ruby: force_ruby).filter_map(&:materialized_for_installation) + + Gem::Platform.sort_best_platform_match(matching, local) + end + + def self.select_all_platform_match(specs, platform, force_ruby: false, prefer_locked: false) + matching = specs.select {|spec| spec.installable_on_platform?(force_ruby ? Gem::Platform::RUBY : platform) } + + specs.each(&:force_ruby_platform!) if force_ruby + + if prefer_locked + locked_originally = matching.select {|spec| spec.is_a?(::Bundler::LazySpecification) } + return locked_originally if locked_originally.any? + end + + matching + end + + def self.generic_local_platform_is_ruby? + Bundler.generic_local_platform == Gem::Platform::RUBY end end end diff --git a/lib/bundler/match_remote_metadata.rb b/lib/bundler/match_remote_metadata.rb new file mode 100644 index 0000000000..601af7e55d --- /dev/null +++ b/lib/bundler/match_remote_metadata.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Bundler + module FetchMetadata + # A fallback is included because the original version of the specification + # API didn't include that field, so some marshalled specs in the index have it + # set to +nil+. + def matches_current_ruby? + ensure_required_ruby_version_loaded + super + end + + def matches_current_rubygems? + ensure_required_rubygems_version_loaded + super + end + + def matches_current_ruby_with_overrides?(overrides) + ensure_required_ruby_version_loaded + super + end + + def matches_current_rubygems_with_overrides?(overrides) + ensure_required_rubygems_version_loaded + super + end + + private + + def ensure_required_ruby_version_loaded + @required_ruby_version ||= _remote_specification.required_ruby_version || Gem::Requirement.default # rubocop:disable Naming/MemoizedInstanceVariableName + end + + def ensure_required_rubygems_version_loaded + @required_rubygems_version ||= _remote_specification.required_rubygems_version || Gem::Requirement.default # rubocop:disable Naming/MemoizedInstanceVariableName + end + end + + module MatchRemoteMetadata + include MatchMetadata + + prepend FetchMetadata + end +end diff --git a/lib/bundler/materialization.rb b/lib/bundler/materialization.rb new file mode 100644 index 0000000000..82e48464a7 --- /dev/null +++ b/lib/bundler/materialization.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Bundler + # + # This class materializes a set of resolved specifications (`LazySpecification`) + # for a given gem into the most appropriate real specifications + # (`StubSepecification`, `EndpointSpecification`, etc), given a dependency and a + # target platform. + # + class Materialization + def initialize(dep, platform, candidates:) + @dep = dep + @platform = platform + @candidates = candidates + end + + def complete? + specs.any? + end + + def specs + @specs ||= if @candidates.nil? + [] + elsif platform + MatchPlatform.select_best_platform_match(@candidates, platform, force_ruby: dep.force_ruby_platform) + else + MatchPlatform.select_best_local_platform_match(@candidates, force_ruby: dep.force_ruby_platform || dep.default_force_ruby_platform) + end + end + + def dependencies + (materialized_spec || specs.first).runtime_dependencies.map {|d| [d, platform] } + end + + def materialized_spec + specs.reject(&:missing?).first&.materialization + end + + def completely_missing_specs + return [] unless specs.all?(&:missing?) + + specs + end + + def partially_missing_specs + specs.select(&:missing?) + end + + def incomplete_specs + return [] if complete? + + @candidates || LazySpecification.new(dep.name, nil, nil) + end + + private + + attr_reader :dep, :platform + end +end diff --git a/lib/bundler/mirror.rb b/lib/bundler/mirror.rb index a6fa070eb8..494a6d6aef 100644 --- a/lib/bundler/mirror.rb +++ b/lib/bundler/mirror.rb @@ -43,11 +43,11 @@ module Bundler config.update_mirror(mirror) end - private + private def fetch_valid_mirror_for(uri) downcased = uri.to_s.downcase - mirror = @mirrors[downcased] || @mirrors[URI(downcased).host] || Mirror.new(uri) + mirror = @mirrors[downcased] || @mirrors[Gem::URI(downcased).host] || Mirror.new(uri) mirror.validate!(@prober) mirror = Mirror.new(uri) unless mirror.valid? mirror @@ -74,7 +74,7 @@ module Bundler @uri = if uri.nil? nil else - URI(uri.to_s) + Gem::URI(uri.to_s) end @valid = nil end @@ -126,7 +126,7 @@ module Bundler if uri == "all" @all = true else - @uri = URI(uri).absolute? ? Settings.normalize_uri(uri) : uri + @uri = Gem::URI(uri).absolute? ? Settings.normalize_uri(uri) : uri end @value = value end @@ -148,17 +148,15 @@ module Bundler class TCPSocketProbe def replies?(mirror) MirrorSockets.new(mirror).any? do |socket, address, timeout| - begin - socket.connect_nonblock(address) - rescue Errno::EINPROGRESS - wait_for_writtable_socket(socket, address, timeout) - rescue # Connection failed somehow, again - false - end + socket.connect_nonblock(address) + rescue Errno::EINPROGRESS + wait_for_writtable_socket(socket, address, timeout) + rescue RuntimeError # Connection failed somehow, again + false end end - private + private def wait_for_writtable_socket(socket, address, timeout) if IO.select(nil, [socket], nil, timeout) @@ -172,7 +170,7 @@ module Bundler socket.connect_nonblock(address) rescue Errno::EISCONN true - rescue # Connection failed + rescue StandardError # Connection failed false end end diff --git a/lib/bundler/override.rb b/lib/bundler/override.rb new file mode 100644 index 0000000000..0e0ec59fd7 --- /dev/null +++ b/lib/bundler/override.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Bundler + class Override + UPPER_BOUND_OPERATORS = ["<", "<="].freeze + + def self.find_for(overrides, name, field) + overrides.find {|o| o.target == name && o.field == field } || + overrides.find {|o| o.target == :all && o.field == field } + end + + # Attach the given overrides onto every LazySpecification in `specs` so + # downstream consumers (LazySpecification#choose_compatible, the install- + # time compatibility check, etc.) can read the override list off the spec + # itself. Non-LazySpec entries (StubSpecification, Gem::Specification, ...) + # are left untouched. + def self.attach(specs, overrides) + return if overrides.nil? || overrides.empty? + specs.each {|s| s.overrides = overrides if s.is_a?(LazySpecification) } + end + + attr_reader :target, :field, :operation, :source_location + + def initialize(target, field, operation, source_location: nil) + @target = target + @field = field + @operation = operation + @source_location = source_location + end + + def source_location_label + return nil unless @source_location + "#{File.basename(@source_location.path)}:#{@source_location.lineno}" + end + + def apply_to(requirement) + case operation + when nil + Gem::Requirement.default + when :ignore_upper + remove_upper_bounds(requirement) + when String + Gem::Requirement.new(operation) + else + raise ArgumentError, "unsupported override operation: #{operation.inspect}" + end + end + + private + + def remove_upper_bounds(requirement) + return Gem::Requirement.default if requirement.nil? || requirement.none? + + preserved = requirement.requirements.filter_map do |op, version| + if UPPER_BOUND_OPERATORS.include?(op) + nil + elsif op == "~>" + [">=", version] + else + [op, version] + end + end + + return Gem::Requirement.default if preserved.empty? + + Gem::Requirement.new(preserved.map {|op, v| "#{op} #{v}" }) + end + end +end diff --git a/lib/bundler/plugin.rb b/lib/bundler/plugin.rb index 99c9a867b0..faca6bea53 100644 --- a/lib/bundler/plugin.rb +++ b/lib/bundler/plugin.rb @@ -1,21 +1,23 @@ # frozen_string_literal: true -require "bundler/plugin/api" +require_relative "plugin/api" module Bundler module Plugin - autoload :DSL, "bundler/plugin/dsl" - autoload :Index, "bundler/plugin/index" - autoload :Installer, "bundler/plugin/installer" - autoload :SourceList, "bundler/plugin/source_list" + autoload :DSL, File.expand_path("plugin/dsl", __dir__) + autoload :Events, File.expand_path("plugin/events", __dir__) + autoload :Index, File.expand_path("plugin/index", __dir__) + autoload :Installer, File.expand_path("plugin/installer", __dir__) + autoload :SourceList, File.expand_path("plugin/source_list", __dir__) class MalformattedPlugin < PluginError; end class UndefinedCommandError < PluginError; end class UnknownSourceError < PluginError; end + class PluginInstallError < PluginError; end - PLUGIN_FILE_NAME = "plugins.rb".freeze + PLUGIN_FILE_NAME = "plugins.rb" - module_function + module_function def reset! instance_variables.each {|i| remove_instance_variable(i) } @@ -34,16 +36,63 @@ module Bundler # @param [Hash] options various parameters as described in description. # Refer to cli/plugin for available options def install(names, options) + raise InvalidOption, "You cannot specify `--branch` and `--ref` at the same time." if options["branch"] && options["ref"] + specs = Installer.new.install(names, options) save_plugins names, specs - rescue PluginError => e - if specs - specs_to_delete = Hash[specs.select {|k, _v| names.include?(k) && !index.commands.values.include?(k) }] - specs_to_delete.values.each {|spec| Bundler.rm_rf(spec.full_gem_path) } + rescue PluginError + specs_to_delete = specs.select {|k, _v| names.include?(k) && !index.commands.values.include?(k) } + specs_to_delete.each_value {|spec| Bundler.rm_rf(spec.full_gem_path) } + + raise + end + + # Uninstalls plugins by the given names + # + # @param [Array<String>] names the names of plugins to be uninstalled + def uninstall(names, options) + if names.empty? && !options[:all] + Bundler.ui.error "No plugins to uninstall. Specify at least 1 plugin to uninstall.\n"\ + "Use --all option to uninstall all the installed plugins." + return end - Bundler.ui.error "Failed to install plugin #{name}: #{e.message}\n #{e.backtrace.join("\n ")}" + names = index.installed_plugins if options[:all] + if names.any? + names.each do |name| + if index.installed?(name) + path = index.plugin_path(name).to_s + Bundler.rm_rf(path) if index.installed_in_plugin_root?(name) + index.unregister_plugin(name) + Bundler.ui.info "Uninstalled plugin #{name}" + else + Bundler.ui.error "Plugin #{name} is not installed \n" + end + end + else + Bundler.ui.info "No plugins installed" + end + end + + # List installed plugins and commands + # + def list + installed_plugins = index.installed_plugins + if installed_plugins.any? + output = String.new + installed_plugins.each do |plugin| + output << "#{plugin}\n" + output << "-----\n" + index.plugin_commands(plugin).each do |command| + output << " #{command}\n" + end + output << "\n" + end + else + output = "No plugins installed" + end + Bundler.ui.info output end # Evaluates the Gemfile with a limited DSL and installs the plugins @@ -52,21 +101,24 @@ module Bundler # @param [Pathname] gemfile path # @param [Proc] block that can be evaluated for (inline) Gemfile def gemfile_install(gemfile = nil, &inline) - builder = DSL.new - if block_given? - builder.instance_eval(&inline) - else - builder.eval_gemfile(gemfile) + Bundler.settings.temporary(frozen: false, deployment: false) do + builder = DSL.new + if block_given? + builder.instance_eval(&inline) + else + builder.eval_gemfile(gemfile) + end + builder.check_primary_source_safety + definition = builder.to_definition(nil, true) + + return if definition.dependencies.empty? + + plugins = definition.dependencies.map(&:name) + installed_specs = Installer.new.install_definition(definition) + + save_plugins plugins, installed_specs, builder.inferred_plugins end - definition = builder.to_definition(nil, true) - - return if definition.dependencies.empty? - - plugins = definition.dependencies.map(&:name).reject {|p| index.installed? p } - installed_specs = Installer.new.install_definition(definition) - - save_plugins plugins, installed_specs, builder.inferred_plugins - rescue => e + rescue RuntimeError => e unless e.is_a?(GemfileError) Bundler.ui.error "Failed to install plugin: #{e.message}\n #{e.backtrace[0]}" end @@ -80,8 +132,8 @@ module Bundler # The directory root for all plugin related data # - # Points to root in app_config_path if ran in an app else points to the one - # in user_bundle_path + # If run in an app, points to local root, in app_config_path + # Otherwise, points to global root, in Bundler.user_bundle_path("plugin") def root @root ||= if SharedHelpers.in_bundle? local_root @@ -96,7 +148,7 @@ module Bundler # The global directory root for all plugin related data def global_root - Bundler.user_bundle_path.join("plugin") + Bundler.user_bundle_path("plugin") end # The cache directory for plugin stuffs @@ -115,7 +167,7 @@ module Bundler end # To be called from Cli class to pass the command and argument to - # approriate plugin class + # appropriate plugin class def exec_command(command, args) raise UndefinedCommandError, "Command `#{command}` not found" unless command? command @@ -134,7 +186,7 @@ module Bundler !index.source_plugin(name.to_s).nil? end - # @return [Class] that handles the source. The calss includes API::Source + # @return [Class] that handles the source. The class includes API::Source def source(name) raise UnknownSourceError, "Source #{name} not found" unless source? name @@ -143,10 +195,10 @@ module Bundler @sources[name] end - # @param [Hash] The options that are present in the lock file + # @param [Hash] The options that are present in the lockfile # @return [API::Source] the instance of the class that handles the source # type passed in locked_opts - def source_from_lock(locked_opts) + def from_lock(locked_opts) src = source(locked_opts["type"]) src.new(locked_opts.merge("uri" => locked_opts["remote"])) @@ -155,6 +207,9 @@ module Bundler # To be called via the API to register a hooks and corresponding block that # will be called to handle the hook def add_hook(event, &block) + unless Events.defined_event?(event) + raise ArgumentError, "Event '#{event}' not defined in Bundler::Plugin::Events" + end @hooks_by_event[event.to_s] << block end @@ -165,12 +220,15 @@ module Bundler # # @param [String] event def hook(event, *args, &arg_blk) - return unless Bundler.feature_flag.plugins? + return unless Bundler.settings[:plugins] + unless Events.defined_event?(event) + raise ArgumentError, "Event '#{event}' not defined in Bundler::Plugin::Events" + end plugins = index.hook_plugins(event) return unless plugins.any? - (plugins - @loaded_plugin_names).each {|name| load_plugin(name) } + plugins.each {|name| load_plugin(name) } @hooks_by_event[event].each {|blk| blk.call(*args, &arg_blk) } end @@ -182,6 +240,11 @@ module Bundler Index.new.installed?(plugin) end + # @return [true, false] whether the plugin is loaded + def loaded?(plugin) + @loaded_plugin_names.include?(plugin) + end + # Post installation processing and registering with index # # @param [Array<String>] plugins list to be installed @@ -191,9 +254,13 @@ module Bundler def save_plugins(plugins, specs, optional_plugins = []) plugins.each do |name| spec = specs[name] - validate_plugin! Pathname.new(spec.full_gem_path) - installed = register_plugin(name, spec, optional_plugins.include?(name)) - Bundler.ui.info "Installed plugin #{name}" if installed + + # It's possible that the `plugin` found in the Gemfile don't appear in the specs. For instance when + # calling `BUNDLE_WITHOUT=default bundle install`, the plugins will not get installed. + next if spec.nil? + next if index.up_to_date?(spec) + + save_plugin(name, spec, optional_plugins.include?(name)) end end @@ -208,6 +275,22 @@ module Bundler raise MalformattedPlugin, "#{PLUGIN_FILE_NAME} was not found in the plugin." unless plugin_file.file? end + # Validates and registers a plugin. + # + # @param [String] name the name of the plugin + # @param [Specification] spec of installed plugin + # @param [Boolean] optional_plugin, removed if there is conflict with any + # other plugin (used for default source plugins) + # + # @raise [PluginInstallError] if validation or registration raises any error + def save_plugin(name, spec, optional_plugin = false) + validate_plugin! Pathname.new(spec.full_gem_path) + installed = register_plugin(name, spec, optional_plugin) + Bundler.ui.info "Installed plugin #{name}" if installed + rescue PluginError => e + raise PluginInstallError, "Failed to install plugin `#{spec.name}`, due to #{e.class} (#{e.message})" + end + # Runs the plugins.rb file in an isolated namespace, records the plugin # actions it registers for and then passes the data to index to be stored. # @@ -227,7 +310,7 @@ module Bundler @hooks_by_event = Hash.new {|h, k| h[k] = [] } load_paths = spec.load_paths - add_to_load_path(load_paths) + Gem.add_to_load_path(*load_paths) path = Pathname.new spec.full_gem_path begin @@ -254,32 +337,45 @@ module Bundler # # @param [String] name of the plugin def load_plugin(name) + return unless name && !name.empty? + return if loaded?(name) + # Need to ensure before this that plugin root where the rest of gems # are installed to be on load path to support plugin deps. Currently not # done to avoid conflicts path = index.plugin_path(name) - add_to_load_path(index.load_paths(name)) + paths = index.load_paths(name) + invalid_paths = paths.reject {|p| File.directory?(p) } + + if invalid_paths.any? + Bundler.ui.warn <<~MESSAGE + The following plugin paths don't exist: #{invalid_paths.join(", ")}. + + This can happen if the plugin was installed with a different version of Ruby that has since been uninstalled. + + If you would like to reinstall the plugin, run: + + bundler plugin uninstall #{name} && bundler plugin install #{name} + + Continuing without installing plugin #{name}. + MESSAGE + + return + end + + Gem.add_to_load_path(*paths) load path.join(PLUGIN_FILE_NAME) @loaded_plugin_names << name - rescue => e + rescue RuntimeError => e Bundler.ui.error "Failed loading plugin #{name}: #{e.message}" raise end - def add_to_load_path(load_paths) - if insert_index = Bundler.rubygems.load_path_insert_index - $LOAD_PATH.insert(insert_index, *load_paths) - else - $LOAD_PATH.unshift(*load_paths) - end - end - class << self - private :load_plugin, :register_plugin, :save_plugins, :validate_plugin!, - :add_to_load_path + private :load_plugin, :register_plugin, :save_plugins, :validate_plugin! end end end diff --git a/lib/bundler/plugin/api.rb b/lib/bundler/plugin/api.rb index a2d5cbb4ac..ee2bffe3ab 100644 --- a/lib/bundler/plugin/api.rb +++ b/lib/bundler/plugin/api.rb @@ -23,7 +23,7 @@ module Bundler # and hooks). module Plugin class API - autoload :Source, "bundler/plugin/api/source" + autoload :Source, File.expand_path("api/source", __dir__) # The plugins should declare that they handle a command through this helper. # diff --git a/lib/bundler/plugin/api/source.rb b/lib/bundler/plugin/api/source.rb index 586477efb5..798326673a 100644 --- a/lib/bundler/plugin/api/source.rb +++ b/lib/bundler/plugin/api/source.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "uri" - module Bundler module Plugin class API @@ -37,11 +35,11 @@ module Bundler # # @!attribute [rw] dependency_names # @return [Array<String>] Names of dependencies that the source should - # try to resolve. It is not necessary to use this list intenally. This + # try to resolve. It is not necessary to use this list internally. This # is present to be compatible with `Definition` and is used by # rubygems source. module Source - attr_reader :uri, :options, :name + attr_reader :uri, :options, :name, :checksum_store attr_accessor :dependency_names def initialize(opts) @@ -50,6 +48,7 @@ module Bundler @uri = opts["uri"] @type = opts["type"] @name = opts["name"] || "#{@type} at #{@uri}" + @checksum_store = Checksum::Store.new end # This is used by the default `spec` method to constructs the @@ -68,13 +67,21 @@ module Bundler # to check out same version of gem later. # # There options are passed when the source plugin is created from the - # lock file. + # lockfile. # # @return [Hash] def options_to_lock {} end + # Download the gem specified by the spec at appropriate path. + # + # A source plugin can implement this method to split the download and the + # installation of a gem. + # + # @return [Boolean] Whether the download of the gem succeeded. + def download(spec, opts); end + # Install the gem specified by the spec at appropriate path. # `install_path` provides a sufficient default, if the source can only # satisfy one gem, but is not binding. @@ -97,7 +104,7 @@ module Bundler # # Note: Do not override if you don't know what you are doing. def post_install(spec, disable_exts = false) - opts = { :env_shebang => false, :disable_extensions => disable_exts } + opts = { env_shebang: false, disable_extensions: disable_exts } installer = Bundler::Source::Path::Installer.new(spec, opts) installer.post_install end @@ -108,7 +115,7 @@ module Bundler def install_path @install_path ||= begin - base_name = File.basename(URI.parse(uri).normalize.path) + base_name = File.basename(Gem::URI.parse(uri).normalize.path) gem_install_dir.join("#{base_name}-#{uri_hash[0..11]}") end @@ -132,7 +139,7 @@ module Bundler Bundler::Index.build do |index| files.each do |file| next unless spec = Bundler.load_gemspec(file) - Bundler.rubygems.set_installed_by_version(spec) + spec.installed_by_version = Gem::VERSION spec.source = self Bundler.rubygems.validate(spec) @@ -142,6 +149,13 @@ module Bundler end end + # Set internal representation to fetch the gems/specs locally. + # + # When this is called, the source should try to fetch the specs and + # install from the local system. + def local! + end + # Set internal representation to fetch the gems/specs from remote. # # When this is called, the source should try to fetch the specs and @@ -170,7 +184,7 @@ module Bundler # # This is used by `app_cache_path` def app_cache_dirname - base_name = File.basename(URI.parse(uri).normalize.path) + base_name = File.basename(Gem::URI.parse(uri).normalize.path) "#{base_name}-#{uri_hash}" end @@ -190,13 +204,14 @@ module Bundler FileUtils.rm_rf(new_cache_path) FileUtils.cp_r(install_path, new_cache_path) + FileUtils.rm_rf(app_cache_path.join(".git")) FileUtils.touch(app_cache_path.join(".bundlecache")) end # This shall check if two source object represent the same source. # # The comparison shall take place only on the attribute that can be - # inferred from the options passed from Gemfile and not on attibutes + # inferred from the options passed from Gemfile and not on attributes # that are used to pin down the gem to specific version (e.g. Git # sources should compare on branch and tag but not on commit hash) # @@ -239,7 +254,21 @@ module Bundler specs.unmet_dependency_names end + # Used by definition. + # + # Note: Do not override if you don't know what you are doing. + def spec_names + specs.spec_names + end + + # Used by definition. + # # Note: Do not override if you don't know what you are doing. + def add_dependency_names(names) + @dependencies |= Array(names) + end + + # NOTE: Do not override if you don't know what you are doing. def can_lock?(spec) spec.source == self end @@ -262,10 +291,11 @@ module Bundler end def to_s - "plugin source for #{options[:type]} with uri #{uri}" + "plugin source for #{@type} with uri #{@uri}" end + alias_method :identifier, :to_s - # Note: Do not override if you don't know what you are doing. + # NOTE: Do not override if you don't know what you are doing. def include?(other) other == self end @@ -274,7 +304,7 @@ module Bundler SharedHelpers.digest(:SHA1).hexdigest(uri) end - # Note: Do not override if you don't know what you are doing. + # NOTE: Do not override if you don't know what you are doing. def gem_install_dir Bundler.install_path end @@ -289,12 +319,6 @@ module Bundler end # @private - # Returns true - def bundler_plugin_api_source? - true - end - - # @private # This API on source might not be stable, and for now we expect plugins # to download all specs in `#specs`, so we implement the method for # compatibility purposes and leave it undocumented (and don't support) diff --git a/lib/bundler/plugin/dsl.rb b/lib/bundler/plugin/dsl.rb index 4bfc8437e0..da751d1774 100644 --- a/lib/bundler/plugin/dsl.rb +++ b/lib/bundler/plugin/dsl.rb @@ -10,7 +10,7 @@ module Bundler # So that we don't have to override all there methods to dummy ones # explicitly. # They will be handled by method_missing - [:gemspec, :gem, :path, :install_if, :platforms, :env].each {|m| undef_method m } + [:gemspec, :gem, :install_if, :platforms, :env].each {|m| undef_method m } # This lists the plugins that was added automatically and not specified by # the user. diff --git a/lib/bundler/plugin/events.rb b/lib/bundler/plugin/events.rb new file mode 100644 index 0000000000..3fbf60307e --- /dev/null +++ b/lib/bundler/plugin/events.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +module Bundler + module Plugin + module Events + def self.define(const, event) + const = const.to_sym.freeze + if const_defined?(const) && const_get(const) != event + raise ArgumentError, "Attempting to reassign #{const} to a different value" + end + const_set(const, event) unless const_defined?(const) + @events ||= {} + @events[event] = const + end + private_class_method :define + + def self.reset + @events.each_value do |const| + remove_const(const) + end + @events = nil + end + private_class_method :reset + + # Check if an event has been defined + # @param event [String] An event to check + # @return [Boolean] A boolean indicating if the event has been defined + def self.defined_event?(event) + @events ||= {} + @events.key?(event) + end + + # @!parse + # A hook called before the Gemfile is evaluated + # Includes the Gemfile path and the Lockfile path + # GEM_BEFORE_EVAL = "before-eval" + define :GEM_BEFORE_EVAL, "before-eval" + + # @!parse + # A hook called after the Gemfile is evaluated + # Includes a Bundler::Definition + # GEM_AFTER_EVAL = "after-eval" + define :GEM_AFTER_EVAL, "after-eval" + + # @!parse + # A hook called before any gems install + # Includes an Array of Bundler::Dependency objects + # GEM_BEFORE_INSTALL_ALL = "before-install-all" + define :GEM_BEFORE_INSTALL_ALL, "before-install-all" + + # @!parse + # A hook called before each individual gem is downloaded from a remote source. + # Includes a spec-like object responding to the Gem::Specification API + # (for example, a Bundler spec proxy such as Bundler::EndpointSpecification + # or Bundler::RemoteSpecification). Does not fire when the gem is already + # present at the initial download-cache check. + # GEM_BEFORE_FETCH = "before-fetch" + define :GEM_BEFORE_FETCH, "before-fetch" + + # @!parse + # A hook called after each individual gem is downloaded from a remote source. + # Includes a spec-like object responding to the Gem::Specification API + # (for example, a Bundler spec proxy such as Bundler::EndpointSpecification + # or Bundler::RemoteSpecification). Does not fire when the gem is already + # present at the initial download-cache check. + # GEM_AFTER_FETCH = "after-fetch" + define :GEM_AFTER_FETCH, "after-fetch" + + # @!parse + # A hook called before a git source is fetched or checked out. + # Includes a Bundler::Source::Git reference. + # GIT_BEFORE_FETCH = "before-git-fetch" + define :GIT_BEFORE_FETCH, "before-git-fetch" + + # @!parse + # A hook called after a git source is fetched or checked out. + # Includes a Bundler::Source::Git reference. + # GIT_AFTER_FETCH = "after-git-fetch" + define :GIT_AFTER_FETCH, "after-git-fetch" + + # @!parse + # A hook called before each individual gem is installed + # Includes a Bundler::ParallelInstaller::SpecInstallation. + # No state, error, post_install_message will be present as nothing has installed yet + # GEM_BEFORE_INSTALL = "before-install" + define :GEM_BEFORE_INSTALL, "before-install" + + # @!parse + # A hook called after each individual gem is installed + # Includes a Bundler::ParallelInstaller::SpecInstallation. + # - If state is failed, an error will be present. + # - If state is success, a post_install_message may be present. + # GEM_AFTER_INSTALL = "after-install" + define :GEM_AFTER_INSTALL, "after-install" + + # @!parse + # A hook called after any gems install + # Includes an Array of Bundler::Dependency objects + # GEM_AFTER_INSTALL_ALL = "after-install-all" + define :GEM_AFTER_INSTALL_ALL, "after-install-all" + + # @!parse + # A hook called before any gems require + # Includes an Array of Bundler::Dependency objects. + # GEM_BEFORE_REQUIRE_ALL = "before-require-all" + define :GEM_BEFORE_REQUIRE_ALL, "before-require-all" + + # @!parse + # A hook called before each individual gem is required + # Includes a Bundler::Dependency. + # GEM_BEFORE_REQUIRE = "before-require" + define :GEM_BEFORE_REQUIRE, "before-require" + + # @!parse + # A hook called after each individual gem is required + # Includes a Bundler::Dependency. + # GEM_AFTER_REQUIRE = "after-require" + define :GEM_AFTER_REQUIRE, "after-require" + + # @!parse + # A hook called after all gems required + # Includes an Array of Bundler::Dependency objects. + # GEM_AFTER_REQUIRE_ALL = "after-require-all" + define :GEM_AFTER_REQUIRE_ALL, "after-require-all" + end + end +end diff --git a/lib/bundler/plugin/index.rb b/lib/bundler/plugin/index.rb index 8dde072f16..1dfb061304 100644 --- a/lib/bundler/plugin/index.rb +++ b/lib/bundler/plugin/index.rb @@ -29,7 +29,16 @@ module Bundler @hooks = {} @load_paths = {} - load_index(global_index_file, true) + begin + load_index(global_index_file, true) + rescue PermissionError + # no need to fail when on a read-only FS, for example + nil + rescue ArgumentError => e + # ruby 3.4 checks writability in Dir.tmpdir + raise unless e.message&.include?("could not find a temporary directory") + nil + end load_index(local_index_file) if SharedHelpers.in_bundle? end @@ -53,16 +62,31 @@ module Bundler raise SourceConflict.new(name, common) unless common.empty? sources.each {|k| @sources[k] = name } - hooks.each {|e| (@hooks[e] ||= []) << name } + hooks.each do |event| + event_hooks = (@hooks[event] ||= []) << name + event_hooks.uniq! + end @plugin_paths[name] = path @load_paths[name] = load_paths save_index - rescue + rescue StandardError @commands = old_commands raise end + def unregister_plugin(name) + @commands.delete_if {|_, v| v == name } + @sources.delete_if {|_, v| v == name } + @hooks.each do |hook, names| + names.delete(name) + @hooks.delete(hook) if names.empty? + end + @plugin_paths.delete(name) + @load_paths.delete(name) + save_index + end + # Path of default index file def index_file Plugin.root.join("index") @@ -95,6 +119,20 @@ module Bundler @plugin_paths[name] end + def up_to_date?(spec) + path = installed?(spec.name) + + path == spec.full_gem_path + end + + def installed_plugins + @plugin_paths.keys + end + + def plugin_commands(plugin) + @commands.find_all {|_, n| n == plugin }.map(&:first) + end + def source?(source) @sources.key? source end @@ -108,7 +146,15 @@ module Bundler @hooks[event] || [] end - private + # This plugin is installed inside the .bundle/plugin directory, + # and thus is managed solely by Bundler + def installed_in_plugin_root?(name) + return false unless (path = installed?(name)) + + path.start_with?("#{Plugin.root}/") + end + + private # Reads the index file from the directory and initializes the instance # variables. @@ -117,19 +163,21 @@ module Bundler # @param [Pathname] index file path # @param [Boolean] is the index file global index def load_index(index_file, global = false) + base = base_for_index(global) + SharedHelpers.filesystem_access(index_file, :read) do |index_f| - valid_file = index_f && index_f.exist? && !index_f.size.zero? + valid_file = index_f&.exist? && !index_f.size.zero? break unless valid_file data = index_f.read - require "bundler/yaml_serializer" + require_relative "../yaml_serializer" index = YAMLSerializer.load(data) @commands.merge!(index["commands"]) @hooks.merge!(index["hooks"]) - @load_paths.merge!(index["load_paths"]) - @plugin_paths.merge!(index["plugin_paths"]) + @load_paths.merge!(transform_index_paths(index["load_paths"]) {|p| absolutize_path(p, base) }) + @plugin_paths.merge!(transform_index_paths(index["plugin_paths"]) {|p| absolutize_path(p, base) }) @sources.merge!(index["sources"]) unless global end end @@ -138,20 +186,56 @@ module Bundler # instance variables in YAML format. (The instance variables are supposed # to be only String key value pairs) def save_index + base = base_for_index(false) + index = { - "commands" => @commands, - "hooks" => @hooks, - "load_paths" => @load_paths, - "plugin_paths" => @plugin_paths, - "sources" => @sources, + "commands" => @commands, + "hooks" => @hooks, + "load_paths" => transform_index_paths(@load_paths) {|p| relativize_path(p, base) }, + "plugin_paths" => transform_index_paths(@plugin_paths) {|p| relativize_path(p, base) }, + "sources" => @sources, } - require "bundler/yaml_serializer" + require_relative "../yaml_serializer" SharedHelpers.filesystem_access(index_file) do |index_f| FileUtils.mkdir_p(index_f.dirname) File.open(index_f, "w") {|f| f.puts YAMLSerializer.dump(index) } end end + + def base_for_index(global) + global ? Plugin.global_root : Plugin.root + end + + def transform_index_paths(paths) + return {} unless paths + + paths.transform_values do |value| + if value.is_a?(Array) + value.map {|path| yield path } + else + yield value + end + end + end + + def relativize_path(path, base) + pathname = Pathname.new(path) + return path unless pathname.absolute? + + base_path = Pathname.new(base) + if pathname == base_path || pathname.to_s.start_with?(base_path.to_s + File::SEPARATOR) + pathname.relative_path_from(base_path).to_s + else + path + end + end + + def absolutize_path(path, base) + pathname = Pathname.new(path) + pathname = Pathname.new(base).join(pathname) unless pathname.absolute? + pathname.to_s + end end end end diff --git a/lib/bundler/plugin/installer.rb b/lib/bundler/plugin/installer.rb index 5379c38979..9be8b36843 100644 --- a/lib/bundler/plugin/installer.rb +++ b/lib/bundler/plugin/installer.rb @@ -8,18 +8,22 @@ module Bundler # are heavily dependent on the Gemfile. module Plugin class Installer - autoload :Rubygems, "bundler/plugin/installer/rubygems" - autoload :Git, "bundler/plugin/installer/git" + autoload :Rubygems, File.expand_path("installer/rubygems", __dir__) + autoload :Git, File.expand_path("installer/git", __dir__) + autoload :Path, File.expand_path("installer/path", __dir__) def install(names, options) + check_sources_consistency!(options) + version = options[:version] || [">= 0"] - Bundler.settings.temporary(:lockfile_uses_separate_rubygems_sources => false, :disable_multisource => false) do - if options[:git] - install_git(names, version, options) - else - sources = options[:source] || Bundler.rubygems.sources - install_rubygems(names, version, sources) - end + + if options[:git] + install_git(names, version, options) + elsif options[:path] + install_path(names, version, options[:path]) + else + sources = options[:source] || Gem.sources + install_rubygems(names, version, sources) end end @@ -30,30 +34,42 @@ module Bundler # @return [Hash] map of names to their specs they are installed with def install_definition(definition) def definition.lock(*); end - definition.resolve_remotely! + definition.remotely! specs = definition.specs install_from_specs specs end - private + private - def install_git(names, version, options) - uri = options.delete(:git) - options["uri"] = uri + def check_sources_consistency!(options) + if (options.keys & [:source, :git, :path]).length > 1 + raise InvalidOption, "Only one of --source, --git, or --path may be specified" + end - source_list = SourceList.new - source_list.add_git_source(options) + if (options.key?(:branch) || options.key?(:ref)) && !options.key?(:git) + raise InvalidOption, "--#{options.key?(:branch) ? "branch" : "ref"} can only be used with git sources" + end - # To support both sources - if options[:source] - source_list.add_rubygems_source("remotes" => options[:source]) + if options.key?(:branch) && options.key?(:ref) + raise InvalidOption, "--branch and --ref can't be both specified" end + end - deps = names.map {|name| Dependency.new name, version } + def install_git(names, version, options) + source_list = SourceList.new + source = source_list.add_git_source({ "uri" => options[:git], + "branch" => options[:branch], + "ref" => options[:ref] }) - definition = Definition.new(nil, deps, source_list, true) - install_definition(definition) + install_all_sources(names, version, source_list, source) + end + + def install_path(names, version, path) + source_list = SourceList.new + source = source_list.add_path_source({ "path" => path, "root_path" => SharedHelpers.pwd }) + + install_all_sources(names, version, source_list, source) end # Installs the plugin from rubygems source and returns the path where the @@ -65,13 +81,23 @@ module Bundler # # @return [Hash] map of names to the specs of plugins installed def install_rubygems(names, version, sources) - deps = names.map {|name| Dependency.new name, version } - source_list = SourceList.new - source_list.add_rubygems_source("remotes" => sources) - definition = Definition.new(nil, deps, source_list, true) - install_definition(definition) + Array(sources).each {|remote| source_list.add_global_rubygems_remote(remote) } + + install_all_sources(names, version, source_list) + end + + def install_all_sources(names, version, source_list, source = nil) + deps = names.map {|name| Dependency.new(name, version, { "source" => source }) } + + Bundler.configure_gem_home_and_path(Plugin.root) + + Bundler.settings.temporary(deployment: false, frozen: false) do + definition = Definition.new(nil, deps, source_list, true) + + install_definition(definition) + end end # Installs the plugins and deps from the provided specs and returns map of @@ -84,7 +110,8 @@ module Bundler paths = {} specs.each do |spec| - spec.source.install spec + spec.source.download(spec) + spec.source.install(spec) paths[spec.name] = spec end diff --git a/lib/bundler/plugin/installer/git.rb b/lib/bundler/plugin/installer/git.rb index fbb6c5e40e..deec5e99b3 100644 --- a/lib/bundler/plugin/installer/git.rb +++ b/lib/bundler/plugin/installer/git.rb @@ -20,10 +20,6 @@ module Bundler end end - def version_message(spec) - "#{spec.name} #{spec.version}" - end - def root Plugin.root end diff --git a/lib/bundler/plugin/installer/path.rb b/lib/bundler/plugin/installer/path.rb new file mode 100644 index 0000000000..58c4924eb0 --- /dev/null +++ b/lib/bundler/plugin/installer/path.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Bundler + module Plugin + class Installer + class Path < Bundler::Source::Path + def root + SharedHelpers.in_bundle? ? Bundler.root : Plugin.root + end + + def eql?(other) + return unless other.class == self.class + expanded_original_path == other.expanded_original_path && + version == other.version + end + + alias_method :==, :eql? + + def generate_bin(spec, disable_extensions = false) + # Need to find a way without code duplication + # For now, we can ignore this + end + end + end + end +end diff --git a/lib/bundler/plugin/installer/rubygems.rb b/lib/bundler/plugin/installer/rubygems.rb index 7ae74fa93b..cb5db9c30e 100644 --- a/lib/bundler/plugin/installer/rubygems.rb +++ b/lib/bundler/plugin/installer/rubygems.rb @@ -4,15 +4,7 @@ module Bundler module Plugin class Installer class Rubygems < Bundler::Source::Rubygems - def version_message(spec) - "#{spec.name} #{spec.version}" - end - - private - - def requires_sudo? - false # Will change on implementation of project level plugins - end + private def rubygems_dir Plugin.root diff --git a/lib/bundler/plugin/source_list.rb b/lib/bundler/plugin/source_list.rb index f0e212205f..d929ade29e 100644 --- a/lib/bundler/plugin/source_list.rb +++ b/lib/bundler/plugin/source_list.rb @@ -9,6 +9,10 @@ module Bundler add_source_to_list Plugin::Installer::Git.new(options), git_sources end + def add_path_source(options = {}) + add_source_to_list Plugin::Installer::Path.new(options), path_sources + end + def add_rubygems_source(options = {}) add_source_to_list Plugin::Installer::Rubygems.new(options), @rubygems_sources end @@ -17,9 +21,9 @@ module Bundler path_sources + git_sources + rubygems_sources + [metadata_source] end - private + private - def rubygems_aggregate_class + def source_class Plugin::Installer::Rubygems end end diff --git a/lib/bundler/process_lock.rb b/lib/bundler/process_lock.rb index 4bd6931577..784b17e363 100644 --- a/lib/bundler/process_lock.rb +++ b/lib/bundler/process_lock.rb @@ -2,23 +2,19 @@ module Bundler class ProcessLock - def self.lock(bundle_path = Bundler.bundle_path) + def self.lock(bundle_path = Bundler.bundle_path, &block) lock_file_path = File.join(bundle_path, "bundler.lock") - has_lock = false + base_lock_file_path = lock_file_path.delete_suffix(".lock") - File.open(lock_file_path, "w") do |f| - f.flock(File::LOCK_EX) - has_lock = true - yield - f.flock(File::LOCK_UN) + require "fileutils" if Bundler.rubygems.provides?("< 3.6.0") + + begin + SharedHelpers.filesystem_access(lock_file_path, :write) do + Gem.open_file_with_lock(base_lock_file_path, &block) + end + rescue PermissionError + block.call end - rescue Errno::EACCES, Errno::ENOLCK - # In the case the user does not have access to - # create the lock file or is using NFS where - # locks are not available we skip locking. - yield - ensure - FileUtils.rm_f(lock_file_path) if has_lock end end end diff --git a/lib/bundler/psyched_yaml.rb b/lib/bundler/psyched_yaml.rb deleted file mode 100644 index e654416a5a..0000000000 --- a/lib/bundler/psyched_yaml.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -# Psych could be a gem, so try to ask for it -begin - gem "psych" -rescue LoadError -end if defined?(gem) - -# Psych could be in the stdlib -# but it's too late if Syck is already loaded -begin - require "psych" unless defined?(Syck) -rescue LoadError - # Apparently Psych wasn't available. Oh well. -end - -# At least load the YAML stdlib, whatever that may be -require "yaml" unless defined?(YAML.dump) - -module Bundler - # On encountering invalid YAML, - # Psych raises Psych::SyntaxError - if defined?(::Psych::SyntaxError) - YamlLibrarySyntaxError = ::Psych::SyntaxError - else # Syck raises ArgumentError - YamlLibrarySyntaxError = ::ArgumentError - end -end - -require "bundler/deprecate" -begin - Bundler::Deprecate.skip_during do - require "rubygems/safe_yaml" - end -rescue LoadError - # it's OK if the file isn't there -end diff --git a/lib/bundler/remote_specification.rb b/lib/bundler/remote_specification.rb index 23e1234330..dcaaf6af2e 100644 --- a/lib/bundler/remote_specification.rb +++ b/lib/bundler/remote_specification.rb @@ -1,26 +1,31 @@ # frozen_string_literal: true -require "uri" - module Bundler # Represents a lazily loaded gem specification, where the full specification # is on the source server in rubygems' "quick" index. The proxy object is to # be seeded with what we're given from the source's abbreviated index - the # full specification will only be fetched when necessary. class RemoteSpecification + include MatchRemoteMetadata include MatchPlatform include Comparable attr_reader :name, :version, :platform attr_writer :dependencies - attr_accessor :source, :remote + attr_accessor :source, :remote, :locked_platform, :created_at def initialize(name, version, platform, spec_fetcher) @name = name @version = Gem::Version.create version - @platform = platform + @original_platform = platform || Gem::Platform::RUBY + @platform = Gem::Platform.new(platform) @spec_fetcher = spec_fetcher @dependencies = nil + @locked_platform = nil + end + + def insecurely_materialized? + @locked_platform.to_s != @platform.to_s end # Needed before installs, since the arch matters then and quick @@ -30,10 +35,10 @@ module Bundler end def full_name - if platform == Gem::Platform::RUBY || platform.nil? + @full_name ||= if @platform == Gem::Platform::RUBY "#{@name}-#{@version}" else - "#{@name}-#{@version}-#{platform}" + "#{@name}-#{@version}-#{@platform}" end end @@ -52,6 +57,8 @@ module Bundler # once the remote gem is downloaded, the backend specification will # be swapped out. def __swap__(spec) + raise APIResponseInvalidDependenciesError unless spec.dependencies.all? {|d| d.is_a?(Gem::Dependency) } + SharedHelpers.ensure_same_dependencies(self, dependencies, spec.dependencies) @_remote_specification = spec end @@ -78,28 +85,33 @@ module Bundler deps = method_missing(:dependencies) # allow us to handle when the specs dependencies are an array of array of string - # see https://github.com/bundler/bundler/issues/5797 + # in order to delay the crash to `#__swap__` where it results in a friendlier error + # see https://github.com/rubygems/bundler/issues/5797 deps = deps.map {|d| d.is_a?(Gem::Dependency) ? d : Gem::Dependency.new(*d) } deps end end + def runtime_dependencies + dependencies.select(&:runtime?) + end + def git_version return unless loaded_from && source.is_a?(Bundler::Source::Git) " #{source.revision[0..6]}" end - private + private def to_ary nil end def _remote_specification - @_remote_specification ||= @spec_fetcher.fetch_spec([@name, @version, @platform]) + @_remote_specification ||= @spec_fetcher.fetch_spec([@name, @version, @original_platform]) @_remote_specification || raise(GemspecError, "Gemspec data for #{full_name} was" \ - " missing from the server! Try installing with `--full-index` as a workaround.") + " missing from the server!") end def method_missing(method, *args, &blk) diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index ddc1d702e0..422b726980 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -1,369 +1,629 @@ # frozen_string_literal: true module Bundler + # + # This class implements the interface needed by PubGrub for resolution. It is + # equivalent to the `PubGrub::BasicPackageSource` class provided by PubGrub by + # default and used by the most simple PubGrub consumers. + # class Resolver - require "bundler/vendored_molinillo" - require "bundler/resolver/spec_group" - - # Figures out the best possible configuration of gems that satisfies - # the list of passed dependencies and any child dependencies without - # causing any gem activation errors. - # - # ==== Parameters - # *dependencies<Gem::Dependency>:: The list of dependencies to resolve - # - # ==== Returns - # <GemBundle>,nil:: If the list of dependencies can be resolved, a - # collection of gemspecs is returned. Otherwise, nil is returned. - def self.resolve(requirements, index, source_requirements = {}, base = [], gem_version_promoter = GemVersionPromoter.new, additional_base_requirements = [], platforms = nil) - platforms = Set.new(platforms) if platforms - base = SpecSet.new(base) unless base.is_a?(SpecSet) - resolver = new(index, source_requirements, base, gem_version_promoter, additional_base_requirements, platforms) - result = resolver.start(requirements) - SpecSet.new(result) - end - - def initialize(index, source_requirements, base, gem_version_promoter, additional_base_requirements, platforms) - @index = index - @source_requirements = source_requirements + require_relative "vendored_pub_grub" + require_relative "resolver/base" + require_relative "resolver/candidate" + require_relative "resolver/incompatibility" + require_relative "resolver/root" + require_relative "resolver/strategy" + + def initialize(base, gem_version_promoter, most_specific_locked_platform = nil) + @source_requirements = base.source_requirements @base = base - @resolver = Molinillo::Resolver.new(self, self) - @search_for = {} - @base_dg = Molinillo::DependencyGraph.new - @base.each do |ls| - dep = Dependency.new(ls.name, ls.version) - @base_dg.add_vertex(ls.name, DepProxy.new(dep, ls.platform), true) - end - additional_base_requirements.each {|d| @base_dg.add_vertex(d.name, d) } - @platforms = platforms @gem_version_promoter = gem_version_promoter - @allow_bundler_dependency_conflicts = Bundler.feature_flag.allow_bundler_dependency_conflicts? - @lockfile_uses_separate_rubygems_sources = Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? - end - - def start(requirements) - @prerelease_specified = {} - requirements.each {|dep| @prerelease_specified[dep.name] ||= dep.prerelease? } - - verify_gemfile_dependencies_are_found!(requirements) - dg = @resolver.resolve(requirements, @base_dg) - dg.map(&:payload). - reject {|sg| sg.name.end_with?("\0") }. - map(&:to_specs).flatten - rescue Molinillo::VersionConflict => e - message = version_conflict_message(e) - raise VersionConflict.new(e.conflicts.keys.uniq, message) - rescue Molinillo::CircularDependencyError => e - names = e.dependencies.sort_by(&:name).map {|d| "gem '#{d.name}'" } - raise CyclicDependencyError, "Your bundle requires gems that depend" \ - " on each other, creating an infinite loop. Please remove" \ - " #{names.count > 1 ? "either " : ""}#{names.join(" or ")}" \ - " and try again." - end - - include Molinillo::UI - - # Conveys debug information to the user. - # - # @param [Integer] depth the current depth of the resolution process. - # @return [void] - def debug(depth = 0) - return unless debug? - debug_info = yield - debug_info = debug_info.inspect unless debug_info.is_a?(String) - STDERR.puts debug_info.split("\n").map {|s| " " * depth + s } + @most_specific_locked_platform = most_specific_locked_platform end - def debug? - return @debug_mode if defined?(@debug_mode) - @debug_mode = ENV["DEBUG_RESOLVER"] || ENV["DEBUG_RESOLVER_TREE"] || false + def start + @requirements = @base.requirements + @packages = @base.packages + + root, logger = setup_solver + + Bundler.ui.info "Resolving dependencies...", true + + solve_versions(root: root, logger: logger) end - def before_resolution - Bundler.ui.info "Resolving dependencies...", debug? + def setup_solver + root = Resolver::Root.new(name_for_explicit_dependency_source) + root_version = Resolver::Candidate.new(0) + + @all_specs = Hash.new do |specs, name| + source = source_for(name) + matches = source.specs.search(name) + + # Don't bother to check for circular deps when no dependency API are + # available, since it's too slow to be usable. That edge case won't work + # but resolution other than that should work fine and reasonably fast. + if source.respond_to?(:dependency_api_available?) && source.dependency_api_available? + matches = filter_invalid_self_dependencies(matches, name) + end + + specs[name] = matches.sort_by {|s| [s.version, s.platform.to_s] } + end + + @all_versions = Hash.new do |candidates, package| + candidates[package] = all_versions_for(package) + end + + @sorted_versions = Hash.new do |candidates, package| + candidates[package] = filtered_versions_for(package).sort + end + + @sorted_versions[root] = [root_version] + + root_dependencies = prepare_dependencies(@requirements, @packages) + + @cached_dependencies = Hash.new do |dependencies, package| + dependencies[package] = Hash.new do |versions, version| + deps = version.dependencies.reject {|d| d.name == package.name } + deps = apply_metadata_overrides(deps, package.name) + versions[version] = to_dependency_hash(deps, @packages) + end + end + + @cached_dependencies[root] = { root_version => root_dependencies } + + logger = Bundler::UI::Shell.new + logger.level = debug? ? "debug" : "warn" + + [root, logger] + end + + def solve_versions(root:, logger:) + solver = PubGrub::VersionSolver.new(source: self, root: root, strategy: Strategy.new(self), logger: logger) + result = solver.solve + resolved_specs = result.flat_map {|package, version| version.to_specs(package, @most_specific_locked_platform) } + Override.attach(resolved_specs, @base.overrides) + SpecSet.new(resolved_specs).specs_with_additional_variants_from(@base.locked_specs) + rescue PubGrub::SolveFailure => e + incompatibility = e.incompatibility + + names_to_unlock, names_to_allow_prereleases_for, names_to_allow_remote_specs_for, extended_explanation = find_names_to_relax(incompatibility) + + names_to_relax = names_to_unlock + names_to_allow_prereleases_for + names_to_allow_remote_specs_for + + if names_to_relax.any? + if names_to_unlock.any? + Bundler.ui.debug "Found conflicts with locked dependencies. Will retry with #{names_to_unlock.join(", ")} unlocked...", true + + @base.unlock_names(names_to_unlock) + end + + if names_to_allow_prereleases_for.any? + Bundler.ui.debug "Found conflicts with dependencies with prereleases. Will retry considering prereleases for #{names_to_allow_prereleases_for.join(", ")}...", true + + @base.include_prereleases(names_to_allow_prereleases_for) + end + + if names_to_allow_remote_specs_for.any? + Bundler.ui.debug "Found conflicts with local versions of #{names_to_allow_remote_specs_for.join(", ")}. Will retry considering remote versions...", true + + @base.include_remote_specs(names_to_allow_remote_specs_for) + end + + root, logger = setup_solver + + Bundler.ui.debug "Retrying resolution...", true + retry + end + + explanation = e.message + + if extended_explanation + explanation << "\n\n" + explanation << extended_explanation + end + + override_summary = override_diagnostic_summary + explanation << override_summary if override_summary + + raise SolveFailure.new(explanation) end - def after_resolution - Bundler.ui.info "" + def override_diagnostic_summary + return nil if @base.overrides.empty? + + lines = ["Bundler applied the following overrides while resolving:"] + @base.overrides.each do |override| + target = override.target == :all ? ":all" : override.target.inspect + location = override.source_location_label + lines << " override #{target}, #{override.field}: #{override.operation.inspect}" \ + "#{location ? " (declared at #{location})" : ""}" + end + "\n\n#{lines.join("\n")}" end - def indicate_progress - Bundler.ui.info ".", false unless debug? + def find_names_to_relax(incompatibility) + names_to_unlock = [] + names_to_allow_prereleases_for = [] + names_to_allow_remote_specs_for = [] + extended_explanation = nil + + while incompatibility.conflict? + cause = incompatibility.cause + incompatibility = cause.incompatibility + + incompatibility.terms.each do |term| + package = term.package + name = package.name + + if base_requirements[name] + names_to_unlock << name + elsif package.ignores_prereleases? && @all_specs[name].any? {|s| s.version.prerelease? } + names_to_allow_prereleases_for << name + elsif package.prefer_local? && @all_specs[name].any? {|s| !s.is_a?(StubSpecification) } + names_to_allow_remote_specs_for << name + end + + no_versions_incompat = [cause.incompatibility, cause.satisfier].find {|incompat| incompat.cause.is_a?(PubGrub::Incompatibility::NoVersions) } + next unless no_versions_incompat + + extended_explanation = no_versions_incompat.extended_explanation + end + end + + [names_to_unlock.uniq, names_to_allow_prereleases_for.uniq, names_to_allow_remote_specs_for.uniq, extended_explanation] end - include Molinillo::SpecificationProvider + def parse_dependency(package, dependency) + range = if repository_for(package).is_a?(Source::Gemspec) + PubGrub::VersionRange.any + else + requirement_to_range(dependency) + end + + PubGrub::VersionConstraint.new(package, range: range) + end - def dependencies_for(specification) - specification.dependencies_for_activated_platforms + def versions_for(package, range = VersionRange.any) + range.select_versions(@sorted_versions[package]) end - def search_for(dependency) - platform = dependency.__platform - dependency = dependency.dep unless dependency.is_a? Gem::Dependency - search = @search_for[dependency] ||= begin - index = index_for(dependency) - results = index.search(dependency, @base[dependency.name]) + def no_versions_incompatibility_for(package, unsatisfied_term) + cause = PubGrub::Incompatibility::NoVersions.new(unsatisfied_term) + name = package.name + constraint = unsatisfied_term.constraint + constraint_string = constraint.constraint_string + requirements = constraint_string.split(" OR ").map {|req| Gem::Requirement.new(req.split(",")) } + + if name == "bundler" && bundler_pinned_to_current_version? + custom_explanation = "the current Bundler version (#{Bundler::VERSION}) does not satisfy #{constraint}" + extended_explanation = bundler_not_found_message(requirements) + else + specs_matching_other_platforms = filter_matching_specs(@all_specs[name], requirements) - unless @prerelease_specified[dependency.name] - # Move prereleases to the beginning of the list, so they're considered - # last during resolution. - pre, results = results.partition {|spec| spec.version.prerelease? } - results = pre + results + platforms_explanation = specs_matching_other_platforms.any? ? " for any resolution platforms (#{package.platforms.join(", ")})" : "" + custom_explanation = "#{constraint} could not be found in #{repository_for(package)}#{platforms_explanation}" + if hint = cooldown_hint(specs_matching_other_platforms) + custom_explanation += " (#{hint})" end - if vertex = @base_dg.vertex_named(dependency.name) - locked_requirement = vertex.payload.requirement + label = "#{name} (#{constraint_string})" + extended_explanation = other_specs_matching_message(specs_matching_other_platforms, label) if specs_matching_other_platforms.any? + end + + Incompatibility.new([unsatisfied_term], cause: cause, custom_explanation: custom_explanation, extended_explanation: extended_explanation) + end + + def debug? + ENV["BUNDLER_DEBUG_RESOLVER"] || + ENV["BUNDLER_DEBUG_RESOLVER_TREE"] || + ENV["DEBUG_RESOLVER"] || + ENV["DEBUG_RESOLVER_TREE"] || + false + end + + def incompatibilities_for(package, version) + package_deps = @cached_dependencies[package] + sorted_versions = @sorted_versions[package] + package_deps[version].map do |dep_package, dep_constraint| + low = high = sorted_versions.index(version) + + # find version low such that all >= low share the same dep + while low > 0 && package_deps[sorted_versions[low - 1]][dep_package] == dep_constraint + low -= 1 end - spec_groups = if results.any? - nested = [] - results.each do |spec| - version, specs = nested.last - if version == spec.version - specs << spec - else - nested << [spec.version, [spec]] - end - end - nested.reduce([]) do |groups, (version, specs)| - next groups if locked_requirement && !locked_requirement.satisfied_by?(version) - spec_group = SpecGroup.new(specs) - spec_group.ignores_bundler_dependencies = @allow_bundler_dependency_conflicts - groups << spec_group + low = + if low == 0 + nil + else + sorted_versions[low] end - else - [] + + # find version high such that all < high share the same dep + while high < sorted_versions.length && package_deps[sorted_versions[high]][dep_package] == dep_constraint + high += 1 end - # GVP handles major itself, but it's still a bit risky to trust it with it - # until we get it settled with new behavior. For 2.x it can take over all cases. - if @gem_version_promoter.major? - spec_groups - else - @gem_version_promoter.sort_versions(dependency, spec_groups) + high = + if high == sorted_versions.length + nil + else + sorted_versions[high] + end + + range = PubGrub::VersionRange.new(min: low, max: high, include_min: !low.nil?) + + self_constraint = PubGrub::VersionConstraint.new(package, range: range) + + dep_term = PubGrub::Term.new(dep_constraint, false) + self_term = PubGrub::Term.new(self_constraint, true) + + custom_explanation = if dep_package.meta? && package.root? + "current #{dep_package} version is #{dep_constraint.constraint_string}" end + + PubGrub::Incompatibility.new([self_term, dep_term], cause: :dependency, custom_explanation: custom_explanation) end - search.select {|sg| sg.for?(platform) }.each {|sg| sg.activate_platform!(platform) } end - def index_for(dependency) - source = @source_requirements[dependency.name] - if source - source.specs - elsif @lockfile_uses_separate_rubygems_sources - Index.build do |idx| - if dependency.all_sources - dependency.all_sources.each {|s| idx.add_source(s.specs) if s } - else - idx.add_source @source_requirements[:default].specs + def all_versions_for(package) + name = package.name + results = (@base[name] + filter_specs(@all_specs[name], package)).uniq {|spec| [spec.version.hash, spec.platform] } + + if name == "bundler" && !bundler_pinned_to_current_version? + bundler_spec = Gem.loaded_specs["bundler"] + results << bundler_spec if bundler_spec + end + + locked_requirement = base_requirements[name] + results = filter_matching_specs(results, locked_requirement) if locked_requirement + + results.group_by(&:version).reduce([]) do |groups, (version, specs)| + platform_specs = package.platform_specs(specs) + + # If package is a top-level dependency, + # candidate is only valid if there are matching versions for all resolution platforms. + # + # If package is not a top-level deependency, + # then it's not necessary that it has matching versions for all platforms, since it may have been introduced only as + # a dependency for a platform specific variant, so it will only need to have a valid version for that platform. + # + if package.top_level? + next groups if platform_specs.any?(&:empty?) + else + next groups if platform_specs.all?(&:empty?) + end + + ruby_specs = MatchPlatform.select_best_platform_match(specs, Gem::Platform::RUBY) + ruby_group = Resolver::SpecGroup.new(ruby_specs) + + unless ruby_group.empty? + platform_specs.each do |s| + ruby_group.merge(Resolver::SpecGroup.new(s)) end + + groups << Resolver::Candidate.new(version, group: ruby_group, priority: -1) + next groups if package.force_ruby_platform? end - else - @index + + platform_group = Resolver::SpecGroup.new(platform_specs.flatten.uniq) + next groups if platform_group == ruby_group + + groups << Resolver::Candidate.new(version, group: platform_group, priority: 1) + + groups end end - def name_for(dependency) - dependency.name + def source_for(name) + @source_requirements[name] || @source_requirements[:default] + end + + def default_bundler_source + @source_requirements[:default_bundler] + end + + def bundler_pinned_to_current_version? + !default_bundler_source.nil? end def name_for_explicit_dependency_source Bundler.default_gemfile.basename.to_s - rescue + rescue StandardError "Gemfile" end - def name_for_locking_dependency_source - Bundler.default_lockfile.basename.to_s - rescue - "Gemfile.lock" + def raise_incomplete!(incomplete_specs) + raise_not_found!(@base.get_package(incomplete_specs.first.name)) end - def requirement_satisfied_by?(requirement, activated, spec) - return false unless requirement.matches_spec?(spec) || spec.source.is_a?(Source::Gemspec) - if spec.version.prerelease? && !requirement.prerelease? && search_for(requirement).any? {|sg| !sg.version.prerelease? } - vertex = activated.vertex_named(spec.name) - return false if vertex.requirements.none?(&:prerelease?) - end - spec.activate_platform!(requirement.__platform) if !@platforms || @platforms.include?(requirement.__platform) - true + def sort_versions_by_preferred(package, versions) + @gem_version_promoter.sort_versions(package, versions) end - def relevant_sources_for_vertex(vertex) - if vertex.root? - [@source_requirements[vertex.name]] - elsif @lockfile_uses_separate_rubygems_sources - vertex.recursive_predecessors.map do |v| - @source_requirements[v.name] - end << @source_requirements[:default] + private + + def raise_not_found!(package) + name = package.name + source = source_for(name) + specs = @all_specs[name] + matching_part = name + requirement_label = SharedHelpers.pretty_dependency(package.dependency) + cache_message = begin + " or in gems cached in #{Bundler.settings.app_cache_path}" if Bundler.app_cache.exist? + rescue GemfileNotFound + nil + end + specs_matching_requirement = filter_matching_specs(specs, package.dependency.requirement) + + not_found_message = if specs_matching_requirement.any? + specs = specs_matching_requirement + matching_part = requirement_label + platforms = package.platforms + + if platforms.size == 1 + "Could not find gem '#{requirement_label}' with platform '#{platforms.first}'" + else + "Could not find gems matching '#{requirement_label}' valid for all resolution platforms (#{platforms.join(", ")})" + end + else + "Could not find gem '#{requirement_label}'" + end + + message = String.new("#{not_found_message} in #{source}#{cache_message}.\n") + + if specs.any? + message << "\n#{other_specs_matching_message(specs, matching_part)}" + end + + if hint = cooldown_hint(specs_matching_requirement) + message << "\n\n#{hint}." end + + if specs_matching_requirement.any? && (hint = platform_mismatch_hint) + message << "\n\n#{hint}" + end + + raise GemNotFound, message end - def sort_dependencies(dependencies, activated, conflicts) - dependencies.sort_by do |dependency| - dependency.all_sources = relevant_sources_for_vertex(activated.vertex_named(dependency.name)) - name = name_for(dependency) - vertex = activated.vertex_named(name) - [ - @base_dg.vertex_named(name) ? 0 : 1, - vertex.payload ? 0 : 1, - vertex.root? ? 0 : 1, - amount_constrained(dependency), - conflicts[name] ? 0 : 1, - vertex.payload ? 0 : search_for(dependency).count, - self.class.platform_sort_key(dependency.__platform), - ] + def platform_mismatch_hint + locked_platforms = Bundler.locked_gems&.platforms + return unless locked_platforms + + local_platform = Bundler.local_platform + return if locked_platforms.include?(local_platform) + return if locked_platforms.any? {|p| p == Gem::Platform::RUBY } + + "Your current platform (#{local_platform}) is not included in the lockfile's platforms (#{locked_platforms.join(", ")}). " \ + "Add the current platform to the lockfile with\n`bundle lock --add-platform #{local_platform}` and try again." + rescue GemfileNotFound + nil + end + + def filtered_versions_for(package) + @gem_version_promoter.filter_versions(package, @all_versions[package]) + end + + def raise_all_versions_filtered_out!(package) + level = @gem_version_promoter.level + name = package.name + locked_version = package.locked_version + requirement = package.dependency + + raise GemNotFound, + "#{name} is locked to #{locked_version}, while Gemfile is requesting #{requirement}. " \ + "--strict --#{level} was specified, but there are no #{level} level upgrades from #{locked_version} satisfying #{requirement}, so version solving has failed" + end + + def filter_matching_specs(specs, requirements) + Array(requirements).flat_map do |requirement| + specs.select {| spec| requirement_satisfied_by?(requirement, spec) } end end - # Sort platforms from most general to most specific - def self.sort_platforms(platforms) - platforms.sort_by do |platform| - platform_sort_key(platform) + def filter_specs(specs, package) + filter_remote_specs(filter_cooldown(filter_prereleases(specs, package)), package) + end + + def filter_prereleases(specs, package) + return specs unless package.ignores_prereleases? && specs.size > 1 + + specs.reject {|s| s.version.prerelease? } + end + + def filter_cooldown(specs) + return specs if specs.empty? + excluded_versions = cooldown_excluded_versions(specs) + return specs if excluded_versions.empty? + specs.reject {|s| excluded_versions.include?([s.name, s.version]) } + end + + def cooldown_excluded_versions(specs) + excluded = {} + specs.each do |spec| + next unless cooldown_excluded?(spec) + excluded[[spec.name, spec.version]] = true end + excluded + end + + def cooldown_hint(specs) + excluded_versions = cooldown_excluded_versions(specs) + return nil if excluded_versions.empty? + "#{excluded_versions.size} version#{"s" if excluded_versions.size > 1} excluded by the cooldown setting; pass `--cooldown 0` to bypass" end - def self.platform_sort_key(platform) - return ["", "", ""] if Gem::Platform::RUBY == platform - platform.to_a.map {|part| part || "" } + def cooldown_excluded?(spec) + return false unless spec.respond_to?(:created_at) && spec.created_at + return false unless spec.respond_to?(:remote) && spec.remote + days = spec.remote.effective_cooldown + return false if days.nil? || days <= 0 + (cooldown_now - spec.created_at) < (days * 86_400) end - private + def cooldown_now + @cooldown_now ||= Time.now + end + + def filter_remote_specs(specs, package) + if package.prefer_local? + local_specs = specs.select {|s| s.is_a?(StubSpecification) } - # returns an integer \in (-\infty, 0] - # a number closer to 0 means the dependency is less constraining - # - # dependencies w/ 0 or 1 possibilities (ignoring version requirements) - # are given very negative values, so they _always_ sort first, - # before dependencies that are unconstrained - def amount_constrained(dependency) - @amount_constrained ||= {} - @amount_constrained[dependency.name] ||= begin - if (base = @base[dependency.name]) && !base.empty? - dependency.requirement.satisfied_by?(base.first.version) ? 0 : 1 + if local_specs.empty? + package.consider_remote_versions! + specs else - all = index_for(dependency).search(dependency.name).size + local_specs + end + else + specs + end + end - if all <= 1 - all - 1_000_000 - else - search = search_for(dependency).size - search - all + # Ignore versions that depend on themselves incorrectly + def filter_invalid_self_dependencies(specs, name) + specs.reject do |s| + s.dependencies.any? {|d| d.name == name && !d.requirement.satisfied_by?(s.version) } + end + end + + def requirement_satisfied_by?(requirement, spec) + requirement.satisfied_by?(spec.version) || spec.source.is_a?(Source::Gemspec) + end + + def repository_for(package) + source_for(package.name) + end + + def base_requirements + @base.base_requirements + end + + def prepare_dependencies(requirements, packages) + to_dependency_hash(requirements, packages).filter_map do |dep_package, dep_constraint| + name = dep_package.name + + next [dep_package, dep_constraint] if name == "bundler" + + dep_range = dep_constraint.range + versions = versions_for(dep_package, dep_range) + if versions.empty? + if dep_package.ignores_prereleases? || dep_package.prefer_local? + @all_versions.delete(dep_package) + @sorted_versions.delete(dep_package) end + dep_package.consider_prereleases! if dep_package.ignores_prereleases? + dep_package.consider_remote_versions! if dep_package.prefer_local? + versions = versions_for(dep_package, dep_range) + end + + if versions.empty? && select_all_versions(dep_package, dep_range).any? + raise_all_versions_filtered_out!(dep_package) + end + + next [dep_package, dep_constraint] unless versions.empty? + + next unless dep_package.current_platform? + + raise_not_found!(dep_package) + end.to_h + end + + def select_all_versions(package, range) + range.select_versions(@all_versions[package]) + end + + def other_specs_matching_message(specs, requirement) + message = String.new("The source contains the following gems matching '#{requirement}':\n") + message << specs.map {|s| " * #{s.full_name}" }.join("\n") + message + end + + def requirement_to_range(requirement) + ranges = requirement.requirements.map do |(op, version)| + ver = Resolver::Candidate.new(version, priority: -1) + platform_ver = Resolver::Candidate.new(version, priority: 1) + + case op + when "~>" + name = "~> #{ver}" + bump = Resolver::Candidate.new(version.bump.to_s + ".A") + PubGrub::VersionRange.new(name: name, min: ver, max: bump, include_min: true) + when ">" + PubGrub::VersionRange.new(min: platform_ver) + when ">=" + PubGrub::VersionRange.new(min: ver, include_min: true) + when "<" + PubGrub::VersionRange.new(max: ver) + when "<=" + PubGrub::VersionRange.new(max: platform_ver, include_max: true) + when "=" + PubGrub::VersionRange.new(min: ver, max: platform_ver, include_min: true, include_max: true) + when "!=" + PubGrub::VersionRange.new(min: ver, max: platform_ver, include_min: true, include_max: true).invert + else + raise "bad version specifier: #{op}" end end + + ranges.inject(&:intersect) end - def verify_gemfile_dependencies_are_found!(requirements) - requirements.each do |requirement| - name = requirement.name - next if name == "bundler" - next unless search_for(requirement).empty? - - cache_message = begin - " or in gems cached in #{Bundler.settings.app_cache_path}" if Bundler.app_cache.exist? - rescue GemfileNotFound - nil - end - - if (base = @base[name]) && !base.empty? - version = base.first.version - message = "You have requested:\n" \ - " #{name} #{requirement.requirement}\n\n" \ - "The bundle currently has #{name} locked at #{version}.\n" \ - "Try running `bundle update #{name}`\n\n" \ - "If you are updating multiple gems in your Gemfile at once,\n" \ - "try passing them all to `bundle update`" - elsif source = @source_requirements[name] - specs = source.specs[name] - versions_with_platforms = specs.map {|s| [s.version, s.platform] } - message = String.new("Could not find gem '#{SharedHelpers.pretty_dependency(requirement)}' in #{source}#{cache_message}.\n") - message << if versions_with_platforms.any? - "The source contains '#{name}' at: #{formatted_versions_with_platforms(versions_with_platforms)}" - else - "The source does not contain any versions of '#{name}'" - end + def to_dependency_hash(dependencies, packages) + apply_overrides(dependencies).inject({}) do |deps, dep| + package = packages[dep.name] + + current_req = deps[package] + new_req = parse_dependency(package, dep.requirement) + + deps[package] = if current_req + current_req.intersect(new_req) else - message = "Could not find gem '#{requirement}' in any of the gem sources " \ - "listed in your Gemfile#{cache_message}." + new_req end - raise GemNotFound, message + + deps end end - def formatted_versions_with_platforms(versions_with_platforms) - version_platform_strs = versions_with_platforms.map do |vwp| - version = vwp.first - platform = vwp.last - version_platform_str = String.new(version.to_s) - version_platform_str << " #{platform}" unless platform.nil? || platform == Gem::Platform::RUBY - version_platform_str + def apply_overrides(dependencies) + return dependencies if @base.overrides.empty? + + dependencies.map do |dep| + override = Override.find_for(@base.overrides, dep.name, :version) + next dep unless override + Gem::Dependency.new(dep.name, override.apply_to(dep.requirement)) end - version_platform_strs.join(", ") - end - - def version_conflict_message(e) - e.message_with_trees( - :solver_name => "Bundler", - :possibility_type => "gem", - :reduce_trees => lambda do |trees| - maximal = 1.upto(trees.size).map do |size| - trees.map(&:last).flatten(1).combination(size).to_a - end.flatten(1).select do |deps| - Bundler::VersionRanges.empty?(*Bundler::VersionRanges.for_many(deps.map(&:requirement))) - end.min_by(&:size) - trees.reject! {|t| !maximal.include?(t.last) } if maximal - - trees = trees.sort_by {|t| t.flatten.map(&:to_s) } - trees.uniq! {|t| t.flatten.map {|dep| [dep.name, dep.requirement] } } - - trees.sort_by {|t| t.reverse.map(&:name) } - end, - :printable_requirement => lambda {|req| SharedHelpers.pretty_dependency(req) }, - :additional_message_for_conflict => lambda do |o, name, conflict| - if name == "bundler" - o << %(\n Current Bundler version:\n bundler (#{Bundler::VERSION})) - other_bundler_required = !conflict.requirement.requirement.satisfied_by?(Gem::Version.new Bundler::VERSION) - end + end - if name == "bundler" && other_bundler_required - o << "\n" - o << "This Gemfile requires a different version of Bundler.\n" - o << "Perhaps you need to update Bundler by running `gem install bundler`?\n" - end - if conflict.locked_requirement - o << "\n" - o << %(Running `bundle update` will rebuild your snapshot from scratch, using only\n) - o << %(the gems in your Gemfile, which may resolve the conflict.\n) - elsif !conflict.existing - o << "\n" - - relevant_sources = if conflict.requirement.source - [conflict.requirement.source] - elsif conflict.requirement.all_sources - conflict.requirement.all_sources - elsif @lockfile_uses_separate_rubygems_sources - # every conflict should have an explicit group of sources when we - # enforce strict pinning - raise "no source set for #{conflict}" - else - [] - end.compact.map(&:to_s).uniq.sort - - o << "Could not find gem '#{SharedHelpers.pretty_dependency(conflict.requirement)}'" - if conflict.requirement_trees.first.size > 1 - o << ", which is required by " - o << "gem '#{SharedHelpers.pretty_dependency(conflict.requirement_trees.first[-2])}'," - end - o << " " - - o << if relevant_sources.empty? - "in any of the sources.\n" - else - "in any of the relevant sources:\n #{relevant_sources * "\n "}\n" - end - end - end, - :version_for_spec => lambda {|spec| spec.version } - ) + METADATA_DEP_FIELD = { + "Ruby\0" => :required_ruby_version, + "RubyGems\0" => :required_rubygems_version, + }.freeze + + def apply_metadata_overrides(dependencies, name) + return dependencies if @base.overrides.empty? + + dependencies.map do |dep| + field = METADATA_DEP_FIELD[dep.name] + next dep unless field + override = Override.find_for(@base.overrides, name, field) + next dep unless override + Gem::Dependency.new(dep.name, override.apply_to(dep.requirement)) + end + end + + def bundler_not_found_message(conflict_dependencies) + candidate_specs = filter_matching_specs(default_bundler_source.specs.search("bundler"), conflict_dependencies) + + if candidate_specs.any? + target_version = candidate_specs.last.version + new_command = [File.basename($PROGRAM_NAME), "_#{target_version}_", *ARGV].join(" ") + "Your bundle requires a different version of Bundler than the one you're running.\n" \ + "Install the necessary version with `gem install bundler:#{target_version}` and rerun bundler using `#{new_command}`\n" + else + "Your bundle requires a different version of Bundler than the one you're running, and that version could not be found.\n" + end end end end diff --git a/lib/bundler/resolver/base.rb b/lib/bundler/resolver/base.rb new file mode 100644 index 0000000000..00bdd08303 --- /dev/null +++ b/lib/bundler/resolver/base.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require_relative "package" + +module Bundler + class Resolver + class Base + attr_reader :packages, :requirements, :source_requirements, :locked_specs, :overrides + + def initialize(source_requirements, dependencies, base, platforms, options) + @overrides = options.delete(:overrides) || [] + @source_requirements = source_requirements + @locked_specs = options[:locked_specs] + + @base = base + + @packages = Hash.new do |hash, name| + hash[name] = Package.new(name, platforms, **options) + end + + @requirements = dependencies.filter_map do |dep| + dep_platforms = dep.gem_platforms(platforms) + + # Dependencies scoped to external platforms are ignored + next if dep_platforms.empty? + + name = dep.name + + @packages[name] = Package.new(name, dep_platforms, **options.merge(dependency: dep)) + + dep + end + end + + def [](name) + @base[name] + end + + def delete(specs) + @base.delete(specs) + end + + def get_package(name) + @packages[name] + end + + def base_requirements + @base_requirements ||= build_base_requirements + end + + def unlock_names(names) + indirect_pins = indirect_pins(names) + + if indirect_pins.any? + loosen_names(indirect_pins) + else + pins = pins(names) + + if pins.any? + loosen_names(pins) + else + unrestrict_names(names) + end + end + end + + def include_prereleases(names) + names.each do |name| + get_package(name).consider_prereleases! + end + end + + def include_remote_specs(names) + names.each do |name| + get_package(name).consider_remote_versions! + end + end + + private + + def indirect_pins(names) + names.select {|name| @base_requirements[name].exact? && @requirements.none? {|dep| dep.name == name } } + end + + def pins(names) + names.select {|name| @base_requirements[name].exact? } + end + + def loosen_names(names) + names.each do |name| + version = @base_requirements[name].requirements.first[1] + + @base_requirements[name] = Gem::Requirement.new(">= #{version}") + + @base.delete_by_name(name) + end + end + + def unrestrict_names(names) + names.each do |name| + @base_requirements.delete(name) + end + end + + def build_base_requirements + base_requirements = {} + @base.each do |ls| + if ls.source_changed? && ls.source.specs.search(ls.name).empty? + raise GemNotFound, "Could not find gem '#{ls.name}' in #{ls.source}" + end + + req = Gem::Requirement.new(ls.version) + base_requirements[ls.name] = req + end + base_requirements + end + end + end +end diff --git a/lib/bundler/resolver/candidate.rb b/lib/bundler/resolver/candidate.rb new file mode 100644 index 0000000000..5298b2530f --- /dev/null +++ b/lib/bundler/resolver/candidate.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require_relative "spec_group" + +module Bundler + class Resolver + # + # This class is a PubGrub compatible "Version" class that takes Bundler + # resolution complexities into account. + # + # Each Resolver::Candidate has a underlying `Gem::Version` plus a set of + # platforms. For example, 1.1.0-x86_64-linux is a different resolution candidate + # from 1.1.0 (generic). This is because different platform variants of the + # same gem version can bring different dependencies, so they need to be + # considered separately. + # + # Some candidates may also keep some information explicitly about the + # package they refer to. These candidates are referred to as "canonical" and + # are used when materializing resolution results back into RubyGems + # specifications that can be installed, written to lockfiles, and so on. + # + class Candidate + include Comparable + + attr_reader :version + + def initialize(version, group: nil, priority: -1) + @spec_group = group || SpecGroup.new([]) + @version = Gem::Version.new(version) + @priority = priority + end + + def dependencies + @spec_group.dependencies + end + + def to_specs(package, most_specific_locked_platform) + return [] if package.meta? + + @spec_group.to_specs(package.force_ruby_platform?, most_specific_locked_platform) + end + + def prerelease? + @version.prerelease? + end + + def segments + @version.segments + end + + def <=>(other) + return unless other.is_a?(self.class) + + version_comparison = version <=> other.version + return version_comparison unless version_comparison.zero? + + priority <=> other.priority + end + + def ==(other) + return unless other.is_a?(self.class) + + version == other.version && priority == other.priority + end + + def eql?(other) + return unless other.is_a?(self.class) + + version.eql?(other.version) && priority.eql?(other.priority) + end + + def hash + [@version, @priority].hash + end + + def to_s + @version.to_s + end + + protected + + attr_reader :priority + end + end +end diff --git a/lib/bundler/resolver/incompatibility.rb b/lib/bundler/resolver/incompatibility.rb new file mode 100644 index 0000000000..4ac1b2e1ea --- /dev/null +++ b/lib/bundler/resolver/incompatibility.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Bundler + class Resolver + class Incompatibility < PubGrub::Incompatibility + attr_reader :extended_explanation + + def initialize(terms, cause:, custom_explanation: nil, extended_explanation: nil) + @extended_explanation = extended_explanation + + super(terms, cause: cause, custom_explanation: custom_explanation) + end + end + end +end diff --git a/lib/bundler/resolver/package.rb b/lib/bundler/resolver/package.rb new file mode 100644 index 0000000000..3906be3f57 --- /dev/null +++ b/lib/bundler/resolver/package.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Bundler + class Resolver + # + # Represents a gem being resolved, in a format PubGrub likes. + # + # The class holds the following information: + # + # * Platforms this gem will be resolved on. + # * The locked version of this gem resolution should favor (if any). + # * Whether the gem should be unlocked to its latest version. + # * The dependency explicit set in the Gemfile for this gem (if any). + # + class Package + attr_reader :name, :platforms, :dependency, :locked_version + + def initialize(name, platforms, locked_specs:, unlock:, prerelease: false, prefer_local: false, dependency: nil, new_platforms: []) + @name = name + @platforms = platforms + @locked_version = locked_specs.version_for(name) + @unlock = unlock + @dependency = dependency || Dependency.new(name, @locked_version) + @platforms |= [Gem::Platform::RUBY] if @dependency.default_force_ruby_platform + @top_level = !dependency.nil? + @prerelease = @dependency.prerelease? || @locked_version&.prerelease? || prerelease ? :consider_first : :ignore + @prefer_local = prefer_local + @new_platforms = new_platforms + end + + def platform_specs(specs) + platforms.map do |platform| + prefer_locked = @new_platforms.include?(platform) ? false : !unlock? + MatchPlatform.select_best_platform_match(specs, platform, prefer_locked: prefer_locked) + end + end + + def to_s + @name.delete("\0") + end + + def root? + false + end + + def top_level? + @top_level + end + + def meta? + @name.end_with?("\0") + end + + def ==(other) + self.class == other.class && @name == other.name + end + + def hash + @name.hash + end + + def unlock? + @unlock == true || @unlock.include?(name) + end + + def ignores_prereleases? + @prerelease == :ignore + end + + def prerelease_specified? + @prerelease == :consider_first + end + + def consider_prereleases! + @prerelease = :consider_last + end + + def prefer_local? + @prefer_local + end + + def consider_remote_versions! + @prefer_local = false + end + + def force_ruby_platform? + @dependency.force_ruby_platform + end + + def current_platform? + @dependency.current_platform? + end + end + end +end diff --git a/lib/bundler/resolver/root.rb b/lib/bundler/resolver/root.rb new file mode 100644 index 0000000000..e5eb634fb8 --- /dev/null +++ b/lib/bundler/resolver/root.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require_relative "package" + +module Bundler + class Resolver + # + # Represents the Gemfile from the resolver's perspective. It's the root + # package and Gemfile entries depend on it. + # + class Root < Package + def initialize(name) + @name = name + end + + def meta? + true + end + + def root? + true + end + end + end +end diff --git a/lib/bundler/resolver/spec_group.rb b/lib/bundler/resolver/spec_group.rb index 9c10a4b733..ac6ba86c4c 100644 --- a/lib/bundler/resolver/spec_group.rb +++ b/lib/bundler/resolver/spec_group.rb @@ -3,108 +3,71 @@ module Bundler class Resolver class SpecGroup - include GemHelpers - - attr_accessor :name, :version, :source - attr_accessor :ignores_bundler_dependencies - - def initialize(all_specs) - raise ArgumentError, "cannot initialize with an empty value" unless exemplary_spec = all_specs.first - @name = exemplary_spec.name - @version = exemplary_spec.version - @source = exemplary_spec.source - - @required_by = [] - @activated_platforms = [] - @dependencies = nil - @specs = Hash.new do |specs, platform| - specs[platform] = select_best_platform_match(all_specs, platform) - end - @ignores_bundler_dependencies = true + attr_reader :specs + + def initialize(specs) + @specs = specs end - def to_specs - @activated_platforms.map do |p| - next unless s = @specs[p] - lazy_spec = LazySpecification.new(name, version, s.platform, source) - lazy_spec.dependencies.replace s.dependencies - lazy_spec - end.compact + def empty? + @specs.empty? end - def activate_platform!(platform) - return unless for?(platform) - return if @activated_platforms.include?(platform) - @activated_platforms << platform + def name + @name ||= exemplary_spec.name end - def for?(platform) - spec = @specs[platform] - !spec.nil? + def version + @version ||= exemplary_spec.version end - def to_s - @to_s ||= "#{name} (#{version})" + def source + @source ||= exemplary_spec.source end - def dependencies_for_activated_platforms - dependencies = @activated_platforms.map {|p| __dependencies[p] } - metadata_dependencies = @activated_platforms.map do |platform| - metadata_dependencies(@specs[platform], platform) + def to_specs(force_ruby_platform, most_specific_locked_platform) + @specs.map do |s| + lazy_spec = LazySpecification.from_spec(s) + lazy_spec.force_ruby_platform = force_ruby_platform + lazy_spec.most_specific_locked_platform = most_specific_locked_platform + lazy_spec end - dependencies.concat(metadata_dependencies).flatten end - def platforms_for_dependency_named(dependency) - __dependencies.select {|_, deps| deps.map(&:name).include? dependency }.keys + def to_s + sorted_spec_names.join(", ") + end + + def dependencies + @dependencies ||= @specs.flat_map(&:expanded_dependencies).uniq.sort end def ==(other) - return unless other.is_a?(SpecGroup) - name == other.name && - version == other.version && - source == other.source + sorted_spec_names == other.sorted_spec_names end - def eql?(other) - name.eql?(other.name) && - version.eql?(other.version) && - source.eql?(other.source) + def merge(other) + return false unless equivalent?(other) + + @specs |= other.specs + + true end - def hash - to_s.hash ^ source.hash + protected + + def sorted_spec_names + @specs.map(&:full_name).sort end - private - - def __dependencies - @dependencies = Hash.new do |dependencies, platform| - dependencies[platform] = [] - if spec = @specs[platform] - spec.dependencies.each do |dep| - next if dep.type == :development - next if @ignores_bundler_dependencies && dep.name == "bundler".freeze - dependencies[platform] << DepProxy.new(dep, platform) - end - end - dependencies[platform] - end + private + + def equivalent?(other) + name == other.name && version == other.version && source == other.source && dependencies == other.dependencies end - def metadata_dependencies(spec, platform) - return [] unless spec - # Only allow endpoint specifications since they won't hit the network to - # fetch the full gemspec when calling required_ruby_version - return [] if !spec.is_a?(EndpointSpecification) && !spec.is_a?(Gem::Specification) - dependencies = [] - if !spec.required_ruby_version.nil? && !spec.required_ruby_version.none? - dependencies << DepProxy.new(Gem::Dependency.new("ruby\0", spec.required_ruby_version), platform) - end - if !spec.required_rubygems_version.nil? && !spec.required_rubygems_version.none? - dependencies << DepProxy.new(Gem::Dependency.new("rubygems\0", spec.required_rubygems_version), platform) - end - dependencies + def exemplary_spec + @specs.first end end end diff --git a/lib/bundler/resolver/strategy.rb b/lib/bundler/resolver/strategy.rb new file mode 100644 index 0000000000..7519d38968 --- /dev/null +++ b/lib/bundler/resolver/strategy.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Bundler + class Resolver + class Strategy + def initialize(source) + @source = source + @package_priority_cache = {} + end + + def next_package_and_version(unsatisfied) + package, range = next_term_to_try_from(unsatisfied) + + [package, most_preferred_version_of(package, range).first] + end + + private + + def next_term_to_try_from(unsatisfied) + unsatisfied.min_by do |package, range| + @package_priority_cache[[package, range]] ||= begin + matching_versions = @source.versions_for(package, range) + higher_versions = @source.versions_for(package, range.upper_invert) + + [matching_versions.count <= 1 ? 0 : 1, higher_versions.count] + end + end + end + + def most_preferred_version_of(package, range) + versions = @source.versions_for(package, range) + + # Conditional avoids (among other things) calling + # sort_versions_by_preferred with the root package + if versions.size > 1 + @source.sort_versions_by_preferred(package, versions) + else + versions + end + end + end + end +end diff --git a/lib/bundler/retry.rb b/lib/bundler/retry.rb index 244606dcc9..49b0f63838 100644 --- a/lib/bundler/retry.rb +++ b/lib/bundler/retry.rb @@ -6,6 +6,8 @@ module Bundler attr_accessor :name, :total_runs, :current_run class << self + attr_accessor :default_base_delay + def default_attempts default_retries + 1 end @@ -16,11 +18,17 @@ module Bundler end end - def initialize(name, exceptions = nil, retries = self.class.default_retries) + # Set default base delay for exponential backoff + self.default_base_delay = 1.0 + + def initialize(name, exceptions = nil, retries = self.class.default_retries, opts = {}) @name = name @retries = retries @exceptions = Array(exceptions) || [] @total_runs = @retries + 1 # will run once, then upto attempts.times + @base_delay = opts[:base_delay] || self.class.default_base_delay + @max_delay = opts[:max_delay] || 60.0 + @jitter = opts[:jitter] || 0.5 end def attempt(&block) @@ -32,13 +40,13 @@ module Bundler end alias_method :attempts, :attempt - private + private def run(&block) - @failed = false + @failed = false @current_run += 1 @result = block.call - rescue => e + rescue StandardError => e fail_attempt(e) end @@ -48,15 +56,33 @@ module Bundler Bundler.ui.info "" unless Bundler.ui.debug? raise e end - return true unless name - Bundler.ui.info "" unless Bundler.ui.debug? # Add new line incase dots preceded this - Bundler.ui.warn "Retrying #{name} due to error (#{current_run.next}/#{total_runs}): #{e.class} #{e.message}", Bundler.ui.debug? + if name + Bundler.ui.info "" unless Bundler.ui.debug? # Add new line in case dots preceded this + Bundler.ui.warn "Retrying #{name} due to error (#{current_run.next}/#{total_runs}): #{e.class} #{e.message}", true + end + backoff_sleep if @base_delay > 0 + true + end + + def backoff_sleep + # Exponential backoff: delay = base_delay * 2^(attempt - 1) + # Add jitter to prevent thundering herd: random value between 0 and jitter seconds + delay = @base_delay * (2**(@current_run - 1)) + delay = [@max_delay, delay].min + jitter_amount = rand * @jitter + total_delay = delay + jitter_amount + Bundler.ui.debug "Sleeping for #{total_delay.round(2)} seconds before retry" + sleep(total_delay) + end + + def sleep(duration) + Kernel.sleep(duration) end def keep_trying? return true if current_run.zero? return false if last_attempt? - return true if @failed + true if @failed end def last_attempt? diff --git a/lib/bundler/ruby_dsl.rb b/lib/bundler/ruby_dsl.rb index f6ba220cd5..5e52f38c8f 100644 --- a/lib/bundler/ruby_dsl.rb +++ b/lib/bundler/ruby_dsl.rb @@ -3,16 +3,65 @@ module Bundler module RubyDsl def ruby(*ruby_version) - options = ruby_version.last.is_a?(Hash) ? ruby_version.pop : {} + options = ruby_version.pop if ruby_version.last.is_a?(Hash) ruby_version.flatten! - raise GemfileError, "Please define :engine_version" if options[:engine] && options[:engine_version].nil? - raise GemfileError, "Please define :engine" if options[:engine_version] && options[:engine].nil? - if options[:engine] == "ruby" && options[:engine_version] && - ruby_version != Array(options[:engine_version]) - raise GemfileEvalError, "ruby_version must match the :engine_version for MRI" + if options + patchlevel = options[:patchlevel] + engine = options[:engine] + engine_version = options[:engine_version] + + raise GemfileError, "Please define :engine_version" if engine && engine_version.nil? + raise GemfileError, "Please define :engine" if engine_version && engine.nil? + + if options[:file] + raise GemfileError, "Do not pass version argument when using :file option" unless ruby_version.empty? + ruby_version << normalize_ruby_file(options[:file]) + end + + if engine == "ruby" && engine_version && ruby_version != Array(engine_version) + raise GemfileEvalError, "ruby_version must match the :engine_version for MRI" + end + end + + @ruby_version = RubyVersion.new(ruby_version, patchlevel, engine, engine_version) + end + + # Support the various file formats found in .ruby-version files. + # + # 3.2.2 + # ruby-3.2.2 + # + # Also supports .tool-versions files for asdf. Lines not starting with "ruby" are ignored. + # + # ruby 2.5.1 # comment is ignored + # ruby 2.5.1# close comment and extra spaces doesn't confuse + # + # Intentionally does not support `3.2.1@gemset` since rvm recommends using .ruby-gemset instead + # + # Loads the file relative to the dirname of the Gemfile itself. + def normalize_ruby_file(filename) + file_content = Bundler.read_file(gemfile.dirname.join(filename)) + # match "ruby-3.2.2", ruby = "3.2.2", ruby = '3.2.2' or "ruby 3.2.2" capturing version string up to the first space or comment + version_match = /^ # Start of line + ruby # Literal "ruby" + [\s-]* # Optional whitespace or hyphens (for "ruby-3.2.2" format) + (?:=\s*)? # Optional equals sign with whitespace (for ruby = "3.2.2" format) + (?: + "([^"]+)" # Double quoted version + | + '([^']+)' # Single quoted version + | + ([^\s#"']+) # Unquoted version + ) + /x.match(file_content) + if version_match + version_match[1] || version_match[2] || version_match[3] + else + file_content.strip end - @ruby_version = RubyVersion.new(ruby_version, options[:patchlevel], options[:engine], options[:engine_version]) + rescue Errno::ENOENT + raise GemfileError, "Could not find version file #{filename}" end end end diff --git a/lib/bundler/ruby_version.rb b/lib/bundler/ruby_version.rb index d4e1bdbfd5..aeff07582e 100644 --- a/lib/bundler/ruby_version.rb +++ b/lib/bundler/ruby_version.rb @@ -23,21 +23,26 @@ module Bundler # specified must match the version. @versions = Array(versions).map do |v| - op, v = Gem::Requirement.parse(v) + normalized_v = normalize_version(v) + + unless Gem::Requirement::PATTERN.match?(normalized_v) + raise InvalidArgumentError, "#{v} is not a valid requirement on the Ruby version" + end + + op, v = Gem::Requirement.parse(normalized_v) op == "=" ? v.to_s : "#{op} #{v}" end @gem_version = Gem::Requirement.create(@versions.first).requirements.first.last - @input_engine = engine && engine.to_s - @engine = engine && engine.to_s || "ruby" + @input_engine = engine&.to_s + @engine = engine&.to_s || "ruby" @engine_versions = (engine_version && Array(engine_version)) || @versions @engine_gem_version = Gem::Requirement.create(@engine_versions.first).requirements.first.last - @patchlevel = patchlevel + @patchlevel = patchlevel || (@gem_version.prerelease? ? "-1" : nil) end def to_s(versions = self.versions) output = String.new("ruby #{versions_string(versions)}") - output << "p#{patchlevel}" if patchlevel output << " (#{engine} #{versions_string(engine_versions)})" unless engine == "ruby" output @@ -46,7 +51,7 @@ module Bundler # @private PATTERN = / ruby\s - ([\d.]+) # ruby version + (\d+\.\d+\.\d+(?:\.\S+)?) # ruby version (?:p(-?\d+))? # optional patchlevel (?:\s\((\S+)\s(.+)\))? # optional engine info /xo @@ -66,15 +71,14 @@ module Bundler def ==(other) versions == other.versions && engine == other.engine && - engine_versions == other.engine_versions && - patchlevel == other.patchlevel + engine_versions == other.engine_versions end def host @host ||= [ RbConfig::CONFIG["host_cpu"], RbConfig::CONFIG["host_vendor"], - RbConfig::CONFIG["host_os"] + RbConfig::CONFIG["host_os"], ].join("-") end @@ -92,8 +96,6 @@ module Bundler [:version, versions_string(versions), versions_string(other.versions)] elsif @input_engine && !matches?(engine_versions, other.engine_gem_version) [:engine_version, versions_string(engine_versions), versions_string(other.engine_versions)] - elsif patchlevel && (!patchlevel.is_a?(String) || !other.patchlevel.is_a?(String) || !matches?(patchlevel, other.patchlevel)) - [:patchlevel, patchlevel, other.patchlevel] end end @@ -102,44 +104,23 @@ module Bundler end def self.system - ruby_engine = if defined?(RUBY_ENGINE) && !RUBY_ENGINE.nil? - RUBY_ENGINE.dup - else - # not defined in ruby 1.8.7 - "ruby" - end - # :sob: mocking RUBY_VERSION breaks stuff on 1.8.7 - ruby_version = ENV.fetch("BUNDLER_SPEC_RUBY_VERSION") { RUBY_VERSION }.dup - ruby_engine_version = case ruby_engine - when "ruby" - ruby_version - when "rbx" - Rubinius::VERSION.dup - when "jruby" - JRUBY_VERSION.dup - else - raise BundlerError, "RUBY_ENGINE value #{RUBY_ENGINE} is not recognized" - end + ruby_engine = RUBY_ENGINE.dup + ruby_version = Gem.ruby_version.to_s + ruby_engine_version = RUBY_ENGINE == "ruby" ? ruby_version : RUBY_ENGINE_VERSION.dup patchlevel = RUBY_PATCHLEVEL.to_s - @ruby_version ||= RubyVersion.new(ruby_version, patchlevel, ruby_engine, ruby_engine_version) + @system ||= RubyVersion.new(ruby_version, patchlevel, ruby_engine, ruby_engine_version) end - def to_gem_version_with_patchlevel - @gem_version_with_patch ||= begin - Gem::Version.create("#{@gem_version}.#{@patchlevel}") - rescue ArgumentError - @gem_version - end - end + private - def exact? - return @exact if defined?(@exact) - @exact = versions.all? {|v| Gem::Requirement.create(v).exact? } + # Ruby's official preview version format uses a `-`: Example: 3.3.0-preview2 + # However, RubyGems recognizes preview version format with a `.`: Example: 3.3.0.preview2 + # Returns version string after replacing `-` with `.` + def normalize_version(version) + version.tr("-", ".") end - private - def matches?(requirements, version) # Handles RUBY_PATCHLEVEL of -1 for instances like ruby-head return requirements == version if requirements.to_s == "-1" || version.to_s == "-1" diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb index e9f0eac355..4ad2bdf46f 100644 --- a/lib/bundler/rubygems_ext.rb +++ b/lib/bundler/rubygems_ext.rb @@ -1,54 +1,211 @@ # frozen_string_literal: true -require "pathname" +require "rubygems" unless defined?(Gem) + +# We can't let `Gem::Source` be autoloaded in the `Gem::Specification#source` +# redefinition below, so we need to load it upfront. The reason is that if +# Bundler monkeypatches are loaded before RubyGems activates an executable (for +# example, through `ruby -rbundler -S irb`), gem activation might end up calling +# the redefined `Gem::Specification#source` and triggering the `Gem::Source` +# autoload. That would result in requiring "rubygems/source" inside another +# require, which would trigger a monitor error and cause the `autoload` to +# eventually fail. A better solution is probably to completely avoid autoloading +# `Gem::Source` from the redefined `Gem::Specification#source`. +require "rubygems/source" -if defined?(Gem::QuickLoader) - # Gem Prelude makes me a sad panda :'( - Gem::QuickLoader.load_full_rubygems_library -end +module Gem + # Can be removed once RubyGems 3.5.11 support is dropped + unless Gem.respond_to?(:freebsd_platform?) + def self.freebsd_platform? + RbConfig::CONFIG["host_os"].to_s.include?("bsd") + end + end -require "rubygems" -require "rubygems/specification" + # Can be removed once RubyGems 3.5.18 support is dropped + unless Gem.respond_to?(:open_file_with_lock) + class << self + remove_method :open_file_with_flock if Gem.respond_to?(:open_file_with_flock) + + def open_file_with_flock(path, &block) + # read-write mode is used rather than read-only in order to support NFS + mode = IO::RDWR | IO::APPEND | IO::CREAT | IO::BINARY + mode |= IO::SHARE_DELETE if IO.const_defined?(:SHARE_DELETE) + + File.open(path, mode) do |io| + begin + io.flock(File::LOCK_EX) + rescue Errno::ENOSYS, Errno::ENOTSUP + end + yield io + end + end -begin - # Possible use in Gem::Specification#source below and require - # shouldn't be deferred. - require "rubygems/source" -rescue LoadError - # Not available before RubyGems 2.0.0, ignore - nil -end + def open_file_with_lock(path, &block) + file_lock = "#{path}.lock" + open_file_with_flock(file_lock, &block) + ensure + FileUtils.rm_f file_lock + end + end + end -require "bundler/match_platform" + require "rubygems/platform" -module Gem - @loaded_stacks = Hash.new {|h, k| h[k] = [] } + class Platform + # Can be removed once RubyGems 3.6.9 support is dropped + unless respond_to?(:generic) + JAVA = Gem::Platform.new("java") # :nodoc: + MSWIN = Gem::Platform.new("mswin32") # :nodoc: + MSWIN64 = Gem::Platform.new("mswin64") # :nodoc: + MINGW = Gem::Platform.new("x86-mingw32") # :nodoc: + X64_MINGW_LEGACY = Gem::Platform.new("x64-mingw32") # :nodoc: + X64_MINGW = Gem::Platform.new("x64-mingw-ucrt") # :nodoc: + UNIVERSAL_MINGW = Gem::Platform.new("universal-mingw") # :nodoc: + WINDOWS = [MSWIN, MSWIN64, UNIVERSAL_MINGW].freeze # :nodoc: + X64_LINUX = Gem::Platform.new("x86_64-linux") # :nodoc: + X64_LINUX_MUSL = Gem::Platform.new("x86_64-linux-musl") # :nodoc: + + GENERICS = [JAVA, *WINDOWS].freeze # :nodoc: + private_constant :GENERICS + + GENERIC_CACHE = GENERICS.each_with_object({}) {|g, h| h[g] = g } # :nodoc: + private_constant :GENERIC_CACHE + + class << self + ## + # Returns the generic platform for the given platform. + + def generic(platform) + return Gem::Platform::RUBY if platform.nil? || platform == Gem::Platform::RUBY + + GENERIC_CACHE[platform] ||= begin + found = GENERICS.find do |match| + platform === match + end + found || Gem::Platform::RUBY + end + end + + ## + # Returns the platform specificity match for the given spec platform and user platform. + + def platform_specificity_match(spec_platform, user_platform) + return -1 if spec_platform == user_platform + return 1_000_000 if spec_platform.nil? || spec_platform == Gem::Platform::RUBY || user_platform == Gem::Platform::RUBY + + os_match(spec_platform, user_platform) + + cpu_match(spec_platform, user_platform) * 10 + + version_match(spec_platform, user_platform) * 100 + end + + ## + # Sorts and filters the best platform match for the given matching specs and platform. + + def sort_and_filter_best_platform_match(matching, platform) + return matching if matching.one? + + exact = matching.select {|spec| spec.platform == platform } + return exact if exact.any? + + sorted_matching = sort_best_platform_match(matching, platform) + exemplary_spec = sorted_matching.first + + sorted_matching.take_while {|spec| same_specificity?(platform, spec, exemplary_spec) && same_deps?(spec, exemplary_spec) } + end + + ## + # Sorts the best platform match for the given matching specs and platform. + + def sort_best_platform_match(matching, platform) + matching.sort_by.with_index do |spec, i| + [ + platform_specificity_match(spec.platform, platform), + i, # for stable sort + ] + end + end + + private + + def same_specificity?(platform, spec, exemplary_spec) + platform_specificity_match(spec.platform, platform) == platform_specificity_match(exemplary_spec.platform, platform) + end + + def same_deps?(spec, exemplary_spec) + spec.required_ruby_version == exemplary_spec.required_ruby_version && + spec.required_rubygems_version == exemplary_spec.required_rubygems_version && + spec.dependencies.sort == exemplary_spec.dependencies.sort + end + + def os_match(spec_platform, user_platform) + if spec_platform.os == user_platform.os + 0 + else + 1 + end + end + + def cpu_match(spec_platform, user_platform) + if spec_platform.cpu == user_platform.cpu + 0 + elsif spec_platform.cpu == "arm" && user_platform.cpu.to_s.start_with?("arm") + 0 + elsif spec_platform.cpu.nil? || spec_platform.cpu == "universal" + 1 + else + 2 + end + end + + def version_match(spec_platform, user_platform) + if spec_platform.version == user_platform.version + 0 + elsif spec_platform.version.nil? + 1 + else + 2 + end + end + end + + end + end + + require "rubygems/specification" + + # Can be removed once RubyGems 3.5.14 support is dropped + VALIDATES_FOR_RESOLUTION = Specification.new.respond_to?(:validate_for_resolution).freeze class Specification - attr_accessor :remote, :location, :relative_loaded_from + # Can be removed once RubyGems 3.5.15 support is dropped + correct_array_attributes = @@default_value.select {|_k,v| v.is_a?(Array) }.keys + unless @@array_attributes == correct_array_attributes + @@array_attributes = correct_array_attributes # rubocop:disable Style/ClassVars + end - if instance_methods(false).map(&:to_sym).include?(:source) - remove_method :source + require_relative "match_metadata" + require_relative "match_platform" + + include ::Bundler::MatchMetadata + + attr_accessor :remote, :relative_loaded_from + + module AllowSettingSource attr_writer :source + def source - (defined?(@source) && @source) || Gem::Source::Installed.new + (defined?(@source) && @source) || super end - else - attr_accessor :source end + prepend AllowSettingSource + alias_method :rg_full_gem_path, :full_gem_path alias_method :rg_loaded_from, :loaded_from - attr_writer :full_gem_path unless instance_methods.include?(:full_gem_path=) - def full_gem_path - # this cannot check source.is_a?(Bundler::Plugin::API::Source) - # because that _could_ trip the autoload, and if there are unresolved - # gems at that time, this method could be called inside another require, - # thus raising with that constant being undefined. Better to check a method - if source.respond_to?(:path) || (source.respond_to?(:bundler_plugin_api_source?) && source.bundler_plugin_api_source?) - Pathname.new(loaded_from).dirname.expand_path(source.root).to_s.untaint + if source.respond_to?(:root) + File.expand_path(File.dirname(loaded_from), source.root) else rg_full_gem_path end @@ -63,36 +220,32 @@ module Gem end def load_paths - return full_require_paths if respond_to?(:full_require_paths) - - require_paths.map do |require_path| - if require_path.include?(full_gem_path) - require_path - else - File.join(full_gem_path, require_path) - end - end + full_require_paths end - if method_defined?(:extension_dir) - alias_method :rg_extension_dir, :extension_dir - def extension_dir - @bundler_extension_dir ||= if source.respond_to?(:extension_dir_name) - File.expand_path(File.join(extensions_dir, source.extension_dir_name)) - else - rg_extension_dir - end + alias_method :rg_extension_dir, :extension_dir + def extension_dir + # following instance variable is already used in original method + # and that is the reason to prefix it with bundler_ and add rubocop exception + @bundler_extension_dir ||= if source.respond_to?(:extension_dir_name) # rubocop:disable Naming/MemoizedInstanceVariableName + unique_extension_dir = [source.extension_dir_name, File.basename(full_gem_path)].uniq.join("-") + File.expand_path(File.join(extensions_dir, unique_extension_dir)) + else + rg_extension_dir end end - # RubyGems 1.8+ used only. - methods = instance_methods(false) - gem_dir = methods.first.is_a?(String) ? "gem_dir" : :gem_dir - remove_method :gem_dir if methods.include?(gem_dir) + # Can be removed once RubyGems 3.5.21 support is dropped + remove_method :gem_dir if method_defined?(:gem_dir, false) + def gem_dir full_gem_path end + def insecurely_materialized? + false + end + def groups @groups ||= [] end @@ -116,7 +269,33 @@ module Gem dependencies - development_dependencies end - private + def installation_missing? + !default_gem? && !File.directory?(full_gem_path) + end + + def lock_name + @lock_name ||= name_tuple.lock_name + end + + unless VALIDATES_FOR_RESOLUTION + def validate_for_resolution + SpecificationPolicy.new(self).validate_for_resolution + end + end + + if Gem.rubygems_version < Gem::Version.new("3.5.22") + module FixPathSourceMissingExtensions + def missing_extensions? + return false if %w[Bundler::Source::Path Bundler::Source::Gemspec].include?(source.class.name) + + super + end + end + + prepend FixPathSourceMissingExtensions + end + + private def dependencies_to_gemfile(dependencies, group = nil) gemfile = String.new @@ -135,21 +314,47 @@ module Gem end end + unless VALIDATES_FOR_RESOLUTION + class SpecificationPolicy + def validate_for_resolution + validate_required! + end + end + end + + module BetterPermissionError + def data + super + rescue Errno::EACCES + raise Bundler::PermissionError.new(loaded_from, :read) + end + end + + require "rubygems/stub_specification" + + class StubSpecification + prepend BetterPermissionError + end + class Dependency - attr_accessor :source, :groups, :all_sources + require_relative "force_platform" + + include ::Bundler::ForcePlatform + + attr_reader :force_ruby_platform + + attr_accessor :source, :groups alias_method :eql?, :== - def encode_with(coder) - to_yaml_properties.each do |ivar| - coder[ivar.to_s.sub(/^@/, "")] = instance_variable_get(ivar) + unless method_defined?(:encode_with, false) + def encode_with(coder) + [:@name, :@requirement, :@type, :@prerelease, :@version_requirements].each do |ivar| + coder[ivar.to_s.sub(/^@/, "")] = instance_variable_get(ivar) + end end end - def to_yaml_properties - instance_variables.reject {|p| ["@source", "@groups", "@all_sources"].include?(p.to_s) } - end - def to_lock out = String.new(" #{name}") unless requirement.none? @@ -159,52 +364,140 @@ module Gem out end - # Backport of performance enhancement added to RubyGems 1.4 - def matches_spec?(spec) - # name can be a Regexp, so use === - return false unless name === spec.name - return true if requirement.none? + if Gem.rubygems_version < Gem::Version.new("3.5.22") + module FilterIgnoredSpecs + def matching_specs(platform_only = false) + super.reject(&:ignored?) + end + end - requirement.satisfied_by?(spec.version) - end unless allocate.respond_to?(:matches_spec?) + prepend FilterIgnoredSpecs + end end - class Requirement - # Backport of performance enhancement added to RubyGems 1.4 - def none? - # note that it might be tempting to replace with with RubyGems 2.0's - # improved implementation. Don't. It requires `DefaultRequirement` to be - # defined, and more importantantly, these overrides are not used when the - # running RubyGems defines these methods - to_s == ">= 0" - end unless allocate.respond_to?(:none?) - - # Backport of performance enhancement added to RubyGems 2.2 - def exact? - return false unless @requirements.size == 1 - @requirements[0][0] == "=" - end unless allocate.respond_to?(:exact?) + # On universal Rubies, resolve the "universal" arch to the real CPU arch, without changing the extension directory. + class BasicSpecification + if /^universal\.(?<arch>.*?)-/ =~ (CROSS_COMPILING || RUBY_PLATFORM) + local_platform = Platform.local + if local_platform.cpu == "universal" + ORIGINAL_LOCAL_PLATFORM = local_platform.to_s.freeze + + local_platform.cpu = if arch == "arm64e" # arm64e is only permitted for Apple system binaries + "arm64" + else + arch + end + + def extensions_dir + @extensions_dir ||= + Gem.default_ext_dir_for(base_dir) || File.join(base_dir, "extensions", ORIGINAL_LOCAL_PLATFORM, Gem.extension_api_version) + end + end + end + + # Can be removed once RubyGems 3.5.22 support is dropped + unless new.respond_to?(:ignored?) + def ignored? + return @ignored unless @ignored.nil? + + @ignored = missing_extensions? + end + end + + # Can be removed once RubyGems 3.6.9 support is dropped + unless new.respond_to?(:installable_on_platform?) + include(::Bundler::MatchPlatform) + end end - class Platform - JAVA = Gem::Platform.new("java") unless defined?(JAVA) - MSWIN = Gem::Platform.new("mswin32") unless defined?(MSWIN) - MSWIN64 = Gem::Platform.new("mswin64") unless defined?(MSWIN64) - MINGW = Gem::Platform.new("x86-mingw32") unless defined?(MINGW) - X64_MINGW = Gem::Platform.new("x64-mingw32") unless defined?(X64_MINGW) + require "rubygems/name_tuple" + + class NameTuple + # Versions of RubyGems before about 3.5.0 don't to_s the platform. + unless Gem::NameTuple.new("a", Gem::Version.new("1"), Gem::Platform.new("x86_64-linux")).platform.is_a?(String) + alias_method :initialize_with_platform, :initialize - undef_method :hash if method_defined? :hash - def hash - @cpu.hash ^ @os.hash ^ @version.hash + def initialize(name, version, platform = Gem::Platform::RUBY) + if Gem::Platform === platform + initialize_with_platform(name, version, platform.to_s) + else + initialize_with_platform(name, version, platform) + end + end end - undef_method :eql? if method_defined? :eql? - alias_method :eql?, :== + def lock_name + if platform == Gem::Platform::RUBY + "#{name} (#{version})" + else + "#{name} (#{version}-#{platform})" + end + end end -end -module Gem - class Specification - include ::Bundler::MatchPlatform + unless Gem.rubygems_version >= Gem::Version.new("3.5.19") + class Resolver::ActivationRequest + remove_method :installed? + + def installed? + case @spec + when Gem::Resolver::VendorSpecification then + true + else + this_spec = full_spec + + Gem::Specification.any? do |s| + s == this_spec && s.base_dir == this_spec.base_dir + end + end + end + end + end + + unless Gem.rubygems_version >= Gem::Version.new("3.6.7") + module UnfreezeCompactIndexParsedResponse + def parse(line) + version, platform, dependencies, requirements = super + [version, platform, dependencies.frozen? ? dependencies.dup : dependencies, requirements.frozen? ? requirements.dup : requirements] + end + end + + Resolver::APISet::GemParser.prepend(UnfreezeCompactIndexParsedResponse) + end + + # RubyGems before 4.0.13 split compact index dependency/requirement entries + # on every colon, which mangles metadata values that contain colons such as + # the `created_at` timestamps the cooldown feature relies on. Split only on + # the first colon so those values survive on older RubyGems. + # + # The module is defined unconditionally so it stays testable on any RubyGems, + # but only prepended when the host RubyGems still has the buggy behavior. + module SplitCompactIndexEntryOnFirstColon + private + + def parse_dependency(string) + dependency = string.split(":", 2) + dependency[-1] = dependency[-1].split("&") if dependency.size > 1 + dependency[0] = -dependency[0] + dependency + end + end + + unless Gem.rubygems_version >= Gem::Version.new("4.0.13") + Resolver::APISet::GemParser.prepend(SplitCompactIndexEntryOnFirstColon) + end + + if Gem.rubygems_version < Gem::Version.new("3.6.0") + class Package; end + require "rubygems/package/tar_reader" + require "rubygems/package/tar_reader/entry" + + module FixFullNameEncoding + def full_name + super.force_encoding(Encoding::UTF_8) + end + end + + Package::TarReader::Entry.prepend(FixFullNameEncoding) end end diff --git a/lib/bundler/rubygems_gem_installer.rb b/lib/bundler/rubygems_gem_installer.rb index 2b7fa8e0f6..fc019f54d2 100644 --- a/lib/bundler/rubygems_gem_installer.rb +++ b/lib/bundler/rubygems_gem_installer.rb @@ -4,95 +4,192 @@ require "rubygems/installer" module Bundler class RubyGemsGemInstaller < Gem::Installer - unless respond_to?(:at) - def self.at(*args) - new(*args) + def check_executable_overwrite(filename) + # Bundler needs to install gems regardless of binstub overwriting + end + + def install + pre_install_checks + + run_pre_install_hooks + + spec.loaded_from = spec_file + + # Completely remove any previous gem files + strict_rm_rf gem_dir + strict_rm_rf spec.extension_dir + + SharedHelpers.filesystem_access(gem_dir, :create) do + FileUtils.mkdir_p gem_dir + end + + SharedHelpers.filesystem_access(gem_dir, :write) do + extract_files + end + + if options[:build_extension] == false + warn_skipped_extensions + elsif spec.extensions.any? + build_extensions + end + write_build_info_file + run_post_build_hooks + + SharedHelpers.filesystem_access(bin_dir, :write) do + generate_bin + end + + if options[:install_plugin] == false + remove_stale_plugins + warn_skipped_plugins + else + generate_plugins end + + write_spec + + SharedHelpers.filesystem_access("#{gem_home}/cache", :write) do + write_cache_file + end + + say spec.post_install_message unless spec.post_install_message.nil? + + run_post_install_hooks + + spec end - def check_executable_overwrite(filename) - # Bundler needs to install gems regardless of binstub overwriting + if Bundler.rubygems.provides?("< 3.5") + def pre_install_checks + super + rescue Gem::FilePermissionError + # Ignore permission checks in RubyGems. Instead, go on, and try to write + # for real. We properly handle permission errors when they happen. + nil + end + end + + def ensure_writable_dir(dir) + super + rescue Gem::FilePermissionError + # Ignore permission checks in RubyGems. Instead, go on, and try to write + # for real. We properly handle permission errors when they happen. + nil + end + + def generate_plugins + return unless Gem::Installer.method_defined?(:generate_plugins, false) + + ensure_writable_dir @plugins_dir + + if spec.plugins.empty? + remove_plugins_for(spec, @plugins_dir) + else + regenerate_plugins_for(spec, @plugins_dir) + end + end + + def warn_skipped_extensions + return if spec.extensions.empty? + + Bundler.ui.warn "#{spec.full_name} contains native extensions that were not built.\n" \ + "To build extensions, unset no_build_extension and run `bundle pristine #{spec.name}`." + end + + def warn_skipped_plugins + return if spec.plugins.empty? + + Bundler.ui.warn "#{spec.full_name} contains plugins that were not installed.\n" \ + "To install plugins, unset no_install_plugin and run `bundle pristine #{spec.name}`." + end + + if Bundler.rubygems.provides?("< 3.5.19") + def generate_bin_script(filename, bindir) + bin_script_path = File.join bindir, formatted_program_filename(filename) + + Gem.open_file_with_lock(bin_script_path) do + require "fileutils" + FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers + + File.open(bin_script_path, "wb", 0o755) do |file| + file.write app_script_text(filename) + file.chmod(options[:prog_mode] || 0o755) + end + end + + verbose bin_script_path + + generate_windows_script filename, bindir + end end - def pre_install_checks - super && validate_bundler_checksum(options[:bundler_expected_checksum]) + def build_jobs + Bundler.settings[:jobs] || super end def build_extensions extension_cache_path = options[:bundler_extension_cache_path] - return super unless extension_cache_path && extension_dir = Bundler.rubygems.spec_extension_dir(spec) + extension_dir = spec.extension_dir + unless extension_cache_path && extension_dir + prepare_extension_build(extension_dir) + return super + end - extension_dir = Pathname.new(extension_dir) build_complete = SharedHelpers.filesystem_access(extension_cache_path.join("gem.build_complete"), :read, &:file?) if build_complete && !options[:force] - SharedHelpers.filesystem_access(extension_dir.parent, &:mkpath) + SharedHelpers.filesystem_access(File.dirname(extension_dir)) do |p| + FileUtils.mkpath p + end SharedHelpers.filesystem_access(extension_cache_path) do - FileUtils.cp_r extension_cache_path, spec.extension_dir + FileUtils.cp_r extension_cache_path, extension_dir end else + prepare_extension_build(extension_dir) super - if extension_dir.directory? # not made for gems without extensions - SharedHelpers.filesystem_access(extension_cache_path.parent, &:mkpath) - SharedHelpers.filesystem_access(extension_cache_path) do - FileUtils.cp_r extension_dir, extension_cache_path - end + SharedHelpers.filesystem_access(extension_cache_path.parent, &:mkpath) + SharedHelpers.filesystem_access(extension_cache_path) do + FileUtils.cp_r extension_dir, extension_cache_path end end end - private - - def validate_bundler_checksum(checksum) - return true if Bundler.settings[:disable_checksum_validation] - return true unless checksum - return true unless source = @package.instance_variable_get(:@gem) - return true unless source.respond_to?(:with_read_io) - digest = source.with_read_io do |io| - digest = SharedHelpers.digest(:SHA256).new - digest << io.read(16_384) until io.eof? - io.rewind - send(checksum_type(checksum), digest) - end - unless digest == checksum - raise SecurityError, <<-MESSAGE - Bundler cannot continue installing #{spec.name} (#{spec.version}). - The checksum for the downloaded `#{spec.full_name}.gem` does not match \ - the checksum given by the server. This means the contents of the downloaded \ - gem is different from what was uploaded to the server, and could be a potential security issue. - - To resolve this issue: - 1. delete the downloaded gem located at: `#{spec.gem_dir}/#{spec.full_name}.gem` - 2. run `bundle install` - - If you wish to continue installing the downloaded gem, and are certain it does not pose a \ - security issue despite the mismatching checksum, do the following: - 1. run `bundle config disable_checksum_validation true` to turn off checksum verification - 2. run `bundle install` - - (More info: The expected SHA256 checksum was #{checksum.inspect}, but the \ - checksum for the downloaded gem was #{digest.inspect}.) - MESSAGE + def spec + if Bundler.rubygems.provides?("< 3.3.12") # RubyGems implementation rescues and re-raises errors before 3.3.12 and we don't want that + @package.spec + else + super end - true end - def checksum_type(checksum) - case checksum.length - when 64 then :hexdigest! - when 44 then :base64digest! - else raise InstallError, "The given checksum for #{spec.full_name} (#{checksum.inspect}) is not a valid SHA256 hexdigest nor base64digest" - end + def gem_checksum + Checksum.from_gem_package(@package) end - def hexdigest!(digest) - digest.hexdigest! + private + + def prepare_extension_build(extension_dir) + SharedHelpers.filesystem_access(extension_dir, :create) do + FileUtils.mkdir_p extension_dir + end end - def base64digest!(digest) - if digest.respond_to?(:base64digest!) - digest.base64digest! - else - [digest.digest!].pack("m0") + def strict_rm_rf(dir) + return unless File.exist?(dir) + return if Dir.empty?(dir) + + parent = File.dirname(dir) + parent_st = File.stat(parent) + + if parent_st.world_writable? && !parent_st.sticky? + raise InsecureInstallPathError.new(spec.full_name, dir) + end + + begin + FileUtils.remove_entry_secure(dir) + rescue StandardError => e + raise unless File.exist?(dir) + + raise DirectoryRemovalError.new(e, "Could not delete previous installation of `#{dir}`") end end end diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb index 0f16b6231d..e04ef23259 100644 --- a/lib/bundler/rubygems_integration.rb +++ b/lib/bundler/rubygems_integration.rb @@ -1,47 +1,37 @@ # frozen_string_literal: true -require "monitor" -require "rubygems" -require "rubygems/config_file" +require "rubygems" unless defined?(Gem) module Bundler class RubygemsIntegration - if defined?(Gem::Ext::Builder::CHDIR_MONITOR) - EXT_LOCK = Gem::Ext::Builder::CHDIR_MONITOR - else - EXT_LOCK = Monitor.new - end + require "monitor" - def self.version - @version ||= Gem::Version.new(Gem::VERSION) - end - - def self.provides?(req_str) - Gem::Requirement.new(req_str).satisfied_by?(version) - end + EXT_LOCK = Monitor.new def initialize @replaced_methods = {} end def version - self.class.version + @version ||= Gem.rubygems_version end def provides?(req_str) - self.class.provides?(req_str) + Gem::Requirement.new(req_str).satisfied_by?(version) end def build_args + require "rubygems/command" Gem::Command.build_args end def build_args=(args) + require "rubygems/command" Gem::Command.build_args = args end - def load_path_insert_index - Gem.load_path_insert_index + def set_target_rbconfig(path) + Gem.set_target_rbconfig(path) end def loaded_specs(name) @@ -58,7 +48,7 @@ module Bundler end def validate(spec) - Bundler.ui.silence { spec.validate(false) } + Bundler.ui.silence { spec.validate_for_resolution } rescue Gem::InvalidSpecificationException => e error_message = "The gemspec at #{spec.loaded_from} is not valid. Please fix this gemspec.\n" \ "The validation error was '#{e.message}'\n" @@ -67,37 +57,6 @@ module Bundler nil end - def set_installed_by_version(spec, installed_by_version = Gem::VERSION) - return unless spec.respond_to?(:installed_by_version=) - spec.installed_by_version = Gem::Version.create(installed_by_version) - end - - def spec_missing_extensions?(spec, default = true) - return spec.missing_extensions? if spec.respond_to?(:missing_extensions?) - - return false if spec_default_gem?(spec) - return false if spec.extensions.empty? - - default - end - - def spec_default_gem?(spec) - spec.respond_to?(:default_gem?) && spec.default_gem? - end - - def spec_matches_for_glob(spec, glob) - return spec.matches_for_glob(glob) if spec.respond_to?(:matches_for_glob) - - spec.load_paths.map do |lp| - Dir["#{lp}/#{glob}#{suffix_pattern}"] - end.flatten(1) - end - - def spec_extension_dir(spec) - return unless spec.respond_to?(:extension_dir) - spec.extension_dir - end - def stub_set_spec(stub, spec) stub.instance_variable_set(:@spec, spec) end @@ -106,23 +65,6 @@ module Bundler obj.to_s end - def platforms - return [Gem::Platform::RUBY] if Bundler.settings[:force_ruby_platform] - Gem.platforms - end - - def configuration - require "bundler/psyched_yaml" - Gem.configuration - rescue Gem::SystemExitException, LoadError => e - Bundler.ui.error "#{e.class}: #{e.message}" - Bundler.ui.trace e - raise - rescue YamlLibrarySyntaxError => e - raise YamlSyntaxError.new(e, "Your RubyGems configuration, which is " \ - "usually located in ~/.gemrc, contains invalid YAML syntax.") - end - def ruby_engine Gem.ruby_engine end @@ -132,20 +74,7 @@ module Bundler end def inflate(obj) - Gem.inflate(obj) - end - - def sources=(val) - # Gem.configuration creates a new Gem::ConfigFile, which by default will read ~/.gemrc - # If that file exists, its settings (including sources) will overwrite the values we - # are about to set here. In order to avoid that, we force memoizing the config file now. - configuration - - Gem.sources = val - end - - def sources - Gem.sources + Gem::Util.inflate(obj) end def gem_dir @@ -183,7 +112,7 @@ module Bundler def spec_cache_dirs @spec_cache_dirs ||= begin dirs = gem_path.map {|dir| File.join(dir, "specifications") } - dirs << Gem.spec_cache_dir if Gem.respond_to?(:spec_cache_dir) # Not in RubyGems 2.0.3 or earlier + dirs << Gem.spec_cache_dir dirs.uniq.select {|dir| File.directory? dir } end end @@ -192,14 +121,6 @@ module Bundler Gem::MARSHAL_SPEC_DIR end - def config_map - Gem::ConfigMap - end - - def repository_subdirectories - %w[cache doc gems specifications] - end - def clear_paths Gem.clear_paths end @@ -208,34 +129,9 @@ module Bundler Gem.bin_path(gem, bin, ver) end - def path_separator - File::PATH_SEPARATOR - end - - def preserve_paths - # this is a no-op outside of RubyGems 1.8 - yield - end - def loaded_gem_paths - # RubyGems 2.2+ can put binary extension into dedicated folders, - # therefore use RubyGems facilities to obtain their load paths. - if Gem::Specification.method_defined? :full_require_paths - loaded_gem_paths = Gem.loaded_specs.map {|_, s| s.full_require_paths } - loaded_gem_paths.flatten - else - $LOAD_PATH.select do |p| - Bundler.rubygems.gem_path.any? {|gp| p =~ /^#{Regexp.escape(gp)}/ } - end - end - end - - def load_plugins - Gem.load_plugins if Gem.respond_to?(:load_plugins) - end - - def load_plugin_files(files) - Gem.load_plugin_files(files) if Gem.respond_to?(:load_plugin_files) + loaded_gem_paths = Gem.loaded_specs.map {|_, s| s.full_require_paths } + loaded_gem_paths.flatten end def ui=(obj) @@ -246,92 +142,15 @@ module Bundler EXT_LOCK end - def fetch_specs(all, pre, &blk) - require "rubygems/spec_fetcher" - specs = Gem::SpecFetcher.new.list(all, pre) - specs.each { yield } if block_given? - specs - end - - def fetch_prerelease_specs - fetch_specs(false, true) - rescue Gem::RemoteFetcher::FetchError - {} # if we can't download them, there aren't any - end - - # TODO: This is for older versions of RubyGems... should we support the - # X-Gemfile-Source header on these old versions? - # Maybe the newer implementation will work on older RubyGems? - # It seems difficult to keep this implementation and still send the header. - def fetch_all_remote_specs(remote) - old_sources = Bundler.rubygems.sources - Bundler.rubygems.sources = [remote.uri.to_s] - # Fetch all specs, minus prerelease specs - spec_list = fetch_specs(true, false) - # Then fetch the prerelease specs - fetch_prerelease_specs.each {|k, v| spec_list[k].concat(v) } - - spec_list.values.first - ensure - Bundler.rubygems.sources = old_sources - end - - def with_build_args(args) - ext_lock.synchronize do - old_args = build_args - begin - self.build_args = args - yield - ensure - self.build_args = old_args - end - end - end - - def install_with_build_args(args) - with_build_args(args) { yield } - end - - def gem_from_path(path, policy = nil) - require "rubygems/format" - Gem::Format.from_file_by_path(path, policy) - end - - def spec_from_gem(path, policy = nil) - require "rubygems/security" - require "bundler/psyched_yaml" - gem_from_path(path, security_policies[policy]).spec - rescue Gem::Package::FormatError - raise GemspecError, "Could not read gem at #{path}. It may be corrupted." - rescue Exception, Gem::Exception, Gem::Security::Exception => e - if e.is_a?(Gem::Security::Exception) || - e.message =~ /unknown trust policy|unsigned gem/i || - e.message =~ /couldn't verify (meta)?data signature/i - raise SecurityError, - "The gem #{File.basename(path, ".gem")} can't be installed because " \ - "the security policy didn't allow it, with the message: #{e.message}" - else - raise e - end - end - - def build(spec, skip_validation = false) - require "rubygems/builder" - Gem::Builder.new(spec).build + def spec_from_gem(path) + require "rubygems/package" + Gem::Package.new(path).spec end def build_gem(gem_dir, spec) build(spec) end - def download_gem(spec, uri, path) - uri = Bundler.settings.mirror_for(uri) - fetcher = Gem::RemoteFetcher.new(configuration[:http_proxy]) - Bundler::Retry.new("download gem from #{uri}").attempts do - fetcher.download(spec, uri, path) - end - end - def security_policy_keys %w[High Medium Low AlmostNo No].map {|level| "#{level}Security" } end @@ -347,32 +166,23 @@ module Bundler def reverse_rubygems_kernel_mixin # Disable rubygems' gem activation system - kernel = (class << ::Kernel; self; end) - [kernel, ::Kernel].each do |k| - if k.private_method_defined?(:gem_original_require) - redefine_method(k, :require, k.instance_method(:gem_original_require)) + if Gem.respond_to?(:discover_gems_on_require=) + Gem.discover_gems_on_require = false + else + [::Kernel.singleton_class, ::Kernel].each do |k| + if k.private_method_defined?(:gem_original_require) + redefine_method(k, :require, k.instance_method(:gem_original_require)) + end end end end - def binstubs_call_gem? - true - end - - def stubs_provide_full_functionality? - false - end - - def replace_gem(specs, specs_by_name) - reverse_rubygems_kernel_mixin - + def replace_gem(specs_by_name) executables = nil - kernel = (class << ::Kernel; self; end) - [kernel, ::Kernel].each do |kernel_class| + [::Kernel.singleton_class, ::Kernel].each do |kernel_class| redefine_method(kernel_class, :gem) do |dep, *reqs| - executables ||= specs.map(&:executables).flatten if ::Bundler.rubygems.binstubs_call_gem? - if executables && executables.include?(File.basename(caller.first.split(":").first)) + if executables&.include?(File.basename(caller_locations(1, 1).first.path)) break end @@ -387,8 +197,13 @@ module Bundler end message = if spec.nil? + target_file = begin + Bundler.default_gemfile.basename + rescue GemfileNotFound + "inline Gemfile" + end "#{dep.name} is not part of the bundle." \ - " Add it to your #{Bundler.default_gemfile.basename}." + " Add it to your #{target_file}." else "can't activate #{dep}, already activated #{spec.full_name}. " \ "Make sure all dependencies are added to Gemfile." @@ -396,183 +211,94 @@ module Bundler e = Gem::LoadError.new(message) e.name = dep.name - if e.respond_to?(:requirement=) - e.requirement = dep.requirement - elsif e.respond_to?(:version_requirement=) - e.version_requirement = dep.requirement - end + e.requirement = dep.requirement raise e end - - # backwards compatibility shim, see https://github.com/bundler/bundler/issues/5102 - kernel_class.send(:public, :gem) if Bundler.feature_flag.setup_makes_kernel_gem_public? end end - def stub_source_index(specs) - Gem::SourceIndex.send(:alias_method, :old_initialize, :initialize) - redefine_method(Gem::SourceIndex, :initialize) do |*args| - @gems = {} - # You're looking at this thinking: Oh! This is how I make those - # rubygems deprecations go away! - # - # You'd be correct BUT using of this method in production code - # must be approved by the rubygems team itself! - # - # This is your warning. If you use this and don't have approval - # we can't protect you. - # - Deprecate.skip_during do - self.spec_dirs = *args - add_specs(*specs) - end - end - end - - # Used to make bin stubs that are not created by bundler work - # under bundler. The new Gem.bin_path only considers gems in - # +specs+ - def replace_bin_path(specs, specs_by_name) - gem_class = (class << Gem; self; end) - + # Used to give better error messages when activating specs outside of the current bundle + def replace_bin_path(specs_by_name) redefine_method(gem_class, :find_spec_for_exe) do |gem_name, *args| exec_name = args.first + raise ArgumentError, "you must supply exec_name" unless exec_name spec_with_name = specs_by_name[gem_name] - spec = if exec_name - if spec_with_name && spec_with_name.executables.include?(exec_name) - spec_with_name - else - specs.find {|s| s.executables.include?(exec_name) } - end - else - spec_with_name - end + matching_specs_by_exec_name = specs_by_name.values.select {|s| s.executables.include?(exec_name) } + spec = matching_specs_by_exec_name.delete(spec_with_name) - unless spec + unless spec || !matching_specs_by_exec_name.empty? message = "can't find executable #{exec_name} for gem #{gem_name}" - if !exec_name || spec_with_name.nil? + if spec_with_name.nil? message += ". #{gem_name} is not currently included in the bundle, " \ "perhaps you meant to add it to your #{Bundler.default_gemfile.basename}?" end raise Gem::Exception, message end - raise Gem::Exception, "no default executable for #{spec.full_name}" unless exec_name ||= spec.default_executable - - unless spec.name == gem_name - Bundler::SharedHelpers.major_deprecation 2, + unless spec + spec = matching_specs_by_exec_name.shift + warn \ "Bundler is using a binstub that was created for a different gem (#{spec.name}).\n" \ "You should run `bundle binstub #{gem_name}` " \ "to work around a system/bundle conflict." end - spec - end - - redefine_method(gem_class, :activate_bin_path) do |name, *args| - exec_name = args.first - return ENV["BUNDLE_BIN_PATH"] if exec_name == "bundle" - - # Copy of Rubygems activate_bin_path impl - requirement = args.last - spec = find_spec_for_exe name, exec_name, [requirement] - - gem_bin = File.join(spec.full_gem_path, spec.bindir, exec_name) - gem_from_path_bin = File.join(File.dirname(spec.loaded_from), spec.bindir, exec_name) - File.exist?(gem_bin) ? gem_bin : gem_from_path_bin - end - redefine_method(gem_class, :bin_path) do |name, *args| - exec_name = args.first - return ENV["BUNDLE_BIN_PATH"] if exec_name == "bundle" - - spec = find_spec_for_exe(name, *args) - exec_name ||= spec.default_executable + unless matching_specs_by_exec_name.empty? + conflicting_names = matching_specs_by_exec_name.map(&:name).join(", ") + warn \ + "The `#{exec_name}` executable in the `#{spec.name}` gem is being loaded, but it's also present in other gems (#{conflicting_names}).\n" \ + "If you meant to run the executable for another gem, make sure you use a project specific binstub (`bundle binstub <gem_name>`).\n" \ + "If you plan to use multiple conflicting executables, generate binstubs for them and disambiguate their names." + end - gem_bin = File.join(spec.full_gem_path, spec.bindir, exec_name) - gem_from_path_bin = File.join(File.dirname(spec.loaded_from), spec.bindir, exec_name) - File.exist?(gem_bin) ? gem_bin : gem_from_path_bin + spec end end - # Because Bundler has a static view of what specs are available, - # we don't #refresh, so stub it out. - def replace_refresh - gem_class = (class << Gem; self; end) - redefine_method(gem_class, :refresh) {} - end - # Replace or hook into RubyGems to provide a bundlerized view # of the world. def replace_entrypoints(specs) - specs_by_name = specs.reduce({}) do |h, s| - h[s.name] = s - h - end + specs_by_name = add_default_gems_to(specs) - replace_gem(specs, specs_by_name) - stub_rubygems(specs) - replace_bin_path(specs, specs_by_name) - replace_refresh - - Gem.clear_paths - end - - # This backports the correct segment generation code from RubyGems 1.4+ - # by monkeypatching it into the method in RubyGems 1.3.6 and 1.3.7. - def backport_segment_generation - redefine_method(Gem::Version, :segments) do - @segments ||= @version.scan(/[0-9]+|[a-z]+/i).map do |s| - /^\d+$/ =~ s ? s.to_i : s - end - end - end - - # This backport fixes the marshaling of @segments. - def backport_yaml_initialize - redefine_method(Gem::Version, :yaml_initialize) do |_, map| - @version = map["version"] - @segments = nil - @hash = nil + reverse_rubygems_kernel_mixin + begin + # bundled_gems only provide with Ruby 3.3 or later + require "bundled_gems" + rescue LoadError + else + Gem::BUNDLED_GEMS.replace_require(specs) if Gem::BUNDLED_GEMS.respond_to?(:replace_require) end - end + replace_gem(specs_by_name) + stub_rubygems(specs_by_name.values) + replace_bin_path(specs_by_name) - # This backports base_dir which replaces installation path - # RubyGems 1.8+ - def backport_base_dir - redefine_method(Gem::Specification, :base_dir) do - return Gem.dir unless loaded_from - File.dirname File.dirname loaded_from - end + Gem.clear_paths end - def backport_cache_file - redefine_method(Gem::Specification, :cache_dir) do - @cache_dir ||= File.join base_dir, "cache" + # Add default gems not already present in specs, and return them as a hash. + def add_default_gems_to(specs) + specs_by_name = specs.reduce({}) do |h, s| + h[s.name] = s + h end - redefine_method(Gem::Specification, :cache_file) do - @cache_file ||= File.join cache_dir, "#{full_name}.gem" - end - end + Bundler.rubygems.default_stubs.each do |stub| + default_spec = stub.to_spec + default_spec_name = default_spec.name + next if specs_by_name.key?(default_spec_name) - def backport_spec_file - redefine_method(Gem::Specification, :spec_dir) do - @spec_dir ||= File.join base_dir, "specifications" + specs_by_name[default_spec_name] = default_spec end - redefine_method(Gem::Specification, :spec_file) do - @spec_file ||= File.join spec_dir, "#{full_name}.gemspec" - end + specs_by_name end def undo_replacements @replaced_methods.each do |(sym, klass), method| redefine_method(klass, sym, method) end - post_reset_hooks.reject! do |proc| - proc.binding.eval("__FILE__") == __FILE__ - end + post_reset_hooks.reject! {|proc| proc.binding.source_location[0] == __FILE__ } @replaced_methods.clear end @@ -607,286 +333,124 @@ module Bundler end end - # RubyGems 1.4 through 1.6 - class Legacy < RubygemsIntegration - def initialize - super - backport_base_dir - backport_cache_file - backport_spec_file - backport_yaml_initialize - end - - def stub_rubygems(specs) - # RubyGems versions lower than 1.7 use SourceIndex#from_gems_in - source_index_class = (class << Gem::SourceIndex; self; end) - redefine_method(source_index_class, :from_gems_in) do |*args| - Gem::SourceIndex.new.tap do |source_index| - source_index.spec_dirs = *args - source_index.add_specs(*specs) - end - end - end + def stub_rubygems(specs) + Gem::Specification.all = specs - def all_specs - Gem.source_index.gems.values - end - - def find_name(name) - Gem.source_index.find_name(name) - end - - def validate(spec) - # These versions of RubyGems always validate in "packaging" mode, - # which is too strict for the kinds of checks we care about. As a - # result, validation is disabled on versions of RubyGems below 1.7. + Gem.post_reset do + Gem::Specification.all = specs end - def post_reset_hooks + redefine_method(gem_class, :finish_resolve) do |*| [] end - def reset + redefine_method(gem_class, :load_plugins) do |*| + load_plugin_files specs.flat_map(&:plugins) end end - # RubyGems versions 1.3.6 and 1.3.7 - class Ancient < Legacy - def initialize - super - backport_segment_generation - end + def plain_specs + Gem::Specification._all end - # RubyGems 1.7 - class Transitional < Legacy - def stub_rubygems(specs) - stub_source_index(specs) - end - - def validate(spec) - # Missing summary is downgraded to a warning in later versions, - # so we set it to an empty string to prevent an exception here. - spec.summary ||= "" - RubygemsIntegration.instance_method(:validate).bind(self).call(spec) - end + def plain_specs=(specs) + Gem::Specification.all = specs end - # RubyGems 1.8.5-1.8.19 - class Modern < RubygemsIntegration - def stub_rubygems(specs) - Gem::Specification.all = specs - - Gem.post_reset do - Gem::Specification.all = specs - end - - stub_source_index(specs) - end - - def all_specs - Gem::Specification.to_a - end - - def find_name(name) - Gem::Specification.find_all_by_name name - end + def fetch_specs(remote, name, fetcher) + require "rubygems/remote_fetcher" + path = remote.uri.to_s + "#{name}.#{Gem.marshal_version}.gz" + string = fetcher.fetch_path(path) + specs = Bundler.safe_load_marshal(string) + raise MarshalError, "Specs #{name} from #{remote} is expected to be an Array but was unexpected class #{specs.class}" unless specs.is_a?(Array) + specs + rescue Gem::RemoteFetcher::FetchError + # it's okay for prerelease to fail + raise unless name == "prerelease_specs" end - # RubyGems 1.8.0 to 1.8.4 - class AlmostModern < Modern - # RubyGems [>= 1.8.0, < 1.8.5] has a bug that changes Gem.dir whenever - # you call Gem::Installer#install with an :install_dir set. We have to - # change it back for our sudo mode to work. - def preserve_paths - old_dir = gem_dir - old_path = gem_path - yield - Gem.use_paths(old_dir, old_path) - end - end + def fetch_all_remote_specs(remote, gem_remote_fetcher) + specs = fetch_specs(remote, "specs", gem_remote_fetcher) + pres = fetch_specs(remote, "prerelease_specs", gem_remote_fetcher) || [] - # RubyGems 1.8.20+ - class MoreModern < Modern - # RubyGems 1.8.20 and adds the skip_validation parameter, so that's - # when we start passing it through. - def build(spec, skip_validation = false) - require "rubygems/builder" - Gem::Builder.new(spec).build(skip_validation) - end + specs.concat(pres) end - # RubyGems 2.0 - class Future < RubygemsIntegration - def stub_rubygems(specs) - Gem::Specification.all = specs - - Gem.post_reset do - Gem::Specification.all = specs - end - - redefine_method((class << Gem; self; end), :finish_resolve) do |*| - [] - end - end - - def all_specs - Gem::Specification.to_a - end - - def find_name(name) - Gem::Specification.find_all_by_name name - end + def download_gem(spec, uri, cache_dir, fetcher) + require "rubygems/remote_fetcher" + uri = Bundler.settings.mirror_for(uri) + redacted_uri = Gem::Uri.redact(uri) - def fetch_specs(source, remote, name) - path = source + "#{name}.#{Gem.marshal_version}.gz" - fetcher = gem_remote_fetcher - fetcher.headers = { "X-Gemfile-Source" => remote.original_uri.to_s } if remote.original_uri - string = fetcher.fetch_path(path) - Bundler.load_marshal(string) - rescue Gem::RemoteFetcher::FetchError => e - # it's okay for prerelease to fail - raise e unless name == "prerelease_specs" - end + Bundler::Retry.new("download gem from #{redacted_uri}").attempts do + gem_file_name = spec.file_name + local_gem_path = File.join cache_dir, gem_file_name + return if File.exist? local_gem_path - def fetch_all_remote_specs(remote) - source = remote.uri.is_a?(URI) ? remote.uri : URI.parse(source.to_s) + begin + remote_gem_path = uri + "gems/#{gem_file_name}" - specs = fetch_specs(source, remote, "specs") - pres = fetch_specs(source, remote, "prerelease_specs") || [] + SharedHelpers.filesystem_access(local_gem_path) do + fetcher.cache_update_path remote_gem_path, local_gem_path + end + rescue Gem::RemoteFetcher::FetchError + raise if spec.original_platform == spec.platform - specs.concat(pres) - end + original_gem_file_name = "#{spec.original_name}.gem" + raise if gem_file_name == original_gem_file_name - def download_gem(spec, uri, path) - uri = Bundler.settings.mirror_for(uri) - fetcher = gem_remote_fetcher - fetcher.headers = { "X-Gemfile-Source" => spec.remote.original_uri.to_s } if spec.remote.original_uri - Bundler::Retry.new("download gem from #{uri}").attempts do - fetcher.download(spec, uri, path) + gem_file_name = original_gem_file_name + retry end end + rescue Gem::RemoteFetcher::FetchError => e + raise Bundler::HTTPError, "Could not download gem from #{redacted_uri} due to underlying error <#{e.message}>" + end - def gem_remote_fetcher - require "resolv" - proxy = configuration[:http_proxy] - dns = Resolv::DNS.new - Bundler::GemRemoteFetcher.new(proxy, dns) - end - - def gem_from_path(path, policy = nil) - require "rubygems/package" - p = Gem::Package.new(path) - p.security_policy = policy if policy - p - end - - def build(spec, skip_validation = false) - require "rubygems/package" - Gem::Package.build(spec, skip_validation) - end - - def repository_subdirectories - Gem::REPOSITORY_SUBDIRECTORIES - end + def build(spec, skip_validation = false) + require "rubygems/package" + Gem::Package.build(spec, skip_validation) + end - def install_with_build_args(args) - yield - end + def path_separator + Gem.path_separator + end - def path_separator - Gem.path_separator - end + def all_specs + SharedHelpers.feature_removed! "Bundler.rubygems.all_specs has been removed in favor of Bundler.rubygems.installed_specs" end - # RubyGems 2.1.0 - class MoreFuture < Future - def initialize - super - backport_ext_builder_monitor + def installed_specs + Gem::Specification.stubs.reject(&:default_gem?).map do |stub| + StubSpecification.from_stub(stub) end + end - def all_specs - require "bundler/remote_specification" - Gem::Specification.stubs.map do |stub| - StubSpecification.from_stub(stub) - end + def default_specs + Gem::Specification.default_stubs.map do |stub| + StubSpecification.from_stub(stub) end + end - def backport_ext_builder_monitor - # So we can avoid requiring "rubygems/ext" in its entirety - Gem.module_eval <<-RB, __FILE__, __LINE__ + 1 - module Ext - end - RB - - require "rubygems/ext/builder" - - Gem::Ext::Builder.class_eval do - unless const_defined?(:CHDIR_MONITOR) - const_set(:CHDIR_MONITOR, EXT_LOCK) - end - - remove_const(:CHDIR_MUTEX) if const_defined?(:CHDIR_MUTEX) - const_set(:CHDIR_MUTEX, const_get(:CHDIR_MONITOR)) - end - end + def find_bundler(version) + find_name("bundler").find {|s| s.version.to_s == version.to_s } + end - if Gem::Specification.respond_to?(:stubs_for) - def find_name(name) - Gem::Specification.stubs_for(name).map(&:to_spec) - end - else - def find_name(name) - Gem::Specification.stubs.find_all do |spec| - spec.name == name - end.map(&:to_spec) - end - end + def find_name(name) + Gem::Specification.stubs_for(name).map(&:to_spec) + end - def use_gemdeps(gemfile) - ENV["BUNDLE_GEMFILE"] ||= File.expand_path(gemfile) - require "bundler/gemdeps" - runtime = Bundler.setup - Bundler.ui = nil - activated_spec_names = runtime.requested_specs.map(&:to_spec).sort_by(&:name) - [Gemdeps.new(runtime), activated_spec_names] - end + def default_stubs + Gem::Specification.default_stubs("*.gemspec") + end - if provides?(">= 2.5.2") - # RubyGems-generated binstubs call Kernel#gem - def binstubs_call_gem? - false - end + private - # only 2.5.2+ has all of the stub methods we want to use, and since this - # is a performance optimization _only_, - # we'll restrict ourselves to the most - # recent RG versions instead of all versions that have stubs - def stubs_provide_full_functionality? - true - end - end + def gem_class + class << Gem; self; end end end def self.rubygems - @rubygems ||= if RubygemsIntegration.provides?(">= 2.1.0") - RubygemsIntegration::MoreFuture.new - elsif RubygemsIntegration.provides?(">= 1.99.99") - RubygemsIntegration::Future.new - elsif RubygemsIntegration.provides?(">= 1.8.20") - RubygemsIntegration::MoreModern.new - elsif RubygemsIntegration.provides?(">= 1.8.5") - RubygemsIntegration::Modern.new - elsif RubygemsIntegration.provides?(">= 1.8.0") - RubygemsIntegration::AlmostModern.new - elsif RubygemsIntegration.provides?(">= 1.7.0") - RubygemsIntegration::Transitional.new - elsif RubygemsIntegration.provides?(">= 1.4.0") - RubygemsIntegration::Legacy.new - else # RubyGems 1.3.6 and 1.3.7 - RubygemsIntegration::Ancient.new - end + @rubygems ||= RubygemsIntegration.new end end diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb index f27597b854..5280e72aa2 100644 --- a/lib/bundler/runtime.rb +++ b/lib/bundler/runtime.rb @@ -10,96 +10,79 @@ module Bundler end def setup(*groups) - @definition.ensure_equivalent_gemfile_and_lockfile if Bundler.frozen? - - groups.map!(&:to_sym) + @definition.ensure_equivalent_gemfile_and_lockfile # Has to happen first clean_load_path - specs = groups.any? ? @definition.specs_for(groups) : requested_specs + specs = @definition.specs_for(groups) SharedHelpers.set_bundle_environment Bundler.rubygems.replace_entrypoints(specs) # Activate the specs load_paths = specs.map do |spec| - unless spec.loaded_from - raise GemNotFound, "#{spec.full_name} is missing. Run `bundle install` to get it." - end - check_for_activated_spec!(spec) Bundler.rubygems.mark_loaded(spec) spec.load_paths.reject {|path| $LOAD_PATH.include?(path) } end.reverse.flatten - # See Gem::Specification#add_self_to_load_path (since RubyGems 1.8) - if insert_index = Bundler.rubygems.load_path_insert_index - # Gem directories must come after -I and ENV['RUBYLIB'] - $LOAD_PATH.insert(insert_index, *load_paths) - else - # We are probably testing in core, -I and RUBYLIB don't apply - $LOAD_PATH.unshift(*load_paths) - end + Gem.add_to_load_path(*load_paths) setup_manpath - lock(:preserve_unknown_sections => true) + lock(preserve_unknown_sections: true) self end - REQUIRE_ERRORS = [ - /^no such file to load -- (.+)$/i, - /^Missing \w+ (?:file\s*)?([^\s]+.rb)$/i, - /^Missing API definition file in (.+)$/i, - /^cannot load such file -- (.+)$/i, - /^dlopen\([^)]*\): Library not loaded: (.+)$/i, - ].freeze - def require(*groups) groups.map!(&:to_sym) groups = [:default] if groups.empty? - @definition.dependencies.each do |dep| - # Skip the dependency if it is not in any of the requested groups, or - # not for the current platform, or doesn't match the gem constraints. - next unless (dep.groups & groups).any? && dep.should_include? - - required_file = nil + dependencies = @definition.dependencies.select do |dep| + # Select the dependency if it is in any of the requested groups, and + # for the current platform, and matches the gem constraints. + (dep.groups & groups).any? && dep.should_include? + end - begin - # Loop through all the specified autorequires for the - # 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 - begin - Kernel.require file - rescue => e - raise e if e.is_a?(LoadError) # we handle this a little later + Plugin.hook(Plugin::Events::GEM_BEFORE_REQUIRE_ALL, dependencies) + + dependencies.each do |dep| + Plugin.hook(Plugin::Events::GEM_BEFORE_REQUIRE, dep) + + # Loop through all the specified autorequires for the + # 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 + begin + Kernel.require required_file + rescue LoadError => e + if dep.autorequire.nil? && e.path == required_file + if required_file.include?("-") + required_file = required_file.tr("-", "/") + retry + end + else raise Bundler::GemRequireError.new e, "There was an error while trying to load the gem '#{file}'." end - end - rescue LoadError => e - REQUIRE_ERRORS.find {|r| r =~ e.message } - raise if dep.autorequire || $1 != required_file - - if dep.autorequire.nil? && dep.name.include?("-") - begin - namespaced_file = dep.name.tr("-", "/") - Kernel.require namespaced_file - rescue LoadError => e - REQUIRE_ERRORS.find {|r| r =~ e.message } - raise if $1 != namespaced_file - end + rescue StandardError => e + raise Bundler::GemRequireError.new e, + "There was an error while trying to load the gem '#{file}'." end end + + Plugin.hook(Plugin::Events::GEM_AFTER_REQUIRE, dep) end + + Plugin.hook(Plugin::Events::GEM_AFTER_REQUIRE_ALL, dependencies) + + dependencies end def self.definition_method(meth) @@ -117,13 +100,13 @@ module Bundler definition_method :requires def lock(opts = {}) - return if @definition.nothing_changed? && !@definition.unlocking? - @definition.lock(Bundler.default_lockfile, opts[:preserve_unknown_sections]) + return if @definition.no_resolve_needed? + @definition.lock(opts[:preserve_unknown_sections]) end alias_method :gems, :specs - def cache(custom_path = nil) + def cache(custom_path = nil, local = false) cache_path = Bundler.app_cache(custom_path) SharedHelpers.filesystem_access(cache_path) do |p| FileUtils.mkdir_p(p) @@ -131,12 +114,31 @@ module Bundler Bundler.ui.info "Updating files in #{Bundler.settings.app_cache_path}" - specs_to_cache = Bundler.settings[:cache_all_platforms] ? @definition.resolve.materialized_for_all_platforms : specs + specs_to_cache = if Bundler.settings[:cache_all_platforms] + @definition.resolve.materialized_for_all_platforms + else + begin + specs + rescue GemNotFound + if local + Bundler.ui.warn "Some gems seem to be missing from your #{Bundler.settings.app_cache_path} directory." + end + + raise + end + end + specs_to_cache.each do |spec| next if spec.name == "bundler" - next if spec.source.is_a?(Source::Gemspec) - spec.source.send(:fetch_gem, spec) if Bundler.settings[:cache_all_platforms] && spec.source.respond_to?(:fetch_gem, true) - spec.source.cache(spec, custom_path) if spec.source.respond_to?(:cache) + + source = spec.source + next if source.is_a?(Source::Gemspec) + + if source.respond_to?(:migrate_cache) + source.migrate_cache(custom_path, local: local) + elsif source.respond_to?(:cache) + source.cache(spec, custom_path) + end end Dir[cache_path.join("*/.git")].each do |git_dir| @@ -163,6 +165,7 @@ module Bundler gem_dirs = Dir["#{Gem.dir}/gems/*"] gem_files = Dir["#{Gem.dir}/cache/*.gem"] gemspec_files = Dir["#{Gem.dir}/specifications/*.gemspec"] + extension_dirs = Dir["#{Gem.dir}/extensions/*/*/*"] + Dir["#{Gem.dir}/bundler/gems/extensions/*/*/*"] spec_gem_paths = [] # need to keep git sources around spec_git_paths = @definition.spec_git_paths @@ -170,7 +173,15 @@ module Bundler spec_gem_executables = [] spec_cache_paths = [] spec_gemspec_paths = [] - specs.each do |spec| + spec_extension_paths = [] + specs_to_keep = Bundler.rubygems.add_default_gems_to(specs).values + + current_bundler = Bundler.rubygems.find_bundler(Bundler.gem_version) + if current_bundler + specs_to_keep << current_bundler + end + + specs_to_keep.each do |spec| spec_gem_paths << spec.full_gem_path # need to check here in case gems are nested like for the rails git repo md = %r{(.+bundler/gems/.+-[a-f0-9]{7,12})}.match(spec.full_gem_path) @@ -181,6 +192,7 @@ module Bundler end spec_cache_paths << spec.cache_file spec_gemspec_paths << spec.spec_file + spec_extension_paths << spec.extension_dir if spec.respond_to?(:extension_dir) spec_git_cache_dirs << spec.source.cache_path.to_s if spec.source.is_a?(Bundler::Source::Git) end spec_gem_paths.uniq! @@ -192,6 +204,7 @@ module Bundler stale_gem_dirs = gem_dirs - spec_gem_paths stale_gem_files = gem_files - spec_cache_paths stale_gemspec_files = gemspec_files - spec_gemspec_paths + stale_extension_dirs = extension_dirs - spec_extension_paths removed_stale_gem_dirs = stale_gem_dirs.collect {|dir| remove_dir(dir, dry_run) } removed_stale_git_dirs = stale_git_dirs.collect {|dir| remove_dir(dir, dry_run) } @@ -204,8 +217,10 @@ module Bundler FileUtils.rm(file) if File.exist?(file) end end - stale_git_cache_dirs.each do |cache_dir| - SharedHelpers.filesystem_access(cache_dir) do |dir| + + stale_dirs = stale_git_cache_dirs + stale_extension_dirs + stale_dirs.each do |stale_dir| + SharedHelpers.filesystem_access(stale_dir) do |dir| FileUtils.rm_rf(dir) if File.exist?(dir) end end @@ -214,7 +229,7 @@ module Bundler output end - private + private def prune_gem_cache(resolve, cache_path) cached = Dir["#{cache_path}/*.gem"] @@ -232,7 +247,11 @@ module Bundler cached.each do |path| Bundler.ui.info " * #{File.basename(path)}" - File.delete(path) + + begin + File.delete(path) + rescue Errno::ENOENT + end end end end @@ -262,14 +281,14 @@ module Bundler def setup_manpath # Add man/ subdirectories from activated bundles to MANPATH for man(1) - manuals = $LOAD_PATH.map do |path| + manuals = $LOAD_PATH.filter_map do |path| man_subdir = path.sub(/lib$/, "man") man_subdir unless Dir[man_subdir + "/man?/"].empty? - end.compact + end return if manuals.empty? Bundler::SharedHelpers.set_env "MANPATH", manuals.concat( - ENV["MANPATH"].to_s.split(File::PATH_SEPARATOR) + ENV["MANPATH"] ? ENV["MANPATH"].to_s.split(File::PATH_SEPARATOR) : [""] ).uniq.join(File::PATH_SEPARATOR) end @@ -295,7 +314,7 @@ module Bundler return unless activated_spec = Bundler.rubygems.loaded_specs(spec.name) return if activated_spec.version == spec.version - suggestion = if Bundler.rubygems.spec_default_gem?(activated_spec) + suggestion = if activated_spec.default_gem? "Since #{spec.name} is a default gem, you can either remove your dependency on it" \ " or try updating to a newer version of bundler that supports #{spec.name} as a default gem." else @@ -305,11 +324,7 @@ module Bundler e = Gem::LoadError.new "You have already activated #{activated_spec.name} #{activated_spec.version}, " \ "but your Gemfile requires #{spec.name} #{spec.version}. #{suggestion}" e.name = spec.name - if e.respond_to?(:requirement=) - e.requirement = Gem::Requirement.new(spec.version.to_s) - else - e.version_requirement = Gem::Requirement.new(spec.version.to_s) - end + e.requirement = Gem::Requirement.new(spec.version.to_s) raise e end end diff --git a/lib/bundler/safe_marshal.rb b/lib/bundler/safe_marshal.rb new file mode 100644 index 0000000000..50aa0f60a6 --- /dev/null +++ b/lib/bundler/safe_marshal.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Bundler + module SafeMarshal + ALLOWED_CLASSES = [ + Array, + FalseClass, + Gem::Specification, + Gem::Version, + Hash, + String, + Symbol, + Time, + TrueClass, + ].freeze + + ERROR = "Unexpected class %s present in marshaled data. Only %s are allowed." + + PROC = proc do |object| + object.tap do + unless ALLOWED_CLASSES.include?(object.class) + raise TypeError, format(ERROR, object.class, ALLOWED_CLASSES.join(", ")) + end + end + end + + def self.proc + PROC + end + end +end diff --git a/lib/bundler/self_manager.rb b/lib/bundler/self_manager.rb new file mode 100644 index 0000000000..82efbf56a4 --- /dev/null +++ b/lib/bundler/self_manager.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +module Bundler + # + # This class handles installing and switching to the version of bundler needed + # by an application. + # + class SelfManager + def restart_with_locked_bundler_if_needed + restart_version = find_restart_version + return unless restart_version && installed?(restart_version) + + restart_with(restart_version) + end + + def install_locked_bundler_and_restart_with_it_if_needed + restart_version = find_restart_version + return unless restart_version + + if restart_version == lockfile_version + Bundler.ui.info \ + "Bundler #{current_version} is running, but your lockfile was generated with #{lockfile_version}. " \ + "Installing Bundler #{lockfile_version} and restarting using that version." + else + Bundler.ui.info \ + "Bundler #{current_version} is running, but your configuration was #{restart_version}. " \ + "Installing Bundler #{restart_version} and restarting using that version." + end + + install_and_restart_with(restart_version) + end + + def update_bundler_and_restart_with_it_if_needed(target) + spec = resolve_update_version_from(target) + return unless spec + + version = spec.version + + Bundler.ui.info "Updating bundler to #{version}." + + install(spec) unless installed?(version) + + restart_with(version) + end + + private + + def install_and_restart_with(version) + requirement = Gem::Requirement.new(version) + spec = find_latest_matching_spec(requirement) + + if spec.nil? + Bundler.ui.warn "Your lockfile is locked to a version of bundler (#{lockfile_version}) that doesn't exist at https://rubygems.org/. Going on using #{current_version}" + return + end + + install(spec) + rescue StandardError => e + Bundler.ui.trace e + Bundler.ui.warn "There was an error installing the locked bundler version (#{lockfile_version}), rerun with the `--verbose` flag for more details. Going on using bundler #{current_version}." + else + restart_with(version) + end + + def install(spec) + spec.source.download(spec) + spec.source.install(spec) + end + + def restart_with(version) + configured_gem_home = ENV["GEM_HOME"] + configured_orig_gem_home = ENV["BUNDLER_ORIG_GEM_HOME"] + configured_gem_path = ENV["GEM_PATH"] + configured_orig_gem_path = ENV["BUNDLER_ORIG_GEM_PATH"] + + argv0 = File.exist?($PROGRAM_NAME) ? $PROGRAM_NAME : Process.argv0 + cmd = [argv0, *ARGV] + cmd.unshift(Gem.ruby) unless File.executable?(argv0) + + Bundler.with_original_env do + Kernel.exec( + { + "GEM_HOME" => configured_gem_home, + "BUNDLER_ORIG_GEM_HOME" => configured_orig_gem_home, + "GEM_PATH" => configured_gem_path, + "BUNDLER_ORIG_GEM_PATH" => configured_orig_gem_path, + "BUNDLER_VERSION" => version.to_s, + }, + *cmd + ) + end + end + + def needs_switching?(restart_version) + autoswitching_applies? && + released?(restart_version) && + !running?(restart_version) + end + + def autoswitching_applies? + (ENV["BUNDLER_VERSION"].nil? || ENV["BUNDLER_VERSION"].empty?) && + ruby_can_restart_with_same_arguments? && + lockfile_version + end + + def resolve_update_version_from(target) + requirement = Gem::Requirement.new(target) + update_candidate = find_latest_matching_spec(requirement) + + if update_candidate.nil? + raise InvalidOption, "The `bundle update --bundler` target version (#{target}) does not exist" + end + + resolved_version = update_candidate.version + needs_update = requirement.specific? ? !running?(resolved_version) : running_older_than?(resolved_version) + + return unless needs_update + + update_candidate + end + + def local_specs + @local_specs ||= Bundler::Source::Rubygems.new("allow_local" => true).specs.select {|spec| spec.name == "bundler" } + end + + def remote_specs + @remote_specs ||= begin + source = Bundler::Source::Rubygems.new("remotes" => "https://rubygems.org") + source.remote! + source.add_dependency_names("bundler") + source.specs.select(&:matches_current_metadata?) + end + end + + def find_latest_matching_spec(requirement) + Bundler.configure + local_result = find_latest_matching_spec_from_collection(local_specs, requirement) + return local_result if local_result && requirement.specific? + + remote_result = find_latest_matching_spec_from_collection(remote_specs, requirement) + return remote_result if local_result.nil? + + [local_result, remote_result].max + end + + def find_latest_matching_spec_from_collection(specs, requirement) + specs.sort.reverse_each.find {|spec| requirement.satisfied_by?(spec.version) } + end + + def running?(version) + version == current_version + end + + def running_older_than?(version) + current_version < version + end + + def released?(version) + !version.to_s.end_with?(".dev") + end + + def ruby_can_restart_with_same_arguments? + $PROGRAM_NAME != "-e" + end + + def installed?(restart_version) + Bundler.configure + + Bundler.rubygems.find_bundler(restart_version.to_s) + end + + def current_version + @current_version ||= Bundler.gem_version + end + + def lockfile_version + return @lockfile_version if defined?(@lockfile_version) + + parsed_version = Bundler::LockfileParser.bundled_with + @lockfile_version = parsed_version ? Gem::Version.new(parsed_version) : nil + rescue ArgumentError + @lockfile_version = nil + end + + def find_restart_version + return unless SharedHelpers.in_bundle? + + configured_version = Bundler.settings[:version] + return if configured_version == "system" + + restart_version = configured_version == "lockfile" ? lockfile_version : Gem::Version.new(configured_version) + return unless needs_switching?(restart_version) + + restart_version + end + end +end diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index f33e9453be..fd77c2f7fc 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -1,61 +1,51 @@ # frozen_string_literal: true -require "uri" - module Bundler class Settings - autoload :Mirror, "bundler/mirror" - autoload :Mirrors, "bundler/mirror" - autoload :Validator, "bundler/settings/validator" + autoload :Mirror, File.expand_path("mirror", __dir__) + autoload :Mirrors, File.expand_path("mirror", __dir__) + autoload :Validator, File.expand_path("settings/validator", __dir__) BOOL_KEYS = %w[ - allow_bundler_dependency_conflicts - allow_deployment_source_credential_changes - allow_offline_install - auto_clean_without_path auto_install cache_all cache_all_platforms - cache_command_is_package - console_command - default_install_uses_path + clean deployment - deployment_means_frozen disable_checksum_validation disable_exec_load disable_local_branch_check - disable_multisource + disable_local_revision_check disable_shared_gems disable_version_check - error_on_stderr force_ruby_platform - forget_cli_options frozen + gem.changelog gem.coc gem.mit + gem.bundle + git.allow_insecure global_gem_cache ignore_messages init_gems_rb - list_command - lockfile_uses_separate_rubygems_sources - major_deprecations + inline + lockfile_checksums + no_build_extension no_install + no_install_plugin no_prune - only_update_to_newer_versions path.system plugins - prefer_gems_rb - print_only_version_number - setup_makes_kernel_gem_public + prefer_patch + silence_deprecations silence_root_warning - skip_default_git_sources - specific_platform - suppress_install_using_messages - unlock_source_unlocks_spec update_requires_all_flag + verbose ].freeze NUMBER_KEYS = %w[ + cooldown + jobs redirect retry ssl_verify_mode @@ -63,56 +53,77 @@ module Bundler ].freeze ARRAY_KEYS = %w[ + only with without ].freeze + STRING_KEYS = %w[ + bin + cache_path + console + default_cli_command + gem.ci + gem.github_username + gem.linter + gem.rubocop + gem.test + gemfile + lockfile + path + shebang + simulate_version + system_bindir + trust-policy + version + ].freeze + DEFAULT_CONFIG = { - :disable_version_check => true, - :redirect => 5, - :retry => 3, - :timeout => 10, + "BUNDLE_SILENCE_DEPRECATIONS" => false, + "BUNDLE_DISABLE_VERSION_CHECK" => true, + "BUNDLE_PREFER_PATCH" => false, + "BUNDLE_REDIRECT" => 5, + "BUNDLE_RETRY" => 3, + "BUNDLE_TIMEOUT" => 10, + "BUNDLE_VERSION" => "lockfile", + "BUNDLE_LOCKFILE_CHECKSUMS" => true, + "BUNDLE_CACHE_ALL" => true, + "BUNDLE_PLUGINS" => true, + "BUNDLE_GLOBAL_GEM_CACHE" => false, + "BUNDLE_UPDATE_REQUIRES_ALL_FLAG" => false, }.freeze def initialize(root = nil) @root = root @local_config = load_config(local_config_file) + @local_root = root || Pathname.new(".bundle").expand_path + + @env_config = ENV.to_h + @env_config.select! {|key, _value| key.start_with?("BUNDLE_") } + @env_config.delete("BUNDLE_") + @global_config = load_config(global_config_file) @temporary = {} + + @key_cache = {} end def [](name) key = key_for(name) - value = @temporary.fetch(key) do - @local_config.fetch(key) do - ENV.fetch(key) do - @global_config.fetch(key) do - DEFAULT_CONFIG.fetch(name) do - nil - end end end end end + + value = nil + configs.each do |_, config| + value = config[key] + next if value.nil? + break + end converted_value(value, name) end def set_command_option(key, value) - if Bundler.feature_flag.forget_cli_options? - temporary(key => value) - value - else - command = if value.nil? - "bundle config --delete #{key}" - else - "bundle config #{key} #{Array(value).join(":")}" - end - - Bundler::SharedHelpers.major_deprecation 2,\ - "flags passed to commands " \ - "will no longer be automatically remembered. Instead please set flags " \ - "you want remembered between commands using `bundle config " \ - "<setting name> <setting value>`, i.e. `#{command}`" - - set_local(key, value) - end + temporary(key => value) + value end def set_command_option_if_given(key, value) @@ -121,7 +132,7 @@ module Bundler end def set_local(key, value) - local_config_file || raise(GemfileNotFound, "Could not locate Gemfile") + local_config_file = @local_root.join("config") set_key(key, value, @local_config, local_config_file) end @@ -144,25 +155,32 @@ module Bundler end def all - env_keys = ENV.keys.grep(/\ABUNDLE_.+/) - - keys = @temporary.keys | @global_config.keys | @local_config.keys | env_keys + keys = @temporary.keys.union(@global_config.keys, @local_config.keys, @env_config.keys) - keys.map do |key| - key.sub(/^BUNDLE_/, "").gsub(/__/, ".").downcase - end + keys.map! do |key| + key = key.delete_prefix("BUNDLE_") + key.gsub!("___", "-") + key.gsub!("__", ".") + key.downcase! + key + end.sort! + keys end def local_overrides repos = {} all.each do |k| - repos[$'] = self[k] if k =~ /^local\./ + repos[k.delete_prefix("local.")] = self[k] if k.start_with?("local.") end repos end def mirror_for(uri) - uri = URI(uri.to_s) unless uri.is_a?(URI) + if uri.is_a?(String) + require_relative "vendored_uri" + uri = Gem::URI(uri) + end + gem_mirrors.for(uri.to_s).uri end @@ -179,13 +197,11 @@ module Bundler def locations(key) key = key_for(key) - locations = {} - locations[:temporary] = @temporary[key] if @temporary.key?(key) - locations[:local] = @local_config[key] if @local_config.key?(key) - locations[:env] = ENV[key] if ENV[key] - locations[:global] = @global_config[key] if @global_config.key?(key) - locations[:default] = DEFAULT_CONFIG[key] if DEFAULT_CONFIG.key?(key) - locations + configs.keys.inject({}) do |partial_locations, level| + value_on_level = configs[level][key] + partial_locations[level] = value_on_level unless value_on_level.nil? + partial_locations + end end def pretty_values_for(exposed_key) @@ -193,50 +209,60 @@ module Bundler locations = [] - if @temporary.key?(key) - locations << "Set for the current command: #{converted_value(@temporary[key], exposed_key).inspect}" + if value = @temporary[key] + locations << "Set for the current command: #{printable_value(value, exposed_key).inspect}" end - if @local_config.key?(key) - locations << "Set for your local app (#{local_config_file}): #{converted_value(@local_config[key], exposed_key).inspect}" + if value = @local_config[key] + locations << "Set for your local app (#{local_config_file}): #{printable_value(value, exposed_key).inspect}" end - if value = ENV[key] - locations << "Set via #{key}: #{converted_value(value, exposed_key).inspect}" + if value = @env_config[key] + locations << "Set via #{key}: #{printable_value(value, exposed_key).inspect}" end - if @global_config.key?(key) - locations << "Set for the current user (#{global_config_file}): #{converted_value(@global_config[key], exposed_key).inspect}" + if value = @global_config[key] + locations << "Set for the current user (#{global_config_file}): #{printable_value(value, exposed_key).inspect}" end return ["You have not configured a value for `#{exposed_key}`"] if locations.empty? locations end - # for legacy reasons, the ruby scope isnt appended when the setting comes from ENV or the global config, - # nor do we respect :disable_shared_gems + def processor_count + require "etc" + Etc.nprocessors + rescue StandardError + 1 + end + + # for legacy reasons, in Bundler 2, we do not respect :disable_shared_gems def path - key = key_for(:path) - path = ENV[key] || @global_config[key] - if path && !@temporary.key?(key) && !@local_config.key?(key) - return Path.new(path, false, false, false) + configs.each do |_level, settings| + path = value_for("path", settings) + path_system = value_for("path.system", settings) + disabled_shared_gems = value_for("disable_shared_gems", settings) + next if path.nil? && path_system.nil? && disabled_shared_gems.nil? + system_path = path_system || (disabled_shared_gems == false) + return Path.new(path, system_path) end - system_path = self["path.system"] || (self[:disable_shared_gems] == false) - Path.new(self[:path], true, system_path, Bundler.feature_flag.default_install_uses_path?) + path = "vendor/bundle" if self[:deployment] + + Path.new(path, false) end - Path = Struct.new(:explicit_path, :append_ruby_scope, :system_path, :default_install_uses_path) do + Path = Struct.new(:explicit_path, :system_path) do def path path = base_path - path = File.join(path, Bundler.ruby_scope) if append_ruby_scope && !use_system_gems? + path = File.join(path, Bundler.ruby_scope) unless use_system_gems? path end def use_system_gems? return true if system_path return false if explicit_path - !default_install_uses_path + !Bundler.feature_flag.bundler_5_mode? end def base_path @@ -246,6 +272,20 @@ module Bundler path end + def base_path_relative_to_pwd + base_path = Pathname.new(self.base_path) + expanded_base_path = base_path.expand_path(Bundler.root) + relative_path = expanded_base_path.relative_path_from(Pathname.pwd) + if relative_path.to_s.start_with?("..") + relative_path = base_path if base_path.absolute? + else + relative_path = Pathname.new(File.join(".", relative_path)) + end + relative_path + rescue ArgumentError + expanded_base_path + end + def validate! return unless explicit_path && system_path path = Bundler.settings.pretty_values_for(:path) @@ -259,12 +299,6 @@ module Bundler end end - def allow_sudo? - key = key_for(:path) - path_configured = @temporary.key?(key) || @local_config.key?(key) - !path_configured - end - def ignore_config? ENV["BUNDLE_IGNORE_CONFIG"] end @@ -273,22 +307,38 @@ module Bundler @app_cache_path ||= self[:cache_path] || "vendor/cache" end + def installation_parallelization + self[:jobs] || processor_count + end + def validate! all.each do |raw_key| - [@local_config, ENV, @global_config].each do |settings| - value = converted_value(settings[key_for(raw_key)], raw_key) - Validator.validate!(raw_key, value, settings.to_hash.dup) + [@local_config, @env_config, @global_config].each do |settings| + value = value_for(raw_key, settings) + Validator.validate!(raw_key, value, settings.dup) end end end def key_for(key) - key = Settings.normalize_uri(key).to_s if key.is_a?(String) && /https?:/ =~ key - key = key.to_s.gsub(".", "__").upcase - "BUNDLE_#{key}" + @key_cache[key] ||= self.class.key_for(key) end - private + private + + def configs + @configs ||= { + temporary: @temporary, + local: @local_config, + env: @env_config, + global: @global_config, + default: DEFAULT_CONFIG, + } + end + + def value_for(name, config) + converted_value(config[key_for(name)], name) + end def parent_setting_for(name) split_specific_setting_for(name)[0] @@ -303,12 +353,20 @@ module Bundler end def is_bool(name) - BOOL_KEYS.include?(name.to_s) || BOOL_KEYS.include?(parent_setting_for(name.to_s)) + name = self.class.key_to_s(name) + BOOL_KEYS.include?(name) || BOOL_KEYS.include?(parent_setting_for(name)) + end + + def is_string(name) + name = self.class.key_to_s(name) + STRING_KEYS.include?(name) || name.start_with?("local.") || name.start_with?("mirror.") || name.start_with?("build.") end def to_bool(value) case value - when nil, /\A(false|f|no|n|0|)\z/i, false + when String + value.match?(/\A(false|f|no|n|0|)\z/i) ? false : true + when nil, false false else true @@ -316,16 +374,24 @@ module Bundler end def is_num(key) - NUMBER_KEYS.include?(key.to_s) + NUMBER_KEYS.include?(self.class.key_to_s(key)) end def is_array(key) - ARRAY_KEYS.include?(key.to_s) + ARRAY_KEYS.include?(self.class.key_to_s(key)) + end + + def is_credential(key) + key == "gem.push_key" + end + + def is_userinfo(value) + value.include?(":") end def to_array(value) return [] unless value - value.split(":").map(&:to_sym) + value.tr(" ", ":").split(":").map(&:to_sym) end def array_to_s(array) @@ -335,7 +401,7 @@ module Bundler end def set_key(raw_key, value, hash, file) - raw_key = raw_key.to_s + raw_key = self.class.key_to_s(raw_key) value = array_to_s(value) if is_array(raw_key) key = key_for(raw_key) @@ -348,14 +414,19 @@ module Bundler Validator.validate!(raw_key, converted_value(value, raw_key), hash) return unless file + + SharedHelpers.filesystem_access(file.dirname, :create) do |p| + FileUtils.mkdir_p(p) + end + SharedHelpers.filesystem_access(file) do |p| - FileUtils.mkdir_p(p.dirname) - require "bundler/yaml_serializer" - p.open("w") {|f| f.write(YAMLSerializer.dump(hash)) } + p.open("w") {|f| f.write(serializer_class.dump(hash)) } end end def converted_value(value, key) + key = self.class.key_to_s(key) + if is_array(key) to_array(value) elsif value.nil? @@ -369,15 +440,38 @@ module Bundler end end + def printable_value(value, key) + converted = converted_value(value, key) + return converted unless converted.is_a?(String) + + if is_string(key) + converted + elsif is_credential(key) + "[REDACTED]" + elsif is_userinfo(converted) + username, pass = converted.split(":", 2) + + if pass == "x-oauth-basic" + username = "[REDACTED]" + else + pass = "[REDACTED]" + end + + [username, pass].join(":") + else + converted + end + end + def global_config_file if ENV["BUNDLE_CONFIG"] && !ENV["BUNDLE_CONFIG"].empty? Pathname.new(ENV["BUNDLE_CONFIG"]) - else - begin - Bundler.user_bundle_path.join("config") - rescue PermissionError, GenericSystemCallError - nil - end + elsif ENV["BUNDLE_USER_CONFIG"] && !ENV["BUNDLE_USER_CONFIG"].empty? + Pathname.new(ENV["BUNDLE_USER_CONFIG"]) + elsif ENV["BUNDLE_USER_HOME"] && !ENV["BUNDLE_USER_HOME"].empty? + Pathname.new(ENV["BUNDLE_USER_HOME"]).join("config") + elsif Bundler.rubygems.user_home && !Bundler.rubygems.user_home.empty? + Pathname.new(Bundler.rubygems.user_home).join(".bundle/config") end end @@ -385,43 +479,64 @@ module Bundler Pathname.new(@root).join("config") if @root end - CONFIG_REGEX = %r{ # rubocop:disable Style/RegexpLiteral - ^ - (BUNDLE_.+):\s # the key - (?: !\s)? # optional exclamation mark found with ruby 1.9.3 - (['"]?) # optional opening quote - (.* # contents of the value - (?: # optionally, up until the next key - (\n(?!BUNDLE).+)* - ) - ) - \2 # matching closing quote - $ - }xo - def load_config(config_file) return {} if !config_file || ignore_config? SharedHelpers.filesystem_access(config_file, :read) do |file| valid_file = file.exist? && !file.size.zero? return {} unless valid_file - require "bundler/yaml_serializer" - YAMLSerializer.load file.read + (serializer_class.load(file.read) || {}).inject({}) do |config, (k, v)| + k = k.dup + k << "/" if /https?:/i.match?(k) && !k.end_with?("/", "__#{FALLBACK_TIMEOUT_URI_OPTION.upcase}") + k.gsub!(".", "__") + + unless k.start_with?("#") + if k.include?("-") + Bundler.ui.warn "Your #{file} config includes `#{k}`, which contains the dash character (`-`).\n" \ + "This is deprecated, because configuration through `ENV` should be possible, but `ENV` keys cannot include dashes.\n" \ + "Please edit #{file} and replace any dashes in configuration keys with a triple underscore (`___`)." + + # string hash keys are frozen + k = k.gsub("-", "___") + end + + config[k] = v + end + + config + end end end - PER_URI_OPTIONS = %w[ - fallback_timeout - ].freeze + def serializer_class + require "rubygems/yaml_serializer" + Gem::YAMLSerializer + rescue LoadError + # TODO: Remove this when RubyGems 3.4 is EOL + require_relative "yaml_serializer" + YAMLSerializer + end + + FALLBACK_TIMEOUT_URI_OPTION = "fallback_timeout" NORMALIZE_URI_OPTIONS_PATTERN = / \A (\w+\.)? # optional prefix key (https?.*?) # URI - (\.#{Regexp.union(PER_URI_OPTIONS)})? # optional suffix key + (\.#{FALLBACK_TIMEOUT_URI_OPTION})? # optional suffix key \z /ix + def self.key_for(key) + key = key_to_s(key) + key = normalize_uri(key) if key.start_with?("http", "mirror.http") + key = key.gsub(".", "__") + key.gsub!("-", "___") + key.upcase! + + key.gsub(/\A([ #]*)/, '\1BUNDLE_') + end + # TODO: duplicates Rubygems#normalize_uri # TODO: is this the correct place to validate mirror URIs? def self.normalize_uri(uri) @@ -431,12 +546,42 @@ module Bundler uri = $2 suffix = $3 end - uri = "#{uri}/" unless uri.end_with?("/") - uri = URI(uri) + uri = URINormalizer.normalize_suffix(uri) + require_relative "vendored_uri" + uri = Gem::URI(uri) unless uri.absolute? raise ArgumentError, format("Gem sources must be absolute. You provided '%s'.", uri) end "#{prefix}#{uri}#{suffix}" end + + # This is a hot method, so avoid respond_to? checks on every invocation + if :read.respond_to?(:name) + def self.key_to_s(key) + case key + when String + key + when Symbol + key.name + when Gem::URI::HTTP + key.to_s + else + raise ArgumentError, "Invalid key: #{key.inspect}" + end + end + else + def self.key_to_s(key) + case key + when String + key + when Symbol + key.to_s + when Gem::URI::HTTP + key.to_s + else + raise ArgumentError, "Invalid key: #{key.inspect}" + end + end + end end end diff --git a/lib/bundler/settings/validator.rb b/lib/bundler/settings/validator.rb index 9aa1627fb2..70a0ca36d4 100644 --- a/lib/bundler/settings/validator.rb +++ b/lib/bundler/settings/validator.rb @@ -74,6 +74,13 @@ module Bundler fail!(key, value, "`#{other_key}` is current set to #{other_setting.inspect}", "the `#{conflicting.join("`, `")}` groups conflict") end end + + rule %w[default_cli_command], "default_cli_command must be either 'install' or 'cli_help'" do |key, value, _settings| + valid_values = %w[install cli_help] + if !value.nil? && !valid_values.include?(value.to_s) + fail!(key, value, "must be one of: #{valid_values.join(", ")}") + end + end end end end diff --git a/lib/bundler/setup.rb b/lib/bundler/setup.rb index ac6a5bf861..5a0fd8e0e3 100644 --- a/lib/bundler/setup.rb +++ b/lib/bundler/setup.rb @@ -1,28 +1,39 @@ # frozen_string_literal: true -require "bundler/shared_helpers" +require_relative "shared_helpers" if Bundler::SharedHelpers.in_bundle? - require "bundler" + require_relative "../bundler" + + # autoswitch to locked Bundler version if available + Bundler.auto_switch + + # try to auto_install first before we get to the `Bundler.ui.silence`, so user knows what is happening + Bundler.auto_install if STDOUT.tty? || ENV["BUNDLER_FORCE_TTY"] begin - Bundler.setup + Bundler.ui.silence { Bundler.setup } rescue Bundler::BundlerError => e - puts "\e[31m#{e.message}\e[0m" - puts e.backtrace.join("\n") if ENV["DEBUG"] + Bundler.ui.error e.message + Bundler.ui.warn e.backtrace.join("\n") if ENV["DEBUG"] if e.is_a?(Bundler::GemNotFound) - puts "\e[33mRun `bundle install` to install missing gems.\e[0m" + default_bundle = Gem.bin_path("bundler", "bundle") + current_bundle = Bundler::SharedHelpers.bundle_bin_path + suggested_bundle = default_bundle == current_bundle ? "bundle" : current_bundle + suggested_cmd = "#{suggested_bundle} install" + original_gemfile = Bundler.original_env["BUNDLE_GEMFILE"] + suggested_cmd += " --gemfile #{original_gemfile}" if original_gemfile + Bundler.ui.warn "Run `#{suggested_cmd}` to install missing gems." end exit e.status_code end else - Bundler.setup + Bundler.ui.silence { Bundler.setup } end - # Add bundler to the load path after disabling system gems - bundler_lib = File.expand_path("../..", __FILE__) - $LOAD_PATH.unshift(bundler_lib) unless $LOAD_PATH.include?(bundler_lib) - - Bundler.ui = nil + # We might be in the middle of shelling out to rubygems + # (RUBYOPT=-rbundler/setup), so we need to give rubygems the opportunity of + # not being silent. + Gem::DefaultUserInteraction.ui = nil end diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb index 5566bc5832..2aa8abe0a0 100644 --- a/lib/bundler/shared_helpers.rb +++ b/lib/bundler/shared_helpers.rb @@ -1,47 +1,37 @@ # frozen_string_literal: true -require "bundler/compatibility_guard" - -require "pathname" -require "rubygems" - -require "bundler/version" -require "bundler/constants" -require "bundler/rubygems_integration" -require "bundler/current_ruby" - -module Gem - class Dependency - # This is only needed for RubyGems < 1.4 - unless method_defined? :requirement - def requirement - version_requirements - end - end - end -end +require_relative "version" +require_relative "rubygems_integration" +require_relative "current_ruby" module Bundler + autoload :WINDOWS, File.expand_path("constants", __dir__) + autoload :FREEBSD, File.expand_path("constants", __dir__) + autoload :NULL, File.expand_path("constants", __dir__) + module SharedHelpers def root gemfile = find_gemfile raise GemfileNotFound, "Could not locate Gemfile" unless gemfile - Pathname.new(gemfile).untaint.expand_path.parent + Pathname.new(gemfile).expand_path.parent end def default_gemfile - gemfile = find_gemfile(:order_matters) + gemfile = find_gemfile raise GemfileNotFound, "Could not locate Gemfile" unless gemfile - Pathname.new(gemfile).untaint.expand_path + Pathname.new(gemfile).expand_path end def default_lockfile + given = ENV["BUNDLE_LOCKFILE"] + return Pathname.new(given) if given && !given.empty? + gemfile = default_gemfile case gemfile.basename.to_s when "gems.rb" then Pathname.new(gemfile.sub(/.rb$/, ".locked")) else Pathname.new("#{gemfile}.lock") - end.untaint + end end def default_bundle_dir @@ -68,7 +58,7 @@ module Bundler def pwd Bundler.rubygems.ext_lock.synchronize do - Pathname.pwd + Dir.pwd end end @@ -107,16 +97,17 @@ module Bundler # given block # # @example - # filesystem_access("vendor/cache", :write) do + # filesystem_access("vendor/cache", :create) do # FileUtils.mkdir_p("vendor/cache") # end # # @see {Bundler::PermissionError} def filesystem_access(path, action = :write, &block) - # Use block.call instead of yield because of a bug in Ruby 2.2.2 - # See https://github.com/bundler/bundler/issues/5341 for details - block.call(path.dup.untaint) - rescue Errno::EACCES + yield(path.dup) + rescue Errno::EACCES => e + path_basename = File.basename(path.to_s) + raise unless e.message.include?(path_basename) || action == :create + raise PermissionError.new(path, action) rescue Errno::EAGAIN raise TemporaryResourceError.new(path, action) @@ -124,56 +115,39 @@ module Bundler raise VirtualProtocolError.new rescue Errno::ENOSPC raise NoSpaceOnDeviceError.new(path, action) - rescue *[const_get_safely(:ENOTSUP, Errno)].compact + rescue Errno::ENOTSUP raise OperationNotSupportedError.new(path, action) + rescue Errno::EPERM + raise OperationNotPermittedError.new(path, action) + rescue Errno::EROFS + raise ReadOnlyFileSystemError.new(path, action) rescue Errno::EEXIST, Errno::ENOENT raise rescue SystemCallError => e - raise GenericSystemCallError.new(e, "There was an error accessing `#{path}`.") + raise GenericSystemCallError.new(e, "There was an error #{[:create, :write].include?(action) ? "creating" : "accessing"} `#{path}`.") end - def const_get_safely(constant_name, namespace) - const_in_namespace = namespace.constants.include?(constant_name.to_s) || - namespace.constants.include?(constant_name.to_sym) - return nil unless const_in_namespace - namespace.const_get(constant_name) - end + def feature_deprecated!(message) + return unless prints_major_deprecations? - def major_deprecation(major_version, message) - if Bundler.bundler_major_version >= major_version - require "bundler/errors" - raise DeprecatedError, "[REMOVED FROM #{major_version}.0] #{message}" - end + Bundler.ui.warn("[DEPRECATED] #{message}") + end - return unless prints_major_deprecations? - @major_deprecation_ui ||= Bundler::UI::Shell.new("no-color" => true) - ui = Bundler.ui.is_a?(@major_deprecation_ui.class) ? Bundler.ui : @major_deprecation_ui - ui.warn("[DEPRECATED FOR #{major_version}.0] #{message}") + def feature_removed!(message) + require_relative "errors" + raise RemovedError, "[REMOVED] #{message}" end def print_major_deprecations! multiple_gemfiles = search_up(".") do |dir| gemfiles = gemfile_names.select {|gf| File.file? File.expand_path(gf, dir) } next if gemfiles.empty? - break false if gemfiles.size == 1 - end - if multiple_gemfiles && Bundler.bundler_major_version == 1 - Bundler::SharedHelpers.major_deprecation 2, \ - "gems.rb and gems.locked will be preferred to Gemfile and Gemfile.lock." - end - - if RUBY_VERSION < "2" - major_deprecation(2, "Bundler will only support ruby >= 2.0, you are running #{RUBY_VERSION}") - end - return if Bundler.rubygems.provides?(">= 2") - major_deprecation(2, "Bundler will only support rubygems >= 2.0, you are running #{Bundler.rubygems.version}") - end - - def trap(signal, override = false, &block) - prior = Signal.trap(signal) do - block.call - prior.call unless override + break gemfiles.size != 1 end + return unless multiple_gemfiles + message = "Multiple gemfiles (gems.rb and Gemfile) detected. " \ + "Make sure you remove Gemfile and Gemfile.lock since bundler is ignoring them in favor of gems.rb and gems.locked." + Bundler.ui.warn message end def ensure_same_dependencies(spec, old_deps, new_deps) @@ -187,21 +161,22 @@ module Bundler extra_deps = new_deps - old_deps return if extra_deps.empty? - Bundler.ui.debug "#{spec.full_name} from #{spec.remote} has either corrupted API or lockfile dependencies" \ + Bundler.ui.debug "#{spec.full_name} from #{spec.remote} has corrupted API dependencies" \ " (was expecting #{old_deps.map(&:to_s)}, but the real spec has #{new_deps.map(&:to_s)})" raise APIResponseMismatchError, - "Downloading #{spec.full_name} revealed dependencies not in the API or the lockfile (#{extra_deps.join(", ")})." \ - "\nEither installing with `--full-index` or running `bundle update #{spec.name}` should fix the problem." + "Downloading #{spec.full_name} revealed dependencies not in the API (#{extra_deps.join(", ")})." \ + "\nRunning `bundle update #{spec.name}` should fix the problem." end - def pretty_dependency(dep, print_source = false) + def pretty_dependency(dep) msg = String.new(dep.name) msg << " (#{dep.requirement})" unless dep.requirement == Gem::Requirement.default + if dep.is_a?(Bundler::Dependency) platform_string = dep.platforms.join(", ") msg << " " << platform_string if !platform_string.empty? && platform_string != Gem::Platform::RUBY end - msg << " from the `#{dep.source}` source" if print_source && dep.source + msg end @@ -209,11 +184,11 @@ module Bundler return @md5_available if defined?(@md5_available) @md5_available = begin require "openssl" - OpenSSL::Digest::MD5.digest("") + ::OpenSSL::Digest.digest("MD5", "") true rescue LoadError true - rescue OpenSSL::Digest::DigestError + rescue ::OpenSSL::Digest::DigestError false end end @@ -223,7 +198,41 @@ module Bundler Digest(name) end - private + def checksum_for_file(path, digest) + return unless path.file? + # This must use File.read instead of Digest.file().hexdigest + # because we need to preserve \n line endings on windows when calculating + # the checksum + SharedHelpers.filesystem_access(path, :read) do + File.open(path, "rb") do |f| + digest = SharedHelpers.digest(digest).new + buf = String.new(capacity: 16_384, encoding: Encoding::BINARY) + digest << buf while f.read(16_384, buf) + digest.hexdigest + end + end + end + + def write_to_gemfile(gemfile_path, contents) + filesystem_access(gemfile_path) {|g| File.open(g, "w") {|file| file.puts contents } } + end + + def relative_gemfile_path + relative_path_to(Bundler.default_gemfile) + end + + def relative_lockfile_path + relative_path_to(Bundler.default_lockfile) + end + + def relative_path_to(destination, from: pwd) + Pathname.new(destination).relative_path_from(from).to_s + rescue ArgumentError + # on Windows, if source and destination are on different drivers, there's no relative path from one to the other + destination + end + + private def validate_bundle_path path_separator = Bundler.rubygems.path_separator @@ -237,16 +246,14 @@ module Bundler raise Bundler::PathError, message end - def find_gemfile(order_matters = false) + def find_gemfile given = ENV["BUNDLE_GEMFILE"] return given if given && !given.empty? - names = gemfile_names - names.reverse! if order_matters && Bundler.feature_flag.prefer_gems_rb? - find_file(*names) + find_file(*gemfile_names) end def gemfile_names - ["Gemfile", "gems.rb"] + ["gems.rb", "Gemfile"] end def find_file(*names) @@ -263,18 +270,12 @@ module Bundler def search_up(*names) previous = nil - current = File.expand_path(SharedHelpers.pwd).untaint + current = File.expand_path(SharedHelpers.pwd) until !File.directory?(current) || current == previous - if ENV["BUNDLE_SPEC_RUN"] + if ENV["BUNDLER_SPEC_RUN"] # avoid stepping above the tmp directory when testing - if !!(ENV["BUNDLE_RUBY"] && ENV["BUNDLE_GEM"]) - # for Ruby Core - gemspec = "lib/bundler.gemspec" - else - gemspec = "bundler.gemspec" - end - return nil if File.file?(File.join(current, gemspec)) + return nil if File.directory?(File.join(current, "tmp")) end names.each do |name| @@ -298,19 +299,43 @@ module Bundler public :set_env def set_bundle_variables - begin - Bundler::SharedHelpers.set_env "BUNDLE_BIN_PATH", Bundler.rubygems.bin_path("bundler", "bundle", VERSION) - rescue Gem::GemNotFoundException - if File.exist?(File.expand_path("../../../exe/bundle", __FILE__)) - Bundler::SharedHelpers.set_env "BUNDLE_BIN_PATH", File.expand_path("../../../exe/bundle", __FILE__) - else - Bundler::SharedHelpers.set_env "BUNDLE_BIN_PATH", File.expand_path("../../../../bin/bundle", __FILE__) - end - end - - # Set BUNDLE_GEMFILE - Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", find_gemfile(:order_matters).to_s + Bundler::SharedHelpers.set_env "BUNDLE_BIN_PATH", bundle_bin_path + Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", find_gemfile.to_s + Bundler::SharedHelpers.set_env "BUNDLE_LOCKFILE", default_lockfile.to_s Bundler::SharedHelpers.set_env "BUNDLER_VERSION", Bundler::VERSION + Bundler::SharedHelpers.set_env "BUNDLER_SETUP", File.expand_path("setup", __dir__) + end + + def bundle_bin_path + # bundler exe & lib folders have same root folder, typical gem installation + exe_file = File.join(source_root, "exe/bundle") + + # for Ruby core repository testing + exe_file = File.join(source_root, "libexec/bundle") unless File.exist?(exe_file) + + # bundler is a default gem, exe path is separate + exe_file = Gem.bin_path("bundler", "bundle", VERSION) unless File.exist?(exe_file) + + exe_file + end + public :bundle_bin_path + + def gemspec_path + # inside a gem repository, typical gem installation + gemspec_file = File.join(source_root, "../../specifications/bundler-#{VERSION}.gemspec") + + # for Ruby core repository testing + gemspec_file = File.expand_path("bundler.gemspec", __dir__) unless File.exist?(gemspec_file) + + # bundler is a default gem + gemspec_file = File.join(Gem.default_specifications_dir, "bundler-#{VERSION}.gemspec") unless File.exist?(gemspec_file) + + gemspec_file + end + public :gemspec_path + + def source_root + File.expand_path("../..", __dir__) end def set_path @@ -322,41 +347,43 @@ module Bundler def set_rubyopt rubyopt = [ENV["RUBYOPT"]].compact - return if !rubyopt.empty? && rubyopt.first =~ %r{-rbundler/setup} - rubyopt.unshift %(-rbundler/setup) + setup_require = "-r#{File.expand_path("setup", __dir__)}" + return if !rubyopt.empty? && rubyopt.first.include?(setup_require) + rubyopt.unshift setup_require Bundler::SharedHelpers.set_env "RUBYOPT", rubyopt.join(" ") end def set_rubylib rubylib = (ENV["RUBYLIB"] || "").split(File::PATH_SEPARATOR) - rubylib.unshift bundler_ruby_lib + rubylib.unshift bundler_ruby_lib unless RbConfig::CONFIG["rubylibdir"] == bundler_ruby_lib Bundler::SharedHelpers.set_env "RUBYLIB", rubylib.uniq.join(File::PATH_SEPARATOR) end def bundler_ruby_lib - File.expand_path("../..", __FILE__) + File.expand_path("..", __dir__) end def clean_load_path - # handle 1.9 where system gems are always on the load path - return unless defined?(::Gem) - - bundler_lib = bundler_ruby_lib - loaded_gem_paths = Bundler.rubygems.loaded_gem_paths $LOAD_PATH.reject! do |p| - next if File.expand_path(p).start_with?(bundler_lib) + resolved_path = resolve_path(p) + next if $LOADED_FEATURES.any? {|lf| lf.start_with?(resolved_path) } loaded_gem_paths.delete(p) end $LOAD_PATH.uniq! end + def resolve_path(path) + expanded = File.expand_path(path) + return expanded unless File.exist?(expanded) + + File.realpath(expanded) + end + def prints_major_deprecations? - require "bundler" - deprecation_release = Bundler::VERSION.split(".").drop(1).include?("99") - return false if !deprecation_release && !Bundler.settings[:major_deprecations] - require "bundler/deprecate" + return false if Bundler.settings[:silence_deprecations] + require_relative "deprecate" return false if Bundler::Deprecate.skip true end diff --git a/lib/bundler/similarity_detector.rb b/lib/bundler/similarity_detector.rb deleted file mode 100644 index b7f3ee7afa..0000000000 --- a/lib/bundler/similarity_detector.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -module Bundler - class SimilarityDetector - SimilarityScore = Struct.new(:string, :distance) - - # initialize with an array of words to be matched against - def initialize(corpus) - @corpus = corpus - end - - # return an array of words similar to 'word' from the corpus - def similar_words(word, limit = 3) - words_by_similarity = @corpus.map {|w| SimilarityScore.new(w, levenshtein_distance(word, w)) } - words_by_similarity.select {|s| s.distance <= limit }.sort_by(&:distance).map(&:string) - end - - # return the result of 'similar_words', concatenated into a list - # (eg "a, b, or c") - def similar_word_list(word, limit = 3) - words = similar_words(word, limit) - if words.length == 1 - words[0] - elsif words.length > 1 - [words[0..-2].join(", "), words[-1]].join(" or ") - end - end - - protected - - # http://www.informit.com/articles/article.aspx?p=683059&seqNum=36 - def levenshtein_distance(this, that, ins = 2, del = 2, sub = 1) - # ins, del, sub are weighted costs - return nil if this.nil? - return nil if that.nil? - dm = [] # distance matrix - - # Initialize first row values - dm[0] = (0..this.length).collect {|i| i * ins } - fill = [0] * (this.length - 1) - - # Initialize first column values - (1..that.length).each do |i| - dm[i] = [i * del, fill.flatten] - end - - # populate matrix - (1..that.length).each do |i| - (1..this.length).each do |j| - # critical comparison - dm[i][j] = [ - dm[i - 1][j - 1] + (this[j - 1] == that[i - 1] ? 0 : sub), - dm[i][j - 1] + ins, - dm[i - 1][j] + del - ].min - end - end - - # The last value in matrix is the Levenshtein distance between the strings - dm[that.length][this.length] - end - end -end diff --git a/lib/bundler/source.rb b/lib/bundler/source.rb index 5a1f05098b..cf71be8801 100644 --- a/lib/bundler/source.rb +++ b/lib/bundler/source.rb @@ -2,25 +2,27 @@ module Bundler class Source - autoload :Gemspec, "bundler/source/gemspec" - autoload :Git, "bundler/source/git" - autoload :Metadata, "bundler/source/metadata" - autoload :Path, "bundler/source/path" - autoload :Rubygems, "bundler/source/rubygems" + autoload :Gemspec, File.expand_path("source/gemspec", __dir__) + autoload :Git, File.expand_path("source/git", __dir__) + autoload :Metadata, File.expand_path("source/metadata", __dir__) + autoload :Path, File.expand_path("source/path", __dir__) + autoload :Rubygems, File.expand_path("source/rubygems", __dir__) + autoload :RubygemsAggregate, File.expand_path("source/rubygems_aggregate", __dir__) attr_accessor :dependency_names + attr_reader :checksum_store + def unmet_deps specs.unmet_dependency_names end - def version_message(spec) + def version_message(spec, locked_spec = nil) message = "#{spec.name} #{spec.version}" message += " (#{spec.platform})" if spec.platform != Gem::Platform::RUBY && !spec.platform.nil? - if Bundler.locked_gems - locked_spec = Bundler.locked_gems.specs.find {|s| s.name == spec.name } - locked_spec_version = locked_spec.version if locked_spec + if locked_spec + locked_spec_version = locked_spec.version if locked_spec_version && spec.version != locked_spec_version message += Bundler.ui.add_color(" (was #{locked_spec_version})", version_color(spec.version, locked_spec_version)) end @@ -29,10 +31,26 @@ module Bundler message end + def download(*); end + def can_lock?(spec) spec.source == self end + def prefer_local!; end + + def local!; end + + def local_only!; end + + def cached!; end + + def remote!; end + + def add_dependency_names(names) + @dependency_names = Array(dependency_names) | Array(names) + end + # it's possible that gems from one source depend on gems from some # other source, so now we download gemspecs and iterate over those # dependencies, looking for gems we don't have info on yet. @@ -42,6 +60,10 @@ module Bundler specs.dependency_names end + def spec_names + specs.spec_names + end + def include?(other) other == self end @@ -50,11 +72,24 @@ module Bundler "#<#{self.class}:0x#{object_id} #{self}>" end + def identifier + to_s + end + def path? instance_of?(Bundler::Source::Path) end - private + def extension_cache_path(spec) + return unless Bundler.settings[:global_gem_cache] + return unless source_slug = extension_cache_slug(spec) + Bundler.user_cache.join( + "extensions", Gem::Platform.local.to_s, Bundler.ruby_scope, + source_slug, spec.full_name + ) + end + + private def version_color(spec_version, locked_spec_version) if Gem::Version.correct?(spec_version) && Gem::Version.correct?(locked_spec_version) @@ -71,22 +106,13 @@ module Bundler end def print_using_message(message) - if !message.include?("(was ") && Bundler.feature_flag.suppress_install_using_messages? + if !message.include?("(was ") Bundler.ui.debug message else Bundler.ui.info message end end - def extension_cache_path(spec) - return unless Bundler.feature_flag.global_gem_cache? - return unless source_slug = extension_cache_slug(spec) - Bundler.user_cache.join( - "extensions", Gem::Platform.local.to_s, Bundler.ruby_scope, - source_slug, spec.full_name - ) - end - def extension_cache_slug(_) nil end diff --git a/lib/bundler/source/gemspec.rb b/lib/bundler/source/gemspec.rb index 7e3447e776..ed766dbe74 100644 --- a/lib/bundler/source/gemspec.rb +++ b/lib/bundler/source/gemspec.rb @@ -4,14 +4,15 @@ module Bundler class Source class Gemspec < Path attr_reader :gemspec + attr_writer :checksum_store def initialize(options) super @gemspec = options["gemspec"] end - def as_path_source - Path.new(options) + def to_s + "gemspec at `#{@path}`" end end end diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb index a1a59ddce5..a002a2570a 100644 --- a/lib/bundler/source/git.rb +++ b/lib/bundler/source/git.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true -require "bundler/vendored_fileutils" -require "uri" +require_relative "../vendored_fileutils" module Bundler class Source class Git < Path - autoload :GitProxy, "bundler/source/git/git_proxy" + autoload :GitProxy, File.expand_path("git/git_proxy", __dir__) - attr_reader :uri, :ref, :branch, :options, :submodules + attr_reader :uri, :ref, :branch, :options, :glob, :submodules def initialize(options) @options = options + @checksum_store = Checksum::Store.new @glob = options["glob"] || DEFAULT_GLOB @allow_cached = false @@ -20,9 +20,10 @@ module Bundler # Stringify options that could be set as symbols %w[ref branch tag revision].each {|k| options[k] = options[k].to_s if options[k] } - @uri = options["uri"] || "" + @uri = URINormalizer.normalize_suffix(options["uri"] || "", trailing_slash: false) + @safe_uri = URICredentialsFilter.credential_filtered_uri(@uri) @branch = options["branch"] - @ref = options["ref"] || options["branch"] || options["tag"] || "master" + @ref = options["ref"] || options["branch"] || options["tag"] @submodules = options["submodules"] @name = options["name"] @version = options["version"].to_s.strip.gsub("-", ".pre.") @@ -31,6 +32,20 @@ module Bundler @local = false end + def remote! + return if @allow_remote + + @local_specs = nil + @allow_remote = true + end + + def cached! + return if @allow_cached + + @local_specs = nil + @allow_cached = true + end + def self.from_lock(options) new(options.merge("uri" => options.delete("remote"))) end @@ -42,42 +57,65 @@ module Bundler %w[ref branch tag submodules].each do |opt| out << " #{opt}: #{options[opt]}\n" if options[opt] end - out << " glob: #{@glob}\n" unless @glob == DEFAULT_GLOB + out << " glob: #{@glob}\n" unless default_glob? out << " specs:\n" end + def to_gemfile + specifiers = %w[ref branch tag submodules glob].map do |opt| + "#{opt}: #{options[opt]}" if options[opt] + end + + uri_with_specifiers(specifiers) + end + def hash - [self.class, uri, ref, branch, name, version, submodules].hash + [self.class, uri, ref, branch, name, glob, submodules].hash end def eql?(other) other.is_a?(Git) && uri == other.uri && ref == other.ref && branch == other.branch && name == other.name && - version == other.version && submodules == other.submodules + glob == other.glob && + submodules == other.submodules end alias_method :==, :eql? + def include?(other) + other.is_a?(Git) && uri == other.uri && + name == other.name && + glob == other.glob && + submodules == other.submodules + end + def to_s - at = if local? - path - elsif user_ref = options["ref"] - if ref =~ /\A[a-z0-9]{4,}\z/i - shortref_for_display(user_ref) - else - user_ref - end - else - ref + begin + at = humanized_ref || current_branch + + rev = "at #{at}@#{shortref_for_display(revision)}" + rescue GitError + "" end - rev = begin - "@#{shortref_for_display(revision)}" - rescue GitError - nil - end + uri_with_specifiers([rev, glob_for_display]) + end + + def identifier + uri_with_specifiers([humanized_ref, locked_revision, glob_for_display]) + end + + def uri_with_specifiers(specifiers) + specifiers.compact! - "#{uri} (at #{at}#{rev})" + suffix = + if specifiers.any? + " (#{specifiers.join(", ")})" + else + "" + end + + "#{@safe_uri}#{suffix}" end def name @@ -91,13 +129,7 @@ module Bundler @install_path ||= begin git_scope = "#{base_name}-#{shortref_for_path(revision)}" - path = Bundler.install_path.join(git_scope) - - if !path.exist? && Bundler.requires_sudo? - Bundler.user_bundle_path.join(Bundler.ruby_scope).join(git_scope) - else - path - end + Bundler.install_path.join(git_scope) end end @@ -117,35 +149,37 @@ module Bundler def local_override!(path) return false if local? + original_path = path path = Pathname.new(path) path = path.expand_path(Bundler.root) unless path.relative? - unless options["branch"] || Bundler.settings[:disable_local_branch_check] + unless branch || Bundler.settings[:disable_local_branch_check] raise GitError, "Cannot use local override for #{name} at #{path} because " \ - ":branch is not specified in Gemfile. Specify a branch or use " \ - "`bundle config --delete` to remove the local override" + ":branch is not specified in Gemfile. Specify a branch or run " \ + "`bundle config unset local.#{override_for(original_path)}` to remove the local override" end unless path.exist? raise GitError, "Cannot use local override for #{name} because #{path} " \ - "does not exist. Check `bundle config --delete` to remove the local override" + "does not exist. Run `bundle config unset local.#{override_for(original_path)}` to remove the local override" end - set_local!(path) + @local = true + set_paths!(path) # Create a new git proxy without the cached revision # so the Gemfile.lock always picks up the new revision. - @git_proxy = GitProxy.new(path, uri, ref) + @git_proxy = GitProxy.new(path, uri, options) - if git_proxy.branch != options["branch"] && !Bundler.settings[:disable_local_branch_check] + if current_branch != branch && !Bundler.settings[:disable_local_branch_check] raise GitError, "Local override for #{name} at #{path} is using branch " \ - "#{git_proxy.branch} but Gemfile specifies #{options["branch"]}" + "#{current_branch} but Gemfile specifies #{branch}" end - changed = cached_revision && cached_revision != git_proxy.revision + changed = locked_revision && locked_revision != revision - if changed && !@unlocked && !git_proxy.contains?(cached_revision) - raise GitError, "The Gemfile lock is pointing to revision #{shortref_for_display(cached_revision)} " \ + if !Bundler.settings[:disable_local_revision_check] && changed && !@unlocked && !git_proxy.contains?(locked_revision) + raise GitError, "The Gemfile lock is pointing to revision #{shortref_for_display(locked_revision)} " \ "but the current branch in your local override for #{name} does not contain such commit. " \ "Please make sure your branch is up to date." end @@ -154,45 +188,47 @@ module Bundler end def specs(*) - set_local!(app_cache_path) if has_app_cache? && !local? + set_cache_path!(app_cache_path) if use_app_cache? if requires_checkout? && !@copied - fetch - git_proxy.copy_to(install_path, submodules) - serialize_gemspecs_in(install_path) - @copied = true + Plugin.hook(Plugin::Events::GIT_BEFORE_FETCH, self) + begin + fetch unless use_app_cache? + checkout + ensure + Plugin.hook(Plugin::Events::GIT_AFTER_FETCH, self) + end end local_specs end def install(spec, options = {}) + return if Bundler.settings[:no_install] force = options[:force] - print_using_message "Using #{version_message(spec)} from #{self}" + print_using_message "Using #{version_message(spec, options[:previous_spec])} from #{self}" if (requires_checkout? && !@copied) || force - Bundler.ui.debug " * Checking out revision: #{ref}" - git_proxy.copy_to(install_path, submodules) - serialize_gemspecs_in(install_path) - @copied = true + checkout end - generate_bin_options = { :disable_extensions => !Bundler.rubygems.spec_missing_extensions?(spec), :build_args => options[:build_args] } + generate_bin_options = { disable_extensions: !spec.missing_extensions?, build_args: options[:build_args] } generate_bin(spec, generate_bin_options) requires_checkout? ? spec.post_install_message : nil end + def migrate_cache(custom_path = nil, local: false) + if local + cache_to(custom_path, try_migrate: false) + else + cache_to(custom_path, try_migrate: true) + end + end + def cache(spec, custom_path = nil) - app_cache_path = app_cache_path(custom_path) - return unless Bundler.feature_flag.cache_all? - return if path == app_cache_path - cached! - FileUtils.rm_rf(app_cache_path) - git_proxy.checkout if requires_checkout? - git_proxy.copy_to(app_cache_path, @submodules) - serialize_gemspecs_in(app_cache_path) + cache_to(custom_path, try_migrate: false) end def load_spec_files @@ -207,28 +243,85 @@ module Bundler # across different projects, this cache will be shared. # When using local git repos, this is set to the local repo. def cache_path - @cache_path ||= begin - if Bundler.requires_sudo? || Bundler.feature_flag.global_gem_cache? - Bundler.user_cache - else - Bundler.bundle_path.join("cache", "bundler") - end.join("git", git_scope) - end + @cache_path ||= if Bundler.settings[:global_gem_cache] + Bundler.user_cache + else + Bundler.bundle_path.join("cache", "bundler") + end.join("git", git_scope) end def app_cache_dirname - "#{base_name}-#{shortref_for_path(cached_revision || revision)}" + "#{base_name}-#{shortref_for_path(locked_revision || revision)}" end def revision git_proxy.revision end + def current_branch + git_proxy.current_branch + end + def allow_git_ops? @allow_remote || @allow_cached end - private + def local? + @local + end + + private + + def cache_to(custom_path, try_migrate: false) + return unless Bundler.settings[:cache_all] + + app_cache_path = app_cache_path(custom_path) + + migrate = try_migrate ? bare_repo?(app_cache_path) : false + + set_cache_path!(nil) if migrate + + return if cache_path == app_cache_path + + cached! + FileUtils.rm_rf(app_cache_path) + git_proxy.checkout if migrate || requires_checkout? + git_proxy.copy_to(app_cache_path, @submodules) + serialize_gemspecs_in(app_cache_path) + end + + def checkout + Bundler.ui.debug " * Checking out revision: #{ref}" + if use_app_cache? && !bare_repo?(app_cache_path) + SharedHelpers.filesystem_access(install_path.dirname) do |p| + FileUtils.mkdir_p(p) + end + FileUtils.cp_r("#{app_cache_path}/.", install_path) + else + if use_app_cache? && bare_repo?(app_cache_path) + Bundler.ui.warn "Installing from cache in old \"bare repository\" format for compatibility. " \ + "Please run `bundle cache` and commit the updated cache to migrate to the new format and get rid of this warning." + end + + git_proxy.copy_to(install_path, submodules) + end + serialize_gemspecs_in(install_path) + @copied = true + end + + def humanized_ref + if local? + path + elsif user_ref = options["ref"] + if /\A[a-z0-9]{4,}\z/i.match?(ref) + shortref_for_display(user_ref) + else + user_ref + end + elsif ref + ref + end + end def serialize_gemspecs_in(destination) destination = destination.expand_path(Bundler.root) if destination.relative? @@ -238,28 +331,45 @@ module Bundler # The gemspecs we cache should already be evaluated. spec = Bundler.load_gemspec(spec_path) next unless spec - Bundler.rubygems.set_installed_by_version(spec) + spec.installed_by_version = Gem::VERSION Bundler.rubygems.validate(spec) File.open(spec_path, "wb") {|file| file.write(spec.to_ruby) } end end - def set_local!(path) - @local = true - @local_specs = @git_proxy = nil - @cache_path = @install_path = path + def set_paths!(path) + set_cache_path!(path) + set_install_path!(path) + end + + def set_cache_path!(path) + @git_proxy = nil + @cache_path = path + end + + def set_install_path!(path) + @local_specs = nil + @install_path = path end def has_app_cache? - cached_revision && super + locked_revision && super end - def local? - @local + def use_app_cache? + has_app_cache? && !local? end def requires_checkout? - allow_git_ops? && !local? + allow_git_ops? && !local? && !locked_revision_checked_out? + end + + def locked_revision_checked_out? + locked_revision && locked_revision == revision && installed? + end + + def installed? + git_proxy.installed_to?(install_path) end def base_name @@ -274,19 +384,29 @@ module Bundler ref[0..11] end + def glob_for_display + default_glob? ? nil : "glob: #{@glob}" + end + + def default_glob? + @glob == DEFAULT_GLOB + end + def uri_hash - if uri =~ %r{^\w+://(\w+@)?} + if %r{^\w+://(\w+@)?}.match?(uri) # Downcase the domain component of the URI # and strip off a trailing slash, if one is present - input = URI.parse(uri).normalize.to_s.sub(%r{/$}, "") + input = Gem::URI.parse(uri).normalize.to_s.sub(%r{/$}, "") else # If there is no URI scheme, assume it is an ssh/git URI input = uri end - SharedHelpers.digest(:SHA1).hexdigest(input) + # We use SHA1 here for historical reason and to preserve backward compatibility. + # But a transition to a simpler mangling algorithm would be welcome. + Bundler::Digest.sha1(input) end - def cached_revision + def locked_revision options["revision"] end @@ -295,23 +415,23 @@ module Bundler end def git_proxy - @git_proxy ||= GitProxy.new(cache_path, uri, ref, cached_revision, self) + @git_proxy ||= GitProxy.new(cache_path, uri, options, locked_revision, self) end def fetch git_proxy.checkout rescue GitError => e - raise unless Bundler.feature_flag.allow_offline_install? Bundler.ui.warn "Using cached git data because of network errors:\n#{e}" end # no-op, since we validate when re-serializing the gemspec def validate_spec(_spec); end - if Bundler.rubygems.stubs_provide_full_functionality? - def load_gemspec(file) + def load_gemspec(file) + dirname = Pathname.new(file).dirname + SharedHelpers.chdir(dirname.to_s) do stub = Gem::StubSpecification.gemspec_stub(file, install_path.parent, install_path.parent) - stub.full_gem_path = Pathname.new(file).dirname.expand_path(root).to_s.untaint + stub.full_gem_path = dirname.expand_path(root).to_s StubSpecification.from_stub(stub) end end @@ -323,6 +443,14 @@ module Bundler def extension_cache_slug(_) extension_dir_name end + + def override_for(path) + Bundler.settings.local_overrides.key(path) + end + + def bare_repo?(path) + File.exist?(path.join("objects")) && File.exist?(path.join("HEAD")) + end end end end diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb index c56dda66ea..8094dcaa9d 100644 --- a/lib/bundler/source/git/git_proxy.rb +++ b/lib/bundler/source/git/git_proxy.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "shellwords" -require "tempfile" module Bundler class Source class Git @@ -17,26 +15,37 @@ module Bundler class GitNotAllowedError < GitError def initialize(command) msg = String.new - msg << "Bundler is trying to run a `git #{command}` at runtime. You probably need to run `bundle install`. However, " - msg << "this error message could probably be more useful. Please submit a ticket at http://github.com/bundler/bundler/issues " + msg << "Bundler is trying to run `#{command}` at runtime. You probably need to run `bundle install`. However, " + msg << "this error message could probably be more useful. Please submit a ticket at https://github.com/ruby/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md " msg << "with steps to reproduce as well as the following\n\nCALLER: #{caller.join("\n")}" super msg end end class GitCommandError < GitError - def initialize(command, path = nil, extra_info = nil) - msg = String.new - msg << "Git error: command `git #{command}` in directory #{SharedHelpers.pwd} has failed." + attr_reader :command + + def initialize(command, path, extra_info = nil) + @command = command + + msg = String.new("Git error: command `#{command}`") + msg << " in directory #{path}" if path + msg << " has failed." msg << "\n#{extra_info}" if extra_info - msg << "\nIf this error persists you could try removing the cache directory '#{path}'" if path && path.exist? super msg end end - class MissingGitRevisionError < GitError - def initialize(ref, repo) + class MissingGitRevisionError < GitCommandError + def initialize(command, destination_path, ref, repo) msg = "Revision #{ref} does not exist in the repository #{repo}. Maybe you misspelled it?" + super command, destination_path, msg + end + end + + class AmbiguousGitReference < GitError + def initialize(options) + msg = "Specification of branch or ref with tag is ambiguous. You specified #{options.inspect}" super msg end end @@ -45,72 +54,87 @@ module Bundler # All actions required by the Git source is encapsulated in this # object. class GitProxy - attr_accessor :path, :uri, :ref + attr_accessor :path, :uri, :branch, :tag, :ref, :explicit_ref attr_writer :revision - def initialize(path, uri, ref, revision = nil, git = nil) + def self.version + @version ||= full_version[/((\.?\d+)+).*/, 1] + end + + def self.full_version + @full_version ||= begin + raise GitNotInstalledError.new unless Bundler.git_present? + + require "open3" + out, err, status = Open3.capture3("git", "--version") + + raise GitCommandError.new("--version", SharedHelpers.pwd, err) unless status.success? + Bundler.ui.warn err unless err.empty? + + out.sub(/git version\s*/, "").strip + end + end + + def self.reset + @version = nil + @full_version = nil + end + + def initialize(path, uri, options = {}, revision = nil, git = nil) @path = path @uri = uri - @ref = ref + @tag = options["tag"] + @branch = options["branch"] + @ref = options["ref"] + if @tag + raise AmbiguousGitReference.new(options) if @branch || @ref + @explicit_ref = @tag + else + @explicit_ref = @ref || @branch + end @revision = revision @git = git - raise GitNotInstalledError.new if allow? && !Bundler.git_present? + @commit_ref = nil end def revision - return @revision if @revision - - begin - @revision ||= find_local_revision - rescue GitCommandError - raise MissingGitRevisionError.new(ref, URICredentialsFilter.credential_filtered_uri(uri)) - end - - @revision + @revision ||= allowed_with_path { find_local_revision } end - def branch - @branch ||= allowed_in_path do - git("rev-parse --abbrev-ref HEAD").strip + def current_branch + @current_branch ||= with_path do + git_local("rev-parse", "--abbrev-ref", "HEAD", dir: path).strip end end def contains?(commit) - allowed_in_path do - result = git_null("branch --contains #{commit}") - $? == 0 && result =~ /^\* (.*)$/ + allowed_with_path do + result, status = git_null("branch", "--contains", commit, dir: path) + status.success? && result.match?(/^\* (.*)$/) end end def version - git("--version").match(/(git version\s*)?((\.?\d+)+).*/)[2] + self.class.version end def full_version - git("--version").sub("git version", "").strip + self.class.full_version end def checkout - return if path.exist? && has_revision_cached? - extra_ref = "#{Shellwords.shellescape(ref)}:#{Shellwords.shellescape(ref)}" if ref && ref.start_with?("refs/") + return if has_revision_cached? - Bundler.ui.info "Fetching #{URICredentialsFilter.credential_filtered_uri(uri)}" + Bundler.ui.info "Fetching #{credential_filtered_uri}" - unless path.exist? - SharedHelpers.filesystem_access(path.dirname) do |p| - FileUtils.mkdir_p(p) - end - git_retry %(clone #{uri_escaped_with_configured_credentials} "#{path}" --bare --no-hardlinks --quiet) - return unless extra_ref - end + extra_fetch_needed = clone_needs_extra_fetch? + unshallow_needed = clone_needs_unshallow? + return unless extra_fetch_needed || unshallow_needed - in_path do - git_retry %(fetch --force --quiet --tags #{uri_escaped_with_configured_credentials} "refs/heads/*:refs/heads/*" #{extra_ref}) - end + git_remote_fetch(unshallow_needed ? ["--unshallow"] : depth_args) end def copy_to(destination, submodules = false) - # method 1 unless File.exist?(destination.join(".git")) begin SharedHelpers.filesystem_access(destination.dirname) do |p| @@ -119,137 +143,359 @@ module Bundler SharedHelpers.filesystem_access(destination) do |p| FileUtils.rm_rf(p) end - git_retry %(clone --no-checkout --quiet "#{path}" "#{destination}") - File.chmod(((File.stat(destination).mode | 0o777) & ~File.umask), destination) + git "clone", "--no-checkout", "--quiet", path.to_s, destination.to_s + File.chmod((File.stat(destination).mode | 0o777) & ~File.umask, destination) rescue Errno::EEXIST => e - file_path = e.message[%r{.*?(/.*)}, 1] + file_path = e.message[%r{.*?((?:[a-zA-Z]:)?/.*)}, 1] raise GitError, "Bundler could not install a gem because it needs to " \ "create a directory, but a file exists - #{file_path}. Please delete " \ "this file and try again." end end - # method 2 - SharedHelpers.chdir(destination) do - git_retry %(fetch --force --quiet --tags "#{path}") - git "reset --hard #{@revision}" - - if submodules - git_retry "submodule update --init --recursive" - elsif Gem::Version.create(version) >= Gem::Version.create("2.9.0") - git_retry "submodule deinit --all --force" + + ref = @commit_ref || (locked_to_full_sha? && @revision) + if ref + git "config", "uploadpack.allowAnySHA1InWant", "true", dir: path.to_s if @commit_ref.nil? && needs_allow_any_sha1_in_want? + + git "fetch", "--force", "--quiet", *extra_fetch_args(ref), dir: destination + end + + git "reset", "--hard", revision, dir: destination + + if submodules + git_retry "submodule", "update", "--init", "--recursive", dir: destination + elsif Gem::Version.create(version) >= Gem::Version.create("2.9.0") + inner_command = "git -C $toplevel submodule deinit --force $sm_path" + git_retry "submodule", "foreach", "--quiet", inner_command, dir: destination + end + end + + def installed_to?(destination) + # if copy_to is interrupted, it may leave a partially installed directory that + # contains .git but no other files -- consider this not to be installed + Dir.exist?(destination) && (Dir.children(destination) - [".git"]).any? + end + + private + + def git_remote_fetch(args) + command = fetch_command(args) + command_with_no_credentials = check_allowed(command) + + Bundler::Retry.new("`#{command_with_no_credentials}` at #{path}", [MissingGitRevisionError]).attempts do + out, err, status = capture(command, path) + return out if status.success? + + if err.include?("couldn't find remote ref") || err.include?("not our ref") + raise MissingGitRevisionError.new(command_with_no_credentials, path, commit || explicit_ref, credential_filtered_uri) + else + if shallow? + args -= depth_args + command = fetch_command(args) + command_with_no_credentials = check_allowed(command) + end + raise GitCommandError.new(command_with_no_credentials, path, err) + end + end + end + + def clone_needs_extra_fetch? + return true if path.exist? + + SharedHelpers.filesystem_access(path.dirname) do |p| + FileUtils.mkdir_p(p) + end + + clone_args = extra_clone_args + command = clone_command(clone_args) + command_with_no_credentials = check_allowed(command) + + Bundler::Retry.new("`#{command_with_no_credentials}`", [MissingGitRevisionError]).attempts do + _, err, status = capture(command, nil) + return extra_ref if status.success? + + if err.include?("Could not find remote branch") || # git up to 2.49 + err.include?("Remote branch #{branch_option} not found") # git 2.49 or higher + raise MissingGitRevisionError.new(command_with_no_credentials, nil, explicit_ref, credential_filtered_uri) + else + if shallow? + clone_args -= depth_args + command = clone_command(clone_args) + command_with_no_credentials = check_allowed(command) + end + raise GitCommandError.new(command_with_no_credentials, path, err) end end end - private + def clone_needs_unshallow? + return false unless path.join("shallow").exist? + return true unless shallow? + + @revision && @revision != head_revision + end + + def extra_ref + return false if not_pinned? + return true if shallow? - # TODO: Do not rely on /dev/null. - # Given that open3 is not cross platform until Ruby 1.9.3, - # the best solution is to pipe to /dev/null if it exists. - # If it doesn't, everything will work fine, but the user - # will get the $stderr messages as well. - def git_null(command) - git("#{command} 2>#{Bundler::NULL}", false) + ref.start_with?("refs/") end - def git_retry(command) - Bundler::Retry.new("`git #{URICredentialsFilter.credential_filtered_string(command, uri)}`", GitNotAllowedError).attempts do - git(command) + def depth + return @depth if defined?(@depth) + + @depth = if !supports_fetching_unreachable_refs? + nil + elsif not_pinned? || pinned_to_full_sha? + 1 + elsif ref.include?("~") + parsed_depth = ref.split("~").last + parsed_depth.to_i + 1 end end - def git(command, check_errors = true, error_msg = nil) - command_with_no_credentials = URICredentialsFilter.credential_filtered_string(command, uri) - raise GitNotAllowedError.new(command_with_no_credentials) unless allow? + def refspec + if commit + @commit_ref = "refs/#{commit}-sha" + return "#{commit}:#{@commit_ref}" + end + + reference = fully_qualified_ref - out = SharedHelpers.with_clean_git_env do - capture_and_filter_stderr(uri) { `git #{command}` } + reference ||= if ref.include?("~") + ref.split("~").first + elsif ref.start_with?("refs/") + ref + else + "refs/*" end - stdout_with_no_credentials = URICredentialsFilter.credential_filtered_string(out, uri) - raise GitCommandError.new(command_with_no_credentials, path, error_msg) if check_errors && !$?.success? - stdout_with_no_credentials + "#{reference}:#{reference}" + end + + def commit + @commit ||= pinned_to_full_sha? ? ref : @revision + end + + def fully_qualified_ref + if branch + "refs/heads/#{branch}" + elsif tag + "refs/tags/#{tag}" + elsif ref.nil? + "refs/heads/#{current_branch}" + end + end + + def not_pinned? + branch_option || ref.nil? + end + + def pinned_to_full_sha? + full_sha_revision?(ref) + end + + def locked_to_full_sha? + full_sha_revision?(@revision) + end + + def full_sha_revision?(ref) + ref&.match?(/\A\h{40}\z/) + end + + def git_null(*command, dir: nil) + check_allowed(command) + + capture(command, dir, ignore_err: true) + end + + def git_retry(*command, dir: nil) + command_with_no_credentials = check_allowed(command) + + Bundler::Retry.new("`#{command_with_no_credentials}` at #{dir || SharedHelpers.pwd}").attempts do + git(*command, dir: dir) + end + end + + def git(*command, dir: nil) + run_command(*command, dir: dir) do |unredacted_command| + check_allowed(unredacted_command) + end + end + + def git_local(*command, dir: nil) + run_command(*command, dir: dir) do |unredacted_command| + redact_and_check_presence(unredacted_command) + end end def has_revision_cached? - return unless @revision - in_path { git("cat-file -e #{@revision}") } + return unless commit && path.exist? + git("cat-file", "-e", commit, dir: path) true rescue GitError false end - def remove_cache - FileUtils.rm_rf(path) + def find_local_revision + return head_revision if explicit_ref.nil? + + find_revision_for(explicit_ref) end - def find_local_revision - allowed_in_path do - git("rev-parse --verify #{Shellwords.shellescape(ref)}", true).strip - end + def head_revision + verify("HEAD") end - # Escape the URI for git commands - def uri_escaped_with_configured_credentials - remote = configured_uri_for(uri) - if Bundler::WINDOWS - # Windows quoting requires double quotes only, with double quotes - # inside the string escaped by being doubled. - '"' + remote.gsub('"') { '""' } + '"' - else - # Bash requires single quoted strings, with the single quotes escaped - # by ending the string, escaping the quote, and restarting the string. - "'" + remote.gsub("'") { "'\\''" } + "'" - end + def find_revision_for(reference) + verify(reference) + rescue GitCommandError => e + raise MissingGitRevisionError.new(e.command, path, reference, credential_filtered_uri) + end + + def verify(reference) + git("rev-parse", "--verify", reference, dir: path).strip end - # Adds credentials to the URI as Fetcher#configured_uri_for does - def configured_uri_for(uri) - if /https?:/ =~ uri - remote = URI(uri) + # Adds credentials to the URI + def configured_uri + if /https?:/.match?(uri) + remote = Gem::URI(uri) config_auth = Bundler.settings[remote.to_s] || Bundler.settings[remote.host] remote.userinfo ||= config_auth remote.to_s else - uri + uri.to_s end end + # Removes credentials from the URI + def credential_filtered_uri + URICredentialsFilter.credential_filtered_uri(uri) + end + def allow? - @git ? @git.allow_git_ops? : true + allowed = @git ? @git.allow_git_ops? : true + + raise GitNotInstalledError.new if allowed && !Bundler.git_present? + + allowed end - def in_path(&blk) + def with_path(&blk) checkout unless path.exist? - _ = URICredentialsFilter # load it before we chdir - SharedHelpers.chdir(path, &blk) + blk.call end - def allowed_in_path - return in_path { yield } if allow? + def allowed_with_path + return with_path { yield } if allow? raise GitError, "The git source #{uri} is not yet checked out. Please run `bundle install` before trying to start your application" end - # TODO: Replace this with Open3 when upgrading to bundler 2 - # Similar to #git_null, as Open3 is not cross-platform, - # a temporary way is to use Tempfile to capture the stderr. - # When replacing this using Open3, make sure git_null is - # also replaced by Open3, so stdout and stderr all got handled properly. - def capture_and_filter_stderr(uri) - return_value, captured_err = "" - backup_stderr = STDERR.dup - begin - Tempfile.open("captured_stderr") do |f| - STDERR.reopen(f) - return_value = yield - f.rewind - captured_err = f.read - end - ensure - STDERR.reopen backup_stderr + def check_allowed(command) + command_with_no_credentials = redact_and_check_presence(command) + raise GitNotAllowedError.new(command_with_no_credentials) unless allow? + command_with_no_credentials + end + + def redact_and_check_presence(command) + raise GitNotInstalledError.new unless Bundler.git_present? + + require "shellwords" + URICredentialsFilter.credential_filtered_string("git #{command.shelljoin}", uri) + end + + def run_command(*command, dir: nil) + command_with_no_credentials = yield(command) + + out, err, status = capture(command, dir) + + raise GitCommandError.new(command_with_no_credentials, dir || SharedHelpers.pwd, err) unless status.success? + + Bundler.ui.warn err unless err.empty? + + out + end + + def capture(cmd, dir, ignore_err: false) + SharedHelpers.with_clean_git_env do + require "open3" + out, err, status = Open3.capture3(*capture3_args_for(cmd, dir)) + + filtered_out = URICredentialsFilter.credential_filtered_string(out, uri) + return [filtered_out, status] if ignore_err + + filtered_err = URICredentialsFilter.credential_filtered_string(err, uri) + [filtered_out, filtered_err, status] end - $stderr.puts URICredentialsFilter.credential_filtered_string(captured_err, uri) if uri && !captured_err.empty? - return_value + end + + def capture3_args_for(cmd, dir) + # Disable automatic maintenance so a background commit-graph write in + # the source repo can't race the hardlinking local clone and fail with + # "hardlink different from source". + opts = ["-c", "gc.auto=0", "-c", "maintenance.auto=false"] + + return ["git", *opts, *cmd] unless dir + + ["git", "-C", dir.to_s, *opts, *cmd] + end + + def extra_clone_args + args = depth_args + return [] if args.empty? + + args += ["--single-branch"] + args.unshift("--no-tags") if supports_cloning_with_no_tags? + + # If there's a locked revision, no need to clone any specific branch + # or tag, since we will end up checking out that locked revision + # anyways. + return args if @revision + + args += ["--branch", branch_option] if branch_option + args + end + + def fetch_command(args) + ["fetch", "--force", "--quiet", "--no-tags", *args, "--", configured_uri, refspec].compact + end + + def clone_command(args) + ["clone", "--bare", "--no-hardlinks", "--quiet", *args, "--", configured_uri, path.to_s] + end + + def depth_args + return [] unless shallow? + + ["--depth", depth.to_s] + end + + def extra_fetch_args(ref) + extra_args = [path.to_s, *depth_args] + extra_args.push(ref) + extra_args + end + + def branch_option + branch || tag + end + + def shallow? + !depth.nil? + end + + def needs_allow_any_sha1_in_want? + @needs_allow_any_sha1_in_want ||= Gem::Version.new(version) <= Gem::Version.new("2.13.7") + end + + def supports_fetching_unreachable_refs? + @supports_fetching_unreachable_refs ||= Gem::Version.new(version) >= Gem::Version.new("2.5.0") + end + + def supports_cloning_with_no_tags? + @supports_cloning_with_no_tags ||= Gem::Version.new(version) >= Gem::Version.new("2.14.0-rc0") end end end diff --git a/lib/bundler/source/metadata.rb b/lib/bundler/source/metadata.rb index 93909002c7..ecf8895187 100644 --- a/lib/bundler/source/metadata.rb +++ b/lib/bundler/source/metadata.rb @@ -5,34 +5,34 @@ module Bundler class Metadata < Source def specs @specs ||= Index.build do |idx| - idx << Gem::Specification.new("ruby\0", RubyVersion.system.to_gem_version_with_patchlevel) - idx << Gem::Specification.new("rubygems\0", Gem::VERSION) - - idx << Gem::Specification.new do |s| - s.name = "bundler" - s.version = VERSION - s.platform = Gem::Platform::RUBY - s.source = self - s.authors = ["bundler team"] - s.bindir = "exe" - s.executables = %w[bundle] - # can't point to the actual gemspec or else the require paths will be wrong - s.loaded_from = File.expand_path("..", __FILE__) + idx << Gem::Specification.new("Ruby\0", Bundler::RubyVersion.system.gem_version) + idx << Gem::Specification.new("RubyGems\0", Gem::VERSION) do |s| + s.required_rubygems_version = Gem::Requirement.default end - if loaded_spec = nil && Bundler.rubygems.loaded_specs("bundler") - idx << loaded_spec # this has to come after the fake gemspec, to override it - elsif local_spec = Bundler.rubygems.find_name("bundler").find {|s| s.version.to_s == VERSION } + + if local_spec = Gem.loaded_specs["bundler"] + raise CorruptBundlerInstallError.new(local_spec) if local_spec.version.to_s != Bundler::VERSION + idx << local_spec + else + idx << Gem::Specification.new do |s| + s.name = "bundler" + s.version = VERSION + s.license = "MIT" + s.platform = Gem::Platform::RUBY + s.authors = ["bundler team"] + s.bindir = "exe" + s.homepage = "https://bundler.io" + s.summary = "The best way to manage your application's dependencies" + s.executables = %w[bundle bundler] + s.loaded_from = SharedHelpers.gemspec_path + end end idx.each {|s| s.source = self } end end - def cached!; end - - def remote!; end - def options {} end @@ -58,6 +58,10 @@ module Bundler def version_message(spec) "#{spec.name} #{spec.version}" end + + def checksum_store + @checksum_store ||= Checksum::Store.new + end end end end diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb index ed734bf549..366a23aea7 100644 --- a/lib/bundler/source/path.rb +++ b/lib/bundler/source/path.rb @@ -3,7 +3,7 @@ module Bundler class Source class Path < Source - autoload :Installer, "bundler/source/path/installer" + autoload :Installer, File.expand_path("path/installer", __dir__) attr_reader :path, :options, :root_path, :original_path attr_writer :name @@ -11,20 +11,23 @@ module Bundler protected :original_path - DEFAULT_GLOB = "{,*,*/*}.gemspec".freeze + DEFAULT_GLOB = "{,*,*/*}.gemspec" def initialize(options) + @checksum_store = Checksum::Store.new @options = options.dup @glob = options["glob"] || DEFAULT_GLOB - @allow_cached = false - @allow_remote = false - - @root_path = options["root_path"] || Bundler.root + @root_path = options["root_path"] || root if options["path"] @path = Pathname.new(options["path"]) - @path = expand(@path) unless @path.relative? + expanded_path = expand(@path) + @path = if @path.relative? + expanded_path.relative_path_from(File.expand_path(root_path)) + else + expanded_path + end end @name = options["name"] @@ -35,16 +38,6 @@ module Bundler @original_path = @path end - def remote! - @local_specs = nil - @allow_remote = true - end - - def cached! - @local_specs = nil - @allow_cached = true - end - def self.from_lock(options) new(options.merge("path" => options.delete("remote"))) end @@ -60,13 +53,17 @@ module Bundler "source at `#{@path}`" end + alias_method :identifier, :to_s + + alias_method :to_gemfile, :path + def hash [self.class, expanded_path, version].hash end def eql?(other) - return unless other.class == self.class - expanded_original_path == other.expanded_original_path && + [Gemspec, Path].include?(other.class) && + expanded_original_path == other.expanded_original_path && version == other.version end @@ -77,14 +74,16 @@ module Bundler end def install(spec, options = {}) - print_using_message "Using #{version_message(spec)} from #{self}" - generate_bin(spec, :disable_extensions => true) + using_message = "Using #{version_message(spec, options[:previous_spec])} from #{self}" + using_message += " and installing its executables" unless spec.executables.empty? + print_using_message using_message + generate_bin(spec, disable_extensions: true) nil # no post-install message end def cache(spec, custom_path = nil) app_cache_path = app_cache_path(custom_path) - return unless Bundler.feature_flag.cache_all? + return unless Bundler.settings[:cache_all] return if expand(@original_path).to_s.index(root_path.to_s + "/") == 0 unless @original_path.exist? @@ -120,7 +119,7 @@ module Bundler @expanded_original_path ||= expand(original_path) end - private + private def expanded_path @expanded_path ||= expand(path) @@ -136,7 +135,7 @@ module Bundler def lockfile_path return relative_path(original_path) if original_path.absolute? - expand(original_path).relative_path_from(Bundler.root) + expand(original_path).relative_path_from(root) end def app_cache_path(custom_path = nil) @@ -149,7 +148,7 @@ module Bundler def load_gemspec(file) return unless spec = Bundler.load_gemspec(file) - Bundler.rubygems.set_installed_by_version(spec) + spec.installed_by_version = Gem::VERSION spec end @@ -162,10 +161,17 @@ module Bundler if File.directory?(expanded_path) # We sort depth-first since `<<` will override the earlier-found specs - Dir["#{expanded_path}/#{@glob}"].sort_by {|p| -p.split(File::SEPARATOR).size }.each do |file| + Gem::Util.glob_files_in_dir(@glob, expanded_path).sort_by {|p| -p.split(File::SEPARATOR).size }.each do |file| next unless spec = load_gemspec(file) spec.source = self + # The ignore attribute is for ignoring installed gems that don't + # have extensions correctly compiled for activation. In the case of + # path sources, there's a single version of each gem in the path + # source available to Bundler, so we always certainly want to + # consider that for activation and never makes sense to ignore it. + spec.ignored = false + # Validation causes extension_dir to be calculated, which depends # on #source, so we validate here instead of load_gemspec validate_spec(spec) @@ -191,10 +197,10 @@ module Bundler else message = String.new("The path `#{expanded_path}` ") message << if File.exist?(expanded_path) - "is not a directory." - else - "does not exist." - end + "is not a directory." + else + "does not exist." + end raise PathError, message end @@ -213,22 +219,23 @@ module Bundler # Some gem authors put absolute paths in their gemspec # and we have to save them from themselves - spec.files = spec.files.map do |p| - next p unless p =~ /\A#{Pathname::SEPARATOR_PAT}/ - next if File.directory?(p) + spec.files = spec.files.filter_map do |path| + pathname = Pathname.new(path) + next path unless pathname.absolute? + next if File.directory?(path) begin - Pathname.new(p).relative_path_from(gem_dir).to_s + pathname.relative_path_from(gem_dir).to_s rescue ArgumentError - p + path end - end.compact + end installer = Path::Installer.new( spec, - :env_shebang => false, - :disable_extensions => options[:disable_extensions], - :build_args => options[:build_args], - :bundler_extension_cache_path => extension_cache_path(spec) + env_shebang: false, + disable_extensions: options[:disable_extensions], + build_args: options[:build_args], + bundler_extension_cache_path: extension_cache_path(spec) ) installer.post_install rescue Gem::InvalidSpecificationException => e diff --git a/lib/bundler/source/path/installer.rb b/lib/bundler/source/path/installer.rb index a0357ffa39..39765e5da2 100644 --- a/lib/bundler/source/path/installer.rb +++ b/lib/bundler/source/path/installer.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative "../../rubygems_gem_installer" + module Bundler class Source class Path @@ -16,47 +18,24 @@ module Bundler @build_args = options[:build_args] || Bundler.rubygems.build_args @gem_bin_dir = "#{Bundler.rubygems.gem_dir}/bin" @disable_extensions = options[:disable_extensions] - - if Bundler.requires_sudo? - @tmp_dir = Bundler.tmp(spec.full_name).to_s - @bin_dir = "#{@tmp_dir}/bin" - else - @bin_dir = @gem_bin_dir - end + @bin_dir = @gem_bin_dir end def post_install - SharedHelpers.chdir(@gem_dir) do - run_hooks(:pre_install) - - unless @disable_extensions - build_extensions - run_hooks(:post_build) - end - - generate_bin unless spec.executables.nil? || spec.executables.empty? + run_hooks(:pre_install) - run_hooks(:post_install) + unless @disable_extensions || Bundler.settings[:no_build_extension] + build_extensions + run_hooks(:post_build) end - ensure - Bundler.rm_rf(@tmp_dir) if Bundler.requires_sudo? - end - - private - def generate_bin - super + generate_bin unless spec.executables.empty? - if Bundler.requires_sudo? - SharedHelpers.filesystem_access(@gem_bin_dir) do |p| - Bundler.mkdir_p(p) - end - spec.executables.each do |exe| - Bundler.sudo "cp -R #{@bin_dir}/#{exe} #{@gem_bin_dir}" - end - end + run_hooks(:post_install) end + private + def run_hooks(type) hooks_meth = "#{type}_hooks" return unless Gem.respond_to?(hooks_meth) diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index 6f4157364f..ed864604fe 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -1,37 +1,75 @@ # frozen_string_literal: true -require "uri" require "rubygems/user_interaction" module Bundler class Source class Rubygems < Source - autoload :Remote, "bundler/source/rubygems/remote" + autoload :Remote, File.expand_path("rubygems/remote", __dir__) - # Use the API when installing less than X gems - API_REQUEST_LIMIT = 500 # Ask for X gems per API request - API_REQUEST_SIZE = 50 + API_REQUEST_SIZE = 100 + REQUIRE_MUTEX = Mutex.new - attr_reader :remotes, :caches + attr_accessor :remotes def initialize(options = {}) @options = options @remotes = [] + @remote_cooldowns = {} @dependency_names = [] @allow_remote = false @allow_cached = false - @caches = [cache_path, *Bundler.rubygems.gem_cache] + @allow_local = options["allow_local"] || false + @prefer_local = false + @checksum_store = Checksum::Store.new + @gem_installers = {} + @gem_installers_mutex = Mutex.new - Array(options["remotes"] || []).reverse_each {|r| add_remote(r) } + cooldown = options["cooldown"] + Array(options["remotes"]).reverse_each {|r| add_remote(r, cooldown: cooldown) } + + @lockfile_remotes = @remotes if options["from_lockfile"] + end + + def caches + @caches ||= [cache_path, *Bundler.rubygems.gem_cache] + end + + def prefer_local! + @prefer_local = true + end + + def local_only! + @specs = nil + @allow_local = true + @allow_cached = false + @allow_remote = false + end + + def local_only? + @allow_local && !@allow_remote + end + + def local! + return if @allow_local + + @specs = nil + @allow_local = true end def remote! + return if @allow_remote + @specs = nil @allow_remote = true end def cached! + return unless File.exist?(cache_path) + + return if @allow_cached + @specs = nil @allow_cached = true end @@ -50,9 +88,17 @@ module Bundler o.is_a?(Rubygems) && (o.credless_remotes - credless_remotes).empty? end + def multiple_remotes? + @remotes.size > 1 + end + + def no_remotes? + @remotes.size == 0 + end + def can_lock?(spec) - return super if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? - spec.source.is_a?(Rubygems) + return super unless multiple_remotes? + include?(spec.source) end def options @@ -60,13 +106,14 @@ module Bundler end def self.from_lock(options) - new(options) + options["remotes"] = Array(options.delete("remote")).reverse + new(options.merge("from_lockfile" => true)) end def to_lock out = String.new("GEM\n") - remotes.reverse_each do |remote| - out << " remote: #{suppress_configured_credentials remote}\n" + lockfile_remotes.reverse_each do |remote| + out << " remote: #{remote}\n" end out << " specs:\n" end @@ -74,126 +121,117 @@ module Bundler def to_s if remotes.empty? "locally installed gems" - else - remote_names = remotes.map(&:to_s).join(", ") + elsif @allow_remote && @allow_cached && @allow_local + "rubygems repository #{remote_names}, cached gems or installed locally" + elsif @allow_remote && @allow_local "rubygems repository #{remote_names} or installed locally" + elsif @allow_remote + "rubygems repository #{remote_names}" + elsif @allow_cached && @allow_local + "cached gems or installed locally" + else + "locally installed gems" + end + end + + def identifier + if remotes.empty? + "locally installed gems" + else + "rubygems repository #{remote_names}" end end - alias_method :name, :to_s + alias_method :name, :identifier + alias_method :to_gemfile, :identifier def specs @specs ||= begin # remote_specs usually generates a way larger Index than the other - # sources, and large_idx.use small_idx is way faster than - # small_idx.use large_idx. - idx = @allow_remote ? remote_specs.dup : Index.new - idx.use(cached_specs, :override_dupes) if @allow_cached || @allow_remote - idx.use(installed_specs, :override_dupes) - idx - end - end + # sources, and large_idx.merge! small_idx is way faster than + # small_idx.merge! large_idx. + index = @allow_remote ? remote_specs.dup : Index.new + + # Snapshot per-version `created_at` from the remote info before installed + # / cached specs overwrite the EndpointSpecification objects that carry + # it. The cooldown filter consults `created_at` on every candidate, so + # local stubs need the published date back-filled to participate. + remote_created_at = collect_remote_created_at(index) + + index.merge!(cached_specs) if @allow_cached + index.merge!(installed_specs) if @allow_local + + if @allow_local + if @prefer_local + index.merge!(default_specs) + else + # complete with default specs, only if not already available in the + # index through remote, cached, or installed specs + index.use(default_specs) + end + end - def install(spec, opts = {}) - force = opts[:force] - ensure_builtin_gems_cached = opts[:ensure_builtin_gems_cached] + backfill_created_at(index, remote_created_at) unless remote_created_at.empty? - if ensure_builtin_gems_cached && builtin_gem?(spec) - if !cached_path(spec) - cached_built_in_gem(spec) unless spec.remote - force = true - else - spec.loaded_from = loaded_from(spec) - end + index end + end - if installed?(spec) && !force - print_using_message "Using #{version_message(spec)}" - return nil # no post-install message + def download(spec, options = {}) + if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force]) + return true end - # Download the gem to get the spec, because some specs that are returned - # by rubygems.org are broken and wrong. + installer = rubygems_gem_installer(spec, options) + if spec.remote - # Check for this spec from other sources - uris = [spec.remote.anonymized_uri] - uris += remotes_for_spec(spec).map(&:anonymized_uri) - uris.uniq! - Installer.ambiguous_gems << [spec.name, *uris] if uris.length > 1 + s = begin + installer.spec + rescue Gem::Package::FormatError + Bundler.rm_rf(installer.gem) + raise + rescue Gem::Security::Exception => e + raise SecurityError, + "The gem #{installer.gem} can't be installed because " \ + "the security policy didn't allow it, with the message: #{e.message}" + end - s = Bundler.rubygems.spec_from_gem(fetch_gem(spec), Bundler.settings["trust-policy"]) spec.__swap__(s) end - unless Bundler.settings[:no_install] - message = "Installing #{version_message(spec)}" - message += " with native extensions" if spec.extensions.any? - Bundler.ui.confirm message + spec + end - path = cached_gem(spec) - if requires_sudo? - install_path = Bundler.tmp(spec.full_name) - bin_path = install_path.join("bin") - else - install_path = rubygems_dir - bin_path = Bundler.system_bindir - end + def install(spec, options = {}) + if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force]) + print_using_message "Using #{version_message(spec, options[:previous_spec])}" + return nil # no post-install message + end - installed_spec = nil - Bundler.rubygems.preserve_paths do - installed_spec = Bundler::RubyGemsGemInstaller.at( - path, - :install_dir => install_path.to_s, - :bin_dir => bin_path.to_s, - :ignore_dependencies => true, - :wrappers => true, - :env_shebang => true, - :build_args => opts[:build_args], - :bundler_expected_checksum => spec.respond_to?(:checksum) && spec.checksum, - :bundler_extension_cache_path => extension_cache_path(spec) - ).install - end - spec.full_gem_path = installed_spec.full_gem_path - - # SUDO HAX - if requires_sudo? - Bundler.rubygems.repository_subdirectories.each do |name| - src = File.join(install_path, name, "*") - dst = File.join(rubygems_dir, name) - if name == "extensions" && Dir.glob(src).any? - src = File.join(src, "*/*") - ext_src = Dir.glob(src).first - ext_src.gsub!(src[0..-6], "") - dst = File.dirname(File.join(dst, ext_src)) - end - SharedHelpers.filesystem_access(dst) do |p| - Bundler.mkdir_p(p) - end - Bundler.sudo "cp -R #{src} #{dst}" if Dir[src].any? - end + return if Bundler.settings[:no_install] - spec.executables.each do |exe| - SharedHelpers.filesystem_access(Bundler.system_bindir) do |p| - Bundler.mkdir_p(p) - end - Bundler.sudo "cp -R #{install_path}/bin/#{exe} #{Bundler.system_bindir}/" - end - end - installed_spec.loaded_from = loaded_from(spec) + installer = rubygems_gem_installer(spec, options) + spec.source.checksum_store.register(spec, installer.gem_checksum) + + message = "Installing #{version_message(spec, options[:previous_spec])}" + message += " with native extensions" if spec.extensions.any? + Bundler.ui.confirm message + + installed_spec = nil + + Gem.time("Installed #{spec.name} in", 0, true) do + installed_spec = installer.install end - spec.loaded_from = loaded_from(spec) + + spec.full_gem_path = installed_spec.full_gem_path + spec.loaded_from = installed_spec.loaded_from + spec.base_dir = installed_spec.base_dir spec.post_install_message - ensure - Bundler.rm_rf(install_path) if requires_sudo? end def cache(spec, custom_path = nil) - if builtin_gem?(spec) - cached_path = cached_built_in_gem(spec) - else - cached_path = cached_gem(spec) - end - raise GemNotFound, "Missing gem file '#{spec.full_name}.gem'." unless cached_path + cached_path = Bundler.settings[:cache_all_platforms] ? fetch_gem_if_possible(spec) : cached_gem(spec) + raise GemNotFound, "Missing gem file '#{spec.file_name}'." unless cached_path return if File.dirname(cached_path) == Bundler.app_cache.to_s Bundler.ui.info " * #{File.basename(cached_path)}" FileUtils.cp(cached_path, Bundler.app_cache(custom_path)) @@ -202,12 +240,13 @@ module Bundler raise InstallError, e.message end - def cached_built_in_gem(spec) - cached_path = cached_path(spec) - if cached_path.nil? + def cached_built_in_gem(spec, local: false) + cached_path = cached_gem(spec) + if cached_path.nil? && !local remote_spec = remote_specs.search(spec).first if remote_spec cached_path = fetch_gem(remote_spec) + spec.remote = remote_spec.remote else Bundler.ui.warn "#{spec.full_name} is built in to Ruby, and can't be cached because your Gemfile doesn't have any sources that contain it." end @@ -215,48 +254,45 @@ module Bundler cached_path end - def add_remote(source) + def add_remote(source, cooldown: nil) uri = normalize_uri(source) @remotes.unshift(uri) unless @remotes.include?(uri) + @remote_cooldowns[uri] = cooldown if cooldown end - def equivalent_remotes?(other_remotes) - other_remotes.map(&method(:remove_auth)) == @remotes.map(&method(:remove_auth)) + def cooldown_for(uri) + @remote_cooldowns[uri] end - def replace_remotes(other_remotes, allow_equivalent = false) - return false if other_remotes == @remotes - - equivalent = allow_equivalent && equivalent_remotes?(other_remotes) - - @remotes = [] - other_remotes.reverse_each do |r| - add_remote r.to_s + def spec_names + if dependency_api_available? + remote_specs.spec_names + else + [] end - - !equivalent end def unmet_deps - if @allow_remote && api_fetchers.any? + if dependency_api_available? remote_specs.unmet_dependency_names else [] end end - def fetchers - @fetchers ||= remotes.map do |uri| - remote = Source::Rubygems::Remote.new(uri) - Bundler::Fetcher.new(remote) - end + def remote_fetchers + @remote_fetchers ||= remotes.to_h do |uri| + remote = Source::Rubygems::Remote.new(uri, cooldown: cooldown_for(uri)) + [remote, Bundler::Fetcher.new(remote)] + end.freeze end - def double_check_for(unmet_dependency_names, override_dupes = false, index = specs) - return unless @allow_remote - raise ArgumentError, "missing index" unless index + def fetchers + @fetchers ||= remote_fetchers.values.freeze + end - return unless api_fetchers.any? + def double_check_for(unmet_dependency_names) + return unless dependency_api_available? unmet_dependency_names = unmet_dependency_names.call unless unmet_dependency_names.nil? @@ -270,7 +306,9 @@ module Bundler Bundler.ui.debug "Double checking for #{unmet_dependency_names || "all specs (due to the size of the request)"} in #{self}" - fetch_names(api_fetchers, unmet_dependency_names, index, override_dupes) + fetch_names(api_fetchers, unmet_dependency_names, remote_specs) + + specs.use remote_specs end def dependency_names_to_double_check @@ -278,78 +316,80 @@ module Bundler remote_specs.each do |spec| case spec when EndpointSpecification, Gem::Specification, StubSpecification, LazySpecification - names.concat(spec.runtime_dependencies) + names.concat(spec.runtime_dependencies.map(&:name)) when RemoteSpecification # from the full index return nil else raise "unhandled spec type (#{spec.inspect})" end end - names.map!(&:name) if names names end - protected + def dependency_api_available? + @allow_remote && api_fetchers.any? + end - def credless_remotes - remotes.map(&method(:suppress_configured_credentials)) + def clear_cache + @specs = nil + @installed_specs = nil + @default_specs = nil + @cached_specs = nil end - def remotes_for_spec(spec) - specs.search_all(spec.name).inject([]) do |uris, s| - uris << s.remote if s.remote - uris - end + protected + + def remote_names + remotes.map(&:to_s).join(", ") end - def loaded_from(spec) - "#{rubygems_dir}/specifications/#{spec.full_name}.gemspec" + def credless_remotes + remotes.map(&method(:remove_auth)) end def cached_gem(spec) - cached_gem = cached_path(spec) - unless cached_gem - raise Bundler::GemNotFound, "Could not find #{spec.file_name} for installation" - end - cached_gem - end + global_cache_path = download_cache_path(spec) + caches << global_cache_path if global_cache_path - def cached_path(spec) - possibilities = @caches.map {|p| "#{p}/#{spec.file_name}" } + possibilities = caches.map {|p| package_path(p, spec) } possibilities.find {|p| File.exist?(p) } end + def package_path(cache_path, spec) + "#{cache_path}/#{spec.file_name}" + end + def normalize_uri(uri) - uri = uri.to_s - uri = "#{uri}/" unless uri =~ %r{/$} - uri = URI(uri) + uri = URINormalizer.normalize_suffix(uri.to_s) + require_relative "../vendored_uri" + uri = Gem::URI(uri) raise ArgumentError, "The source must be an absolute URI. For example:\n" \ - "source 'https://rubygems.org'" if !uri.absolute? || (uri.is_a?(URI::HTTP) && uri.host.nil?) + "source 'https://rubygems.org'" if !uri.absolute? || (uri.is_a?(Gem::URI::HTTP) && uri.host.nil?) uri end - def suppress_configured_credentials(remote) - remote_nouser = remove_auth(remote) - if remote.userinfo && remote.userinfo == Bundler.settings[remote_nouser] - remote_nouser + def remove_auth(remote) + if remote.user || remote.password + remote.dup.tap {|uri| uri.user = uri.password = nil }.to_s else - remote + remote.to_s end end - def remove_auth(remote) - remote.dup.tap {|uri| uri.user = uri.password = nil }.to_s - end - def installed_specs @installed_specs ||= Index.build do |idx| - Bundler.rubygems.all_specs.reverse_each do |spec| - next if spec.name == "bundler" + Bundler.rubygems.installed_specs.reverse_each do |spec| + spec.source = self + next if spec.ignored? + idx << spec + end + end + end + + def default_specs + @default_specs ||= Index.build do |idx| + Bundler.rubygems.default_specs.each do |spec| spec.source = self - if Bundler.rubygems.spec_missing_extensions?(spec, false) - Bundler.ui.debug "Source #{self} is ignoring #{spec} because it is missing extensions" - next - end idx << spec end end @@ -357,16 +397,11 @@ module Bundler def cached_specs @cached_specs ||= begin - idx = installed_specs.dup + idx = Index.new Dir["#{cache_path}/*.gem"].each do |gemfile| - next if gemfile =~ /^bundler\-[\d\.]+?\.gem/ s ||= Bundler.rubygems.spec_from_gem(gemfile) s.source = self - if Bundler.rubygems.spec_missing_extensions?(s, false) - Bundler.ui.debug "Source #{self} is ignoring #{s} because it is missing extensions" - next - end idx << s end @@ -375,90 +410,103 @@ module Bundler end def api_fetchers - fetchers.select {|f| f.use_api && f.fetchers.first.api_fetcher? } + fetchers.select(&:api_fetcher?) end def remote_specs @remote_specs ||= Index.build do |idx| index_fetchers = fetchers - api_fetchers - # gather lists from non-api sites - fetch_names(index_fetchers, nil, idx, false) - - # because ensuring we have all the gems we need involves downloading - # the gemspecs of those gems, if the non-api sites contain more than - # about 500 gems, we treat all sites as non-api for speed. - allow_api = idx.size < API_REQUEST_LIMIT && dependency_names.size < API_REQUEST_LIMIT - Bundler.ui.debug "Need to query more than #{API_REQUEST_LIMIT} gems." \ - " Downloading full index instead..." unless allow_api - - fetch_names(api_fetchers, allow_api && dependency_names, idx, false) + if index_fetchers.empty? + fetch_names(api_fetchers, dependency_names, idx) + else + fetch_names(fetchers, nil, idx) + end end end - def fetch_names(fetchers, dependency_names, index, override_dupes) + def fetch_names(fetchers, dependency_names, index) fetchers.each do |f| if dependency_names - Bundler.ui.info "Fetching gem metadata from #{f.uri}", Bundler.ui.debug? - index.use f.specs_with_retry(dependency_names, self), override_dupes + Bundler.ui.info "Fetching gem metadata from #{URICredentialsFilter.credential_filtered_uri(f.uri)}", Bundler.ui.debug? + index.use f.specs_with_retry(dependency_names, self) Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over else - Bundler.ui.info "Fetching source index from #{f.uri}" - index.use f.specs_with_retry(nil, self), override_dupes + Bundler.ui.info "Fetching source index from #{URICredentialsFilter.credential_filtered_uri(f.uri)}" + index.use f.specs_with_retry(nil, self) end end end - def fetch_gem(spec) - return false unless spec.remote + def fetch_gem_if_possible(spec, previous_spec = nil) + if spec.remote + fetch_gem(spec, previous_spec) + else + cached_gem(spec) + end + end + def fetch_gem(spec, previous_spec = nil) spec.fetch_platform - download_path = requires_sudo? ? Bundler.tmp(spec.full_name) : rubygems_dir - gem_path = "#{rubygems_dir}/cache/#{spec.full_name}.gem" + cache_path = download_cache_path(spec) || default_cache_path_for(rubygems_dir) + gem_path = package_path(cache_path, spec) + return gem_path if File.exist?(gem_path) - SharedHelpers.filesystem_access("#{download_path}/cache") do |p| + SharedHelpers.filesystem_access(cache_path) do |p| FileUtils.mkdir_p(p) end - download_gem(spec, download_path) - - if requires_sudo? - SharedHelpers.filesystem_access("#{rubygems_dir}/cache") do |p| - Bundler.mkdir_p(p) - end - Bundler.sudo "mv #{download_path}/cache/#{spec.full_name}.gem #{gem_path}" - end + download_gem(spec, cache_path, previous_spec) gem_path - ensure - Bundler.rm_rf(download_path) if requires_sudo? - end - - def builtin_gem?(spec) - # Ruby 2.1, where all included gems have this summary - return true if spec.summary =~ /is bundled with Ruby/ - - # Ruby 2.0, where gemspecs are stored in specifications/default/ - spec.loaded_from && spec.loaded_from.include?("specifications/default/") end def installed?(spec) - installed_specs[spec].any? + installed_specs[spec].any? && !spec.installation_missing? end - def requires_sudo? - Bundler.requires_sudo? + def rubygems_dir + Bundler.bundle_path end - def rubygems_dir - Bundler.rubygems.gem_dir + def default_cache_path_for(dir) + "#{dir}/cache" end def cache_path Bundler.app_cache end - private + private + + def collect_remote_created_at(index) + return {} unless @allow_remote + + snapshot = {} + index.each do |spec| + next unless spec.respond_to?(:created_at) && spec.created_at + # Remember the remote that supplied the date too: when a source has + # several remotes with different per-URI cooldown settings we must + # restore the same one during backfill so `effective_cooldown` agrees. + snapshot[[spec.name, spec.version]] = [spec.created_at, spec.remote] + end + snapshot + end + + def backfill_created_at(index, snapshot) + index.each do |spec| + next unless spec.respond_to?(:created_at=) + next if spec.created_at + remote_created_at, remote = snapshot[[spec.name, spec.version]] + next unless remote_created_at + spec.created_at = remote_created_at + spec.remote ||= remote if remote && spec.respond_to?(:remote=) + end + end + + def lockfile_remotes + @lockfile_remotes || credless_remotes + end # Checks if the requested spec exists in the global cache. If it does, # we copy it to the download path, and if it does not, we download it. @@ -466,41 +514,24 @@ module Bundler # @param [Specification] spec # the spec we want to download or retrieve from the cache. # - # @param [String] download_path + # @param [String] download_cache_path # the local directory the .gem will end up in. # - def download_gem(spec, download_path) - local_path = File.join(download_path, "cache/#{spec.full_name}.gem") - - if (cache_path = download_cache_path(spec)) && cache_path.file? - SharedHelpers.filesystem_access(local_path) do - FileUtils.cp(cache_path, local_path) - end - else - uri = spec.remote.uri - Bundler.ui.confirm("Fetching #{version_message(spec)}") - Bundler.rubygems.download_gem(spec, uri, download_path) - cache_globally(spec, local_path) - end - end - - # Checks if the requested spec exists in the global cache. If it does - # not, we create the relevant global cache subdirectory if it does not - # exist and copy the spec from the local cache to the global cache. - # - # @param [Specification] spec - # the spec we want to copy to the global cache. + # @param [Specification] previous_spec + # the spec previously locked # - # @param [String] local_cache_path - # the local directory from which we want to copy the .gem. - # - def cache_globally(spec, local_cache_path) - return unless cache_path = download_cache_path(spec) - return if cache_path.exist? - - SharedHelpers.filesystem_access(cache_path.dirname, &:mkpath) - SharedHelpers.filesystem_access(cache_path) do - FileUtils.cp(local_cache_path, cache_path) + def download_gem(spec, download_cache_path, previous_spec = nil) + uri = spec.remote.uri + Bundler.ui.confirm("Fetching #{version_message(spec, previous_spec)}") + gem_remote_fetcher = remote_fetchers.fetch(spec.remote).gem_remote_fetcher + + Plugin.hook(Plugin::Events::GEM_BEFORE_FETCH, spec) + begin + Gem.time("Downloaded #{spec.name} in", 0, true) do + Bundler.rubygems.download_gem(spec, uri, download_cache_path, gem_remote_fetcher) + end + ensure + Plugin.hook(Plugin::Events::GEM_AFTER_FETCH, spec) end end @@ -516,17 +547,52 @@ module Bundler # @return [Pathname] The global cache path. # def download_cache_path(spec) - return unless Bundler.feature_flag.global_gem_cache? + return unless Bundler.settings[:global_gem_cache] return unless remote = spec.remote return unless cache_slug = remote.cache_slug - Bundler.user_cache.join("gems", cache_slug, spec.file_name) + if Gem.respond_to?(:global_gem_cache_path) + Pathname.new(Gem.global_gem_cache_path).join(cache_slug) + else + # Fall back to old location for older RubyGems versions + Bundler.user_cache.join("gems", cache_slug) + end end def extension_cache_slug(spec) return unless remote = spec.remote remote.cache_slug end + + # We are using a mutex to read and write from/to the hash. + # The reason this double synchronization was added is for performance + # and to lock the mutex for the shortest possible amount of time. Otherwise, + # all threads are fighting over this mutex and when it gets acquired it gets locked + # until a thread finishes downloading a gem, leaving the other threads waiting + # doing nothing. + def rubygems_gem_installer(spec, options) + @gem_installers_mutex.synchronize { @gem_installers[spec.name] } || begin + path = fetch_gem_if_possible(spec, options[:previous_spec]) + raise GemNotFound, "Could not find #{spec.file_name} for installation" unless path + + REQUIRE_MUTEX.synchronize { require_relative "../rubygems_gem_installer" } + + installer = Bundler::RubyGemsGemInstaller.at( + path, + security_policy: Bundler.rubygems.security_policies[Bundler.settings["trust-policy"]], + install_dir: rubygems_dir.to_s, + bin_dir: Bundler.system_bindir.to_s, + ignore_dependencies: true, + wrappers: true, + env_shebang: true, + build_args: options[:build_args], + bundler_extension_cache_path: extension_cache_path(spec), + build_extension: Bundler.settings[:no_build_extension] ? false : nil, + install_plugin: Bundler.settings[:no_install_plugin] ? false : nil + ) + @gem_installers_mutex.synchronize { @gem_installers[spec.name] ||= installer } + end + end end end end diff --git a/lib/bundler/source/rubygems/remote.rb b/lib/bundler/source/rubygems/remote.rb index e73baaa992..3d847424b7 100644 --- a/lib/bundler/source/rubygems/remote.rb +++ b/lib/bundler/source/rubygems/remote.rb @@ -4,9 +4,9 @@ module Bundler class Source class Rubygems class Remote - attr_reader :uri, :anonymized_uri, :original_uri + attr_reader :uri, :anonymized_uri, :original_uri, :cooldown - def initialize(uri) + def initialize(uri, cooldown: nil) orig_uri = uri uri = Bundler.settings.mirror_for(uri) @original_uri = orig_uri if orig_uri != uri @@ -14,8 +14,21 @@ module Bundler @uri = apply_auth(uri, fallback_auth).freeze @anonymized_uri = remove_auth(@uri).freeze + @cooldown = cooldown end + # Returns the cooldown days that apply to this remote, resolving the + # precedence CLI > config > Gemfile per-source. Returns nil if no + # cooldown applies. + def effective_cooldown + override = Bundler.settings[:cooldown] + return override if override + @cooldown + end + + MAX_CACHE_SLUG_HOST_SIZE = 255 - 1 - 32 # 255 minus dot minus MD5 length + private_constant :MAX_CACHE_SLUG_HOST_SIZE + # @return [String] A slug suitable for use as a cache key for this # remote. # @@ -25,11 +38,18 @@ module Bundler cache_uri = original_uri || uri - uri_parts = [cache_uri.host, cache_uri.user, cache_uri.port, cache_uri.path] - uri_digest = SharedHelpers.digest(:MD5).hexdigest(uri_parts.compact.join(".")) + host = cache_uri.to_s.start_with?("file://") ? nil : cache_uri.host + + uri_parts = [host, cache_uri.user, cache_uri.port, cache_uri.path] + uri_parts.compact! + uri_digest = SharedHelpers.digest(:MD5).hexdigest(uri_parts.join(".")) + + uri_parts.pop + host_parts = uri_parts.join(".") + return uri_digest if host_parts.empty? - uri_parts[-1] = uri_digest - uri_parts.compact.join(".") + shortened_host_parts = host_parts[0...MAX_CACHE_SLUG_HOST_SIZE] + [shortened_host_parts, uri_digest].join(".") end end @@ -37,7 +57,7 @@ module Bundler "rubygems remote at #{anonymized_uri}" end - private + private def apply_auth(uri, auth) if auth && uri.userinfo.nil? @@ -46,7 +66,7 @@ module Bundler end uri - rescue URI::InvalidComponentError + rescue Gem::URI::InvalidComponentError error_message = "Please CGI escape your usernames and passwords before " \ "setting them for authentication." raise HTTPError.new(error_message) diff --git a/lib/bundler/source/rubygems_aggregate.rb b/lib/bundler/source/rubygems_aggregate.rb new file mode 100644 index 0000000000..8aeaa375fa --- /dev/null +++ b/lib/bundler/source/rubygems_aggregate.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Bundler + class Source + class RubygemsAggregate + attr_reader :source_map, :sources + + def initialize(sources, source_map, excluded_sources = []) + @sources = sources + @source_map = source_map + @excluded_sources = excluded_sources + + @index = build_index + end + + def specs + @index + end + + def identifier + to_s + end + + def to_s + "any of the sources" + end + + private + + def build_index + Index.build do |idx| + dependency_names = source_map.pinned_spec_names + + sources.all_sources.each do |source| + next if @excluded_sources.include?(source) + + source.dependency_names = dependency_names - source_map.pinned_spec_names(source) + idx.add_source source.specs + dependency_names.concat(source.unmet_deps).uniq! + end + + double_check_for_index(idx, dependency_names) + end + end + + # Suppose the gem Foo depends on the gem Bar. Foo exists in Source A. Bar has some versions that exist in both + # sources A and B. At this point, the API request will have found all the versions of Bar in source A, + # but will not have found any versions of Bar from source B, which is a problem if the requested version + # of Foo specifically depends on a version of Bar that is only found in source B. This ensures that for + # each spec we found, we add all possible versions from all sources to the index. + def double_check_for_index(idx, dependency_names) + pinned_names = source_map.pinned_spec_names + + names = :names # do this so we only have to traverse to get dependency_names from the index once + unmet_dependency_names = lambda do + return names unless names == :names + new_names = sources.all_sources.map(&:dependency_names_to_double_check) + return names = nil if new_names.compact! + names = new_names.flatten(1).concat(dependency_names) + names.uniq! + names -= pinned_names + names + end + + sources.all_sources.each do |source| + source.double_check_for(unmet_dependency_names) + end + end + end + end +end diff --git a/lib/bundler/source_list.rb b/lib/bundler/source_list.rb index ac2adacb3d..ab7002d6e5 100644 --- a/lib/bundler/source_list.rb +++ b/lib/bundler/source_list.rb @@ -5,24 +5,40 @@ module Bundler attr_reader :path_sources, :git_sources, :plugin_sources, - :global_rubygems_source, + :global_path_source, :metadata_source + def global_rubygems_source + @global_rubygems_source ||= source_class.new("allow_local" => true) + end + def initialize @path_sources = [] @git_sources = [] @plugin_sources = [] @global_rubygems_source = nil - @rubygems_aggregate = rubygems_aggregate_class.new + @global_path_source = nil @rubygems_sources = [] @metadata_source = Source::Metadata.new + + @local_mode = true + end + + def aggregate_global_source? + global_rubygems_source.multiple_remotes? + end + + def implicit_global_source? + global_rubygems_source.no_remotes? end def add_path_source(options = {}) if options["gemspec"] add_source_to_list Source::Gemspec.new(options), path_sources else - add_source_to_list Source::Path.new(options), path_sources + path_source = add_source_to_list Source::Path.new(options), path_sources + @global_path_source ||= path_source if options["global"] + path_source end end @@ -33,78 +49,81 @@ module Bundler end def add_rubygems_source(options = {}) - add_source_to_list Source::Rubygems.new(options), @rubygems_sources + new_source = Source::Rubygems.new(options) + return @global_rubygems_source if @global_rubygems_source == new_source + + add_source_to_list new_source, @rubygems_sources end def add_plugin_source(source, options = {}) add_source_to_list Plugin.source(source).new(options), @plugin_sources end - def global_rubygems_source=(uri) - if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? - @global_rubygems_source ||= rubygems_aggregate_class.new("remotes" => uri) - end - add_rubygems_remote(uri) + def add_global_rubygems_remote(uri, cooldown: nil) + global_rubygems_source.add_remote(uri, cooldown: cooldown) + global_rubygems_source end - def add_rubygems_remote(uri) - if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? - return if Bundler.feature_flag.disable_multisource? - raise InvalidOption, "`lockfile_uses_separate_rubygems_sources` cannot be set without `disable_multisource` being set" - end - @rubygems_aggregate.add_remote(uri) - @rubygems_aggregate + def local_mode? + @local_mode end def default_source - global_rubygems_source || @rubygems_aggregate + global_path_source || global_rubygems_source end def rubygems_sources - @rubygems_sources + [default_source] + non_global_rubygems_sources + [global_rubygems_source] end - def rubygems_remotes - rubygems_sources.map(&:remotes).flatten.uniq + def non_global_rubygems_sources + @rubygems_sources end def all_sources path_sources + git_sources + plugin_sources + rubygems_sources + [metadata_source] end + def non_default_explicit_sources + all_sources - [default_source, metadata_source] + end + def get(source) - source_list_for(source).find {|s| equal_source?(source, s) || equivalent_source?(source, s) } + source_list_for(source).find {|s| s.include?(source) } end def lock_sources - if Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? - [[default_source], @rubygems_sources, git_sources, path_sources, plugin_sources].map do |sources| - sources.sort_by(&:to_s) - end.flatten(1) - else - lock_sources = (path_sources + git_sources + plugin_sources).sort_by(&:to_s) - lock_sources << combine_rubygems_sources - end + lock_other_sources + lock_rubygems_sources + end + + def lock_other_sources + (path_sources + git_sources + plugin_sources).sort_by(&:identifier) + end + + def lock_rubygems_sources + rubygems_sources.sort_by(&:identifier) end # Returns true if there are changes def replace_sources!(replacement_sources) - return true if replacement_sources.empty? + return false if replacement_sources.empty? - [path_sources, git_sources, plugin_sources].each do |source_list| - source_list.map! do |source| - replacement_sources.find {|s| s == source } || source - end - end + @rubygems_sources, @path_sources, @git_sources, @plugin_sources = map_sources(replacement_sources) + @global_rubygems_source = global_replacement_source(replacement_sources) - replacement_rubygems = !Bundler.feature_flag.lockfile_uses_separate_rubygems_sources? && - replacement_sources.detect {|s| s.is_a?(Source::Rubygems) } - @rubygems_aggregate = replacement_rubygems if replacement_rubygems + !equivalent_sources?(lock_sources, replacement_sources) + end + + def prefer_local! + all_sources.each(&:prefer_local!) + end - return true if !equal_sources?(lock_sources, replacement_sources) && !equivalent_sources?(lock_sources, replacement_sources) - return true if replacement_rubygems && rubygems_remotes.to_set != replacement_rubygems.remotes.to_set + def local_only! + all_sources.each(&:local_only!) + end - false + def local! + all_sources.each(&:local!) end def cached! @@ -112,16 +131,71 @@ module Bundler end def remote! + @local_mode = false + all_sources.each(&:remote!) end - def rubygems_primary_remotes - @rubygems_aggregate.remotes + def clear_cache + rubygems_sources.each(&:clear_cache) + end + + private + + def map_sources(replacement_sources) + rubygems = @rubygems_sources.map do |source| + replace_rubygems_source(replacement_sources, source) + end + + git, plugin = [@git_sources, @plugin_sources].map do |sources| + sources.map do |source| + replace_source(replacement_sources, source) + end + end + + path = @path_sources.map do |source| + replace_path_source(replacement_sources, source) + end + + [rubygems, path, git, plugin] + end + + def global_replacement_source(replacement_sources) + replace_rubygems_source(replacement_sources, global_rubygems_source, &:local!) + end + + def replace_rubygems_source(replacement_sources, gemfile_source) + replace_source(replacement_sources, gemfile_source) do |replacement_source| + # locked sources never include credentials so always prefer remotes from the gemfile + replacement_source.remotes = gemfile_source.remotes + + yield replacement_source if block_given? + + replacement_source + end end - private + def replace_source(replacement_sources, gemfile_source) + replacement_source = replacement_sources.find {|s| s == gemfile_source } + return gemfile_source unless replacement_source + + replacement_source = yield(replacement_source) if block_given? - def rubygems_aggregate_class + replacement_source + end + + def replace_path_source(replacement_sources, gemfile_source) + replace_source(replacement_sources, gemfile_source) do |replacement_source| + if gemfile_source.is_a?(Source::Gemspec) + gemfile_source.checksum_store = replacement_source.checksum_store + gemfile_source + else + replacement_source + end + end + end + + def source_class Source::Rubygems end @@ -140,47 +214,19 @@ module Bundler end end - def combine_rubygems_sources - Source::Rubygems.new("remotes" => rubygems_remotes) - end - def warn_on_git_protocol(source) return if Bundler.settings["git.allow_insecure"] - if source.uri =~ /^git\:/ + if /^git\:/.match?(source.uri) Bundler.ui.warn "The git source `#{source.uri}` uses the `git` protocol, " \ "which transmits data without encryption. Disable this warning with " \ - "`bundle config git.allow_insecure true`, or switch to the `https` " \ + "`bundle config set --local git.allow_insecure true`, or switch to the `https` " \ "protocol to keep your data secure." end end - def equal_sources?(lock_sources, replacement_sources) - lock_sources.to_set == replacement_sources.to_set - end - - def equal_source?(source, other_source) - source == other_source - end - - def equivalent_source?(source, other_source) - return false unless Bundler.settings[:allow_deployment_source_credential_changes] && source.is_a?(Source::Rubygems) - - equivalent_rubygems_sources?([source], [other_source]) - end - def equivalent_sources?(lock_sources, replacement_sources) - return false unless Bundler.settings[:allow_deployment_source_credential_changes] - - lock_rubygems_sources, lock_other_sources = lock_sources.partition {|s| s.is_a?(Source::Rubygems) } - replacement_rubygems_sources, replacement_other_sources = replacement_sources.partition {|s| s.is_a?(Source::Rubygems) } - - equivalent_rubygems_sources?(lock_rubygems_sources, replacement_rubygems_sources) && equal_sources?(lock_other_sources, replacement_other_sources) - end - - def equivalent_rubygems_sources?(lock_sources, replacement_sources) - actual_remotes = replacement_sources.map(&:remotes).flatten.uniq - lock_sources.all? {|s| s.equivalent_remotes?(actual_remotes) } + lock_sources.sort_by(&:identifier) == replacement_sources.sort_by(&:identifier) end end end diff --git a/lib/bundler/source_map.rb b/lib/bundler/source_map.rb new file mode 100644 index 0000000000..513eb37f8b --- /dev/null +++ b/lib/bundler/source_map.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Bundler + class SourceMap + attr_reader :sources, :dependencies, :locked_specs + + def initialize(sources, dependencies, locked_specs) + @sources = sources + @dependencies = dependencies + @locked_specs = locked_specs + end + + def pinned_spec_names(skip = nil) + direct_requirements.reject {|_, source| source == skip }.keys + end + + def all_requirements(excluded_sources = []) + requirements = direct_requirements.dup + + explicit_sources = sources.non_default_explicit_sources.reject do |source| + excluded_sources.include?(source) + end + + unmet_deps = explicit_sources.map do |source| + (source.spec_names - pinned_spec_names).each do |indirect_dependency_name| + previous_source = requirements[indirect_dependency_name] + if previous_source.nil? + requirements[indirect_dependency_name] = source + else + msg = ["The gem '#{indirect_dependency_name}' was found in multiple relevant sources."] + msg.concat [previous_source, source].map {|s| " * #{s}" }.sort + msg << "You must add this gem to the source block for the source you wish it to be installed from." + msg = msg.join("\n") + + raise SecurityError, msg + end + end + + source.unmet_deps + end + + sources.default_source.add_dependency_names(unmet_deps.flatten - requirements.keys) + + requirements + end + + def direct_requirements + @direct_requirements ||= begin + requirements = {} + default = sources.default_source + dependencies.each do |dep| + dep_source = dep.source || default + dep_source.add_dependency_names(dep.name) + requirements[dep.name] = dep_source + end + requirements + end + end + + def locked_requirements + @locked_requirements ||= begin + requirements = {} + locked_specs.each do |locked_spec| + source = locked_spec.source + source.add_dependency_names(locked_spec.name) + requirements[locked_spec.name] = source + end + requirements + end + end + end +end diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb index 7cd3021997..ae5e5cbaa9 100644 --- a/lib/bundler/spec_set.rb +++ b/lib/bundler/spec_set.rb @@ -1,67 +1,113 @@ # frozen_string_literal: true -require "tsort" -require "forwardable" -require "set" +require_relative "vendored_tsort" module Bundler class SpecSet - extend Forwardable - include TSort, Enumerable - - def_delegators :@specs, :<<, :length, :add, :remove, :size, :empty? - def_delegators :sorted, :each + include Enumerable + include TSort def initialize(specs) @specs = specs end - def for(dependencies, skip = [], check = false, match_current_platform = false, raise_on_missing = true) - handled = Set.new - deps = dependencies.dup - specs = [] - skip += ["bundler"] + def for(dependencies, platforms = [nil], legacy_platforms = [nil], skips: []) + if [true, false].include?(platforms) + Bundler::SharedHelpers.feature_removed! \ + "SpecSet#for received a `check` parameter, but that's no longer used and deprecated. " \ + "SpecSet#for always implicitly performs validation. Please remove this parameter" + end - loop do - break unless dep = deps.shift - next if !handled.add?(dep) || skip.include?(dep.name) + materialize_dependencies(dependencies, platforms, skips: skips) - if spec = spec_for_dependency(dep, match_current_platform) - specs << spec + @materializations.flat_map(&:specs).uniq + end - spec.dependencies.each do |d| - next if d.type == :development - d = DepProxy.new(d, dep.__platform) unless match_current_platform - deps << d - end - elsif check - return false - elsif raise_on_missing - raise "Unable to find a spec satisfying #{dep} in the set. Perhaps the lockfile is corrupted?" + def normalize_platforms!(deps, platforms) + remove_invalid_platforms!(deps, platforms) + add_extra_platforms!(platforms) + + platforms.map! do |platform| + next platform if platform == Gem::Platform::RUBY + + begin + Integer(platform.version) + rescue ArgumentError, TypeError + next platform end + + less_specific_platform = Gem::Platform.new([platform.cpu, platform.os, nil]) + next platform if incomplete_for_platform?(deps, less_specific_platform) + + less_specific_platform + end.uniq! + end + + def add_originally_invalid_platforms!(platforms, originally_invalid_platforms) + originally_invalid_platforms.each do |originally_invalid_platform| + platforms << originally_invalid_platform if complete_platform(originally_invalid_platform) + end + end + + def remove_invalid_platforms!(deps, platforms, skips: []) + invalid_platforms = [] + + platforms.reject! do |platform| + next false if skips.include?(platform) + + invalid = incomplete_for_platform?(deps, platform) + invalid_platforms << platform if invalid + invalid end - if spec = lookup["bundler"].first - specs << spec + invalid_platforms + end + + def add_extra_platforms!(platforms) + if @specs.empty? + platforms.concat([Gem::Platform::RUBY]).uniq + return + end + + new_platforms = all_platforms.select do |platform| + next if platforms.include?(platform) + next unless Gem::Platform.generic(platform) == Gem::Platform::RUBY + + complete_platform(platform) end + return if new_platforms.empty? - check ? true : SpecSet.new(specs) + platforms.concat(new_platforms) + return if new_platforms.include?(Bundler.local_platform) + + less_specific_platform = new_platforms.find {|platform| platform != Gem::Platform::RUBY && Bundler.local_platform === platform && platform === Bundler.local_platform } + platforms.delete(Bundler.local_platform) if less_specific_platform end - def valid_for?(deps) - self.for(deps, [], true) + def validate_deps(s) + s.runtime_dependencies.each do |dep| + next if dep.name == "bundler" + + return :missing unless names.include?(dep.name) + return :invalid if none? {|spec| dep.matches_spec?(spec) } + end + + :valid end def [](key) key = key.name if key.respond_to?(:name) - lookup[key].reverse + lookup[key]&.reverse || [] end def []=(key, value) - @specs << value - @lookup = nil - @sorted = nil - value + delete_by_name(key) + + add_spec(value) + end + + def delete(specs) + Array(specs).each {|spec| remove_spec(spec) } end def sort! @@ -76,86 +122,227 @@ module Bundler lookup.dup end - def materialize(deps, missing_specs = nil) - materialized = self.for(deps, [], false, true, !missing_specs).to_a - deps = materialized.map(&:name).uniq - materialized.map! do |s| - next s unless s.is_a?(LazySpecification) - s.source.dependency_names = deps if s.source.respond_to?(:dependency_names=) - spec = s.__materialize__ - unless spec - unless missing_specs - raise GemNotFound, "Could not find #{s.full_name} in any of the sources" - end - missing_specs << s - end - spec - end - SpecSet.new(missing_specs ? materialized.compact : materialized) + def materialize(deps) + materialize_dependencies(deps) + + SpecSet.new(materialized_specs) end # Materialize for all the specs in the spec set, regardless of what platform they're for - # This is in contrast to how for does platform filtering (and specifically different from how `materialize` calls `for` only for the current platform) # @return [Array<Gem::Specification>] def materialized_for_all_platforms - names = @specs.map(&:name).uniq @specs.map do |s| next s unless s.is_a?(LazySpecification) - s.source.dependency_names = names if s.source.respond_to?(:dependency_names=) - spec = s.__materialize__ + spec = s.materialize_for_cache raise GemNotFound, "Could not find #{s.full_name} in any of the sources" unless spec spec end end - def merge(set) - arr = sorted.dup - set.each do |set_spec| - full_name = set_spec.full_name - next if arr.any? {|spec| spec.full_name == full_name } - arr << set_spec - end - SpecSet.new(arr) + def incomplete_for_platform?(deps, platform) + incomplete_specs_for_platform(deps, platform).any? + end + + def incomplete_specs_for_platform(deps, platform) + return [] if @specs.empty? + + validation_set = self.class.new(@specs) + validation_set.for(deps, [platform]) + validation_set.incomplete_specs + end + + def missing_specs_for(deps) + materialize_dependencies(deps) + + missing_specs + end + + def missing_specs + @materializations.flat_map(&:completely_missing_specs) + end + + def partially_missing_specs + @materializations.flat_map(&:partially_missing_specs) + end + + def incomplete_specs + @materializations.flat_map(&:incomplete_specs) + end + + def insecurely_materialized_specs + materialized_specs.select(&:insecurely_materialized?) + end + + def -(other) + SharedHelpers.feature_removed! "SpecSet#- has been removed with no replacement" end def find_by_name_and_platform(name, platform) - @specs.detect {|spec| spec.name == name && spec.match_platform(platform) } + lookup[name]&.detect {|spec| spec.installable_on_platform?(platform) } + end + + def specs_with_additional_variants_from(other) + sorted | additional_variants_from(other) + end + + def delete_by_name(name) + @specs.reject! {|spec| spec.name == name } + @sorted&.reject! {|spec| spec.name == name } + return if @lookup.nil? + + @lookup[name] = nil + end + + def version_for(name) + exemplary_spec(name)&.version end def what_required(spec) - unless req = find {|s| s.dependencies.any? {|d| d.type == :runtime && d.name == spec.name } } + unless req = find {|s| s.runtime_dependencies.any? {|d| d.name == spec.name } } return [spec] end what_required(req) << spec end - private + def <<(spec) + SharedHelpers.feature_removed! "SpecSet#<< has been removed with no replacement" + end - def sorted - rake = @specs.find {|s| s.name == "rake" } - begin - @sorted ||= ([rake] + tsort).compact.uniq - rescue TSort::Cyclic => error - cgems = extract_circular_gems(error) - raise CyclicDependencyError, "Your bundle requires gems that depend" \ - " on each other, creating an infinite loop. Please remove either" \ - " gem '#{cgems[1]}' or gem '#{cgems[0]}' and try again." + def length + @specs.length + end + + def size + @specs.size + end + + def empty? + @specs.empty? + end + + def each(&b) + sorted.each(&b) + end + + def names + lookup.keys + end + + def valid?(s) + s.matches_current_metadata? && valid_dependencies?(s) + end + + def to_s + map(&:full_name).to_s + end + + private + + def materialize_dependencies(dependencies, platforms = [nil], skips: []) + handled = ["bundler"].product(platforms).map {|k| [k, true] }.to_h + deps = dependencies.product(platforms) + @materializations = [] + + loop do + break unless dep = deps.shift + + dependency = dep[0] + platform = dep[1] + name = dependency.name + + key = [name, platform] + next if handled.key?(key) + + handled[key] = true + + materialization = Materialization.new(dependency, platform, candidates: lookup[name]) + + deps.concat(materialization.dependencies) if materialization.complete? + + @materializations << materialization unless skips.include?(name) end + + @materializations 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 + def materialized_specs + @materializations.filter_map(&:materialized_spec) + end + + def complete_platform(platform) + new_specs = [] + + valid_platform = lookup.all? do |_, specs| + spec = specs.first + # The matching candidates returned by source.specs.search are remote + # specs that do not carry the override list themselves. Borrow it from + # the LazySpec we are validating so platform-variant validation honors + # the same overrides the install/resolve path already applies. + overrides = spec.is_a?(LazySpecification) ? Array(spec.overrides) : [] + matching_specs = spec.source.specs.search([spec.name, spec.version]) + platform_spec = MatchPlatform.select_best_platform_match(matching_specs, platform).find do |s| + s.matches_current_metadata_with_overrides?(overrides) && valid_dependencies?(s) + end + + if platform_spec + unless specs.include?(platform_spec) + new_lazy = LazySpecification.from_spec(platform_spec) + # Carry the overrides forward so a follow-up complete_platform + # call that picks this synthesized variant as its exemplar still + # honors the user's override list. + new_lazy.overrides = overrides if overrides.any? + new_specs << new_lazy + end + true + else + false + end + end + + if valid_platform && new_specs.any? + new_specs.each {|spec| add_spec(spec) } end + + valid_platform + end + + def all_platforms + @specs.flat_map {|spec| spec.source.specs.search([spec.name, spec.version]).map(&:platform) }.uniq + end + + def additional_variants_from(other) + other.select do |other_spec| + spec = exemplary_spec(other_spec.name) + next unless spec + + selected = spec.version == other_spec.version && valid_dependencies?(other_spec) + other_spec.source = spec.source if selected + selected + end + end + + def valid_dependencies?(s) + validate_deps(s) == :valid + end + + def sorted + @sorted ||= ([lookup["rake"]&.first] + tsort).compact.uniq + rescue TSort::Cyclic => error + cgems = extract_circular_gems(error) + raise CyclicDependencyError, "Your bundle requires gems that depend" \ + " on each other, creating an infinite loop. Please remove either" \ + " gem '#{cgems[0]}' or gem '#{cgems[1]}' and try again." + end + + def extract_circular_gems(error) + error.message.scan(/@name="(.*?)"/).flatten end def lookup @lookup ||= begin - lookup = Hash.new {|h, k| h[k] = [] } - Index.sort_specs(@specs).reverse_each do |s| - lookup[s.name] << s + lookup = {} + @specs.each do |s| + index_spec(lookup, s.name, s) end lookup end @@ -166,24 +353,50 @@ module Bundler @specs.sort_by(&:name).each {|s| yield s } end - def spec_for_dependency(dep, match_current_platform) - specs_for_platforms = lookup[dep.name] - if match_current_platform - Bundler.rubygems.platforms.reverse_each do |pl| - match = GemHelpers.select_best_platform_match(specs_for_platforms, pl) - return match if match - end - nil - else - GemHelpers.select_best_platform_match(specs_for_platforms, dep.__platform) - end - end - def tsort_each_child(s) s.dependencies.sort_by(&:name).each do |d| next if d.type == :development - lookup[d.name].each {|s2| yield s2 } + + specs_for_name = lookup[d.name] + next unless specs_for_name + + specs_for_name.each {|s2| yield s2 } + end + end + + def add_spec(spec) + @specs << spec + + name = spec.name + + @sorted&.insert(@sorted.bsearch_index {|s| s.name >= name } || @sorted.size, spec) + return if @lookup.nil? + + index_spec(@lookup, name, spec) + end + + def remove_spec(spec) + @specs.delete(spec) + @sorted&.delete(spec) + return if @lookup.nil? + + indexed_specs = @lookup[spec.name] + return unless indexed_specs + + if indexed_specs.size > 1 + @lookup[spec.name].delete(spec) + else + @lookup[spec.name] = nil end end + + def index_spec(hash, key, value) + hash[key] ||= [] + hash[key] << value + end + + def exemplary_spec(name) + self[name].first + end end end diff --git a/lib/bundler/ssl_certs/certificate_manager.rb b/lib/bundler/ssl_certs/certificate_manager.rb deleted file mode 100644 index 26fc38ec18..0000000000 --- a/lib/bundler/ssl_certs/certificate_manager.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require "bundler/vendored_fileutils" -require "net/https" -require "openssl" - -module Bundler - module SSLCerts - class CertificateManager - attr_reader :bundler_cert_path, :bundler_certs, :rubygems_certs - - def self.update_from!(rubygems_path) - new(rubygems_path).update! - end - - def initialize(rubygems_path = nil) - if rubygems_path - rubygems_cert_path = File.join(rubygems_path, "lib/rubygems/ssl_certs") - @rubygems_certs = certificates_in(rubygems_cert_path) - end - - @bundler_cert_path = File.expand_path("..", __FILE__) - @bundler_certs = certificates_in(bundler_cert_path) - end - - def up_to_date? - rubygems_certs.all? do |rc| - bundler_certs.find do |bc| - File.basename(bc) == File.basename(rc) && FileUtils.compare_file(bc, rc) - end - end - end - - def update! - return if up_to_date? - - FileUtils.rm bundler_certs - FileUtils.cp rubygems_certs, bundler_cert_path - end - - def connect_to(host) - http = Net::HTTP.new(host, 443) - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_PEER - http.cert_store = store - http.head("/") - end - - private - - def certificates_in(path) - Dir[File.join(path, "**/*.pem")].sort - end - - def store - @store ||= begin - store = OpenSSL::X509::Store.new - bundler_certs.each do |cert| - store.add_file cert - end - store - end - end - end - end -end diff --git a/lib/bundler/ssl_certs/index.rubygems.org/GlobalSignRootCA.pem b/lib/bundler/ssl_certs/index.rubygems.org/GlobalSignRootCA.pem deleted file mode 100644 index f4ce4ca43d..0000000000 --- a/lib/bundler/ssl_certs/index.rubygems.org/GlobalSignRootCA.pem +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG -A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv -b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw -MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i -YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT -aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ -jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp -xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp -1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG -snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ -U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 -9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E -BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B -AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz -yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE -38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP -AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad -DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME -HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== ------END CERTIFICATE----- diff --git a/lib/bundler/ssl_certs/rubygems.global.ssl.fastly.net/DigiCertHighAssuranceEVRootCA.pem b/lib/bundler/ssl_certs/rubygems.global.ssl.fastly.net/DigiCertHighAssuranceEVRootCA.pem deleted file mode 100644 index 9e6810ab70..0000000000 --- a/lib/bundler/ssl_certs/rubygems.global.ssl.fastly.net/DigiCertHighAssuranceEVRootCA.pem +++ /dev/null @@ -1,23 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j -ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL -MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 -LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug -RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm -+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW -PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM -xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB -Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 -hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg -EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF -MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA -FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec -nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z -eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF -hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 -Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe -vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep -+OkuE6N36B9K ------END CERTIFICATE----- diff --git a/lib/bundler/ssl_certs/rubygems.org/AddTrustExternalCARoot.pem b/lib/bundler/ssl_certs/rubygems.org/AddTrustExternalCARoot.pem deleted file mode 100644 index 20585f1c01..0000000000 --- a/lib/bundler/ssl_certs/rubygems.org/AddTrustExternalCARoot.pem +++ /dev/null @@ -1,25 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU -MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs -IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 -MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux -FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h -bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v -dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt -H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 -uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX -mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX -a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN -E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 -WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD -VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 -Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU -cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx -IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN -AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH -YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 -6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC -Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX -c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a -mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= ------END CERTIFICATE----- diff --git a/lib/bundler/stub_specification.rb b/lib/bundler/stub_specification.rb index 0dd024024a..b353642b40 100644 --- a/lib/bundler/stub_specification.rb +++ b/lib/bundler/stub_specification.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "bundler/remote_specification" - module Bundler class StubSpecification < RemoteSpecification def self.from_stub(stub) @@ -11,19 +9,21 @@ module Bundler spec end + def insecurely_materialized? + false + end + + attr_reader :checksum attr_accessor :stub, :ignored - # Pre 2.2.0 did not include extension_dir - # https://github.com/rubygems/rubygems/commit/9485ca2d101b82a946d6f327f4bdcdea6d4946ea - if Bundler.rubygems.provides?(">= 2.2.0") - def source=(source) - super - # Stub has no concept of source, which means that extension_dir may be wrong - # This is the case for git-based gems. So, instead manually assign the extension dir - return unless source.respond_to?(:extension_dir_name) - path = File.join(stub.extensions_dir, source.extension_dir_name) - stub.extension_dir = File.expand_path(path) - end + def source=(source) + super + # Stub has no concept of source, which means that extension_dir may be wrong + # This is the case for git-based gems. So, instead manually assign the extension dir + return unless source.respond_to?(:extension_dir_name) + unique_extension_dir = [source.extension_dir_name, File.basename(full_gem_path)].uniq.join("-") + path = File.join(stub.extensions_dir, unique_extension_dir) + stub.extension_dir = File.expand_path(path) end def to_yaml @@ -32,58 +32,96 @@ module Bundler # @!group Stub Delegates - if Bundler.rubygems.provides?(">= 2.3") - # This is defined directly to avoid having to load every installed spec - def missing_extensions? - stub.missing_extensions? - end + def ignored? + return @ignored unless @ignored.nil? + + @ignored = missing_extensions? + return false unless @ignored + + warn "Source #{source} is ignoring #{self} because it is missing extensions" + + true end - def activated - stub.activated + def manually_installed? + # This is for manually installed gems which are gems that were fixed in place after a + # failed installation. Once the issue was resolved, the user then manually created + # the gem specification using the instructions provided by `gem help install` + installed_by_version == Gem::Version.new(0) + end + + # This is defined directly to avoid having to loading the full spec + def missing_extensions? + return false if RUBY_ENGINE == "jruby" + return false if default_gem? + return false if extensions.empty? + return false if File.exist? gem_build_complete_path + return false if manually_installed? + + true + end + + def activated? + stub.activated? end def activated=(activated) stub.instance_variable_set(:@activated, activated) end - def default_gem - stub.default_gem + def extensions + stub.extensions + end + + def gem_build_complete_path + stub.gem_build_complete_path + end + + def default_gem? + stub.default_gem? end def full_gem_path - # deleted gems can have their stubs return nil, so in that case grab the - # expired path from the full spec - stub.full_gem_path || method_missing(:full_gem_path) + stub.full_gem_path end - if Bundler.rubygems.provides?(">= 2.2.0") - def full_require_paths - stub.full_require_paths - end + def full_gem_path=(path) + stub.full_gem_path = path + end - # This is what we do in bundler/rubygems_ext - # full_require_paths is always implemented in >= 2.2.0 - def load_paths - full_require_paths - end + def full_require_paths + stub.full_require_paths + end + + def require_paths + stub.require_paths + end + + def base_dir=(path) + stub.base_dir = path + end + + def load_paths + full_require_paths end def loaded_from stub.loaded_from end - if Bundler.rubygems.stubs_provide_full_functionality? - def matches_for_glob(glob) - stub.matches_for_glob(glob) - end + def matches_for_glob(glob) + stub.matches_for_glob(glob) end def raw_require_paths stub.raw_require_paths end - private + def inspect + "#<#{self.class} @name=\"#{name}\" (#{full_name.delete_prefix("#{name}-")})>" + end + + private def _remote_specification @_remote_specification ||= begin @@ -100,6 +138,7 @@ module Bundler end rs.source = source + rs.base_dir = stub.base_dir rs end diff --git a/lib/bundler/templates/.document b/lib/bundler/templates/.document new file mode 100644 index 0000000000..fb66f13c33 --- /dev/null +++ b/lib/bundler/templates/.document @@ -0,0 +1 @@ +# Ignore all files in this directory diff --git a/lib/bundler/templates/Executable b/lib/bundler/templates/Executable index 9289debc26..b085c24da6 100755..100644 --- a/lib/bundler/templates/Executable +++ b/lib/bundler/templates/Executable @@ -8,12 +8,7 @@ # this file is here to facilitate running it. # -bundle_binstub = File.expand_path("../bundle", __FILE__) -load(bundle_binstub) if File.file?(bundle_binstub) - -require "pathname" -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../<%= relative_gemfile_path %>", - Pathname.new(__FILE__).realpath) +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("<%= relative_gemfile_path %>", __dir__) require "rubygems" require "bundler/setup" diff --git a/lib/bundler/templates/Executable.bundler b/lib/bundler/templates/Executable.bundler deleted file mode 100644 index eeda90b584..0000000000 --- a/lib/bundler/templates/Executable.bundler +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env <%= Bundler.settings[:shebang] || RbConfig::CONFIG["ruby_install_name"] %> -# frozen_string_literal: true - -# -# This file was generated by Bundler. -# -# The application '<%= executable %>' is installed as part of a gem, and -# this file is here to facilitate running it. -# - -require "rubygems" - -m = Module.new do - module_function - - def invoked_as_script? - File.expand_path($0) == File.expand_path(__FILE__) - end - - def env_var_version - ENV["BUNDLER_VERSION"] - end - - def cli_arg_version - return unless invoked_as_script? # don't want to hijack other binstubs - return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` - bundler_version = nil - update_index = nil - ARGV.each_with_index do |a, i| - if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN - bundler_version = a - end - next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ - bundler_version = $1 || ">= 0.a" - update_index = i - end - bundler_version - end - - def gemfile - gemfile = ENV["BUNDLE_GEMFILE"] - return gemfile if gemfile && !gemfile.empty? - - File.expand_path("../<%= relative_gemfile_path %>", __FILE__) - end - - def lockfile - lockfile = - case File.basename(gemfile) - when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) - else "#{gemfile}.lock" - end - File.expand_path(lockfile) - end - - def lockfile_version - return unless File.file?(lockfile) - lockfile_contents = File.read(lockfile) - return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ - Regexp.last_match(1) - end - - def bundler_version - @bundler_version ||= begin - env_var_version || cli_arg_version || - lockfile_version || "#{Gem::Requirement.default}.a" - end - end - - def load_bundler! - ENV["BUNDLE_GEMFILE"] ||= gemfile - - # must dup string for RG < 1.8 compatibility - activate_bundler(bundler_version.dup) - end - - def activate_bundler(bundler_version) - if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0") - bundler_version = "< 2" - end - gem_error = activation_error_handling do - gem "bundler", bundler_version - end - return if gem_error.nil? - require_error = activation_error_handling do - require "bundler/version" - end - return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION)) - warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`" - exit 42 - end - - def activation_error_handling - yield - nil - rescue StandardError, LoadError => e - e - end -end - -m.load_bundler! - -if m.invoked_as_script? - load Gem.bin_path("<%= spec.name %>", "<%= executable %>") -end diff --git a/lib/bundler/templates/Executable.standalone b/lib/bundler/templates/Executable.standalone index 4bf0753f44..3117a27e86 100644 --- a/lib/bundler/templates/Executable.standalone +++ b/lib/bundler/templates/Executable.standalone @@ -1,4 +1,6 @@ #!/usr/bin/env <%= Bundler.settings[:shebang] || RbConfig::CONFIG["ruby_install_name"] %> +# frozen_string_literal: true + # # This file was generated by Bundler. # @@ -6,9 +8,7 @@ # this file is here to facilitate running it. # -require "pathname" -path = Pathname.new(__FILE__) -$:.unshift File.expand_path "../<%= standalone_path %>", path.realpath +$:.unshift File.expand_path "<%= standalone_path %>", __dir__ require "bundler/setup" -load File.expand_path "../<%= executable_path %>", path.realpath +load File.expand_path "<%= executable_path %>", __dir__ diff --git a/lib/bundler/templates/Gemfile b/lib/bundler/templates/Gemfile index 1afd2cce67..d2403f18b2 100644 --- a/lib/bundler/templates/Gemfile +++ b/lib/bundler/templates/Gemfile @@ -2,6 +2,4 @@ source "https://rubygems.org" -git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } - # gem "rails" diff --git a/lib/bundler/templates/gems.rb b/lib/bundler/templates/gems.rb deleted file mode 100644 index 547cd6e8d9..0000000000 --- a/lib/bundler/templates/gems.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# A sample gems.rb -source "https://rubygems.org" - -git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } - -# gem "rails" diff --git a/lib/bundler/templates/newgem/CHANGELOG.md.tt b/lib/bundler/templates/newgem/CHANGELOG.md.tt new file mode 100644 index 0000000000..c9ea96d453 --- /dev/null +++ b/lib/bundler/templates/newgem/CHANGELOG.md.tt @@ -0,0 +1,5 @@ +## [Unreleased] + +## [0.1.0] - <%= Time.now.strftime('%F') %> + +- Initial release diff --git a/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt b/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt index a3833d29d7..633baebdd5 100644 --- a/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt +++ b/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt @@ -1,74 +1,10 @@ -# Contributor Covenant Code of Conduct +# Code of Conduct -## Our Pledge +<%= config[:name].inspect %> follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.): -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. +* Participants will be tolerant of opposing views. +* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks. +* When interpreting the words and actions of others, participants should always assume good intentions. +* Behaviour which can be reasonably considered harassment will not be tolerated. -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at <%= config[:email] %>. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +If you have any concerns about behaviour within this project, please contact us at [<%= config[:email].inspect %>](mailto:<%= config[:email].inspect %>). diff --git a/lib/bundler/templates/newgem/Cargo.toml.tt b/lib/bundler/templates/newgem/Cargo.toml.tt new file mode 100644 index 0000000000..cd00f97e5a --- /dev/null +++ b/lib/bundler/templates/newgem/Cargo.toml.tt @@ -0,0 +1,13 @@ +# This Cargo.toml is here to let externals tools (IDEs, etc.) know that this is +# a Rust project. Your extensions dependencies should be added to the Cargo.toml +# in the ext/ directory. + +[workspace] +members = ["./ext/<%= config[:name] %>"] +resolver = "2" + +[profile.release] +# By default, debug symbols are stripped from the final binary which makes it +# harder to debug if something goes wrong. It's recommended to keep debug +# symbols in the release build so that you can debug the final binary if needed. +debug = true diff --git a/lib/bundler/templates/newgem/Gemfile.tt b/lib/bundler/templates/newgem/Gemfile.tt index c114bd6665..85dc593b8f 100644 --- a/lib/bundler/templates/newgem/Gemfile.tt +++ b/lib/bundler/templates/newgem/Gemfile.tt @@ -1,6 +1,24 @@ -source "https://rubygems.org" +# frozen_string_literal: true -git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } +source "https://rubygems.org" # Specify your gem's dependencies in <%= config[:name] %>.gemspec gemspec + +gem "irb" +gem "rake", ">= 13.0" +<%- if config[:ext] -%> + +gem "rake-compiler" +<%- end -%> +<%- if config[:test] -%> + +gem "<%= config[:test] %>" +<%- end -%> +<%- if config[:linter] == "rubocop" -%> + +gem "rubocop" +<%- elsif config[:linter] == "standard" -%> + +gem "standard" +<%- end -%> diff --git a/lib/bundler/templates/newgem/README.md.tt b/lib/bundler/templates/newgem/README.md.tt index 868a0afe67..0ec6a12fa7 100644 --- a/lib/bundler/templates/newgem/README.md.tt +++ b/lib/bundler/templates/newgem/README.md.tt @@ -1,24 +1,24 @@ # <%= config[:constant_name] %> -Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/<%= config[:namespaced_path] %>`. To experiment with that code, run `bin/console` for an interactive prompt. +TODO: Delete this and the text below, and describe your gem -TODO: Delete this and the text above, and describe your gem +Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/<%= config[:namespaced_path] %>`. To experiment with that code, run `bin/console` for an interactive prompt. ## Installation -Add this line to your application's Gemfile: - -```ruby -gem '<%= config[:name] %>' -``` +TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org. -And then execute: +Install the gem and add to the application's Gemfile by executing: - $ bundle +```bash +bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG +``` -Or install it yourself as: +If bundler is not being used to manage dependencies, install the gem by executing: - $ gem install <%= config[:name] %> +```bash +gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG +``` ## Usage @@ -26,22 +26,24 @@ TODO: Write usage instructions here ## Development -After checking out the repo, run `bin/setup` to install dependencies.<% if config[:test] %> Then, run `rake <%= config[:test].sub('mini', '').sub('rspec', 'spec') %>` to run the tests.<% end %> You can also run `bin/console` for an interactive prompt that will allow you to experiment.<% if config[:bin] %> Run `bundle exec <%= config[:name] %>` to use the gem in this directory, ignoring other installed copies of this gem.<% end %> +After checking out the repo, run `bin/setup` to install dependencies.<% if config[:test] %> Then, run `rake <%= config[:test_task] %>` to run the tests.<% end %> You can also run `bin/console` for an interactive prompt that will allow you to experiment.<% if config[:bin] %> Run `bundle exec <%= config[:name] %>` to use the gem in this directory, ignoring other installed copies of this gem.<% end %> -To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). +To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). +<% if config[:git] -%> ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/<%= config[:github_username] %>/<%= config[:name] %>.<% if config[:coc] %> This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.<% end %> +Bug reports and pull requests are welcome on GitHub at https://github.com/<%= config[:github_username] %>/<%= config[:name] %>.<% if config[:coc] %> This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/<%= config[:github_username] %>/<%= config[:name] %>/blob/<%= config[:git_default_branch] %>/CODE_OF_CONDUCT.md).<% end %> +<% end -%> <% if config[:mit] -%> ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). <% end -%> -<% if config[:coc] -%> +<% if config[:git] && config[:coc] -%> ## Code of Conduct -Everyone interacting in the <%= config[:constant_name] %> project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/<%= config[:github_username] %>/<%= config[:name] %>/blob/master/CODE_OF_CONDUCT.md). +Everyone interacting in the <%= config[:constant_name] %> project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/<%= config[:github_username] %>/<%= config[:name] %>/blob/<%= config[:git_default_branch] %>/CODE_OF_CONDUCT.md). <% end -%> diff --git a/lib/bundler/templates/newgem/Rakefile.tt b/lib/bundler/templates/newgem/Rakefile.tt index 099da6f3ec..83f10009c7 100644 --- a/lib/bundler/templates/newgem/Rakefile.tt +++ b/lib/bundler/templates/newgem/Rakefile.tt @@ -1,5 +1,14 @@ +# frozen_string_literal: true + require "bundler/gem_tasks" -<% if config[:test] == "minitest" -%> +<% default_task_names = [config[:test_task]].compact -%> +<% case config[:test] -%> +<% when "minitest" -%> +require "minitest/test_task" + +Minitest::TestTask.create + +<% when "test-unit" -%> require "rake/testtask" Rake::TestTask.new(:test) do |t| @@ -8,22 +17,56 @@ Rake::TestTask.new(:test) do |t| t.test_files = FileList["test/**/*_test.rb"] end -<% elsif config[:test] == "rspec" -%> +<% when "rspec" -%> require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) <% end -%> +<% if config[:linter] == "rubocop" -%> +<% default_task_names << :rubocop -%> +require "rubocop/rake_task" + +RuboCop::RakeTask.new + +<% elsif config[:linter] == "standard" -%> +<% default_task_names << :standard -%> +require "standard/rake" + +<% end -%> <% if config[:ext] -%> +<% default_task_names.unshift(:compile) -%> +<% default_task_names.unshift(:clobber) unless config[:ext] == 'rust' -%> +<% if config[:ext] == 'rust' -%> +require "rb_sys/extensiontask" + +task build: :compile + +GEMSPEC = Gem::Specification.load("<%= config[:underscored_name] %>.gemspec") + +RbSys::ExtensionTask.new(<%= config[:name].inspect %>, GEMSPEC) do |ext| + ext.lib_dir = "lib/<%= config[:namespaced_path] %>" +end +<% else -%> require "rake/extensiontask" -task :build => :compile +task build: :compile + +GEMSPEC = Gem::Specification.load("<%= config[:underscored_name] %>.gemspec") -Rake::ExtensionTask.new("<%= config[:underscored_name] %>") do |ext| +Rake::ExtensionTask.new("<%= config[:underscored_name] %>", GEMSPEC) do |ext| ext.lib_dir = "lib/<%= config[:namespaced_path] %>" end +<% end -%> + +<% if config[:ext] == "go" -%> +require "go_gem/rake_task" -task :default => [:clobber, :compile, :<%= config[:test_task] %>] +GoGem::RakeTask.new("<%= config[:underscored_name] %>") +<% end -%> +<% end -%> +<% if default_task_names.size == 1 -%> +task default: <%= default_task_names.first.inspect %> <% else -%> -task :default => :<%= config[:test_task] %> +task default: %i[<%= default_task_names.join(" ") %>] <% end -%> diff --git a/lib/bundler/templates/newgem/bin/console.tt b/lib/bundler/templates/newgem/bin/console.tt index a27f82430f..c91ee65f93 100644 --- a/lib/bundler/templates/newgem/bin/console.tt +++ b/lib/bundler/templates/newgem/bin/console.tt @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require "bundler/setup" require "<%= config[:namespaced_path] %>" @@ -6,9 +7,5 @@ require "<%= config[:namespaced_path] %>" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. -# (If you use this, don't forget to add pry to your Gemfile!) -# require "pry" -# Pry.start - require "irb" IRB.start(__FILE__) diff --git a/lib/bundler/templates/newgem/circleci/config.yml.tt b/lib/bundler/templates/newgem/circleci/config.yml.tt new file mode 100644 index 0000000000..c4dd9d0647 --- /dev/null +++ b/lib/bundler/templates/newgem/circleci/config.yml.tt @@ -0,0 +1,37 @@ +version: 2.1 +jobs: + build: + docker: + - image: ruby:<%= RUBY_VERSION %> +<%- if config[:ext] == 'rust' -%> + environment: + RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN: 'true' +<%- end -%> +<%- if config[:ext] == 'go' -%> + environment: + GO_VERSION: '1.23.0' +<%- end -%> + steps: + - checkout +<%- if config[:ext] == 'rust' -%> + - run: + name: Install Rust/Cargo dependencies + command: apt-get update && apt-get install -y clang + - run: + name: Install a RubyGems version that can compile rust extensions + command: gem update --system '<%= ::Gem.rubygems_version %>' +<%- end -%> +<%- if config[:ext] == 'go' -%> + - run: + name: Install Go + command: | + wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz + tar -C /usr/local -xzf /tmp/go.tar.gz + echo 'export PATH=/usr/local/go/bin:"$PATH"' >> "$BASH_ENV" +<%- end -%> + - run: + name: Run the default task + command: | + gem install bundler -v <%= Bundler::VERSION %> + bundle install + bundle exec rake diff --git a/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt b/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt new file mode 100644 index 0000000000..a06166aee7 --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt @@ -0,0 +1,22 @@ +[package] +name = <%= config[:name].inspect %> +version = "0.1.0" +edition = "2021" +authors = ["<%= config[:author] %> <<%= config[:email] %>>"] +<%- if config[:mit] -%> +license = "MIT" +<%- end -%> +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +magnus = { version = "0.8.2" } +rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] } + +[build-dependencies] +rb-sys-env = "0.2.2" + +[dev-dependencies] +rb-sys-test-helpers = { version = "0.2.2" } diff --git a/lib/bundler/templates/newgem/ext/newgem/build.rs.tt b/lib/bundler/templates/newgem/ext/newgem/build.rs.tt new file mode 100644 index 0000000000..80a7842753 --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/build.rs.tt @@ -0,0 +1,5 @@ +pub fn main() -> Result<(), Box<dyn std::error::Error>> { + let _ = rb_sys_env::activate()?; + + Ok(()) +} diff --git a/lib/bundler/templates/newgem/ext/newgem/extconf-c.rb.tt b/lib/bundler/templates/newgem/ext/newgem/extconf-c.rb.tt new file mode 100644 index 0000000000..0a0c5a3d09 --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/extconf-c.rb.tt @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require "mkmf" + +# Makes all symbols private by default to avoid unintended conflict +# with other gems. To explicitly export symbols you can use RUBY_FUNC_EXPORTED +# selectively, or entirely remove this flag. +append_cflags("-fvisibility=hidden") + +create_makefile(<%= config[:makefile_path].inspect %>) diff --git a/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt b/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt new file mode 100644 index 0000000000..a689e21ebe --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/extconf-go.rb.tt @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "mkmf" +require "go_gem/mkmf" + +# Makes all symbols private by default to avoid unintended conflict +# with other gems. To explicitly export symbols you can use RUBY_FUNC_EXPORTED +# selectively, or entirely remove this flag. +append_cflags("-fvisibility=hidden") + +create_go_makefile(<%= config[:makefile_path].inspect %>) diff --git a/lib/bundler/templates/newgem/ext/newgem/extconf-rust.rb.tt b/lib/bundler/templates/newgem/ext/newgem/extconf-rust.rb.tt new file mode 100644 index 0000000000..e24566a17a --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/extconf-rust.rb.tt @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "mkmf" +require "rb_sys/mkmf" + +create_rust_makefile(<%= config[:makefile_path].inspect %>) diff --git a/lib/bundler/templates/newgem/ext/newgem/extconf.rb.tt b/lib/bundler/templates/newgem/ext/newgem/extconf.rb.tt deleted file mode 100644 index 8cfc828f94..0000000000 --- a/lib/bundler/templates/newgem/ext/newgem/extconf.rb.tt +++ /dev/null @@ -1,3 +0,0 @@ -require "mkmf" - -create_makefile(<%= config[:makefile_path].inspect %>) diff --git a/lib/bundler/templates/newgem/ext/newgem/go.mod.tt b/lib/bundler/templates/newgem/ext/newgem/go.mod.tt new file mode 100644 index 0000000000..3f4819d004 --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/go.mod.tt @@ -0,0 +1,5 @@ +module github.com/<%= config[:go_module_username] %>/<%= config[:underscored_name] %> + +go 1.23 + +require github.com/ruby-go-gem/go-gem-wrapper latest diff --git a/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt b/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt new file mode 100644 index 0000000000..119c0c96ea --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/newgem-go.c.tt @@ -0,0 +1,2 @@ +#include "<%= config[:underscored_name] %>.h" +#include "_cgo_export.h" diff --git a/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt b/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt index 8177c4d202..bcd5148569 100644 --- a/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt +++ b/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt @@ -2,7 +2,7 @@ VALUE rb_m<%= config[:constant_array].join %>; -void +RUBY_FUNC_EXPORTED void Init_<%= config[:underscored_name] %>(void) { rb_m<%= config[:constant_array].join %> = rb_define_module(<%= config[:constant_name].inspect %>); diff --git a/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt b/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt new file mode 100644 index 0000000000..f19b750e58 --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/newgem.go.tt @@ -0,0 +1,31 @@ +package main + +/* +#include "<%= config[:underscored_name] %>.h" + +VALUE rb_<%= config[:underscored_name] %>_sum(VALUE self, VALUE a, VALUE b); +*/ +import "C" + +import ( + "github.com/ruby-go-gem/go-gem-wrapper/ruby" +) + +//export rb_<%= config[:underscored_name] %>_sum +func rb_<%= config[:underscored_name] %>_sum(_ C.VALUE, a C.VALUE, b C.VALUE) C.VALUE { + longA := ruby.NUM2LONG(ruby.VALUE(a)) + longB := ruby.NUM2LONG(ruby.VALUE(b)) + + sum := longA + longB + + return C.VALUE(ruby.LONG2NUM(sum)) +} + +//export Init_<%= config[:underscored_name] %> +func Init_<%= config[:underscored_name] %>() { + rb_m<%= config[:constant_array].join %> := ruby.RbDefineModule(<%= config[:constant_name].inspect %>) + ruby.RbDefineSingletonMethod(rb_m<%= config[:constant_array].join %>, "sum", C.rb_<%= config[:underscored_name] %>_sum, 2) +} + +func main() { +} diff --git a/lib/bundler/templates/newgem/ext/newgem/src/lib.rs.tt b/lib/bundler/templates/newgem/ext/newgem/src/lib.rs.tt new file mode 100644 index 0000000000..09ce97682d --- /dev/null +++ b/lib/bundler/templates/newgem/ext/newgem/src/lib.rs.tt @@ -0,0 +1,23 @@ +use magnus::{function, prelude::*, Error, Ruby}; + +pub fn hello(subject: String) -> String { + format!("Hello {subject}, from Rust!") +} + +#[magnus::init] +fn init(ruby: &Ruby) -> Result<(), Error> { + let module = ruby.<%= config[:constant_array].map {|c| "define_module(#{c.dump})?"}.join(".") %>; + module.define_singleton_method("hello", function!(hello, 1))?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use rb_sys_test_helpers::ruby_test; + use super::hello; + + #[ruby_test] + fn test_hello() { + assert_eq!("Hello world, from Rust!", hello("world".to_string())); + } +} diff --git a/lib/bundler/templates/newgem/github/workflows/build-gems.yml.tt b/lib/bundler/templates/newgem/github/workflows/build-gems.yml.tt new file mode 100644 index 0000000000..d49954d2cd --- /dev/null +++ b/lib/bundler/templates/newgem/github/workflows/build-gems.yml.tt @@ -0,0 +1,69 @@ +--- +name: Build gems + +on: + push: + tags: + - "v*" + - "cross-gem/*" + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + ci-data: + runs-on: ubuntu-latest + outputs: + result: ${{ steps.fetch.outputs.result }} + steps: + - uses: oxidize-rb/actions/fetch-ci-data@v1 + id: fetch + with: + supported-ruby-platforms: | + exclude: ["arm-linux", "x64-mingw32"] + stable-ruby-versions: | + exclude: ["head"] + + source-gem: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Build gem + run: bundle exec rake build + + - uses: actions/upload-artifact@v7 + with: + name: source-gem + path: pkg/*.gem + + cross-gem: + name: Compile native gem for ${{ matrix.platform }} + runs-on: ubuntu-latest + needs: ci-data + strategy: + matrix: + platform: ${{ fromJSON(needs.ci-data.outputs.result).supported-ruby-platforms }} + steps: + - uses: actions/checkout@v6 + + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - uses: oxidize-rb/actions/cross-gem@v1 + id: cross-gem + with: + platform: ${{ matrix.platform }} + ruby-versions: ${{ join(fromJSON(needs.ci-data.outputs.result).stable-ruby-versions, ',') }} + + - uses: actions/upload-artifact@v7 + with: + name: cross-gem + path: ${{ steps.cross-gem.outputs.gem-path }} diff --git a/lib/bundler/templates/newgem/github/workflows/main.yml.tt b/lib/bundler/templates/newgem/github/workflows/main.yml.tt new file mode 100644 index 0000000000..cc8f04dd33 --- /dev/null +++ b/lib/bundler/templates/newgem/github/workflows/main.yml.tt @@ -0,0 +1,48 @@ +name: Ruby + +on: + push: + branches: + - <%= config[:git_default_branch] %> + + pull_request: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + name: Ruby ${{ matrix.ruby }} + strategy: + matrix: + ruby: + - '<%= RUBY_VERSION %>' + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false +<%- if config[:ext] == 'rust' -%> + - name: Set up Ruby & Rust + uses: oxidize-rb/actions/setup-ruby-and-rust@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + cargo-cache: true + rubygems: '<%= ::Gem.rubygems_version %>' +<%- else -%> + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true +<%- end -%> +<%- if config[:ext] == 'go' -%> + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: ext/<%= config[:underscored_name] %>/go.mod +<%- end -%> + - name: Run the default task + run: bundle exec rake diff --git a/lib/bundler/templates/newgem/gitignore.tt b/lib/bundler/templates/newgem/gitignore.tt index b1c9f9986c..9b40ba5a58 100644 --- a/lib/bundler/templates/newgem/gitignore.tt +++ b/lib/bundler/templates/newgem/gitignore.tt @@ -12,6 +12,9 @@ *.o *.a mkmf.log +<%- if config[:ext] == 'rust' -%> +target/ +<%- end -%> <%- end -%> <%- if config[:test] == "rspec" -%> diff --git a/lib/bundler/templates/newgem/gitlab-ci.yml.tt b/lib/bundler/templates/newgem/gitlab-ci.yml.tt new file mode 100644 index 0000000000..adbd70cbc0 --- /dev/null +++ b/lib/bundler/templates/newgem/gitlab-ci.yml.tt @@ -0,0 +1,27 @@ +default: + image: ruby:<%= RUBY_VERSION %> + + before_script: +<%- if config[:ext] == 'rust' -%> + - apt-get update && apt-get install -y clang + - gem update --system '<%= ::Gem.rubygems_version %>' +<%- end -%> +<%- if config[:ext] == 'go' -%> + - wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz + - tar -C /usr/local -xzf /tmp/go.tar.gz + - export PATH=/usr/local/go/bin:$PATH +<%- end -%> + - gem install bundler -v <%= Bundler::VERSION %> + - bundle install + +example_job: +<%- if config[:ext] == 'rust' -%> + variables: + RB_SYS_FORCE_INSTALL_RUST_TOOLCHAIN: 'true' +<%- end -%> +<%- if config[:ext] == 'go' -%> + variables: + GO_VERSION: '1.23.0' +<%- end -%> + script: + - bundle exec rake diff --git a/lib/bundler/templates/newgem/lib/newgem.rb.tt b/lib/bundler/templates/newgem/lib/newgem.rb.tt index 7d8ad90ab0..3aedee0d25 100644 --- a/lib/bundler/templates/newgem/lib/newgem.rb.tt +++ b/lib/bundler/templates/newgem/lib/newgem.rb.tt @@ -1,11 +1,14 @@ -require "<%= config[:namespaced_path] %>/version" +# frozen_string_literal: true + +require_relative "<%= File.basename(config[:namespaced_path]) %>/version" <%- if config[:ext] -%> -require "<%= config[:namespaced_path] %>/<%= config[:underscored_name] %>" +require "<%= File.basename(config[:namespaced_path]) %>/<%= config[:underscored_name] %>" <%- end -%> <%- config[:constant_array].each_with_index do |c, i| -%> <%= " " * i %>module <%= c %> <%- end -%> +<%= " " * config[:constant_array].size %>class Error < StandardError; end <%= " " * config[:constant_array].size %># Your code goes here... <%- (config[:constant_array].size-1).downto(0) do |i| -%> <%= " " * i %>end diff --git a/lib/bundler/templates/newgem/lib/newgem/version.rb.tt b/lib/bundler/templates/newgem/lib/newgem/version.rb.tt index 389daf5048..b5cd4cb232 100644 --- a/lib/bundler/templates/newgem/lib/newgem/version.rb.tt +++ b/lib/bundler/templates/newgem/lib/newgem/version.rb.tt @@ -1,3 +1,5 @@ +# frozen_string_literal: true + <%- config[:constant_array].each_with_index do |c, i| -%> <%= " " * i %>module <%= c %> <%- end -%> diff --git a/lib/bundler/templates/newgem/newgem.gemspec.tt b/lib/bundler/templates/newgem/newgem.gemspec.tt index 9a87a1374a..1ab1c28f46 100644 --- a/lib/bundler/templates/newgem/newgem.gemspec.tt +++ b/lib/bundler/templates/newgem/newgem.gemspec.tt @@ -1,49 +1,58 @@ -<%- if RUBY_VERSION < "2.0.0" -%> -# coding: utf-8 -<%- end -%> +# frozen_string_literal: true -lib = File.expand_path("../lib", __FILE__) -$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require "<%= config[:namespaced_path] %>/version" +require_relative "lib/<%=config[:namespaced_path]%>/version" Gem::Specification.new do |spec| - spec.name = <%= config[:name].inspect %> - spec.version = <%= config[:constant_name] %>::VERSION - spec.authors = [<%= config[:author].inspect %>] - spec.email = [<%= config[:email].inspect %>] + spec.name = <%= config[:name].inspect %> + spec.version = <%= config[:constant_name] %>::VERSION + spec.authors = [<%= config[:author].inspect %>] + spec.email = [<%= config[:email].inspect %>] - spec.summary = %q{TODO: Write a short summary, because RubyGems requires one.} - spec.description = %q{TODO: Write a longer description or delete this line.} - spec.homepage = "TODO: Put your gem's website or public repo URL here." + spec.summary = "TODO: Write a short summary, because RubyGems requires one." + spec.description = "TODO: Write a longer description or delete this line." + spec.homepage = "<%= config[:homepage_uri] %>" <%- if config[:mit] -%> - spec.license = "MIT" + spec.license = "MIT" +<%- end -%> + spec.required_ruby_version = ">= <%= config[:required_ruby_version] %>" + spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'" + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "<%= config[:source_code_uri] %>" +<%- if config[:changelog] -%> + spec.metadata["changelog_uri"] = "<%= config[:changelog_uri] %>" <%- end -%> - # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' - # to allow pushing to a single host or delete this section to allow pushing to any host. - if spec.respond_to?(:metadata) - spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" - else - raise "RubyGems 2.0 or newer is required to protect against " \ - "public gem pushes." - end + # Uncomment the line below to require MFA for gem pushes. + # This helps protect your gem from supply chain attacks by ensuring + # no one can publish a new version without multi-factor authentication. + # See: https://guides.rubygems.org/mfa-requirement-opt-in/ + # spec.metadata["rubygems_mfa_required"] = "true" - spec.files = `git ls-files -z`.split("\x0").reject do |f| - f.match(%r{^(test|spec|features)/}) + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + gemspec = File.basename(__FILE__) + spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| + ls.readlines("\x0", chomp: true).reject do |f| + (f == gemspec) || + f.start_with?(*%w[<%= config[:ignore_paths].join(" ") %>]) + end end - spec.bindir = "exe" - spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] -<%- if config[:ext] -%> - spec.extensions = ["ext/<%= config[:underscored_name] %>/extconf.rb"] +<%- if %w(c rust go).include?(config[:ext]) -%> + spec.extensions = ["ext/<%= config[:underscored_name] %>/extconf.rb"] <%- end -%> - spec.add_development_dependency "bundler", "~> <%= config[:bundler_version] %>" - spec.add_development_dependency "rake", "~> 10.0" -<%- if config[:ext] -%> - spec.add_development_dependency "rake-compiler" + # Uncomment to register a new dependency of your gem + # spec.add_dependency "example-gem", ">= 1.0" +<%- if config[:ext] == 'rust' -%> + spec.add_dependency "rb_sys", ">= 0.9.128" <%- end -%> -<%- if config[:test] -%> - spec.add_development_dependency "<%= config[:test] %>", "~> <%= config[:test_framework_version] %>" +<%- if config[:ext] == 'go' -%> + spec.add_dependency "go_gem", ">= 0.2" <%- end -%> + + # For more information and examples about making a new gem, check out our + # guide at: https://guides.rubygems.org/make-your-own-gem/ end diff --git a/lib/bundler/templates/newgem/rubocop.yml.tt b/lib/bundler/templates/newgem/rubocop.yml.tt new file mode 100644 index 0000000000..3d1c4ee7b2 --- /dev/null +++ b/lib/bundler/templates/newgem/rubocop.yml.tt @@ -0,0 +1,8 @@ +AllCops: + TargetRubyVersion: <%= ::Gem::Version.new(config[:required_ruby_version]).segments[0..1].join(".") %> + +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes diff --git a/lib/bundler/templates/newgem/sig/newgem.rbs.tt b/lib/bundler/templates/newgem/sig/newgem.rbs.tt new file mode 100644 index 0000000000..eb7b380bbb --- /dev/null +++ b/lib/bundler/templates/newgem/sig/newgem.rbs.tt @@ -0,0 +1,8 @@ +<%- config[:constant_array].each_with_index do |c, i| -%> +<%= " " * i %>module <%= c %> +<%- end -%> +<%= " " * config[:constant_array].size %>VERSION: String +<%= " " * config[:constant_array].size %># See the writing guide of rbs: https://github.com/ruby/rbs#guides +<%- (config[:constant_array].size-1).downto(0) do |i| -%> +<%= " " * i %>end +<%- end -%> diff --git a/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt b/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt index c63b487830..7c6cde170b 100644 --- a/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt +++ b/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt @@ -1,9 +1,19 @@ +# frozen_string_literal: true + RSpec.describe <%= config[:constant_name] %> do it "has a version number" do expect(<%= config[:constant_name] %>::VERSION).not_to be nil end +<%- if config[:ext] == 'rust' -%> + it "can call into Rust" do + result = <%= config[:constant_name] %>.hello("world") + + expect(result).to eq("Hello world, from Rust!") + end +<%- else -%> it "does something useful" do expect(false).to eq(true) end +<%- end -%> end diff --git a/lib/bundler/templates/newgem/spec/spec_helper.rb.tt b/lib/bundler/templates/newgem/spec/spec_helper.rb.tt index 805cf57e01..70c6d1fcde 100644 --- a/lib/bundler/templates/newgem/spec/spec_helper.rb.tt +++ b/lib/bundler/templates/newgem/spec/spec_helper.rb.tt @@ -1,4 +1,5 @@ -require "bundler/setup" +# frozen_string_literal: true + require "<%= config[:namespaced_path] %>" RSpec.configure do |config| diff --git a/lib/bundler/templates/newgem/standard.yml.tt b/lib/bundler/templates/newgem/standard.yml.tt new file mode 100644 index 0000000000..a0696cd2e9 --- /dev/null +++ b/lib/bundler/templates/newgem/standard.yml.tt @@ -0,0 +1,3 @@ +# For available configuration options, see: +# https://github.com/standardrb/standard +ruby_version: <%= ::Gem::Version.new(config[:required_ruby_version]).segments[0..1].join(".") %> diff --git a/lib/bundler/templates/newgem/test/minitest/test_helper.rb.tt b/lib/bundler/templates/newgem/test/minitest/test_helper.rb.tt new file mode 100644 index 0000000000..e05c387bfa --- /dev/null +++ b/lib/bundler/templates/newgem/test/minitest/test_helper.rb.tt @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) +require "<%= config[:namespaced_path] %>" + +require "minitest/autorun" diff --git a/lib/bundler/templates/newgem/test/minitest/test_newgem.rb.tt b/lib/bundler/templates/newgem/test/minitest/test_newgem.rb.tt new file mode 100644 index 0000000000..844d3aff81 --- /dev/null +++ b/lib/bundler/templates/newgem/test/minitest/test_newgem.rb.tt @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "test_helper" + +class <%= config[:minitest_constant_name] %> < Minitest::Test + def test_that_it_has_a_version_number + refute_nil ::<%= config[:constant_name] %>::VERSION + end + +<%- if config[:ext] == 'rust' -%> + def test_hello_world + assert_equal "Hello world, from Rust!", <%= config[:constant_name] %>.hello("world") + end +<%- else -%> + def test_it_does_something_useful + assert false + end +<%- end -%> +end diff --git a/lib/bundler/templates/newgem/test/newgem_test.rb.tt b/lib/bundler/templates/newgem/test/newgem_test.rb.tt deleted file mode 100644 index f2af9f90e0..0000000000 --- a/lib/bundler/templates/newgem/test/newgem_test.rb.tt +++ /dev/null @@ -1,11 +0,0 @@ -require "test_helper" - -class <%= config[:constant_name] %>Test < Minitest::Test - def test_that_it_has_a_version_number - refute_nil ::<%= config[:constant_name] %>::VERSION - end - - def test_it_does_something_useful - assert false - end -end diff --git a/lib/bundler/templates/newgem/test/test-unit/newgem_test.rb.tt b/lib/bundler/templates/newgem/test/test-unit/newgem_test.rb.tt new file mode 100644 index 0000000000..5c61094e62 --- /dev/null +++ b/lib/bundler/templates/newgem/test/test-unit/newgem_test.rb.tt @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "test_helper" + +class <%= config[:constant_name] %>Test < Test::Unit::TestCase + test "VERSION" do + assert do + ::<%= config[:constant_name] %>.const_defined?(:VERSION) + end + end + + test "something useful" do + assert_equal("expected", "actual") + end +end diff --git a/lib/bundler/templates/newgem/test/test-unit/test_helper.rb.tt b/lib/bundler/templates/newgem/test/test-unit/test_helper.rb.tt new file mode 100644 index 0000000000..6f633c6039 --- /dev/null +++ b/lib/bundler/templates/newgem/test/test-unit/test_helper.rb.tt @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) +require "<%= config[:namespaced_path] %>" + +require "test-unit" diff --git a/lib/bundler/templates/newgem/test/test_helper.rb.tt b/lib/bundler/templates/newgem/test/test_helper.rb.tt deleted file mode 100644 index 725e3e4647..0000000000 --- a/lib/bundler/templates/newgem/test/test_helper.rb.tt +++ /dev/null @@ -1,4 +0,0 @@ -$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) -require "<%= config[:namespaced_path] %>" - -require "minitest/autorun" diff --git a/lib/bundler/templates/newgem/travis.yml.tt b/lib/bundler/templates/newgem/travis.yml.tt deleted file mode 100644 index fe0761cc23..0000000000 --- a/lib/bundler/templates/newgem/travis.yml.tt +++ /dev/null @@ -1,5 +0,0 @@ -sudo: false -language: ruby -rvm: - - <%= RUBY_VERSION %> -before_install: gem install bundler -v <%= Bundler::VERSION %> diff --git a/lib/bundler/ui.rb b/lib/bundler/ui.rb index 8138b30d38..7a4fa03669 100644 --- a/lib/bundler/ui.rb +++ b/lib/bundler/ui.rb @@ -2,8 +2,8 @@ module Bundler module UI - autoload :RGProxy, "bundler/ui/rg_proxy" - autoload :Shell, "bundler/ui/shell" - autoload :Silent, "bundler/ui/silent" + autoload :RGProxy, File.expand_path("ui/rg_proxy", __dir__) + autoload :Shell, File.expand_path("ui/shell", __dir__) + autoload :Silent, File.expand_path("ui/silent", __dir__) end end diff --git a/lib/bundler/ui/rg_proxy.rb b/lib/bundler/ui/rg_proxy.rb index e2f98481db..b17ca65f53 100644 --- a/lib/bundler/ui/rg_proxy.rb +++ b/lib/bundler/ui/rg_proxy.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "bundler/ui" +require_relative "../ui" require "rubygems/user_interaction" module Bundler @@ -12,7 +12,7 @@ module Bundler end def say(message) - @ui && @ui.debug(message) + @ui&.debug(message) end end end diff --git a/lib/bundler/ui/shell.rb b/lib/bundler/ui/shell.rb index 3b3b6bfb53..b836208da8 100644 --- a/lib/bundler/ui/shell.rb +++ b/lib/bundler/ui/shell.rb @@ -1,52 +1,75 @@ # frozen_string_literal: true -require "bundler/vendored_thor" +require_relative "../vendored_thor" module Bundler module UI class Shell LEVELS = %w[silent error warn confirm info debug].freeze + OUTPUT_STREAMS = [:stdout, :stderr].freeze attr_writer :shell + attr_reader :output_stream def initialize(options = {}) - if options["no-color"] || !$stdout.tty? - Thor::Base.shell = Thor::Shell::Basic - end + Thor::Base.shell = options["no-color"] ? Thor::Shell::Basic : nil @shell = Thor::Base.shell.new @level = ENV["DEBUG"] ? "debug" : "info" @warning_history = [] + @output_stream = :stdout + @thread_safe_logger_key = "logger_level_#{object_id}" end def add_color(string, *color) @shell.set_color(string, *color) end - def info(msg, newline = nil) - tell_me(msg, nil, newline) if level("info") + def info(msg = nil, newline = nil) + return unless info? + + tell_me(msg || yield, nil, newline) end - def confirm(msg, newline = nil) - tell_me(msg, :green, newline) if level("confirm") + def confirm(msg = nil, newline = nil) + return unless confirm? + + tell_me(msg || yield, :green, newline) end - def warn(msg, newline = nil) - return unless level("warn") + def warn(msg = nil, newline = nil, color = :yellow) + return unless warn? return if @warning_history.include? msg @warning_history << msg - return tell_err(msg, :yellow, newline) if Bundler.feature_flag.error_on_stderr? - tell_me(msg, :yellow, newline) + tell_err(msg || yield, color, newline) + end + + def error(msg = nil, newline = nil, color = :red) + return unless error? + + tell_err(msg || yield, color, newline) + end + + def debug(msg = nil, newline = nil) + return unless debug? + + tell_me(msg || yield, nil, newline) + end + + def info? + level("info") + end + + def confirm? + level("confirm") end - def error(msg, newline = nil) - return unless level("error") - return tell_err(msg, :red, newline) if Bundler.feature_flag.error_on_stderr? - tell_me(msg, :red, newline) + def warn? + level("warn") end - def debug(msg, newline = nil) - tell_me(msg, nil, newline) if debug? + def error? + level("error") end def debug? @@ -58,14 +81,14 @@ module Bundler end def ask(msg) - @shell.ask(msg) + @shell.ask(msg, :green) end def yes?(msg) - @shell.yes?(msg) + @shell.yes?(msg, :green) end - def no? + def no?(msg) @shell.no?(msg) end @@ -75,31 +98,44 @@ module Bundler end def level(name = nil) - return @level unless name + current_level = Thread.current.thread_variable_get(@thread_safe_logger_key) || @level + return current_level unless name + unless index = LEVELS.index(name) raise "#{name.inspect} is not a valid level" end - index <= LEVELS.index(@level) + index <= LEVELS.index(current_level) + end + + def output_stream=(symbol) + raise ArgumentError unless OUTPUT_STREAMS.include?(symbol) + @output_stream = symbol end def trace(e, newline = nil, force = false) return unless debug? || force msg = "#{e.class}: #{e.message}\n#{e.backtrace.join("\n ")}" - tell_me(msg, nil, newline) + tell_err(msg, nil, newline) end def silence(&blk) with_level("silent", &blk) end + def progress(&blk) + with_output_stream(:stderr, &blk) + end + def unprinted_warnings [] end - private + private # valimism def tell_me(msg, color = nil, newline = nil) + return tell_err(msg, color, newline) if output_stream == :stderr + msg = word_wrap(msg) if newline.is_a?(Hash) && newline[:wrap] if newline.nil? @shell.say(msg, color) @@ -109,7 +145,9 @@ module Bundler end def tell_err(message, color = nil, newline = nil) - newline = message.to_s !~ /( |\t)\Z/ unless newline + return if @shell.send(:stderr).closed? + + newline = !message.to_s.match?(/( |\t)\Z/) if newline.nil? message = word_wrap(message) if newline.is_a?(Hash) && newline[:wrap] color = nil if color && !$stderr.tty? @@ -126,18 +164,27 @@ module Bundler spaces ? text.gsub(/#{spaces}/, "") : text end - def word_wrap(text, line_width = @shell.terminal_width) + def word_wrap(text, line_width = Thor::Terminal.terminal_width) strip_leading_spaces(text).split("\n").collect do |line| line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line end * "\n" end - def with_level(level) - original = @level - @level = level + def with_level(desired_level) + old_level = level + Thread.current.thread_variable_set(@thread_safe_logger_key, desired_level) + + yield + ensure + Thread.current.thread_variable_set(@thread_safe_logger_key, old_level) + end + + def with_output_stream(symbol) + original = output_stream + self.output_stream = symbol yield ensure - @level = original + @output_stream = original end end end diff --git a/lib/bundler/ui/silent.rb b/lib/bundler/ui/silent.rb index dca1b2ac86..83d31d4b55 100644 --- a/lib/bundler/ui/silent.rb +++ b/lib/bundler/ui/silent.rb @@ -13,30 +13,53 @@ module Bundler string end - def info(message, newline = nil) + def info(message = nil, newline = nil) end - def confirm(message, newline = nil) + def confirm(message = nil, newline = nil) end - def warn(message, newline = nil) + def warn(message = nil, newline = nil) @warnings |= [message] end - def error(message, newline = nil) + def error(message = nil, newline = nil) end - def debug(message, newline = nil) + def debug(message = nil, newline = nil) + end + + def confirm? + false + end + + def error? + false end def debug? false end + def info? + false + end + def quiet? false end + def warn? + false + end + + def output_stream=(_symbol) + end + + def output_stream + nil + end + def ask(message) end @@ -44,7 +67,7 @@ module Bundler raise "Cannot ask yes? with a silent shell" end - def no? + def no?(msg) raise "Cannot ask no? with a silent shell" end @@ -61,6 +84,10 @@ module Bundler yield end + def progress + yield + end + def unprinted_warnings @warnings end diff --git a/lib/bundler/uri_credentials_filter.rb b/lib/bundler/uri_credentials_filter.rb index ee3692268c..6804187433 100644 --- a/lib/bundler/uri_credentials_filter.rb +++ b/lib/bundler/uri_credentials_filter.rb @@ -2,24 +2,30 @@ module Bundler module URICredentialsFilter - module_function + module_function def credential_filtered_uri(uri_to_anonymize) return uri_to_anonymize if uri_to_anonymize.nil? uri = uri_to_anonymize.dup - uri = URI(uri.to_s) unless uri.is_a?(URI) + if uri.is_a?(String) + return uri if File.exist?(uri) + + require_relative "vendored_uri" + uri = Gem::URI(uri) + end + if uri.userinfo # oauth authentication - if uri.password == "x-oauth-basic" || uri.password == "x" + if uri.password == "x-oauth-basic" || uri.password == "x" || uri.password.nil? # URI as string does not display with password if no user is set oauth_designation = uri.password uri.user = oauth_designation end uri.password = nil end - return uri if uri_to_anonymize.is_a?(URI) return uri.to_s if uri_to_anonymize.is_a?(String) - rescue URI::InvalidURIError # uri is not canonical uri scheme + uri + rescue Gem::URI::InvalidURIError # uri is not canonical uri scheme uri end diff --git a/lib/bundler/uri_normalizer.rb b/lib/bundler/uri_normalizer.rb new file mode 100644 index 0000000000..ad08593256 --- /dev/null +++ b/lib/bundler/uri_normalizer.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Bundler + module URINormalizer + module_function + + # Normalizes uri to a consistent version, either with or without trailing + # slash. + # + # TODO: Currently gem sources are locked with a trailing slash, while git + # sources are locked without a trailing slash. This should be normalized but + # the inconsistency is there for now to avoid changing all lockfiles + # including GIT sources. We could normalize this on the next major. + # + def normalize_suffix(uri, trailing_slash: true) + if trailing_slash + uri.end_with?("/") ? uri : "#{uri}/" + else + uri.end_with?("/") ? uri.delete_suffix("/") : uri + end + end + end +end diff --git a/lib/bundler/vendor/.document b/lib/bundler/vendor/.document new file mode 100644 index 0000000000..0c43bbd6b3 --- /dev/null +++ b/lib/bundler/vendor/.document @@ -0,0 +1 @@ +# Vendored files do not need to be documented diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool.rb new file mode 100644 index 0000000000..e8aaf70016 --- /dev/null +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool.rb @@ -0,0 +1,233 @@ +require_relative "../../../vendored_timeout" +require_relative "connection_pool/version" + +class Bundler::ConnectionPool + class Error < ::RuntimeError; end + + class PoolShuttingDownError < ::Bundler::ConnectionPool::Error; end + + class TimeoutError < ::Gem::Timeout::Error; end +end + +# Generic connection pool class for sharing a limited number of objects or network connections +# among many threads. Note: pool elements are lazily created. +# +# Example usage with block (faster): +# +# @pool = Bundler::ConnectionPool.new { Redis.new } +# @pool.with do |redis| +# redis.lpop('my-list') if redis.llen('my-list') > 0 +# end +# +# Using optional timeout override (for that single invocation) +# +# @pool.with(timeout: 2.0) do |redis| +# redis.lpop('my-list') if redis.llen('my-list') > 0 +# end +# +# Example usage replacing an existing connection (slower): +# +# $redis = Bundler::ConnectionPool.wrap { Redis.new } +# +# def do_work +# $redis.lpop('my-list') if $redis.llen('my-list') > 0 +# end +# +# Accepts the following options: +# - :size - number of connections to pool, defaults to 5 +# - :timeout - amount of time to wait for a connection if none currently available, defaults to 5 seconds +# - :auto_reload_after_fork - automatically drop all connections after fork, defaults to true +# +class Bundler::ConnectionPool + DEFAULTS = {size: 5, timeout: 5, auto_reload_after_fork: true}.freeze + + def self.wrap(options, &block) + Wrapper.new(options, &block) + end + + if Process.respond_to?(:fork) + INSTANCES = ObjectSpace::WeakMap.new + private_constant :INSTANCES + + def self.after_fork + INSTANCES.values.each do |pool| + next unless pool.auto_reload_after_fork + + # We're on after fork, so we know all other threads are dead. + # All we need to do is to ensure the main thread doesn't have a + # checked out connection + pool.checkin(force: true) + pool.reload do |connection| + # Unfortunately we don't know what method to call to close the connection, + # so we try the most common one. + connection.close if connection.respond_to?(:close) + end + end + nil + end + + if ::Process.respond_to?(:_fork) # MRI 3.1+ + module ForkTracker + def _fork + pid = super + if pid == 0 + Bundler::ConnectionPool.after_fork + end + pid + end + end + Process.singleton_class.prepend(ForkTracker) + end + else + INSTANCES = nil + private_constant :INSTANCES + + def self.after_fork + # noop + end + end + + def initialize(options = {}, &block) + raise ArgumentError, "Connection pool requires a block" unless block + + options = DEFAULTS.merge(options) + + @size = Integer(options.fetch(:size)) + @timeout = options.fetch(:timeout) + @auto_reload_after_fork = options.fetch(:auto_reload_after_fork) + + @available = TimedStack.new(@size, &block) + @key = :"pool-#{@available.object_id}" + @key_count = :"pool-#{@available.object_id}-count" + @discard_key = :"pool-#{@available.object_id}-discard" + INSTANCES[self] = self if @auto_reload_after_fork && INSTANCES + end + + def with(options = {}) + # We need to manage exception handling manually here in order + # to work correctly with `Gem::Timeout.timeout` and `Thread#raise`. + # Otherwise an interrupted Thread can leak connections. + Thread.handle_interrupt(Exception => :never) do + conn = checkout(options) + begin + Thread.handle_interrupt(Exception => :immediate) do + yield conn + end + ensure + checkin + end + end + end + alias_method :then, :with + + ## + # Marks the current thread's checked-out connection for discard. + # + # When a connection is marked for discard, it will not be returned to the pool + # when checked in. Instead, the connection will be discarded. + # This is useful when a connection has become invalid or corrupted + # and should not be reused. + # + # Takes an optional block that will be called with the connection to be discarded. + # The block should perform any necessary clean-up on the connection. + # + # @yield [conn] + # @yieldparam conn [Object] The connection to be discarded. + # @yieldreturn [void] + # + # + # Note: This only affects the connection currently checked out by the calling thread. + # The connection will be discarded when +checkin+ is called. + # + # @return [void] + # + # @example + # pool.with do |conn| + # begin + # conn.execute("SELECT 1") + # rescue SomeConnectionError + # pool.discard_current_connection # Mark connection as bad + # raise + # end + # end + def discard_current_connection(&block) + ::Thread.current[@discard_key] = block || proc { |conn| conn } + end + + def checkout(options = {}) + if ::Thread.current[@key] + ::Thread.current[@key_count] += 1 + ::Thread.current[@key] + else + ::Thread.current[@key_count] = 1 + ::Thread.current[@key] = @available.pop(options[:timeout] || @timeout, options) + end + end + + def checkin(force: false) + if ::Thread.current[@key] + if ::Thread.current[@key_count] == 1 || force + if ::Thread.current[@discard_key] + begin + @available.decrement_created + ::Thread.current[@discard_key].call(::Thread.current[@key]) + rescue + nil + ensure + ::Thread.current[@discard_key] = nil + end + else + @available.push(::Thread.current[@key]) + end + ::Thread.current[@key] = nil + ::Thread.current[@key_count] = nil + else + ::Thread.current[@key_count] -= 1 + end + elsif !force + raise Bundler::ConnectionPool::Error, "no connections are checked out" + end + + nil + end + + ## + # Shuts down the Bundler::ConnectionPool by passing each connection to +block+ and + # then removing it from the pool. Attempting to checkout a connection after + # shutdown will raise +Bundler::ConnectionPool::PoolShuttingDownError+. + def shutdown(&block) + @available.shutdown(&block) + end + + ## + # Reloads the Bundler::ConnectionPool by passing each connection to +block+ and then + # removing it the pool. Subsequent checkouts will create new connections as + # needed. + def reload(&block) + @available.shutdown(reload: true, &block) + end + + ## Reaps idle connections that have been idle for over +idle_seconds+. + # +idle_seconds+ defaults to 60. + def reap(idle_seconds = 60, &block) + @available.reap(idle_seconds, &block) + end + + # Size of this connection pool + attr_reader :size + # Automatically drop all connections after fork + attr_reader :auto_reload_after_fork + + # Number of pool entries available for checkout at this instant. + def available + @available.length + end + + # Number of pool entries created and idle in the pool. + def idle + @available.idle + end +end + +require_relative "connection_pool/timed_stack" +require_relative "connection_pool/wrapper" diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb new file mode 100644 index 0000000000..026d2c5be2 --- /dev/null +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb @@ -0,0 +1,237 @@ +## +# The TimedStack manages a pool of homogeneous connections (or any resource +# you wish to manage). Connections are created lazily up to a given maximum +# number. +# +# Examples: +# +# ts = TimedStack.new(1) { MyConnection.new } +# +# # fetch a connection +# conn = ts.pop +# +# # return a connection +# ts.push conn +# +# conn = ts.pop +# ts.pop timeout: 5 +# #=> raises Bundler::ConnectionPool::TimeoutError after 5 seconds +class Bundler::ConnectionPool::TimedStack + attr_reader :max + + ## + # Creates a new pool with +size+ connections that are created from the given + # +block+. + def initialize(size = 0, &block) + @create_block = block + @created = 0 + @que = [] + @max = size + @mutex = Thread::Mutex.new + @resource = Thread::ConditionVariable.new + @shutdown_block = nil + end + + ## + # Returns +obj+ to the stack. +options+ is ignored in TimedStack but may be + # used by subclasses that extend TimedStack. + def push(obj, options = {}) + @mutex.synchronize do + if @shutdown_block + @created -= 1 unless @created == 0 + @shutdown_block.call(obj) + else + store_connection obj, options + end + + @resource.broadcast + end + end + alias_method :<<, :push + + ## + # Retrieves a connection from the stack. If a connection is available it is + # immediately returned. If no connection is available within the given + # timeout a Bundler::ConnectionPool::TimeoutError is raised. + # + # @option options [Float] :timeout (0.5) Wait this many seconds for an available entry + # @option options [Class] :exception (Bundler::ConnectionPool::TimeoutError) Exception class to raise + # if an entry was not available within the timeout period. Use `exception: false` to return nil. + # + # The +timeout+ argument will be removed in 3.0. + # Other options may be used by subclasses that extend TimedStack. + def pop(timeout = 0.5, options = {}) + options, timeout = timeout, 0.5 if Hash === timeout + timeout = options.fetch :timeout, timeout + + deadline = current_time + timeout + @mutex.synchronize do + loop do + raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block + if (conn = try_fetch_connection(options)) + return conn + end + + connection = try_create(options) + return connection if connection + + to_wait = deadline - current_time + if to_wait <= 0 + exc = options.fetch(:exception, Bundler::ConnectionPool::TimeoutError) + if exc + raise Bundler::ConnectionPool::TimeoutError, "Waited #{timeout} sec, #{length}/#{@max} available" + else + return nil + end + end + @resource.wait(@mutex, to_wait) + end + end + end + + ## + # Shuts down the TimedStack by passing each connection to +block+ and then + # removing it from the pool. Attempting to checkout a connection after + # shutdown will raise +Bundler::ConnectionPool::PoolShuttingDownError+ unless + # +:reload+ is +true+. + def shutdown(reload: false, &block) + raise ArgumentError, "shutdown must receive a block" unless block + + @mutex.synchronize do + @shutdown_block = block + @resource.broadcast + + shutdown_connections + @shutdown_block = nil if reload + end + end + + ## + # Reaps connections that were checked in more than +idle_seconds+ ago. + def reap(idle_seconds, &block) + raise ArgumentError, "reap must receive a block" unless block + raise ArgumentError, "idle_seconds must be a number" unless idle_seconds.is_a?(Numeric) + raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block + + idle.times do + conn = + @mutex.synchronize do + raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block + + reserve_idle_connection(idle_seconds) + end + break unless conn + + block.call(conn) + end + end + + ## + # Returns +true+ if there are no available connections. + def empty? + (@created - @que.length) >= @max + end + + ## + # The number of connections available on the stack. + def length + @max - @created + @que.length + end + + ## + # The number of connections created and available on the stack. + def idle + @que.length + end + + ## + # Reduce the created count + def decrement_created + @created -= 1 unless @created == 0 + end + + private + + def current_time + Process.clock_gettime(Process::CLOCK_MONOTONIC) + end + + ## + # This is an extension point for TimedStack and is called with a mutex. + # + # This method must returns a connection from the stack if one exists. Allows + # subclasses with expensive match/search algorithms to avoid double-handling + # their stack. + def try_fetch_connection(options = nil) + connection_stored?(options) && fetch_connection(options) + end + + ## + # This is an extension point for TimedStack and is called with a mutex. + # + # This method must returns true if a connection is available on the stack. + def connection_stored?(options = nil) + !@que.empty? + end + + ## + # This is an extension point for TimedStack and is called with a mutex. + # + # This method must return a connection from the stack. + def fetch_connection(options = nil) + @que.pop&.first + end + + ## + # This is an extension point for TimedStack and is called with a mutex. + # + # This method must shut down all connections on the stack. + def shutdown_connections(options = nil) + while (conn = try_fetch_connection(options)) + @created -= 1 unless @created == 0 + @shutdown_block.call(conn) + end + end + + ## + # This is an extension point for TimedStack and is called with a mutex. + # + # This method returns the oldest idle connection if it has been idle for more than idle_seconds. + # This requires that the stack is kept in order of checked in time (oldest first). + def reserve_idle_connection(idle_seconds) + return unless idle_connections?(idle_seconds) + + @created -= 1 unless @created == 0 + + @que.shift.first + end + + ## + # This is an extension point for TimedStack and is called with a mutex. + # + # Returns true if the first connection in the stack has been idle for more than idle_seconds + def idle_connections?(idle_seconds) + connection_stored? && (current_time - @que.first.last > idle_seconds) + end + + ## + # This is an extension point for TimedStack and is called with a mutex. + # + # This method must return +obj+ to the stack. + def store_connection(obj, options = nil) + @que.push [obj, current_time] + end + + ## + # This is an extension point for TimedStack and is called with a mutex. + # + # This method must create a connection if and only if the total number of + # connections allowed has not been met. + def try_create(options = nil) + unless @created == @max + object = @create_block.call + @created += 1 + object + end + end +end diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb new file mode 100644 index 0000000000..2e9eebdbb6 --- /dev/null +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb @@ -0,0 +1,3 @@ +class Bundler::ConnectionPool + VERSION = "2.5.5" +end diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb new file mode 100644 index 0000000000..dd796d1021 --- /dev/null +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb @@ -0,0 +1,56 @@ +class Bundler::ConnectionPool + class Wrapper < ::BasicObject + METHODS = [:with, :pool_shutdown, :wrapped_pool] + + def initialize(options = {}, &block) + @pool = options.fetch(:pool) { ::Bundler::ConnectionPool.new(options, &block) } + end + + def wrapped_pool + @pool + end + + def with(&block) + @pool.with(&block) + end + + def pool_shutdown(&block) + @pool.shutdown(&block) + end + + def pool_size + @pool.size + end + + def pool_available + @pool.available + end + + def respond_to?(id, *args) + METHODS.include?(id) || with { |c| c.respond_to?(id, *args) } + end + + # rubocop:disable Style/MissingRespondToMissing + if ::RUBY_VERSION >= "3.0.0" + def method_missing(name, *args, **kwargs, &block) + with do |connection| + connection.send(name, *args, **kwargs, &block) + end + end + elsif ::RUBY_VERSION >= "2.7.0" + ruby2_keywords def method_missing(name, *args, &block) + with do |connection| + connection.send(name, *args, &block) + end + end + else + def method_missing(name, *args, &block) + with do |connection| + connection.send(name, *args, &block) + end + end + end + # rubocop:enable Style/MethodMissingSuper + # rubocop:enable Style/MissingRespondToMissing + end +end diff --git a/lib/bundler/vendor/fileutils/lib/fileutils.rb b/lib/bundler/vendor/fileutils/lib/fileutils.rb index cc69740845..a11fdc7176 100644 --- a/lib/bundler/vendor/fileutils/lib/fileutils.rb +++ b/lib/bundler/vendor/fileutils/lib/fileutils.rb @@ -1,91 +1,187 @@ # frozen_string_literal: true + +begin + require 'rbconfig' +rescue LoadError + # for make rjit-headers +end + +# Namespace for file utility methods for copying, moving, removing, etc. # -# = fileutils.rb +# == What's Here # -# Copyright (c) 2000-2007 Minero Aoki +# First, what’s elsewhere. \Module \Bundler::FileUtils: # -# This program is free software. -# You can distribute/modify this program under the same terms of ruby. +# - Inherits from {class Object}[rdoc-ref:Object]. +# - Supplements {class File}[rdoc-ref:File] +# (but is not included or extended there). # -# == module Bundler::FileUtils +# Here, module \Bundler::FileUtils provides methods that are useful for: # -# Namespace for several file utility methods for copying, moving, removing, etc. +# - {Creating}[rdoc-ref:FileUtils@Creating]. +# - {Deleting}[rdoc-ref:FileUtils@Deleting]. +# - {Querying}[rdoc-ref:FileUtils@Querying]. +# - {Setting}[rdoc-ref:FileUtils@Setting]. +# - {Comparing}[rdoc-ref:FileUtils@Comparing]. +# - {Copying}[rdoc-ref:FileUtils@Copying]. +# - {Moving}[rdoc-ref:FileUtils@Moving]. +# - {Options}[rdoc-ref:FileUtils@Options]. # -# === Module Functions +# === Creating # -# require 'bundler/vendor/fileutils/lib/fileutils' +# - ::mkdir: Creates directories. +# - ::mkdir_p, ::makedirs, ::mkpath: Creates directories, +# also creating ancestor directories as needed. +# - ::link_entry: Creates a hard link. +# - ::ln, ::link: Creates hard links. +# - ::ln_s, ::symlink: Creates symbolic links. +# - ::ln_sf: Creates symbolic links, overwriting if necessary. +# - ::ln_sr: Creates symbolic links relative to targets # -# Bundler::FileUtils.cd(dir, options) -# Bundler::FileUtils.cd(dir, options) {|dir| block } -# Bundler::FileUtils.pwd() -# Bundler::FileUtils.mkdir(dir, options) -# Bundler::FileUtils.mkdir(list, options) -# Bundler::FileUtils.mkdir_p(dir, options) -# Bundler::FileUtils.mkdir_p(list, options) -# Bundler::FileUtils.rmdir(dir, options) -# Bundler::FileUtils.rmdir(list, options) -# Bundler::FileUtils.ln(target, link, options) -# Bundler::FileUtils.ln(targets, dir, options) -# Bundler::FileUtils.ln_s(target, link, options) -# Bundler::FileUtils.ln_s(targets, dir, options) -# Bundler::FileUtils.ln_sf(target, link, options) -# Bundler::FileUtils.cp(src, dest, options) -# Bundler::FileUtils.cp(list, dir, options) -# Bundler::FileUtils.cp_r(src, dest, options) -# Bundler::FileUtils.cp_r(list, dir, options) -# Bundler::FileUtils.mv(src, dest, options) -# Bundler::FileUtils.mv(list, dir, options) -# Bundler::FileUtils.rm(list, options) -# Bundler::FileUtils.rm_r(list, options) -# Bundler::FileUtils.rm_rf(list, options) -# Bundler::FileUtils.install(src, dest, options) -# Bundler::FileUtils.chmod(mode, list, options) -# Bundler::FileUtils.chmod_R(mode, list, options) -# Bundler::FileUtils.chown(user, group, list, options) -# Bundler::FileUtils.chown_R(user, group, list, options) -# Bundler::FileUtils.touch(list, options) +# === Deleting # -# The <tt>options</tt> parameter is a hash of options, taken from the list -# <tt>:force</tt>, <tt>:noop</tt>, <tt>:preserve</tt>, and <tt>:verbose</tt>. -# <tt>:noop</tt> means that no changes are made. The other three are obvious. -# Each method documents the options that it honours. +# - ::remove_dir: Removes a directory and its descendants. +# - ::remove_entry: Removes an entry, including its descendants if it is a directory. +# - ::remove_entry_secure: Like ::remove_entry, but removes securely. +# - ::remove_file: Removes a file entry. +# - ::rm, ::remove: Removes entries. +# - ::rm_f, ::safe_unlink: Like ::rm, but removes forcibly. +# - ::rm_r: Removes entries and their descendants. +# - ::rm_rf, ::rmtree: Like ::rm_r, but removes forcibly. +# - ::rmdir: Removes directories. # -# All methods that have the concept of a "source" file or directory can take -# either one file or a list of files in that argument. See the method -# documentation for examples. +# === Querying # -# There are some `low level' methods, which do not accept any option: +# - ::pwd, ::getwd: Returns the path to the working directory. +# - ::uptodate?: Returns whether a given entry is newer than given other entries. # -# Bundler::FileUtils.copy_entry(src, dest, preserve = false, dereference = false) -# Bundler::FileUtils.copy_file(src, dest, preserve = false, dereference = true) -# Bundler::FileUtils.copy_stream(srcstream, deststream) -# Bundler::FileUtils.remove_entry(path, force = false) -# Bundler::FileUtils.remove_entry_secure(path, force = false) -# Bundler::FileUtils.remove_file(path, force = false) -# Bundler::FileUtils.compare_file(path_a, path_b) -# Bundler::FileUtils.compare_stream(stream_a, stream_b) -# Bundler::FileUtils.uptodate?(file, cmp_list) +# === Setting # -# == module Bundler::FileUtils::Verbose +# - ::cd, ::chdir: Sets the working directory. +# - ::chmod: Sets permissions for an entry. +# - ::chmod_R: Sets permissions for an entry and its descendants. +# - ::chown: Sets the owner and group for entries. +# - ::chown_R: Sets the owner and group for entries and their descendants. +# - ::touch: Sets modification and access times for entries, +# creating if necessary. # -# This module has all methods of Bundler::FileUtils module, but it outputs messages -# before acting. This equates to passing the <tt>:verbose</tt> flag to methods -# in Bundler::FileUtils. +# === Comparing # -# == module Bundler::FileUtils::NoWrite +# - ::compare_file, ::cmp, ::identical?: Returns whether two entries are identical. +# - ::compare_stream: Returns whether two streams are identical. # -# This module has all methods of Bundler::FileUtils module, but never changes -# files/directories. This equates to passing the <tt>:noop</tt> flag to methods -# in Bundler::FileUtils. +# === Copying # -# == module Bundler::FileUtils::DryRun +# - ::copy_entry: Recursively copies an entry. +# - ::copy_file: Copies an entry. +# - ::copy_stream: Copies a stream. +# - ::cp, ::copy: Copies files. +# - ::cp_lr: Recursively creates hard links. +# - ::cp_r: Recursively copies files, retaining mode, owner, and group. +# - ::install: Recursively copies files, optionally setting mode, +# owner, and group. # -# This module has all methods of Bundler::FileUtils module, but never changes -# files/directories. This equates to passing the <tt>:noop</tt> and -# <tt>:verbose</tt> flags to methods in Bundler::FileUtils. +# === Moving +# +# - ::mv, ::move: Moves entries. +# +# === Options +# +# - ::collect_method: Returns the names of methods that accept a given option. +# - ::commands: Returns the names of methods that accept options. +# - ::have_option?: Returns whether a given method accepts a given option. +# - ::options: Returns all option names. +# - ::options_of: Returns the names of the options for a given method. +# +# == Path Arguments +# +# Some methods in \Bundler::FileUtils accept _path_ arguments, +# which are interpreted as paths to filesystem entries: +# +# - If the argument is a string, that value is the path. +# - If the argument has method +:to_path+, it is converted via that method. +# - If the argument has method +:to_str+, it is converted via that method. +# +# == About the Examples +# +# Some examples here involve trees of file entries. +# For these, we sometimes display trees using the +# {tree command-line utility}[https://en.wikipedia.org/wiki/Tree_(command)], +# which is a recursive directory-listing utility that produces +# a depth-indented listing of files and directories. +# +# We use a helper method to launch the command and control the format: +# +# def tree(dirpath = '.') +# command = "tree --noreport --charset=ascii #{dirpath}" +# system(command) +# end +# +# To illustrate: +# +# tree('src0') +# # => src0 +# # |-- sub0 +# # | |-- src0.txt +# # | `-- src1.txt +# # `-- sub1 +# # |-- src2.txt +# # `-- src3.txt +# +# == Avoiding the TOCTTOU Vulnerability +# +# For certain methods that recursively remove entries, +# there is a potential vulnerability called the +# {Time-of-check to time-of-use}[https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use], +# or TOCTTOU, vulnerability that can exist when: +# +# - An ancestor directory of the entry at the target path is world writable; +# such directories include <tt>/tmp</tt>. +# - The directory tree at the target path includes: +# +# - A world-writable descendant directory. +# - A symbolic link. +# +# To avoid that vulnerability, you can use this method to remove entries: +# +# - Bundler::FileUtils.remove_entry_secure: removes recursively +# if the target path points to a directory. +# +# Also available are these methods, +# each of which calls \Bundler::FileUtils.remove_entry_secure: +# +# - Bundler::FileUtils.rm_r with keyword argument <tt>secure: true</tt>. +# - Bundler::FileUtils.rm_rf with keyword argument <tt>secure: true</tt>. +# +# Finally, this method for moving entries calls \Bundler::FileUtils.remove_entry_secure +# if the source and destination are on different file systems +# (which means that the "move" is really a copy and remove): +# +# - Bundler::FileUtils.mv with keyword argument <tt>secure: true</tt>. +# +# \Method \Bundler::FileUtils.remove_entry_secure removes securely +# by applying a special pre-process: +# +# - If the target path points to a directory, this method uses methods +# {File#chown}[rdoc-ref:File#chown] +# and {File#chmod}[rdoc-ref:File#chmod] +# in removing directories. +# - The owner of the target directory should be either the current process +# or the super user (root). +# +# WARNING: You must ensure that *ALL* parent directories cannot be +# moved by other untrusted users. For example, parent directories +# should not be owned by untrusted users, and should not be world +# writable except when the sticky bit is set. +# +# For details of this security vulnerability, see Perl cases: +# +# - {CVE-2005-0448}[https://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-0448]. +# - {CVE-2004-0452}[https://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2004-0452]. # - module Bundler::FileUtils + # The version number. + VERSION = "1.8.0" def self.private_module_function(name) #:nodoc: module_function name @@ -93,7 +189,11 @@ module Bundler::FileUtils end # - # Returns the name of the current directory. + # Returns a string containing the path to the current directory: + # + # Bundler::FileUtils.pwd # => "/rdoc/fileutils" + # + # Related: Bundler::FileUtils.cd. # def pwd Dir.pwd @@ -103,22 +203,44 @@ module Bundler::FileUtils alias getwd pwd module_function :getwd + # Changes the working directory to the given +dir+, which + # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]: + # + # With no block given, + # changes the current directory to the directory at +dir+; returns zero: + # + # Bundler::FileUtils.pwd # => "/rdoc/fileutils" + # Bundler::FileUtils.cd('..') + # Bundler::FileUtils.pwd # => "/rdoc" + # Bundler::FileUtils.cd('fileutils') + # + # With a block given, changes the current directory to the directory + # at +dir+, calls the block with argument +dir+, + # and restores the original current directory; returns the block's value: + # + # Bundler::FileUtils.pwd # => "/rdoc/fileutils" + # Bundler::FileUtils.cd('..') { |arg| [arg, Bundler::FileUtils.pwd] } # => ["..", "/rdoc"] + # Bundler::FileUtils.pwd # => "/rdoc/fileutils" # - # Changes the current directory to the directory +dir+. + # Keyword arguments: # - # If this method is called with block, resumes to the old - # working directory after the block execution finished. + # - <tt>verbose: true</tt> - prints an equivalent command: # - # Bundler::FileUtils.cd('/', :verbose => true) # chdir and report it + # Bundler::FileUtils.cd('..') + # Bundler::FileUtils.cd('fileutils') # - # Bundler::FileUtils.cd('/') do # chdir - # # ... # do something - # end # return to original directory + # Output: + # + # cd .. + # cd fileutils + # + # Related: Bundler::FileUtils.pwd. # def cd(dir, verbose: nil, &block) # :yield: dir fu_output_message "cd #{dir}" if verbose - Dir.chdir(dir, &block) + result = Dir.chdir(dir, &block) fu_output_message 'cd -' if verbose and block + result end module_function :cd @@ -126,11 +248,19 @@ module Bundler::FileUtils module_function :chdir # - # Returns true if +new+ is newer than all +old_list+. - # Non-existent files are older than any file. + # Returns +true+ if the file at path +new+ + # is newer than all the files at paths in array +old_list+; + # +false+ otherwise. + # + # Argument +new+ and the elements of +old_list+ + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]: + # + # Bundler::FileUtils.uptodate?('Rakefile', ['Gemfile', 'README.md']) # => true + # Bundler::FileUtils.uptodate?('Gemfile', ['Rakefile', 'README.md']) # => false + # + # A non-existent file is considered to be infinitely old. # - # Bundler::FileUtils.uptodate?('hello.o', %w(hello.c hello.h)) or \ - # system 'make hello.o' + # Related: Bundler::FileUtils.touch. # def uptodate?(new, old_list) return false unless File.exist?(new) @@ -150,12 +280,39 @@ module Bundler::FileUtils private_module_function :remove_trailing_slash # - # Creates one or more directories. + # Creates directories at the paths in the given +list+ + # (a single path or an array of paths); + # returns +list+ if it is an array, <tt>[list]</tt> otherwise. # - # Bundler::FileUtils.mkdir 'test' - # Bundler::FileUtils.mkdir %w( tmp data ) - # Bundler::FileUtils.mkdir 'notexist', :noop => true # Does not really create. - # Bundler::FileUtils.mkdir 'tmp', :mode => 0700 + # Argument +list+ or its elements + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. + # + # With no keyword arguments, creates a directory at each +path+ in +list+ + # by calling: <tt>Dir.mkdir(path, mode)</tt>; + # see {Dir.mkdir}[rdoc-ref:Dir.mkdir]: + # + # Bundler::FileUtils.mkdir(%w[tmp0 tmp1]) # => ["tmp0", "tmp1"] + # Bundler::FileUtils.mkdir('tmp4') # => ["tmp4"] + # + # Keyword arguments: + # + # - <tt>mode: <i>mode</i></tt> - also calls <tt>File.chmod(mode, path)</tt>; + # see {File.chmod}[rdoc-ref:File.chmod]. + # - <tt>noop: true</tt> - does not create directories. + # - <tt>verbose: true</tt> - prints an equivalent command: + # + # Bundler::FileUtils.mkdir(%w[tmp0 tmp1], verbose: true) + # Bundler::FileUtils.mkdir(%w[tmp2 tmp3], mode: 0700, verbose: true) + # + # Output: + # + # mkdir tmp0 tmp1 + # mkdir -m 700 tmp2 tmp3 + # + # Raises an exception if any path points to an existing + # file or directory, or if for any reason a directory cannot be created. + # + # Related: Bundler::FileUtils.mkdir_p. # def mkdir(list, mode: nil, noop: nil, verbose: nil) list = fu_list(list) @@ -169,40 +326,56 @@ module Bundler::FileUtils module_function :mkdir # - # Creates a directory and all its parent directories. - # For example, + # Creates directories at the paths in the given +list+ + # (a single path or an array of paths), + # also creating ancestor directories as needed; + # returns +list+ if it is an array, <tt>[list]</tt> otherwise. + # + # Argument +list+ or its elements + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. + # + # With no keyword arguments, creates a directory at each +path+ in +list+, + # along with any needed ancestor directories, + # by calling: <tt>Dir.mkdir(path, mode)</tt>; + # see {Dir.mkdir}[rdoc-ref:Dir.mkdir]: # - # Bundler::FileUtils.mkdir_p '/usr/local/lib/ruby' + # Bundler::FileUtils.mkdir_p(%w[tmp0/tmp1 tmp2/tmp3]) # => ["tmp0/tmp1", "tmp2/tmp3"] + # Bundler::FileUtils.mkdir_p('tmp4/tmp5') # => ["tmp4/tmp5"] # - # causes to make following directories, if it does not exist. + # Keyword arguments: # - # * /usr - # * /usr/local - # * /usr/local/lib - # * /usr/local/lib/ruby + # - <tt>mode: <i>mode</i></tt> - also calls <tt>File.chmod(mode, path)</tt>; + # see {File.chmod}[rdoc-ref:File.chmod]. + # - <tt>noop: true</tt> - does not create directories. + # - <tt>verbose: true</tt> - prints an equivalent command: # - # You can pass several directories at a time in a list. + # Bundler::FileUtils.mkdir_p(%w[tmp0 tmp1], verbose: true) + # Bundler::FileUtils.mkdir_p(%w[tmp2 tmp3], mode: 0700, verbose: true) + # + # Output: + # + # mkdir -p tmp0 tmp1 + # mkdir -p -m 700 tmp2 tmp3 + # + # Raises an exception if for any reason a directory cannot be created. + # + # Bundler::FileUtils.mkpath and Bundler::FileUtils.makedirs are aliases for Bundler::FileUtils.mkdir_p. + # + # Related: Bundler::FileUtils.mkdir. # def mkdir_p(list, mode: nil, noop: nil, verbose: nil) list = fu_list(list) fu_output_message "mkdir -p #{mode ? ('-m %03o ' % mode) : ''}#{list.join ' '}" if verbose return *list if noop - list.map {|path| remove_trailing_slash(path)}.each do |path| - # optimize for the most common case - begin - fu_mkdir path, mode - next - rescue SystemCallError - next if File.directory?(path) - end + list.each do |item| + path = remove_trailing_slash(item) stack = [] - until path == stack.last # dirname("/")=="/", dirname("C:/")=="C:/" + until File.directory?(path) || File.dirname(path) == path stack.push path path = File.dirname(path) end - stack.pop # root directory should exist stack.reverse_each do |dir| begin fu_mkdir dir, mode @@ -233,52 +406,113 @@ module Bundler::FileUtils private_module_function :fu_mkdir # - # Removes one or more directories. + # Removes directories at the paths in the given +list+ + # (a single path or an array of paths); + # returns +list+, if it is an array, <tt>[list]</tt> otherwise. + # + # Argument +list+ or its elements + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. + # + # With no keyword arguments, removes the directory at each +path+ in +list+, + # by calling: <tt>Dir.rmdir(path)</tt>; + # see {Dir.rmdir}[rdoc-ref:Dir.rmdir]: # - # Bundler::FileUtils.rmdir 'somedir' - # Bundler::FileUtils.rmdir %w(somedir anydir otherdir) - # # Does not really remove directory; outputs message. - # Bundler::FileUtils.rmdir 'somedir', :verbose => true, :noop => true + # Bundler::FileUtils.rmdir(%w[tmp0/tmp1 tmp2/tmp3]) # => ["tmp0/tmp1", "tmp2/tmp3"] + # Bundler::FileUtils.rmdir('tmp4/tmp5') # => ["tmp4/tmp5"] + # + # Keyword arguments: + # + # - <tt>parents: true</tt> - removes successive ancestor directories + # if empty. + # - <tt>noop: true</tt> - does not remove directories. + # - <tt>verbose: true</tt> - prints an equivalent command: + # + # Bundler::FileUtils.rmdir(%w[tmp0/tmp1 tmp2/tmp3], parents: true, verbose: true) + # Bundler::FileUtils.rmdir('tmp4/tmp5', parents: true, verbose: true) + # + # Output: + # + # rmdir -p tmp0/tmp1 tmp2/tmp3 + # rmdir -p tmp4/tmp5 + # + # Raises an exception if a directory does not exist + # or if for any reason a directory cannot be removed. + # + # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. # def rmdir(list, parents: nil, noop: nil, verbose: nil) list = fu_list(list) fu_output_message "rmdir #{parents ? '-p ' : ''}#{list.join ' '}" if verbose return if noop list.each do |dir| - begin - Dir.rmdir(dir = remove_trailing_slash(dir)) - if parents + Dir.rmdir(dir = remove_trailing_slash(dir)) + if parents + begin until (parent = File.dirname(dir)) == '.' or parent == dir dir = parent Dir.rmdir(dir) end + rescue Errno::ENOTEMPTY, Errno::EEXIST, Errno::ENOENT end - rescue Errno::ENOTEMPTY, Errno::EEXIST, Errno::ENOENT end end end module_function :rmdir + # Creates {hard links}[https://en.wikipedia.org/wiki/Hard_link]. + # + # Arguments +src+ (a single path or an array of paths) + # and +dest+ (a single path) + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. # - # :call-seq: - # Bundler::FileUtils.ln(target, link, force: nil, noop: nil, verbose: nil) - # Bundler::FileUtils.ln(target, dir, force: nil, noop: nil, verbose: nil) - # Bundler::FileUtils.ln(targets, dir, force: nil, noop: nil, verbose: nil) + # When +src+ is the path to an existing file + # and +dest+ is the path to a non-existent file, + # creates a hard link at +dest+ pointing to +src+; returns zero: # - # In the first form, creates a hard link +link+ which points to +target+. - # If +link+ already exists, raises Errno::EEXIST. - # But if the :force option is set, overwrites +link+. + # Dir.children('tmp0/') # => ["t.txt"] + # Dir.children('tmp1/') # => [] + # Bundler::FileUtils.ln('tmp0/t.txt', 'tmp1/t.lnk') # => 0 + # Dir.children('tmp1/') # => ["t.lnk"] # - # Bundler::FileUtils.ln 'gcc', 'cc', verbose: true - # Bundler::FileUtils.ln '/usr/bin/emacs21', '/usr/bin/emacs' + # When +src+ is the path to an existing file + # and +dest+ is the path to an existing directory, + # creates a hard link at <tt>dest/src</tt> pointing to +src+; returns zero: # - # In the second form, creates a link +dir/target+ pointing to +target+. - # In the third form, creates several hard links in the directory +dir+, - # pointing to each item in +targets+. - # If +dir+ is not a directory, raises Errno::ENOTDIR. + # Dir.children('tmp2') # => ["t.dat"] + # Dir.children('tmp3') # => [] + # Bundler::FileUtils.ln('tmp2/t.dat', 'tmp3') # => 0 + # Dir.children('tmp3') # => ["t.dat"] # - # Bundler::FileUtils.cd '/sbin' - # Bundler::FileUtils.ln %w(cp mv mkdir), '/bin' # Now /sbin/cp and /bin/cp are linked. + # When +src+ is an array of paths to existing files + # and +dest+ is the path to an existing directory, + # then for each path +target+ in +src+, + # creates a hard link at <tt>dest/target</tt> pointing to +target+; + # returns +src+: + # + # Dir.children('tmp4/') # => [] + # Bundler::FileUtils.ln(['tmp0/t.txt', 'tmp2/t.dat'], 'tmp4/') # => ["tmp0/t.txt", "tmp2/t.dat"] + # Dir.children('tmp4/') # => ["t.dat", "t.txt"] + # + # Keyword arguments: + # + # - <tt>force: true</tt> - overwrites +dest+ if it exists. + # - <tt>noop: true</tt> - does not create links. + # - <tt>verbose: true</tt> - prints an equivalent command: + # + # Bundler::FileUtils.ln('tmp0/t.txt', 'tmp1/t.lnk', verbose: true) + # Bundler::FileUtils.ln('tmp2/t.dat', 'tmp3', verbose: true) + # Bundler::FileUtils.ln(['tmp0/t.txt', 'tmp2/t.dat'], 'tmp4/', verbose: true) + # + # Output: + # + # ln tmp0/t.txt tmp1/t.lnk + # ln tmp2/t.dat tmp3 + # ln tmp0/t.txt tmp2/t.dat tmp4/ + # + # Raises an exception if +dest+ is the path to an existing file + # and keyword argument +force+ is not +true+. + # + # Related: Bundler::FileUtils.link_entry (has different options). # def ln(src, dest, force: nil, noop: nil, verbose: nil) fu_output_message "ln#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose @@ -293,30 +527,191 @@ module Bundler::FileUtils alias link ln module_function :link + # Creates {hard links}[https://en.wikipedia.org/wiki/Hard_link]. + # + # Arguments +src+ (a single path or an array of paths) + # and +dest+ (a single path) + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. + # + # If +src+ is the path to a directory and +dest+ does not exist, + # creates links +dest+ and descendents pointing to +src+ and its descendents: + # + # tree('src0') + # # => src0 + # # |-- sub0 + # # | |-- src0.txt + # # | `-- src1.txt + # # `-- sub1 + # # |-- src2.txt + # # `-- src3.txt + # File.exist?('dest0') # => false + # Bundler::FileUtils.cp_lr('src0', 'dest0') + # tree('dest0') + # # => dest0 + # # |-- sub0 + # # | |-- src0.txt + # # | `-- src1.txt + # # `-- sub1 + # # |-- src2.txt + # # `-- src3.txt + # + # If +src+ and +dest+ are both paths to directories, + # creates links <tt>dest/src</tt> and descendents + # pointing to +src+ and its descendents: + # + # tree('src1') + # # => src1 + # # |-- sub0 + # # | |-- src0.txt + # # | `-- src1.txt + # # `-- sub1 + # # |-- src2.txt + # # `-- src3.txt + # Bundler::FileUtils.mkdir('dest1') + # Bundler::FileUtils.cp_lr('src1', 'dest1') + # tree('dest1') + # # => dest1 + # # `-- src1 + # # |-- sub0 + # # | |-- src0.txt + # # | `-- src1.txt + # # `-- sub1 + # # |-- src2.txt + # # `-- src3.txt + # + # If +src+ is an array of paths to entries and +dest+ is the path to a directory, + # for each path +filepath+ in +src+, creates a link at <tt>dest/filepath</tt> + # pointing to that path: + # + # tree('src2') + # # => src2 + # # |-- sub0 + # # | |-- src0.txt + # # | `-- src1.txt + # # `-- sub1 + # # |-- src2.txt + # # `-- src3.txt + # Bundler::FileUtils.mkdir('dest2') + # Bundler::FileUtils.cp_lr(['src2/sub0', 'src2/sub1'], 'dest2') + # tree('dest2') + # # => dest2 + # # |-- sub0 + # # | |-- src0.txt + # # | `-- src1.txt + # # `-- sub1 + # # |-- src2.txt + # # `-- src3.txt + # + # Keyword arguments: + # + # - <tt>dereference_root: false</tt> - if +src+ is a symbolic link, + # does not dereference it. + # - <tt>noop: true</tt> - does not create links. + # - <tt>remove_destination: true</tt> - removes +dest+ before creating links. + # - <tt>verbose: true</tt> - prints an equivalent command: + # + # Bundler::FileUtils.cp_lr('src0', 'dest0', noop: true, verbose: true) + # Bundler::FileUtils.cp_lr('src1', 'dest1', noop: true, verbose: true) + # Bundler::FileUtils.cp_lr(['src2/sub0', 'src2/sub1'], 'dest2', noop: true, verbose: true) + # + # Output: + # + # cp -lr src0 dest0 + # cp -lr src1 dest1 + # cp -lr src2/sub0 src2/sub1 dest2 + # + # Raises an exception if +dest+ is the path to an existing file or directory + # and keyword argument <tt>remove_destination: true</tt> is not given. + # + # Related: {methods for copying}[rdoc-ref:FileUtils@Copying]. + # + def cp_lr(src, dest, noop: nil, verbose: nil, + dereference_root: true, remove_destination: false) + fu_output_message "cp -lr#{remove_destination ? ' --remove-destination' : ''} #{[src,dest].flatten.join ' '}" if verbose + return if noop + fu_each_src_dest(src, dest) do |s, d| + link_entry s, d, dereference_root, remove_destination + end + end + module_function :cp_lr + + # Creates {symbolic links}[https://en.wikipedia.org/wiki/Symbolic_link]. + # + # Arguments +src+ (a single path or an array of paths) + # and +dest+ (a single path) + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. + # + # If +src+ is the path to an existing file: + # + # - When +dest+ is the path to a non-existent file, + # creates a symbolic link at +dest+ pointing to +src+: + # + # Bundler::FileUtils.touch('src0.txt') + # File.exist?('dest0.txt') # => false + # Bundler::FileUtils.ln_s('src0.txt', 'dest0.txt') + # File.symlink?('dest0.txt') # => true + # + # - When +dest+ is the path to an existing file, + # creates a symbolic link at +dest+ pointing to +src+ + # if and only if keyword argument <tt>force: true</tt> is given + # (raises an exception otherwise): + # + # Bundler::FileUtils.touch('src1.txt') + # Bundler::FileUtils.touch('dest1.txt') + # Bundler::FileUtils.ln_s('src1.txt', 'dest1.txt', force: true) + # FileTest.symlink?('dest1.txt') # => true + # + # Bundler::FileUtils.ln_s('src1.txt', 'dest1.txt') # Raises Errno::EEXIST. + # + # If +dest+ is the path to a directory, + # creates a symbolic link at <tt>dest/src</tt> pointing to +src+: # - # :call-seq: - # Bundler::FileUtils.ln_s(target, link, force: nil, noop: nil, verbose: nil) - # Bundler::FileUtils.ln_s(target, dir, force: nil, noop: nil, verbose: nil) - # Bundler::FileUtils.ln_s(targets, dir, force: nil, noop: nil, verbose: nil) + # Bundler::FileUtils.touch('src2.txt') + # Bundler::FileUtils.mkdir('destdir2') + # Bundler::FileUtils.ln_s('src2.txt', 'destdir2') + # File.symlink?('destdir2/src2.txt') # => true # - # In the first form, creates a symbolic link +link+ which points to +target+. - # If +link+ already exists, raises Errno::EEXIST. - # But if the :force option is set, overwrites +link+. + # If +src+ is an array of paths to existing files and +dest+ is a directory, + # for each child +child+ in +src+ creates a symbolic link <tt>dest/child</tt> + # pointing to +child+: # - # Bundler::FileUtils.ln_s '/usr/bin/ruby', '/usr/local/bin/ruby' - # Bundler::FileUtils.ln_s 'verylongsourcefilename.c', 'c', force: true + # Bundler::FileUtils.mkdir('srcdir3') + # Bundler::FileUtils.touch('srcdir3/src0.txt') + # Bundler::FileUtils.touch('srcdir3/src1.txt') + # Bundler::FileUtils.mkdir('destdir3') + # Bundler::FileUtils.ln_s(['srcdir3/src0.txt', 'srcdir3/src1.txt'], 'destdir3') + # File.symlink?('destdir3/src0.txt') # => true + # File.symlink?('destdir3/src1.txt') # => true # - # In the second form, creates a link +dir/target+ pointing to +target+. - # In the third form, creates several symbolic links in the directory +dir+, - # pointing to each item in +targets+. - # If +dir+ is not a directory, raises Errno::ENOTDIR. + # Keyword arguments: # - # Bundler::FileUtils.ln_s Dir.glob('/bin/*.rb'), '/home/foo/bin' + # - <tt>force: true</tt> - overwrites +dest+ if it exists. + # - <tt>relative: false</tt> - create links relative to +dest+. + # - <tt>noop: true</tt> - does not create links. + # - <tt>verbose: true</tt> - prints an equivalent command: # - def ln_s(src, dest, force: nil, noop: nil, verbose: nil) - fu_output_message "ln -s#{force ? 'f' : ''} #{[src,dest].flatten.join ' '}" if verbose + # Bundler::FileUtils.ln_s('src0.txt', 'dest0.txt', noop: true, verbose: true) + # Bundler::FileUtils.ln_s('src1.txt', 'destdir1', noop: true, verbose: true) + # Bundler::FileUtils.ln_s('src2.txt', 'dest2.txt', force: true, noop: true, verbose: true) + # Bundler::FileUtils.ln_s(['srcdir3/src0.txt', 'srcdir3/src1.txt'], 'destdir3', noop: true, verbose: true) + # + # Output: + # + # ln -s src0.txt dest0.txt + # ln -s src1.txt destdir1 + # ln -sf src2.txt dest2.txt + # ln -s srcdir3/src0.txt srcdir3/src1.txt destdir3 + # + # Related: Bundler::FileUtils.ln_sf. + # + def ln_s(src, dest, force: nil, relative: false, target_directory: true, noop: nil, verbose: nil) + if relative + return ln_sr(src, dest, force: force, target_directory: target_directory, noop: noop, verbose: verbose) + end + fu_output_message "ln -s#{force ? 'f' : ''}#{ + target_directory ? '' : 'T'} #{[src,dest].flatten.join ' '}" if verbose return if noop - fu_each_src_dest0(src, dest) do |s,d| + fu_each_src_dest0(src, dest, target_directory) do |s,d| remove_file d, true if force File.symlink s, d end @@ -326,29 +721,151 @@ module Bundler::FileUtils alias symlink ln_s module_function :symlink - # - # :call-seq: - # Bundler::FileUtils.ln_sf(*args) - # - # Same as - # - # Bundler::FileUtils.ln_s(*args, force: true) + # Like Bundler::FileUtils.ln_s, but always with keyword argument <tt>force: true</tt> given. # def ln_sf(src, dest, noop: nil, verbose: nil) ln_s src, dest, force: true, noop: noop, verbose: verbose end module_function :ln_sf + # Like Bundler::FileUtils.ln_s, but create links relative to +dest+. + # + def ln_sr(src, dest, target_directory: true, force: nil, noop: nil, verbose: nil) + cmd = "ln -s#{force ? 'f' : ''}#{target_directory ? '' : 'T'}" if verbose + fu_each_src_dest0(src, dest, target_directory) do |s,d| + if target_directory + parent = File.dirname(d) + destdirs = fu_split_path(parent) + real_ddirs = fu_split_path(File.realpath(parent)) + else + destdirs ||= fu_split_path(dest) + real_ddirs ||= fu_split_path(File.realdirpath(dest)) + end + srcdirs = fu_split_path(s) + i = fu_common_components(srcdirs, destdirs) + n = destdirs.size - i + n -= 1 unless target_directory + link1 = fu_clean_components(*Array.new([n, 0].max, '..'), *srcdirs[i..-1]) + begin + real_sdirs = fu_split_path(File.realdirpath(s)) rescue nil + rescue + else + i = fu_common_components(real_sdirs, real_ddirs) + n = real_ddirs.size - i + n -= 1 unless target_directory + link2 = fu_clean_components(*Array.new([n, 0].max, '..'), *real_sdirs[i..-1]) + link1 = link2 if link1.size > link2.size + end + s = File.join(link1) + fu_output_message [cmd, s, d].flatten.join(' ') if verbose + next if noop + remove_file d, true if force + File.symlink s, d + end + end + module_function :ln_sr + + # Creates {hard links}[https://en.wikipedia.org/wiki/Hard_link]; returns +nil+. # - # Copies a file content +src+ to +dest+. If +dest+ is a directory, - # copies +src+ to +dest/src+. + # Arguments +src+ and +dest+ + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. # - # If +src+ is a list of files, then +dest+ must be a directory. + # If +src+ is the path to a file and +dest+ does not exist, + # creates a hard link at +dest+ pointing to +src+: # - # Bundler::FileUtils.cp 'eval.c', 'eval.c.org' - # Bundler::FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6' - # Bundler::FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6', :verbose => true - # Bundler::FileUtils.cp 'symlink', 'dest' # copy content, "dest" is not a symlink + # Bundler::FileUtils.touch('src0.txt') + # File.exist?('dest0.txt') # => false + # Bundler::FileUtils.link_entry('src0.txt', 'dest0.txt') + # File.file?('dest0.txt') # => true + # + # If +src+ is the path to a directory and +dest+ does not exist, + # recursively creates hard links at +dest+ pointing to paths in +src+: + # + # Bundler::FileUtils.mkdir_p(['src1/dir0', 'src1/dir1']) + # src_file_paths = [ + # 'src1/dir0/t0.txt', + # 'src1/dir0/t1.txt', + # 'src1/dir1/t2.txt', + # 'src1/dir1/t3.txt', + # ] + # Bundler::FileUtils.touch(src_file_paths) + # File.directory?('dest1') # => true + # Bundler::FileUtils.link_entry('src1', 'dest1') + # File.file?('dest1/dir0/t0.txt') # => true + # File.file?('dest1/dir0/t1.txt') # => true + # File.file?('dest1/dir1/t2.txt') # => true + # File.file?('dest1/dir1/t3.txt') # => true + # + # Optional arguments: + # + # - +dereference_root+ - dereferences +src+ if it is a symbolic link (+false+ by default). + # - +remove_destination+ - removes +dest+ before creating links (+false+ by default). + # + # Raises an exception if +dest+ is the path to an existing file or directory + # and optional argument +remove_destination+ is not given. + # + # Related: Bundler::FileUtils.ln (has different options). + # + def link_entry(src, dest, dereference_root = false, remove_destination = false) + Entry_.new(src, nil, dereference_root).traverse do |ent| + destent = Entry_.new(dest, ent.rel, false) + File.unlink destent.path if remove_destination && File.file?(destent.path) + ent.link destent.path + end + end + module_function :link_entry + + # Copies files. + # + # Arguments +src+ (a single path or an array of paths) + # and +dest+ (a single path) + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. + # + # If +src+ is the path to a file and +dest+ is not the path to a directory, + # copies +src+ to +dest+: + # + # Bundler::FileUtils.touch('src0.txt') + # File.exist?('dest0.txt') # => false + # Bundler::FileUtils.cp('src0.txt', 'dest0.txt') + # File.file?('dest0.txt') # => true + # + # If +src+ is the path to a file and +dest+ is the path to a directory, + # copies +src+ to <tt>dest/src</tt>: + # + # Bundler::FileUtils.touch('src1.txt') + # Bundler::FileUtils.mkdir('dest1') + # Bundler::FileUtils.cp('src1.txt', 'dest1') + # File.file?('dest1/src1.txt') # => true + # + # If +src+ is an array of paths to files and +dest+ is the path to a directory, + # copies from each +src+ to +dest+: + # + # src_file_paths = ['src2.txt', 'src2.dat'] + # Bundler::FileUtils.touch(src_file_paths) + # Bundler::FileUtils.mkdir('dest2') + # Bundler::FileUtils.cp(src_file_paths, 'dest2') + # File.file?('dest2/src2.txt') # => true + # File.file?('dest2/src2.dat') # => true + # + # Keyword arguments: + # + # - <tt>preserve: true</tt> - preserves file times. + # - <tt>noop: true</tt> - does not copy files. + # - <tt>verbose: true</tt> - prints an equivalent command: + # + # Bundler::FileUtils.cp('src0.txt', 'dest0.txt', noop: true, verbose: true) + # Bundler::FileUtils.cp('src1.txt', 'dest1', noop: true, verbose: true) + # Bundler::FileUtils.cp(src_file_paths, 'dest2', noop: true, verbose: true) + # + # Output: + # + # cp src0.txt dest0.txt + # cp src1.txt dest1 + # cp src2.txt src2.dat dest2 + # + # Raises an exception if +src+ is a directory. + # + # Related: {methods for copying}[rdoc-ref:FileUtils@Copying]. # def cp(src, dest, preserve: nil, noop: nil, verbose: nil) fu_output_message "cp#{preserve ? ' -p' : ''} #{[src,dest].flatten.join ' '}" if verbose @@ -362,26 +879,105 @@ module Bundler::FileUtils alias copy cp module_function :copy - # - # Copies +src+ to +dest+. If +src+ is a directory, this method copies - # all its contents recursively. If +dest+ is a directory, copies - # +src+ to +dest/src+. - # - # +src+ can be a list of files. - # - # # Installing Ruby library "mylib" under the site_ruby - # Bundler::FileUtils.rm_r site_ruby + '/mylib', :force - # Bundler::FileUtils.cp_r 'lib/', site_ruby + '/mylib' - # - # # Examples of copying several files to target directory. - # Bundler::FileUtils.cp_r %w(mail.rb field.rb debug/), site_ruby + '/tmail' - # Bundler::FileUtils.cp_r Dir.glob('*.rb'), '/home/foo/lib/ruby', :noop => true, :verbose => true - # - # # If you want to copy all contents of a directory instead of the - # # directory itself, c.f. src/x -> dest/x, src/y -> dest/y, - # # use following code. - # Bundler::FileUtils.cp_r 'src/.', 'dest' # cp_r('src', 'dest') makes dest/src, - # # but this doesn't. + # Recursively copies files. + # + # Arguments +src+ (a single path or an array of paths) + # and +dest+ (a single path) + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. + # + # The mode, owner, and group are retained in the copy; + # to change those, use Bundler::FileUtils.install instead. + # + # If +src+ is the path to a file and +dest+ is not the path to a directory, + # copies +src+ to +dest+: + # + # Bundler::FileUtils.touch('src0.txt') + # File.exist?('dest0.txt') # => false + # Bundler::FileUtils.cp_r('src0.txt', 'dest0.txt') + # File.file?('dest0.txt') # => true + # + # If +src+ is the path to a file and +dest+ is the path to a directory, + # copies +src+ to <tt>dest/src</tt>: + # + # Bundler::FileUtils.touch('src1.txt') + # Bundler::FileUtils.mkdir('dest1') + # Bundler::FileUtils.cp_r('src1.txt', 'dest1') + # File.file?('dest1/src1.txt') # => true + # + # If +src+ is the path to a directory and +dest+ does not exist, + # recursively copies +src+ to +dest+: + # + # tree('src2') + # # => src2 + # # |-- dir0 + # # | |-- src0.txt + # # | `-- src1.txt + # # `-- dir1 + # # |-- src2.txt + # # `-- src3.txt + # Bundler::FileUtils.exist?('dest2') # => false + # Bundler::FileUtils.cp_r('src2', 'dest2') + # tree('dest2') + # # => dest2 + # # |-- dir0 + # # | |-- src0.txt + # # | `-- src1.txt + # # `-- dir1 + # # |-- src2.txt + # # `-- src3.txt + # + # If +src+ and +dest+ are paths to directories, + # recursively copies +src+ to <tt>dest/src</tt>: + # + # tree('src3') + # # => src3 + # # |-- dir0 + # # | |-- src0.txt + # # | `-- src1.txt + # # `-- dir1 + # # |-- src2.txt + # # `-- src3.txt + # Bundler::FileUtils.mkdir('dest3') + # Bundler::FileUtils.cp_r('src3', 'dest3') + # tree('dest3') + # # => dest3 + # # `-- src3 + # # |-- dir0 + # # | |-- src0.txt + # # | `-- src1.txt + # # `-- dir1 + # # |-- src2.txt + # # `-- src3.txt + # + # If +src+ is an array of paths and +dest+ is a directory, + # recursively copies from each path in +src+ to +dest+; + # the paths in +src+ may point to files and/or directories. + # + # Keyword arguments: + # + # - <tt>dereference_root: false</tt> - if +src+ is a symbolic link, + # does not dereference it. + # - <tt>noop: true</tt> - does not copy files. + # - <tt>preserve: true</tt> - preserves file times. + # - <tt>remove_destination: true</tt> - removes +dest+ before copying files. + # - <tt>verbose: true</tt> - prints an equivalent command: + # + # Bundler::FileUtils.cp_r('src0.txt', 'dest0.txt', noop: true, verbose: true) + # Bundler::FileUtils.cp_r('src1.txt', 'dest1', noop: true, verbose: true) + # Bundler::FileUtils.cp_r('src2', 'dest2', noop: true, verbose: true) + # Bundler::FileUtils.cp_r('src3', 'dest3', noop: true, verbose: true) + # + # Output: + # + # cp -r src0.txt dest0.txt + # cp -r src1.txt dest1 + # cp -r src2 dest2 + # cp -r src3 dest3 + # + # Raises an exception of +src+ is the path to a directory + # and +dest+ is the path to a file. + # + # Related: {methods for copying}[rdoc-ref:FileUtils@Copying]. # def cp_r(src, dest, preserve: nil, noop: nil, verbose: nil, dereference_root: true, remove_destination: nil) @@ -393,26 +989,59 @@ module Bundler::FileUtils end module_function :cp_r + # Recursively copies files from +src+ to +dest+. # - # Copies a file system entry +src+ to +dest+. - # If +src+ is a directory, this method copies its contents recursively. - # This method preserves file types, c.f. symlink, directory... - # (FIFO, device files and etc. are not supported yet) + # Arguments +src+ and +dest+ + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. # - # Both of +src+ and +dest+ must be a path name. - # +src+ must exist, +dest+ must not exist. + # If +src+ is the path to a file, copies +src+ to +dest+: # - # If +preserve+ is true, this method preserves owner, group, and - # modified time. Permissions are copied regardless +preserve+. + # Bundler::FileUtils.touch('src0.txt') + # File.exist?('dest0.txt') # => false + # Bundler::FileUtils.copy_entry('src0.txt', 'dest0.txt') + # File.file?('dest0.txt') # => true # - # If +dereference_root+ is true, this method dereference tree root. + # If +src+ is a directory, recursively copies +src+ to +dest+: # - # If +remove_destination+ is true, this method removes each destination file before copy. + # tree('src1') + # # => src1 + # # |-- dir0 + # # | |-- src0.txt + # # | `-- src1.txt + # # `-- dir1 + # # |-- src2.txt + # # `-- src3.txt + # Bundler::FileUtils.copy_entry('src1', 'dest1') + # tree('dest1') + # # => dest1 + # # |-- dir0 + # # | |-- src0.txt + # # | `-- src1.txt + # # `-- dir1 + # # |-- src2.txt + # # `-- src3.txt + # + # The recursive copying preserves file types for regular files, + # directories, and symbolic links; + # other file types (FIFO streams, device files, etc.) are not supported. + # + # Optional arguments: + # + # - +dereference_root+ - if +src+ is a symbolic link, + # follows the link (+false+ by default). + # - +preserve+ - preserves file times (+false+ by default). + # - +remove_destination+ - removes +dest+ before copying files (+false+ by default). + # + # Related: {methods for copying}[rdoc-ref:FileUtils@Copying]. # def copy_entry(src, dest, preserve = false, dereference_root = false, remove_destination = false) - Entry_.new(src, nil, dereference_root).wrap_traverse(proc do |ent| + if dereference_root + src = File.realpath(src) + end + + Entry_.new(src, nil, false).wrap_traverse(proc do |ent| destent = Entry_.new(dest, ent.rel, false) - File.unlink destent.path if remove_destination && File.file?(destent.path) + File.unlink destent.path if remove_destination && (File.file?(destent.path) || File.symlink?(destent.path)) ent.copy destent.path end, proc do |ent| destent = Entry_.new(dest, ent.rel, false) @@ -421,9 +1050,25 @@ module Bundler::FileUtils end module_function :copy_entry + # Copies file from +src+ to +dest+, which should not be directories. + # + # Arguments +src+ and +dest+ + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. + # + # Examples: + # + # Bundler::FileUtils.touch('src0.txt') + # Bundler::FileUtils.copy_file('src0.txt', 'dest0.txt') + # File.file?('dest0.txt') # => true # - # Copies file contents of +src+ to +dest+. - # Both of +src+ and +dest+ must be a path name. + # Optional arguments: + # + # - +dereference+ - if +src+ is a symbolic link, + # follows the link (+true+ by default). + # - +preserve+ - preserves file times (+false+ by default). + # - +remove_destination+ - removes +dest+ before copying files (+false+ by default). + # + # Related: {methods for copying}[rdoc-ref:FileUtils@Copying]. # def copy_file(src, dest, preserve = false, dereference = true) ent = Entry_.new(src, nil, dereference) @@ -432,25 +1077,79 @@ module Bundler::FileUtils end module_function :copy_file + # Copies \IO stream +src+ to \IO stream +dest+ via + # {IO.copy_stream}[rdoc-ref:IO.copy_stream]. # - # Copies stream +src+ to +dest+. - # +src+ must respond to #read(n) and - # +dest+ must respond to #write(str). + # Related: {methods for copying}[rdoc-ref:FileUtils@Copying]. # def copy_stream(src, dest) IO.copy_stream(src, dest) end module_function :copy_stream - # - # Moves file(s) +src+ to +dest+. If +file+ and +dest+ exist on the different - # disk partition, the file is copied then the original file is removed. - # - # Bundler::FileUtils.mv 'badname.rb', 'goodname.rb' - # Bundler::FileUtils.mv 'stuff.rb', '/notexist/lib/ruby', :force => true # no error - # - # Bundler::FileUtils.mv %w(junk.txt dust.txt), '/home/foo/.trash/' - # Bundler::FileUtils.mv Dir.glob('test*.rb'), 'test', :noop => true, :verbose => true + # Moves entries. + # + # Arguments +src+ (a single path or an array of paths) + # and +dest+ (a single path) + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. + # + # If +src+ and +dest+ are on different file systems, + # first copies, then removes +src+. + # + # May cause a local vulnerability if not called with keyword argument + # <tt>secure: true</tt>; + # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability]. + # + # If +src+ is the path to a single file or directory and +dest+ does not exist, + # moves +src+ to +dest+: + # + # tree('src0') + # # => src0 + # # |-- src0.txt + # # `-- src1.txt + # File.exist?('dest0') # => false + # Bundler::FileUtils.mv('src0', 'dest0') + # File.exist?('src0') # => false + # tree('dest0') + # # => dest0 + # # |-- src0.txt + # # `-- src1.txt + # + # If +src+ is an array of paths to files and directories + # and +dest+ is the path to a directory, + # copies from each path in the array to +dest+: + # + # File.file?('src1.txt') # => true + # tree('src1') + # # => src1 + # # |-- src.dat + # # `-- src.txt + # Dir.empty?('dest1') # => true + # Bundler::FileUtils.mv(['src1.txt', 'src1'], 'dest1') + # tree('dest1') + # # => dest1 + # # |-- src1 + # # | |-- src.dat + # # | `-- src.txt + # # `-- src1.txt + # + # Keyword arguments: + # + # - <tt>force: true</tt> - if the move includes removing +src+ + # (that is, if +src+ and +dest+ are on different file systems), + # ignores raised exceptions of StandardError and its descendants. + # - <tt>noop: true</tt> - does not move files. + # - <tt>secure: true</tt> - removes +src+ securely; + # see details at Bundler::FileUtils.remove_entry_secure. + # - <tt>verbose: true</tt> - prints an equivalent command: + # + # Bundler::FileUtils.mv('src0', 'dest0', noop: true, verbose: true) + # Bundler::FileUtils.mv(['src1.txt', 'src1'], 'dest1', noop: true, verbose: true) + # + # Output: + # + # mv src0 dest0 + # mv src1.txt src1 dest1 # def mv(src, dest, force: nil, noop: nil, verbose: nil, secure: nil) fu_output_message "mv#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose @@ -461,13 +1160,12 @@ module Bundler::FileUtils if destent.exist? if destent.directory? raise Errno::EEXIST, d - else - destent.remove_file if rename_cannot_overwrite_file? end end begin File.rename s, d - rescue Errno::EXDEV + rescue Errno::EXDEV, + Errno::EPERM # move from unencrypted to encrypted dir (ext4) copy_entry s, d, true if secure remove_entry_secure s, force @@ -485,18 +1183,32 @@ module Bundler::FileUtils alias move mv module_function :move - def rename_cannot_overwrite_file? #:nodoc: - /emx/ =~ RUBY_PLATFORM - end - private_module_function :rename_cannot_overwrite_file? - + # Removes entries at the paths in the given +list+ + # (a single path or an array of paths) + # returns +list+, if it is an array, <tt>[list]</tt> otherwise. + # + # Argument +list+ or its elements + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. + # + # With no keyword arguments, removes files at the paths given in +list+: # - # Remove file(s) specified in +list+. This method cannot remove directories. - # All StandardErrors are ignored when the :force option is set. + # Bundler::FileUtils.touch(['src0.txt', 'src0.dat']) + # Bundler::FileUtils.rm(['src0.dat', 'src0.txt']) # => ["src0.dat", "src0.txt"] # - # Bundler::FileUtils.rm %w( junk.txt dust.txt ) - # Bundler::FileUtils.rm Dir.glob('*.so') - # Bundler::FileUtils.rm 'NotExistFile', :force => true # never raises exception + # Keyword arguments: + # + # - <tt>force: true</tt> - ignores raised exceptions of StandardError + # and its descendants. + # - <tt>noop: true</tt> - does not remove files; returns +nil+. + # - <tt>verbose: true</tt> - prints an equivalent command: + # + # Bundler::FileUtils.rm(['src0.dat', 'src0.txt'], noop: true, verbose: true) + # + # Output: + # + # rm src0.dat src0.txt + # + # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. # def rm(list, force: nil, noop: nil, verbose: nil) list = fu_list(list) @@ -512,10 +1224,16 @@ module Bundler::FileUtils alias remove rm module_function :remove + # Equivalent to: + # + # Bundler::FileUtils.rm(list, force: true, **kwargs) # - # Equivalent to + # Argument +list+ (a single path or an array of paths) + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. # - # Bundler::FileUtils.rm(list, :force => true) + # See Bundler::FileUtils.rm for keyword arguments. + # + # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. # def rm_f(list, noop: nil, verbose: nil) rm list, force: true, noop: noop, verbose: verbose @@ -525,24 +1243,55 @@ module Bundler::FileUtils alias safe_unlink rm_f module_function :safe_unlink + # Removes entries at the paths in the given +list+ + # (a single path or an array of paths); + # returns +list+, if it is an array, <tt>[list]</tt> otherwise. + # + # Argument +list+ or its elements + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. + # + # May cause a local vulnerability if not called with keyword argument + # <tt>secure: true</tt>; + # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability]. + # + # For each file path, removes the file at that path: + # + # Bundler::FileUtils.touch(['src0.txt', 'src0.dat']) + # Bundler::FileUtils.rm_r(['src0.dat', 'src0.txt']) + # File.exist?('src0.txt') # => false + # File.exist?('src0.dat') # => false # - # remove files +list+[0] +list+[1]... If +list+[n] is a directory, - # removes its all contents recursively. This method ignores - # StandardError when :force option is set. + # For each directory path, recursively removes files and directories: # - # Bundler::FileUtils.rm_r Dir.glob('/tmp/*') - # Bundler::FileUtils.rm_r 'some_dir', :force => true + # tree('src1') + # # => src1 + # # |-- dir0 + # # | |-- src0.txt + # # | `-- src1.txt + # # `-- dir1 + # # |-- src2.txt + # # `-- src3.txt + # Bundler::FileUtils.rm_r('src1') + # File.exist?('src1') # => false # - # WARNING: This method causes local vulnerability - # if one of parent directories or removing directory tree are world - # writable (including /tmp, whose permission is 1777), and the current - # process has strong privilege such as Unix super user (root), and the - # system has symbolic link. For secure removing, read the documentation - # of #remove_entry_secure carefully, and set :secure option to true. - # Default is :secure=>false. + # Keyword arguments: # - # NOTE: This method calls #remove_entry_secure if :secure option is set. - # See also #remove_entry_secure. + # - <tt>force: true</tt> - ignores raised exceptions of StandardError + # and its descendants. + # - <tt>noop: true</tt> - does not remove entries; returns +nil+. + # - <tt>secure: true</tt> - removes +src+ securely; + # see details at Bundler::FileUtils.remove_entry_secure. + # - <tt>verbose: true</tt> - prints an equivalent command: + # + # Bundler::FileUtils.rm_r(['src0.dat', 'src0.txt'], noop: true, verbose: true) + # Bundler::FileUtils.rm_r('src1', noop: true, verbose: true) + # + # Output: + # + # rm -r src0.dat src0.txt + # rm -r src1 + # + # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. # def rm_r(list, force: nil, noop: nil, verbose: nil, secure: nil) list = fu_list(list) @@ -558,13 +1307,20 @@ module Bundler::FileUtils end module_function :rm_r + # Equivalent to: + # + # Bundler::FileUtils.rm_r(list, force: true, **kwargs) # - # Equivalent to + # Argument +list+ or its elements + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. # - # Bundler::FileUtils.rm_r(list, :force => true) + # May cause a local vulnerability if not called with keyword argument + # <tt>secure: true</tt>; + # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability]. # - # WARNING: This method causes local vulnerability. - # Read the documentation of #rm_r first. + # See Bundler::FileUtils.rm_r for keyword arguments. + # + # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. # def rm_rf(list, noop: nil, verbose: nil, secure: nil) rm_r list, force: true, noop: noop, verbose: verbose, secure: secure @@ -574,37 +1330,20 @@ module Bundler::FileUtils alias rmtree rm_rf module_function :rmtree + # Securely removes the entry given by +path+, + # which should be the entry for a regular file, a symbolic link, + # or a directory. # - # This method removes a file system entry +path+. +path+ shall be a - # regular file, a directory, or something. If +path+ is a directory, - # remove it recursively. This method is required to avoid TOCTTOU - # (time-of-check-to-time-of-use) local security vulnerability of #rm_r. - # #rm_r causes security hole when: - # - # * Parent directory is world writable (including /tmp). - # * Removing directory tree includes world writable directory. - # * The system has symbolic link. - # - # To avoid this security hole, this method applies special preprocess. - # If +path+ is a directory, this method chown(2) and chmod(2) all - # removing directories. This requires the current process is the - # owner of the removing whole directory tree, or is the super user (root). - # - # WARNING: You must ensure that *ALL* parent directories cannot be - # moved by other untrusted users. For example, parent directories - # should not be owned by untrusted users, and should not be world - # writable except when the sticky bit set. - # - # WARNING: Only the owner of the removing directory tree, or Unix super - # user (root) should invoke this method. Otherwise this method does not - # work. + # Argument +path+ + # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]. # - # For details of this security vulnerability, see Perl's case: + # Avoids a local vulnerability that can exist in certain circumstances; + # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability]. # - # * http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-0448 - # * http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2004-0452 + # Optional argument +force+ specifies whether to ignore + # raised exceptions of StandardError and its descendants. # - # For fileutils.rb, this vulnerability is reported in [ruby-dev:26100]. + # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. # def remove_entry_secure(path, force = false) unless fu_have_symlink? @@ -626,22 +1365,38 @@ module Bundler::FileUtils unless parent_st.sticky? raise ArgumentError, "parent directory is world writable, Bundler::FileUtils#remove_entry_secure does not work; abort: #{path.inspect} (parent directory mode #{'%o' % parent_st.mode})" end + # freeze tree root euid = Process.euid - File.open(fullpath + '/.') {|f| - unless fu_stat_identical_entry?(st, f.stat) - # symlink (TOC-to-TOU attack?) - File.unlink fullpath - return - end - f.chown euid, -1 - f.chmod 0700 - unless fu_stat_identical_entry?(st, File.lstat(fullpath)) - # TOC-to-TOU attack? - File.unlink fullpath - return - end - } + dot_file = fullpath + "/." + begin + File.open(dot_file) {|f| + unless fu_stat_identical_entry?(st, f.stat) + # symlink (TOC-to-TOU attack?) + File.unlink fullpath + return + end + f.chown euid, -1 + f.chmod 0700 + } + rescue Errno::EISDIR # JRuby in non-native mode can't open files as dirs + File.lstat(dot_file).tap {|fstat| + unless fu_stat_identical_entry?(st, fstat) + # symlink (TOC-to-TOU attack?) + File.unlink fullpath + return + end + File.chown euid, -1, dot_file + File.chmod 0700, dot_file + } + end + + unless fu_stat_identical_entry?(st, File.lstat(fullpath)) + # TOC-to-TOU attack? + File.unlink fullpath + return + end + # ---- tree root is frozen ---- root = Entry_.new(path) root.preorder_traverse do |ent| @@ -676,12 +1431,17 @@ module Bundler::FileUtils end private_module_function :fu_stat_identical_entry? + # Removes the entry given by +path+, + # which should be the entry for a regular file, a symbolic link, + # or a directory. + # + # Argument +path+ + # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]. # - # This method removes a file system entry +path+. - # +path+ might be a regular file, a directory, or something. - # If +path+ is a directory, remove it recursively. + # Optional argument +force+ specifies whether to ignore + # raised exceptions of StandardError and its descendants. # - # See also #remove_entry_secure. + # Related: Bundler::FileUtils.remove_entry_secure. # def remove_entry(path, force = false) Entry_.new(path).postorder_traverse do |ent| @@ -696,9 +1456,16 @@ module Bundler::FileUtils end module_function :remove_entry + # Removes the file entry given by +path+, + # which should be the entry for a regular file or a symbolic link. # - # Removes a file +path+. - # This method ignores StandardError if +force+ is true. + # Argument +path+ + # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]. + # + # Optional argument +force+ specifies whether to ignore + # raised exceptions of StandardError and its descendants. + # + # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. # def remove_file(path, force = false) Entry_.new(path).remove_file @@ -707,20 +1474,33 @@ module Bundler::FileUtils end module_function :remove_file + # Recursively removes the directory entry given by +path+, + # which should be the entry for a regular file, a symbolic link, + # or a directory. + # + # Argument +path+ + # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]. # - # Removes a directory +dir+ and its contents recursively. - # This method ignores StandardError if +force+ is true. + # Optional argument +force+ specifies whether to ignore + # raised exceptions of StandardError and its descendants. + # + # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. # def remove_dir(path, force = false) - remove_entry path, force # FIXME?? check if it is a directory + raise Errno::ENOTDIR, path unless force or File.directory?(path) + remove_entry path, force end module_function :remove_dir + # Returns +true+ if the contents of files +a+ and +b+ are identical, + # +false+ otherwise. + # + # Arguments +a+ and +b+ + # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]. # - # Returns true if the contents of a file +a+ and a file +b+ are identical. + # Bundler::FileUtils.identical? and Bundler::FileUtils.cmp are aliases for Bundler::FileUtils.compare_file. # - # Bundler::FileUtils.compare_file('somefile', 'somefile') #=> true - # Bundler::FileUtils.compare_file('/dev/null', '/dev/urandom') #=> false + # Related: Bundler::FileUtils.compare_stream. # def compare_file(a, b) return false unless File.size(a) == File.size(b) @@ -737,13 +1517,20 @@ module Bundler::FileUtils module_function :identical? module_function :cmp + # Returns +true+ if the contents of streams +a+ and +b+ are identical, + # +false+ otherwise. # - # Returns true if the contents of a stream +a+ and +b+ are identical. + # Arguments +a+ and +b+ + # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]. + # + # Related: Bundler::FileUtils.compare_file. # def compare_stream(a, b) bsize = fu_stream_blksize(a, b) + sa = String.new(capacity: bsize) sb = String.new(capacity: bsize) + begin a.read(bsize, sa) b.read(bsize, sb) @@ -753,13 +1540,69 @@ module Bundler::FileUtils end module_function :compare_stream + # Copies a file entry. + # See {install(1)}[https://man7.org/linux/man-pages/man1/install.1.html]. + # + # Arguments +src+ (a single path or an array of paths) + # and +dest+ (a single path) + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]; + # + # If the entry at +dest+ does not exist, copies from +src+ to +dest+: + # + # File.read('src0.txt') # => "aaa\n" + # File.exist?('dest0.txt') # => false + # Bundler::FileUtils.install('src0.txt', 'dest0.txt') + # File.read('dest0.txt') # => "aaa\n" + # + # If +dest+ is a file entry, copies from +src+ to +dest+, overwriting: + # + # File.read('src1.txt') # => "aaa\n" + # File.read('dest1.txt') # => "bbb\n" + # Bundler::FileUtils.install('src1.txt', 'dest1.txt') + # File.read('dest1.txt') # => "aaa\n" + # + # If +dest+ is a directory entry, copies from +src+ to <tt>dest/src</tt>, + # overwriting if necessary: + # + # File.read('src2.txt') # => "aaa\n" + # File.read('dest2/src2.txt') # => "bbb\n" + # Bundler::FileUtils.install('src2.txt', 'dest2') + # File.read('dest2/src2.txt') # => "aaa\n" + # + # If +src+ is an array of paths and +dest+ points to a directory, + # copies each path +path+ in +src+ to <tt>dest/path</tt>: # - # If +src+ is not same as +dest+, copies it and changes the permission - # mode to +mode+. If +dest+ is a directory, destination is +dest+/+src+. - # This method removes destination before copy. + # File.file?('src3.txt') # => true + # File.file?('src3.dat') # => true + # Bundler::FileUtils.mkdir('dest3') + # Bundler::FileUtils.install(['src3.txt', 'src3.dat'], 'dest3') + # File.file?('dest3/src3.txt') # => true + # File.file?('dest3/src3.dat') # => true # - # Bundler::FileUtils.install 'ruby', '/usr/local/bin/ruby', :mode => 0755, :verbose => true - # Bundler::FileUtils.install 'lib.rb', '/usr/local/lib/ruby/site_ruby', :verbose => true + # Keyword arguments: + # + # - <tt>group: <i>group</i></tt> - changes the group if not +nil+, + # using {File.chown}[rdoc-ref:File.chown]. + # - <tt>mode: <i>permissions</i></tt> - changes the permissions. + # using {File.chmod}[rdoc-ref:File.chmod]. + # - <tt>noop: true</tt> - does not copy entries; returns +nil+. + # - <tt>owner: <i>owner</i></tt> - changes the owner if not +nil+, + # using {File.chown}[rdoc-ref:File.chown]. + # - <tt>preserve: true</tt> - preserve timestamps + # using {File.utime}[rdoc-ref:File.utime]. + # - <tt>verbose: true</tt> - prints an equivalent command: + # + # Bundler::FileUtils.install('src0.txt', 'dest0.txt', noop: true, verbose: true) + # Bundler::FileUtils.install('src1.txt', 'dest1.txt', noop: true, verbose: true) + # Bundler::FileUtils.install('src2.txt', 'dest2', noop: true, verbose: true) + # + # Output: + # + # install -c src0.txt dest0.txt + # install -c src1.txt dest1.txt + # install -c src2.txt dest2 + # + # Related: {methods for copying}[rdoc-ref:FileUtils@Copying]. # def install(src, dest, mode: nil, owner: nil, group: nil, preserve: nil, noop: nil, verbose: nil) @@ -779,7 +1622,13 @@ module Bundler::FileUtils st = File.stat(s) unless File.exist?(d) and compare_file(s, d) remove_file d, true - copy_file s, d + if d.end_with?('/') + mkdir_p d + copy_file s, d + File.basename(s) + else + mkdir_p File.expand_path('..', d) + copy_file s, d + end File.utime st.atime, st.mtime, d if preserve File.chmod fu_mode(mode, st), d if mode File.chown uid, gid, d if uid or gid @@ -800,7 +1649,7 @@ module Bundler::FileUtils when "a" mask | 07777 else - raise ArgumentError, "invalid `who' symbol in file mode: #{chr}" + raise ArgumentError, "invalid 'who' symbol in file mode: #{chr}" end end end @@ -819,11 +1668,8 @@ module Bundler::FileUtils private_module_function :apply_mask def symbolic_modes_to_i(mode_sym, path) #:nodoc: - mode = if File::Stat === path - path.mode - else - File.stat(path).mode - end + path = File.stat(path) unless File::Stat === path + mode = path.mode mode_sym.split(/,/).inject(mode & 07777) do |current_mode, clause| target, *actions = clause.split(/([=+-])/) raise ArgumentError, "invalid file mode: #{mode_sym}" if actions.empty? @@ -840,7 +1686,7 @@ module Bundler::FileUtils when "x" mask | 0111 when "X" - if FileTest.directory? path + if path.directory? mask | 0111 else mask @@ -857,7 +1703,7 @@ module Bundler::FileUtils copy_mask = user_mask(chr) (current_mode & copy_mask) / (copy_mask & 0111) * (user_mask & 0111) else - raise ArgumentError, "invalid `perm' symbol in file mode: #{chr}" + raise ArgumentError, "invalid 'perm' symbol in file mode: #{chr}" end end @@ -880,37 +1726,78 @@ module Bundler::FileUtils end private_module_function :mode_to_s + # Changes permissions on the entries at the paths given in +list+ + # (a single path or an array of paths) + # to the permissions given by +mode+; + # returns +list+ if it is an array, <tt>[list]</tt> otherwise: + # + # - Modifies each entry that is a regular file using + # {File.chmod}[rdoc-ref:File.chmod]. + # - Modifies each entry that is a symbolic link using + # {File.lchmod}[rdoc-ref:File.lchmod]. + # + # Argument +list+ or its elements + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. + # + # Argument +mode+ may be either an integer or a string: + # + # - \Integer +mode+: represents the permission bits to be set: + # + # Bundler::FileUtils.chmod(0755, 'src0.txt') + # Bundler::FileUtils.chmod(0644, ['src0.txt', 'src0.dat']) + # + # - \String +mode+: represents the permissions to be set: + # + # The string is of the form <tt>[targets][[operator][perms[,perms]]</tt>, where: + # + # - +targets+ may be any combination of these letters: + # + # - <tt>'u'</tt>: permissions apply to the file's owner. + # - <tt>'g'</tt>: permissions apply to users in the file's group. + # - <tt>'o'</tt>: permissions apply to other users not in the file's group. + # - <tt>'a'</tt> (the default): permissions apply to all users. + # + # - +operator+ may be one of these letters: + # + # - <tt>'+'</tt>: adds permissions. + # - <tt>'-'</tt>: removes permissions. + # - <tt>'='</tt>: sets (replaces) permissions. + # + # - +perms+ (may be repeated, with separating commas) + # may be any combination of these letters: + # + # - <tt>'r'</tt>: Read. + # - <tt>'w'</tt>: Write. + # - <tt>'x'</tt>: Execute (search, for a directory). + # - <tt>'X'</tt>: Search (for a directories only; + # must be used with <tt>'+'</tt>) + # - <tt>'s'</tt>: Uid or gid. + # - <tt>'t'</tt>: Sticky bit. + # + # Examples: + # + # Bundler::FileUtils.chmod('u=wrx,go=rx', 'src1.txt') + # Bundler::FileUtils.chmod('u=wrx,go=rx', '/usr/bin/ruby') + # + # Keyword arguments: + # + # - <tt>noop: true</tt> - does not change permissions; returns +nil+. + # - <tt>verbose: true</tt> - prints an equivalent command: + # + # Bundler::FileUtils.chmod(0755, 'src0.txt', noop: true, verbose: true) + # Bundler::FileUtils.chmod(0644, ['src0.txt', 'src0.dat'], noop: true, verbose: true) + # Bundler::FileUtils.chmod('u=wrx,go=rx', 'src1.txt', noop: true, verbose: true) + # Bundler::FileUtils.chmod('u=wrx,go=rx', '/usr/bin/ruby', noop: true, verbose: true) + # + # Output: + # + # chmod 755 src0.txt + # chmod 644 src0.txt src0.dat + # chmod u=wrx,go=rx src1.txt + # chmod u=wrx,go=rx /usr/bin/ruby + # + # Related: Bundler::FileUtils.chmod_R. # - # Changes permission bits on the named files (in +list+) to the bit pattern - # represented by +mode+. - # - # +mode+ is the symbolic and absolute mode can be used. - # - # Absolute mode is - # Bundler::FileUtils.chmod 0755, 'somecommand' - # Bundler::FileUtils.chmod 0644, %w(my.rb your.rb his.rb her.rb) - # Bundler::FileUtils.chmod 0755, '/usr/bin/ruby', :verbose => true - # - # Symbolic mode is - # Bundler::FileUtils.chmod "u=wrx,go=rx", 'somecommand' - # Bundler::FileUtils.chmod "u=wr,go=rr", %w(my.rb your.rb his.rb her.rb) - # Bundler::FileUtils.chmod "u=wrx,go=rx", '/usr/bin/ruby', :verbose => true - # - # "a" :: is user, group, other mask. - # "u" :: is user's mask. - # "g" :: is group's mask. - # "o" :: is other's mask. - # "w" :: is write permission. - # "r" :: is read permission. - # "x" :: is execute permission. - # "X" :: - # is execute permission for directories only, must be used in conjunction with "+" - # "s" :: is uid, gid. - # "t" :: is sticky bit. - # "+" :: is added to a class given the specified mode. - # "-" :: Is removed from a given class given mode. - # "=" :: Is the exact nature of the class will be given a specified mode. - def chmod(mode, list, noop: nil, verbose: nil) list = fu_list(list) fu_output_message sprintf('chmod %s %s', mode_to_s(mode), list.join(' ')) if verbose @@ -921,12 +1808,7 @@ module Bundler::FileUtils end module_function :chmod - # - # Changes permission bits on the named files (in +list+) - # to the bit pattern represented by +mode+. - # - # Bundler::FileUtils.chmod_R 0700, "/tmp/app.#{$$}" - # Bundler::FileUtils.chmod_R "u=wrx", "/tmp/app.#{$$}" + # Like Bundler::FileUtils.chmod, but changes permissions recursively. # def chmod_R(mode, list, noop: nil, verbose: nil, force: nil) list = fu_list(list) @@ -946,15 +1828,68 @@ module Bundler::FileUtils end module_function :chmod_R + # Changes the owner and group on the entries at the paths given in +list+ + # (a single path or an array of paths) + # to the given +user+ and +group+; + # returns +list+ if it is an array, <tt>[list]</tt> otherwise: + # + # - Modifies each entry that is a regular file using + # {File.chown}[rdoc-ref:File.chown]. + # - Modifies each entry that is a symbolic link using + # {File.lchown}[rdoc-ref:File.lchown]. + # + # Argument +list+ or its elements + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. + # + # User and group: + # + # - Argument +user+ may be a user name or a user id; + # if +nil+ or +-1+, the user is not changed. + # - Argument +group+ may be a group name or a group id; + # if +nil+ or +-1+, the group is not changed. + # - The user must be a member of the group. + # + # Examples: + # + # # One path. + # # User and group as string names. + # File.stat('src0.txt').uid # => 1004 + # File.stat('src0.txt').gid # => 1004 + # Bundler::FileUtils.chown('user2', 'group1', 'src0.txt') + # File.stat('src0.txt').uid # => 1006 + # File.stat('src0.txt').gid # => 1005 + # + # # User and group as uid and gid. + # Bundler::FileUtils.chown(1004, 1004, 'src0.txt') + # File.stat('src0.txt').uid # => 1004 + # File.stat('src0.txt').gid # => 1004 + # + # # Array of paths. + # Bundler::FileUtils.chown(1006, 1005, ['src0.txt', 'src0.dat']) + # + # # Directory (not recursive). + # Bundler::FileUtils.chown('user2', 'group1', '.') + # + # Keyword arguments: # - # Changes owner and group on the named files (in +list+) - # to the user +user+ and the group +group+. +user+ and +group+ - # may be an ID (Integer/String) or a name (String). - # If +user+ or +group+ is nil, this method does not change - # the attribute. + # - <tt>noop: true</tt> - does not change permissions; returns +nil+. + # - <tt>verbose: true</tt> - prints an equivalent command: # - # Bundler::FileUtils.chown 'root', 'staff', '/usr/local/bin/ruby' - # Bundler::FileUtils.chown nil, 'bin', Dir.glob('/usr/bin/*'), :verbose => true + # Bundler::FileUtils.chown('user2', 'group1', 'src0.txt', noop: true, verbose: true) + # Bundler::FileUtils.chown(1004, 1004, 'src0.txt', noop: true, verbose: true) + # Bundler::FileUtils.chown(1006, 1005, ['src0.txt', 'src0.dat'], noop: true, verbose: true) + # Bundler::FileUtils.chown('user2', 'group1', path, noop: true, verbose: true) + # Bundler::FileUtils.chown('user2', 'group1', '.', noop: true, verbose: true) + # + # Output: + # + # chown user2:group1 src0.txt + # chown 1004:1004 src0.txt + # chown 1006:1005 src0.txt src0.dat + # chown user2:group1 src0.txt + # chown user2:group1 . + # + # Related: Bundler::FileUtils.chown_R. # def chown(user, group, list, noop: nil, verbose: nil) list = fu_list(list) @@ -970,15 +1905,7 @@ module Bundler::FileUtils end module_function :chown - # - # Changes owner and group on the named files (in +list+) - # to the user +user+ and the group +group+ recursively. - # +user+ and +group+ may be an ID (Integer/String) or - # a name (String). If +user+ or +group+ is nil, this - # method does not change the attribute. - # - # Bundler::FileUtils.chown_R 'www', 'www', '/var/www/htdocs' - # Bundler::FileUtils.chown_R 'cvs', 'cvs', '/var/cvs', :verbose => true + # Like Bundler::FileUtils.chown, but changes owner and group recursively. # def chown_R(user, group, list, noop: nil, verbose: nil, force: nil) list = fu_list(list) @@ -1001,11 +1928,6 @@ module Bundler::FileUtils end module_function :chown_R - begin - require 'etc' - rescue LoadError # rescue LoadError for miniruby - end - def fu_get_uid(user) #:nodoc: return nil unless user case user @@ -1014,6 +1936,7 @@ module Bundler::FileUtils when /\A\d+\z/ user.to_i else + require 'etc' Etc.getpwnam(user) ? Etc.getpwnam(user).uid : nil end end @@ -1027,17 +1950,56 @@ module Bundler::FileUtils when /\A\d+\z/ group.to_i else + require 'etc' Etc.getgrnam(group) ? Etc.getgrnam(group).gid : nil end end private_module_function :fu_get_gid + # Updates modification times (mtime) and access times (atime) + # of the entries given by the paths in +list+ + # (a single path or an array of paths); + # returns +list+ if it is an array, <tt>[list]</tt> otherwise. + # + # By default, creates an empty file for any path to a non-existent entry; + # use keyword argument +nocreate+ to raise an exception instead. + # + # Argument +list+ or its elements + # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. + # + # Examples: # - # Updates modification time (mtime) and access time (atime) of file(s) in - # +list+. Files are created if they don't exist. + # # Single path. + # f = File.new('src0.txt') # Existing file. + # f.atime # => 2022-06-10 11:11:21.200277 -0700 + # f.mtime # => 2022-06-10 11:11:21.200277 -0700 + # Bundler::FileUtils.touch('src0.txt') + # f = File.new('src0.txt') + # f.atime # => 2022-06-11 08:28:09.8185343 -0700 + # f.mtime # => 2022-06-11 08:28:09.8185343 -0700 # - # Bundler::FileUtils.touch 'timestamp' - # Bundler::FileUtils.touch Dir.glob('*.c'); system 'make' + # # Array of paths. + # Bundler::FileUtils.touch(['src0.txt', 'src0.dat']) + # + # Keyword arguments: + # + # - <tt>mtime: <i>time</i></tt> - sets the entry's mtime to the given time, + # instead of the current time. + # - <tt>nocreate: true</tt> - raises an exception if the entry does not exist. + # - <tt>noop: true</tt> - does not touch entries; returns +nil+. + # - <tt>verbose: true</tt> - prints an equivalent command: + # + # Bundler::FileUtils.touch('src0.txt', noop: true, verbose: true) + # Bundler::FileUtils.touch(['src0.txt', 'src0.dat'], noop: true, verbose: true) + # Bundler::FileUtils.touch(path, noop: true, verbose: true) + # + # Output: + # + # touch src0.txt + # touch src0.txt src0.dat + # touch src0.txt + # + # Related: Bundler::FileUtils.uptodate?. # def touch(list, noop: nil, verbose: nil, mtime: nil, nocreate: nil) list = fu_list(list) @@ -1064,18 +2026,22 @@ module Bundler::FileUtils private - module StreamUtils_ + module StreamUtils_ # :nodoc: + private - def fu_windows? - /mswin|mingw|bccwin|emx/ =~ RUBY_PLATFORM + case (defined?(::RbConfig) ? ::RbConfig::CONFIG['host_os'] : ::RUBY_PLATFORM) + when /mswin|mingw/ + def fu_windows?; true end #:nodoc: + else + def fu_windows?; false end #:nodoc: end def fu_copy_stream0(src, dest, blksize = nil) #:nodoc: IO.copy_stream(src, dest) end - def fu_stream_blksize(*streams) + def fu_stream_blksize(*streams) #:nodoc: streams.each do |s| next unless s.respond_to?(:stat) size = fu_blksize(s.stat) @@ -1084,14 +2050,14 @@ module Bundler::FileUtils fu_default_blksize() end - def fu_blksize(st) + def fu_blksize(st) #:nodoc: s = st.blksize return nil unless s return nil if s == 0 s end - def fu_default_blksize + def fu_default_blksize #:nodoc: 1024 end end @@ -1192,10 +2158,12 @@ module Bundler::FileUtils def entries opts = {} - opts[:encoding] = ::Encoding::UTF_8 if fu_windows? - Dir.entries(path(), opts)\ - .reject {|n| n == '.' or n == '..' }\ - .map {|n| Entry_.new(prefix(), join(rel(), n.untaint)) } + opts[:encoding] = fu_windows? ? ::Encoding::UTF_8 : path.encoding + + files = Dir.children(path, **opts) + + untaint = RUBY_VERSION < '2.7' + files.map {|n| Entry_.new(prefix(), join(rel(), untaint ? n.untaint : n)) } end def stat @@ -1240,6 +2208,7 @@ module Bundler::FileUtils else File.chmod mode, path() end + rescue Errno::EOPNOTSUPP end def chown(uid, gid) @@ -1250,6 +2219,22 @@ module Bundler::FileUtils end end + def link(dest) + case + when directory? + if !File.exist?(dest) and descendant_directory?(dest, path) + raise ArgumentError, "cannot link directory %s to itself %s" % [path, dest] + end + begin + Dir.mkdir dest + rescue + raise unless File.directory?(dest) + end + else + File.link path(), dest + end + end + def copy(dest) lstat case @@ -1266,18 +2251,21 @@ module Bundler::FileUtils end when symlink? File.symlink File.readlink(path()), dest - when chardev? - raise "cannot handle device file" unless File.respond_to?(:mknod) - mknod dest, ?c, 0666, lstat().rdev - when blockdev? - raise "cannot handle device file" unless File.respond_to?(:mknod) - mknod dest, ?b, 0666, lstat().rdev + when chardev?, blockdev? + raise "cannot handle device file" when socket? - raise "cannot handle socket" unless File.respond_to?(:mknod) - mknod dest, nil, lstat().mode, 0 + begin + require 'socket' + rescue LoadError + raise "cannot handle socket" + else + raise "cannot handle socket" unless defined?(UNIXServer) + end + UNIXServer.new(dest).close + File.chmod lstat().mode, dest when pipe? raise "cannot handle FIFO" unless File.respond_to?(:mkfifo) - mkfifo dest, 0666 + File.mkfifo dest, lstat().mode when door? raise "cannot handle door: #{path()}" else @@ -1315,7 +2303,7 @@ module Bundler::FileUtils if st.symlink? begin File.lchmod mode, path - rescue NotImplementedError + rescue NotImplementedError, Errno::EOPNOTSUPP end else File.chmod mode, path @@ -1374,13 +2362,21 @@ module Bundler::FileUtils def postorder_traverse if directory? - entries().each do |ent| + begin + children = entries() + rescue Errno::EACCES + # Failed to get the list of children. + # Assuming there is no children, try to process the parent directory. + yield self + return + end + + children.each do |ent| ent.postorder_traverse do |e| yield e end end end - ensure yield self end @@ -1396,14 +2392,14 @@ module Bundler::FileUtils private - $fileutils_rb_have_lchmod = nil + @@fileutils_rb_have_lchmod = nil def have_lchmod? # This is not MT-safe, but it does not matter. - if $fileutils_rb_have_lchmod == nil - $fileutils_rb_have_lchmod = check_have_lchmod? + if @@fileutils_rb_have_lchmod == nil + @@fileutils_rb_have_lchmod = check_have_lchmod? end - $fileutils_rb_have_lchmod + @@fileutils_rb_have_lchmod end def check_have_lchmod? @@ -1414,14 +2410,14 @@ module Bundler::FileUtils return false end - $fileutils_rb_have_lchown = nil + @@fileutils_rb_have_lchown = nil def have_lchown? # This is not MT-safe, but it does not matter. - if $fileutils_rb_have_lchown == nil - $fileutils_rb_have_lchown = check_have_lchown? + if @@fileutils_rb_have_lchown == nil + @@fileutils_rb_have_lchown = check_have_lchown? end - $fileutils_rb_have_lchown + @@fileutils_rb_have_lchown end def check_have_lchown? @@ -1435,7 +2431,15 @@ module Bundler::FileUtils def join(dir, base) return File.path(dir) if not base or base == '.' return File.path(base) if not dir or dir == '.' - File.join(dir, base) + begin + File.join(dir, base) + rescue EncodingError + if fu_windows? + File.join(dir.encode(::Encoding::UTF_8), base.encode(::Encoding::UTF_8)) + else + raise + end + end end if File::ALT_SEPARATOR @@ -1443,10 +2447,13 @@ module Bundler::FileUtils else DIRECTORY_TERM = "(?=/|\\z)" end - SYSCASE = File::FNM_SYSCASE.nonzero? ? "-i" : "" def descendant_directory?(descendant, ascendant) - /\A(?#{SYSCASE}:#{Regexp.quote(ascendant)})#{DIRECTORY_TERM}/ =~ File.dirname(descendant) + if File::FNM_SYSCASE.nonzero? + File.expand_path(File.dirname(descendant)).casecmp(File.expand_path(ascendant)) == 0 + else + File.expand_path(File.dirname(descendant)) == File.expand_path(ascendant) + end end end # class Entry_ @@ -1463,15 +2470,19 @@ module Bundler::FileUtils end private_module_function :fu_each_src_dest - def fu_each_src_dest0(src, dest) #:nodoc: + def fu_each_src_dest0(src, dest, target_directory = true) #:nodoc: if tmp = Array.try_convert(src) + unless target_directory or tmp.size <= 1 + tmp = tmp.map {|f| File.path(f)} # A workaround for RBS + raise ArgumentError, "extra target #{tmp}" + end tmp.each do |s| s = File.path(s) - yield s, File.join(dest, File.basename(s)) + yield s, (target_directory ? File.join(dest, File.basename(s)) : dest) end else src = File.path(src) - if File.directory?(dest) + if target_directory and File.directory?(dest) yield src, File.join(dest, File.basename(src)) else yield src, File.path(dest) @@ -1485,16 +2496,70 @@ module Bundler::FileUtils end private_module_function :fu_same? - @fileutils_output = $stderr - @fileutils_label = '' - def fu_output_message(msg) #:nodoc: - @fileutils_output ||= $stderr - @fileutils_label ||= '' - @fileutils_output.puts @fileutils_label + msg + output = @fileutils_output if defined?(@fileutils_output) + output ||= $stdout + if defined?(@fileutils_label) + msg = @fileutils_label + msg + end + output.puts msg end private_module_function :fu_output_message + def fu_split_path(path) #:nodoc: + path = File.path(path) + list = [] + until (parent, base = File.split(path); parent == path or parent == ".") + if base != '..' and list.last == '..' and !(fu_have_symlink? && File.symlink?(path)) + list.pop + else + list << base + end + path = parent + end + list << path + list.reverse! + end + private_module_function :fu_split_path + + def fu_common_components(target, base) #:nodoc: + i = 0 + while target[i]&.== base[i] + i += 1 + end + i + end + private_module_function :fu_common_components + + def fu_clean_components(*comp) #:nodoc: + comp.shift while comp.first == "." + return comp if comp.empty? + clean = [comp.shift] + path = File.join(*clean, "") # ending with File::SEPARATOR + while c = comp.shift + if c == ".." and clean.last != ".." and !(fu_have_symlink? && File.symlink?(path)) + clean.pop + path.sub!(%r((?<=\A|/)[^/]+/\z), "") + else + clean << c + path << c << "/" + end + end + clean + end + private_module_function :fu_clean_components + + if fu_windows? + def fu_starting_path?(path) #:nodoc: + path&.start_with?(%r(\w:|/)) + end + else + def fu_starting_path?(path) #:nodoc: + path&.start_with?("/") + end + end + private_module_function :fu_starting_path? + # This hash table holds command options. OPT_TABLE = {} #:nodoc: internal use only (private_instance_methods & methods(false)).inject(OPT_TABLE) {|tbl, name| @@ -1502,62 +2567,66 @@ module Bundler::FileUtils tbl } + public + + # Returns an array of the string names of \Bundler::FileUtils methods + # that accept one or more keyword arguments: # - # Returns an Array of method names which have any options. - # - # p Bundler::FileUtils.commands #=> ["chmod", "cp", "cp_r", "install", ...] + # Bundler::FileUtils.commands.sort.take(3) # => ["cd", "chdir", "chmod"] # def self.commands OPT_TABLE.keys end + # Returns an array of the string keyword names: # - # Returns an Array of option names. - # - # p Bundler::FileUtils.options #=> ["noop", "force", "verbose", "preserve", "mode"] + # Bundler::FileUtils.options.take(3) # => ["noop", "verbose", "force"] # def self.options OPT_TABLE.values.flatten.uniq.map {|sym| sym.to_s } end + # Returns +true+ if method +mid+ accepts the given option +opt+, +false+ otherwise; + # the arguments may be strings or symbols: # - # Returns true if the method +mid+ have an option +opt+. - # - # p Bundler::FileUtils.have_option?(:cp, :noop) #=> true - # p Bundler::FileUtils.have_option?(:rm, :force) #=> true - # p Bundler::FileUtils.have_option?(:rm, :preserve) #=> false + # Bundler::FileUtils.have_option?(:chmod, :noop) # => true + # Bundler::FileUtils.have_option?('chmod', 'secure') # => false # def self.have_option?(mid, opt) li = OPT_TABLE[mid.to_s] or raise ArgumentError, "no such method: #{mid}" li.include?(opt) end + # Returns an array of the string keyword name for method +mid+; + # the argument may be a string or a symbol: # - # Returns an Array of option names of the method +mid+. - # - # p Bundler::FileUtils.options_of(:rm) #=> ["noop", "verbose", "force"] + # Bundler::FileUtils.options_of(:rm) # => ["force", "noop", "verbose"] + # Bundler::FileUtils.options_of('mv') # => ["force", "noop", "verbose", "secure"] # def self.options_of(mid) OPT_TABLE[mid.to_s].map {|sym| sym.to_s } end + # Returns an array of the string method names of the methods + # that accept the given keyword option +opt+; + # the argument must be a symbol: # - # Returns an Array of method names which have the option +opt+. - # - # p Bundler::FileUtils.collect_method(:preserve) #=> ["cp", "cp_r", "copy", "install"] + # Bundler::FileUtils.collect_method(:preserve) # => ["cp", "copy", "cp_r", "install"] # def self.collect_method(opt) OPT_TABLE.keys.select {|m| OPT_TABLE[m].include?(opt) } end - LOW_METHODS = singleton_methods(false) - collect_method(:noop).map(&:intern) - module LowMethods + private + + LOW_METHODS = singleton_methods(false) - collect_method(:noop).map(&:intern) # :nodoc: + module LowMethods # :nodoc: internal use only private def _do_nothing(*)end ::Bundler::FileUtils::LOW_METHODS.map {|name| alias_method name, :_do_nothing} end - METHODS = singleton_methods() - [:private_module_function, + METHODS = singleton_methods() - [:private_module_function, # :nodoc: :commands, :options, :have_option?, :options_of, :collect_method] # @@ -1567,8 +2636,6 @@ module Bundler::FileUtils # module Verbose include Bundler::FileUtils - @fileutils_output = $stderr - @fileutils_label = '' names = ::Bundler::FileUtils.collect_method(:verbose) names.each do |name| module_eval(<<-EOS, __FILE__, __LINE__ + 1) @@ -1592,8 +2659,6 @@ module Bundler::FileUtils module NoWrite include Bundler::FileUtils include LowMethods - @fileutils_output = $stderr - @fileutils_label = '' names = ::Bundler::FileUtils.collect_method(:noop) names.each do |name| module_eval(<<-EOS, __FILE__, __LINE__ + 1) @@ -1618,8 +2683,6 @@ module Bundler::FileUtils module DryRun include Bundler::FileUtils include LowMethods - @fileutils_output = $stderr - @fileutils_label = '' names = ::Bundler::FileUtils.collect_method(:noop) names.each do |name| module_eval(<<-EOS, __FILE__, __LINE__ + 1) diff --git a/lib/bundler/vendor/molinillo/lib/molinillo.rb b/lib/bundler/vendor/molinillo/lib/molinillo.rb deleted file mode 100644 index 9e2867144f..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -require 'bundler/vendor/molinillo/lib/molinillo/compatibility' -require 'bundler/vendor/molinillo/lib/molinillo/gem_metadata' -require 'bundler/vendor/molinillo/lib/molinillo/errors' -require 'bundler/vendor/molinillo/lib/molinillo/resolver' -require 'bundler/vendor/molinillo/lib/molinillo/modules/ui' -require 'bundler/vendor/molinillo/lib/molinillo/modules/specification_provider' - -# Bundler::Molinillo is a generic dependency resolution algorithm. -module Bundler::Molinillo -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/compatibility.rb b/lib/bundler/vendor/molinillo/lib/molinillo/compatibility.rb deleted file mode 100644 index 3eba8e4083..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/compatibility.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -module Bundler::Molinillo - # Hacks needed for old Ruby versions. - module Compatibility - module_function - - if [].respond_to?(:flat_map) - # Flat map - # @param [Enumerable] enum an enumerable object - # @block the block to flat-map with - # @return The enum, flat-mapped - def flat_map(enum, &blk) - enum.flat_map(&blk) - end - else - # Flat map - # @param [Enumerable] enum an enumerable object - # @block the block to flat-map with - # @return The enum, flat-mapped - def flat_map(enum, &blk) - enum.map(&blk).flatten(1) - end - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb deleted file mode 100644 index bcacf35243..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -module Bundler::Molinillo - # @!visibility private - module Delegates - # Delegates all {Bundler::Molinillo::ResolutionState} methods to a `#state` property. - module ResolutionState - # (see Bundler::Molinillo::ResolutionState#name) - def name - current_state = state || Bundler::Molinillo::ResolutionState.empty - current_state.name - end - - # (see Bundler::Molinillo::ResolutionState#requirements) - def requirements - current_state = state || Bundler::Molinillo::ResolutionState.empty - current_state.requirements - end - - # (see Bundler::Molinillo::ResolutionState#activated) - def activated - current_state = state || Bundler::Molinillo::ResolutionState.empty - current_state.activated - end - - # (see Bundler::Molinillo::ResolutionState#requirement) - def requirement - current_state = state || Bundler::Molinillo::ResolutionState.empty - current_state.requirement - end - - # (see Bundler::Molinillo::ResolutionState#possibilities) - def possibilities - current_state = state || Bundler::Molinillo::ResolutionState.empty - current_state.possibilities - end - - # (see Bundler::Molinillo::ResolutionState#depth) - def depth - current_state = state || Bundler::Molinillo::ResolutionState.empty - current_state.depth - end - - # (see Bundler::Molinillo::ResolutionState#conflicts) - def conflicts - current_state = state || Bundler::Molinillo::ResolutionState.empty - current_state.conflicts - end - - # (see Bundler::Molinillo::ResolutionState#unused_unwind_options) - def unused_unwind_options - current_state = state || Bundler::Molinillo::ResolutionState.empty - current_state.unused_unwind_options - end - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb deleted file mode 100644 index ec9c770a28..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -module Bundler::Molinillo - module Delegates - # Delegates all {Bundler::Molinillo::SpecificationProvider} methods to a - # `#specification_provider` property. - module SpecificationProvider - # (see Bundler::Molinillo::SpecificationProvider#search_for) - def search_for(dependency) - with_no_such_dependency_error_handling do - specification_provider.search_for(dependency) - end - end - - # (see Bundler::Molinillo::SpecificationProvider#dependencies_for) - def dependencies_for(specification) - with_no_such_dependency_error_handling do - specification_provider.dependencies_for(specification) - end - end - - # (see Bundler::Molinillo::SpecificationProvider#requirement_satisfied_by?) - def requirement_satisfied_by?(requirement, activated, spec) - with_no_such_dependency_error_handling do - specification_provider.requirement_satisfied_by?(requirement, activated, spec) - end - end - - # (see Bundler::Molinillo::SpecificationProvider#name_for) - def name_for(dependency) - with_no_such_dependency_error_handling do - specification_provider.name_for(dependency) - end - end - - # (see Bundler::Molinillo::SpecificationProvider#name_for_explicit_dependency_source) - def name_for_explicit_dependency_source - with_no_such_dependency_error_handling do - specification_provider.name_for_explicit_dependency_source - end - end - - # (see Bundler::Molinillo::SpecificationProvider#name_for_locking_dependency_source) - def name_for_locking_dependency_source - with_no_such_dependency_error_handling do - specification_provider.name_for_locking_dependency_source - end - end - - # (see Bundler::Molinillo::SpecificationProvider#sort_dependencies) - def sort_dependencies(dependencies, activated, conflicts) - with_no_such_dependency_error_handling do - specification_provider.sort_dependencies(dependencies, activated, conflicts) - end - end - - # (see Bundler::Molinillo::SpecificationProvider#allow_missing?) - def allow_missing?(dependency) - with_no_such_dependency_error_handling do - specification_provider.allow_missing?(dependency) - end - end - - private - - # Ensures any raised {NoSuchDependencyError} has its - # {NoSuchDependencyError#required_by} set. - # @yield - def with_no_such_dependency_error_handling - yield - rescue NoSuchDependencyError => error - if state - vertex = activated.vertex_named(name_for(error.dependency)) - error.required_by += vertex.incoming_edges.map { |e| e.origin.name } - error.required_by << name_for_explicit_dependency_source unless vertex.explicit_requirements.empty? - end - raise - end - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb deleted file mode 100644 index 677a8bd916..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb +++ /dev/null @@ -1,223 +0,0 @@ -# frozen_string_literal: true - -require 'set' -require 'tsort' - -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/log' -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex' - -module Bundler::Molinillo - # A directed acyclic graph that is tuned to hold named dependencies - class DependencyGraph - include Enumerable - - # Enumerates through the vertices of the graph. - # @return [Array<Vertex>] The graph's vertices. - def each - return vertices.values.each unless block_given? - vertices.values.each { |v| yield v } - end - - include TSort - - # @!visibility private - alias tsort_each_node each - - # @!visibility private - def tsort_each_child(vertex, &block) - vertex.successors.each(&block) - end - - # Topologically sorts the given vertices. - # @param [Enumerable<Vertex>] vertices the vertices to be sorted, which must - # all belong to the same graph. - # @return [Array<Vertex>] The sorted vertices. - def self.tsort(vertices) - TSort.tsort( - lambda { |b| vertices.each(&b) }, - lambda { |v, &b| (v.successors & vertices).each(&b) } - ) - end - - # A directed edge of a {DependencyGraph} - # @attr [Vertex] origin The origin of the directed edge - # @attr [Vertex] destination The destination of the directed edge - # @attr [Object] requirement The requirement the directed edge represents - Edge = Struct.new(:origin, :destination, :requirement) - - # @return [{String => Vertex}] the vertices of the dependency graph, keyed - # by {Vertex#name} - attr_reader :vertices - - # @return [Log] the op log for this graph - attr_reader :log - - # Initializes an empty dependency graph - def initialize - @vertices = {} - @log = Log.new - end - - # Tags the current state of the dependency as the given tag - # @param [Object] tag an opaque tag for the current state of the graph - # @return [Void] - def tag(tag) - log.tag(self, tag) - end - - # Rewinds the graph to the state tagged as `tag` - # @param [Object] tag the tag to rewind to - # @return [Void] - def rewind_to(tag) - log.rewind_to(self, tag) - end - - # Initializes a copy of a {DependencyGraph}, ensuring that all {#vertices} - # are properly copied. - # @param [DependencyGraph] other the graph to copy. - def initialize_copy(other) - super - @vertices = {} - @log = other.log.dup - traverse = lambda do |new_v, old_v| - return if new_v.outgoing_edges.size == old_v.outgoing_edges.size - old_v.outgoing_edges.each do |edge| - destination = add_vertex(edge.destination.name, edge.destination.payload) - add_edge_no_circular(new_v, destination, edge.requirement) - traverse.call(destination, edge.destination) - end - end - other.vertices.each do |name, vertex| - new_vertex = add_vertex(name, vertex.payload, vertex.root?) - new_vertex.explicit_requirements.replace(vertex.explicit_requirements) - traverse.call(new_vertex, vertex) - end - end - - # @return [String] a string suitable for debugging - def inspect - "#{self.class}:#{vertices.values.inspect}" - end - - # @param [Hash] options options for dot output. - # @return [String] Returns a dot format representation of the graph - def to_dot(options = {}) - edge_label = options.delete(:edge_label) - raise ArgumentError, "Unknown options: #{options.keys}" unless options.empty? - - dot_vertices = [] - dot_edges = [] - vertices.each do |n, v| - dot_vertices << " #{n} [label=\"{#{n}|#{v.payload}}\"]" - v.outgoing_edges.each do |e| - label = edge_label ? edge_label.call(e) : e.requirement - dot_edges << " #{e.origin.name} -> #{e.destination.name} [label=#{label.to_s.dump}]" - end - end - - dot_vertices.uniq! - dot_vertices.sort! - dot_edges.uniq! - dot_edges.sort! - - dot = dot_vertices.unshift('digraph G {').push('') + dot_edges.push('}') - dot.join("\n") - end - - # @return [Boolean] whether the two dependency graphs are equal, determined - # by a recursive traversal of each {#root_vertices} and its - # {Vertex#successors} - def ==(other) - return false unless other - return true if equal?(other) - vertices.each do |name, vertex| - other_vertex = other.vertex_named(name) - return false unless other_vertex - return false unless vertex.payload == other_vertex.payload - return false unless other_vertex.successors.to_set == vertex.successors.to_set - end - end - - # @param [String] name - # @param [Object] payload - # @param [Array<String>] parent_names - # @param [Object] requirement the requirement that is requiring the child - # @return [void] - def add_child_vertex(name, payload, parent_names, requirement) - root = !parent_names.delete(nil) { true } - vertex = add_vertex(name, payload, root) - vertex.explicit_requirements << requirement if root - parent_names.each do |parent_name| - parent_vertex = vertex_named(parent_name) - add_edge(parent_vertex, vertex, requirement) - end - vertex - end - - # Adds a vertex with the given name, or updates the existing one. - # @param [String] name - # @param [Object] payload - # @return [Vertex] the vertex that was added to `self` - def add_vertex(name, payload, root = false) - log.add_vertex(self, name, payload, root) - end - - # Detaches the {#vertex_named} `name` {Vertex} from the graph, recursively - # removing any non-root vertices that were orphaned in the process - # @param [String] name - # @return [Array<Vertex>] the vertices which have been detached - def detach_vertex_named(name) - log.detach_vertex_named(self, name) - end - - # @param [String] name - # @return [Vertex,nil] the vertex with the given name - def vertex_named(name) - vertices[name] - end - - # @param [String] name - # @return [Vertex,nil] the root vertex with the given name - def root_vertex_named(name) - vertex = vertex_named(name) - vertex if vertex && vertex.root? - end - - # Adds a new {Edge} to the dependency graph - # @param [Vertex] origin - # @param [Vertex] destination - # @param [Object] requirement the requirement that this edge represents - # @return [Edge] the added edge - def add_edge(origin, destination, requirement) - if destination.path_to?(origin) - raise CircularDependencyError.new([origin, destination]) - end - add_edge_no_circular(origin, destination, requirement) - end - - # Deletes an {Edge} from the dependency graph - # @param [Edge] edge - # @return [Void] - def delete_edge(edge) - log.delete_edge(self, edge.origin.name, edge.destination.name, edge.requirement) - end - - # Sets the payload of the vertex with the given name - # @param [String] name the name of the vertex - # @param [Object] payload the payload - # @return [Void] - def set_payload(name, payload) - log.set_payload(self, name, payload) - end - - private - - # Adds a new {Edge} to the dependency graph without checking for - # circularity. - # @param (see #add_edge) - # @return (see #add_edge) - def add_edge_no_circular(origin, destination, requirement) - log.add_edge_no_circular(self, origin.name, destination.name, requirement) - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb deleted file mode 100644 index c04c7eec9c..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -module Bundler::Molinillo - class DependencyGraph - # An action that modifies a {DependencyGraph} that is reversible. - # @abstract - class Action - # rubocop:disable Lint/UnusedMethodArgument - - # @return [Symbol] The name of the action. - def self.action_name - raise 'Abstract' - end - - # Performs the action on the given graph. - # @param [DependencyGraph] graph the graph to perform the action on. - # @return [Void] - def up(graph) - raise 'Abstract' - end - - # Reverses the action on the given graph. - # @param [DependencyGraph] graph the graph to reverse the action on. - # @return [Void] - def down(graph) - raise 'Abstract' - end - - # @return [Action,Nil] The previous action - attr_accessor :previous - - # @return [Action,Nil] The next action - attr_accessor :next - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb deleted file mode 100644 index 9849aea2fe..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' -module Bundler::Molinillo - class DependencyGraph - # @!visibility private - # (see DependencyGraph#add_edge_no_circular) - class AddEdgeNoCircular < Action - # @!group Action - - # (see Action.action_name) - def self.action_name - :add_vertex - end - - # (see Action#up) - def up(graph) - edge = make_edge(graph) - edge.origin.outgoing_edges << edge - edge.destination.incoming_edges << edge - edge - end - - # (see Action#down) - def down(graph) - edge = make_edge(graph) - delete_first(edge.origin.outgoing_edges, edge) - delete_first(edge.destination.incoming_edges, edge) - end - - # @!group AddEdgeNoCircular - - # @return [String] the name of the origin of the edge - attr_reader :origin - - # @return [String] the name of the destination of the edge - attr_reader :destination - - # @return [Object] the requirement that the edge represents - attr_reader :requirement - - # @param [DependencyGraph] graph the graph to find vertices from - # @return [Edge] The edge this action adds - def make_edge(graph) - Edge.new(graph.vertex_named(origin), graph.vertex_named(destination), requirement) - end - - # Initialize an action to add an edge to a dependency graph - # @param [String] origin the name of the origin of the edge - # @param [String] destination the name of the destination of the edge - # @param [Object] requirement the requirement that the edge represents - def initialize(origin, destination, requirement) - @origin = origin - @destination = destination - @requirement = requirement - end - - private - - def delete_first(array, item) - return unless index = array.index(item) - array.delete_at(index) - end - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb deleted file mode 100644 index 0a1e08255b..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' -module Bundler::Molinillo - class DependencyGraph - # @!visibility private - # (see DependencyGraph#add_vertex) - class AddVertex < Action # :nodoc: - # @!group Action - - # (see Action.action_name) - def self.action_name - :add_vertex - end - - # (see Action#up) - def up(graph) - if existing = graph.vertices[name] - @existing_payload = existing.payload - @existing_root = existing.root - end - vertex = existing || Vertex.new(name, payload) - graph.vertices[vertex.name] = vertex - vertex.payload ||= payload - vertex.root ||= root - vertex - end - - # (see Action#down) - def down(graph) - if defined?(@existing_payload) - vertex = graph.vertices[name] - vertex.payload = @existing_payload - vertex.root = @existing_root - else - graph.vertices.delete(name) - end - end - - # @!group AddVertex - - # @return [String] the name of the vertex - attr_reader :name - - # @return [Object] the payload for the vertex - attr_reader :payload - - # @return [Boolean] whether the vertex is root or not - attr_reader :root - - # Initialize an action to add a vertex to a dependency graph - # @param [String] name the name of the vertex - # @param [Object] payload the payload for the vertex - # @param [Boolean] root whether the vertex is root or not - def initialize(name, payload, root) - @name = name - @payload = payload - @root = root - end - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb deleted file mode 100644 index 1d9f4b327d..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' -module Bundler::Molinillo - class DependencyGraph - # @!visibility private - # (see DependencyGraph#delete_edge) - class DeleteEdge < Action - # @!group Action - - # (see Action.action_name) - def self.action_name - :delete_edge - end - - # (see Action#up) - def up(graph) - edge = make_edge(graph) - edge.origin.outgoing_edges.delete(edge) - edge.destination.incoming_edges.delete(edge) - end - - # (see Action#down) - def down(graph) - edge = make_edge(graph) - edge.origin.outgoing_edges << edge - edge.destination.incoming_edges << edge - edge - end - - # @!group DeleteEdge - - # @return [String] the name of the origin of the edge - attr_reader :origin_name - - # @return [String] the name of the destination of the edge - attr_reader :destination_name - - # @return [Object] the requirement that the edge represents - attr_reader :requirement - - # @param [DependencyGraph] graph the graph to find vertices from - # @return [Edge] The edge this action adds - def make_edge(graph) - Edge.new( - graph.vertex_named(origin_name), - graph.vertex_named(destination_name), - requirement - ) - end - - # Initialize an action to add an edge to a dependency graph - # @param [String] origin_name the name of the origin of the edge - # @param [String] destination_name the name of the destination of the edge - # @param [Object] requirement the requirement that the edge represents - def initialize(origin_name, destination_name, requirement) - @origin_name = origin_name - @destination_name = destination_name - @requirement = requirement - end - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb deleted file mode 100644 index 385dcbdd06..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' -module Bundler::Molinillo - class DependencyGraph - # @!visibility private - # @see DependencyGraph#detach_vertex_named - class DetachVertexNamed < Action - # @!group Action - - # (see Action#name) - def self.action_name - :add_vertex - end - - # (see Action#up) - def up(graph) - return [] unless @vertex = graph.vertices.delete(name) - - removed_vertices = [@vertex] - @vertex.outgoing_edges.each do |e| - v = e.destination - v.incoming_edges.delete(e) - if !v.root? && v.incoming_edges.empty? - removed_vertices.concat graph.detach_vertex_named(v.name) - end - end - - @vertex.incoming_edges.each do |e| - v = e.origin - v.outgoing_edges.delete(e) - end - - removed_vertices - end - - # (see Action#down) - def down(graph) - return unless @vertex - graph.vertices[@vertex.name] = @vertex - @vertex.outgoing_edges.each do |e| - e.destination.incoming_edges << e - end - @vertex.incoming_edges.each do |e| - e.origin.outgoing_edges << e - end - end - - # @!group DetachVertexNamed - - # @return [String] the name of the vertex to detach - attr_reader :name - - # Initialize an action to detach a vertex from a dependency graph - # @param [String] name the name of the vertex to detach - def initialize(name) - @name = name - end - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb deleted file mode 100644 index 8582dd19c1..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular' -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex' -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge' -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named' -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload' -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag' - -module Bundler::Molinillo - class DependencyGraph - # A log for dependency graph actions - class Log - # Initializes an empty log - def initialize - @current_action = @first_action = nil - end - - # @!macro [new] action - # {include:DependencyGraph#$0} - # @param [Graph] graph the graph to perform the action on - # @param (see DependencyGraph#$0) - # @return (see DependencyGraph#$0) - - # @macro action - def tag(graph, tag) - push_action(graph, Tag.new(tag)) - end - - # @macro action - def add_vertex(graph, name, payload, root) - push_action(graph, AddVertex.new(name, payload, root)) - end - - # @macro action - def detach_vertex_named(graph, name) - push_action(graph, DetachVertexNamed.new(name)) - end - - # @macro action - def add_edge_no_circular(graph, origin, destination, requirement) - push_action(graph, AddEdgeNoCircular.new(origin, destination, requirement)) - end - - # {include:DependencyGraph#delete_edge} - # @param [Graph] graph the graph to perform the action on - # @param [String] origin_name - # @param [String] destination_name - # @param [Object] requirement - # @return (see DependencyGraph#delete_edge) - def delete_edge(graph, origin_name, destination_name, requirement) - push_action(graph, DeleteEdge.new(origin_name, destination_name, requirement)) - end - - # @macro action - def set_payload(graph, name, payload) - push_action(graph, SetPayload.new(name, payload)) - end - - # Pops the most recent action from the log and undoes the action - # @param [DependencyGraph] graph - # @return [Action] the action that was popped off the log - def pop!(graph) - return unless action = @current_action - unless @current_action = action.previous - @first_action = nil - end - action.down(graph) - action - end - - extend Enumerable - - # @!visibility private - # Enumerates each action in the log - # @yield [Action] - def each - return enum_for unless block_given? - action = @first_action - loop do - break unless action - yield action - action = action.next - end - self - end - - # @!visibility private - # Enumerates each action in the log in reverse order - # @yield [Action] - def reverse_each - return enum_for(:reverse_each) unless block_given? - action = @current_action - loop do - break unless action - yield action - action = action.previous - end - self - end - - # @macro action - def rewind_to(graph, tag) - loop do - action = pop!(graph) - raise "No tag #{tag.inspect} found" unless action - break if action.class.action_name == :tag && action.tag == tag - end - end - - private - - # Adds the given action to the log, running the action - # @param [DependencyGraph] graph - # @param [Action] action - # @return The value returned by `action.up` - def push_action(graph, action) - action.previous = @current_action - @current_action.next = action if @current_action - @current_action = action - @first_action ||= action - action.up(graph) - end - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb deleted file mode 100644 index 37286d104a..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' -module Bundler::Molinillo - class DependencyGraph - # @!visibility private - # @see DependencyGraph#set_payload - class SetPayload < Action # :nodoc: - # @!group Action - - # (see Action.action_name) - def self.action_name - :set_payload - end - - # (see Action#up) - def up(graph) - vertex = graph.vertex_named(name) - @old_payload = vertex.payload - vertex.payload = payload - end - - # (see Action#down) - def down(graph) - graph.vertex_named(name).payload = @old_payload - end - - # @!group SetPayload - - # @return [String] the name of the vertex - attr_reader :name - - # @return [Object] the payload for the vertex - attr_reader :payload - - # Initialize an action to add set the payload for a vertex in a dependency - # graph - # @param [String] name the name of the vertex - # @param [Object] payload the payload for the vertex - def initialize(name, payload) - @name = name - @payload = payload - end - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb deleted file mode 100644 index d6ad16e07a..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' -module Bundler::Molinillo - class DependencyGraph - # @!visibility private - # @see DependencyGraph#tag - class Tag < Action - # @!group Action - - # (see Action.action_name) - def self.action_name - :tag - end - - # (see Action#up) - def up(_graph) - end - - # (see Action#down) - def down(_graph) - end - - # @!group Tag - - # @return [Object] An opaque tag - attr_reader :tag - - # Initialize an action to tag a state of a dependency graph - # @param [Object] tag an opaque tag - def initialize(tag) - @tag = tag - end - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb deleted file mode 100644 index e4d016de24..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -module Bundler::Molinillo - class DependencyGraph - # A vertex in a {DependencyGraph} that encapsulates a {#name} and a - # {#payload} - class Vertex - # @return [String] the name of the vertex - attr_accessor :name - - # @return [Object] the payload the vertex holds - attr_accessor :payload - - # @return [Array<Object>] the explicit requirements that required - # this vertex - attr_reader :explicit_requirements - - # @return [Boolean] whether the vertex is considered a root vertex - attr_accessor :root - alias root? root - - # Initializes a vertex with the given name and payload. - # @param [String] name see {#name} - # @param [Object] payload see {#payload} - def initialize(name, payload) - @name = name.frozen? ? name : name.dup.freeze - @payload = payload - @explicit_requirements = [] - @outgoing_edges = [] - @incoming_edges = [] - end - - # @return [Array<Object>] all of the requirements that required - # this vertex - def requirements - (incoming_edges.map(&:requirement) + explicit_requirements).uniq - end - - # @return [Array<Edge>] the edges of {#graph} that have `self` as their - # {Edge#origin} - attr_accessor :outgoing_edges - - # @return [Array<Edge>] the edges of {#graph} that have `self` as their - # {Edge#destination} - attr_accessor :incoming_edges - - # @return [Array<Vertex>] the vertices of {#graph} that have an edge with - # `self` as their {Edge#destination} - def predecessors - incoming_edges.map(&:origin) - end - - # @return [Array<Vertex>] the vertices of {#graph} where `self` is a - # {#descendent?} - def recursive_predecessors - vertices = predecessors - vertices += Compatibility.flat_map(vertices, &:recursive_predecessors) - vertices.uniq! - vertices - end - - # @return [Array<Vertex>] the vertices of {#graph} that have an edge with - # `self` as their {Edge#origin} - def successors - outgoing_edges.map(&:destination) - end - - # @return [Array<Vertex>] the vertices of {#graph} where `self` is an - # {#ancestor?} - def recursive_successors - vertices = successors - vertices += Compatibility.flat_map(vertices, &:recursive_successors) - vertices.uniq! - vertices - end - - # @return [String] a string suitable for debugging - def inspect - "#{self.class}:#{name}(#{payload.inspect})" - end - - # @return [Boolean] whether the two vertices are equal, determined - # by a recursive traversal of each {Vertex#successors} - def ==(other) - return true if equal?(other) - shallow_eql?(other) && - successors.to_set == other.successors.to_set - end - - # @param [Vertex] other the other vertex to compare to - # @return [Boolean] whether the two vertices are equal, determined - # solely by {#name} and {#payload} equality - def shallow_eql?(other) - return true if equal?(other) - other && - name == other.name && - payload == other.payload - end - - alias eql? == - - # @return [Fixnum] a hash for the vertex based upon its {#name} - def hash - name.hash - end - - # Is there a path from `self` to `other` following edges in the - # dependency graph? - # @return true iff there is a path following edges within this {#graph} - def path_to?(other) - equal?(other) || successors.any? { |v| v.path_to?(other) } - end - - alias descendent? path_to? - - # Is there a path from `other` to `self` following edges in the - # dependency graph? - # @return true iff there is a path following edges within this {#graph} - def ancestor?(other) - other.path_to?(self) - end - - alias is_reachable_from? ancestor? - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb b/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb deleted file mode 100644 index fb343250b1..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb +++ /dev/null @@ -1,138 +0,0 @@ -# frozen_string_literal: true - -module Bundler::Molinillo - # An error that occurred during the resolution process - class ResolverError < StandardError; end - - # An error caused by searching for a dependency that is completely unknown, - # i.e. has no versions available whatsoever. - class NoSuchDependencyError < ResolverError - # @return [Object] the dependency that could not be found - attr_accessor :dependency - - # @return [Array<Object>] the specifications that depended upon {#dependency} - attr_accessor :required_by - - # Initializes a new error with the given missing dependency. - # @param [Object] dependency @see {#dependency} - # @param [Array<Object>] required_by @see {#required_by} - def initialize(dependency, required_by = []) - @dependency = dependency - @required_by = required_by - super() - end - - # The error message for the missing dependency, including the specifications - # that had this dependency. - def message - sources = required_by.map { |r| "`#{r}`" }.join(' and ') - message = "Unable to find a specification for `#{dependency}`" - message += " depended upon by #{sources}" unless sources.empty? - message - end - end - - # An error caused by attempting to fulfil a dependency that was circular - # - # @note This exception will be thrown iff a {Vertex} is added to a - # {DependencyGraph} that has a {DependencyGraph::Vertex#path_to?} an - # existing {DependencyGraph::Vertex} - class CircularDependencyError < ResolverError - # [Set<Object>] the dependencies responsible for causing the error - attr_reader :dependencies - - # Initializes a new error with the given circular vertices. - # @param [Array<DependencyGraph::Vertex>] vertices the vertices in the dependency - # that caused the error - def initialize(vertices) - super "There is a circular dependency between #{vertices.map(&:name).join(' and ')}" - @dependencies = vertices.map { |vertex| vertex.payload.possibilities.last }.to_set - end - end - - # An error caused by conflicts in version - class VersionConflict < ResolverError - # @return [{String => Resolution::Conflict}] the conflicts that caused - # resolution to fail - attr_reader :conflicts - - # @return [SpecificationProvider] the specification provider used during - # resolution - attr_reader :specification_provider - - # Initializes a new error with the given version conflicts. - # @param [{String => Resolution::Conflict}] conflicts see {#conflicts} - # @param [SpecificationProvider] specification_provider see {#specification_provider} - def initialize(conflicts, specification_provider) - pairs = [] - Compatibility.flat_map(conflicts.values.flatten, &:requirements).each do |conflicting| - conflicting.each do |source, conflict_requirements| - conflict_requirements.each do |c| - pairs << [c, source] - end - end - end - - super "Unable to satisfy the following requirements:\n\n" \ - "#{pairs.map { |r, d| "- `#{r}` required by `#{d}`" }.join("\n")}" - - @conflicts = conflicts - @specification_provider = specification_provider - end - - require 'bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider' - include Delegates::SpecificationProvider - - # @return [String] An error message that includes requirement trees, - # which is much more detailed & customizable than the default message - # @param [Hash] opts the options to create a message with. - # @option opts [String] :solver_name The user-facing name of the solver - # @option opts [String] :possibility_type The generic name of a possibility - # @option opts [Proc] :reduce_trees A proc that reduced the list of requirement trees - # @option opts [Proc] :printable_requirement A proc that pretty-prints requirements - # @option opts [Proc] :additional_message_for_conflict A proc that appends additional - # messages for each conflict - # @option opts [Proc] :version_for_spec A proc that returns the version number for a - # possibility - def message_with_trees(opts = {}) - solver_name = opts.delete(:solver_name) { self.class.name.split('::').first } - possibility_type = opts.delete(:possibility_type) { 'possibility named' } - reduce_trees = opts.delete(:reduce_trees) { proc { |trees| trees.uniq.sort_by(&:to_s) } } - printable_requirement = opts.delete(:printable_requirement) { proc { |req| req.to_s } } - additional_message_for_conflict = opts.delete(:additional_message_for_conflict) { proc {} } - version_for_spec = opts.delete(:version_for_spec) { proc(&:to_s) } - - conflicts.sort.reduce(''.dup) do |o, (name, conflict)| - o << %(\n#{solver_name} could not find compatible versions for #{possibility_type} "#{name}":\n) - if conflict.locked_requirement - o << %( In snapshot (#{name_for_locking_dependency_source}):\n) - o << %( #{printable_requirement.call(conflict.locked_requirement)}\n) - o << %(\n) - end - o << %( In #{name_for_explicit_dependency_source}:\n) - trees = reduce_trees.call(conflict.requirement_trees) - - o << trees.map do |tree| - t = ''.dup - depth = 2 - tree.each do |req| - t << ' ' * depth << req.to_s - unless tree.last == req - if spec = conflict.activated_by_name[name_for(req)] - t << %( was resolved to #{version_for_spec.call(spec)}, which) - end - t << %( depends on) - end - t << %(\n) - depth += 1 - end - t - end.join("\n") - - additional_message_for_conflict.call(o, name, conflict) - - o - end.strip - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb b/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb deleted file mode 100644 index 3feb7be9b5..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -module Bundler::Molinillo - # The version of Bundler::Molinillo. - VERSION = '0.6.4'.freeze -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb b/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb deleted file mode 100644 index fa094c1981..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -module Bundler::Molinillo - # Provides information about specifcations and dependencies to the resolver, - # allowing the {Resolver} class to remain generic while still providing power - # and flexibility. - # - # This module contains the methods that users of Bundler::Molinillo must to implement, - # using knowledge of their own model classes. - module SpecificationProvider - # Search for the specifications that match the given dependency. - # The specifications in the returned array will be considered in reverse - # order, so the latest version ought to be last. - # @note This method should be 'pure', i.e. the return value should depend - # only on the `dependency` parameter. - # - # @param [Object] dependency - # @return [Array<Object>] the specifications that satisfy the given - # `dependency`. - def search_for(dependency) - [] - end - - # Returns the dependencies of `specification`. - # @note This method should be 'pure', i.e. the return value should depend - # only on the `specification` parameter. - # - # @param [Object] specification - # @return [Array<Object>] the dependencies that are required by the given - # `specification`. - def dependencies_for(specification) - [] - end - - # Determines whether the given `requirement` is satisfied by the given - # `spec`, in the context of the current `activated` dependency graph. - # - # @param [Object] requirement - # @param [DependencyGraph] activated the current dependency graph in the - # resolution process. - # @param [Object] spec - # @return [Boolean] whether `requirement` is satisfied by `spec` in the - # context of the current `activated` dependency graph. - def requirement_satisfied_by?(requirement, activated, spec) - true - end - - # Returns the name for the given `dependency`. - # @note This method should be 'pure', i.e. the return value should depend - # only on the `dependency` parameter. - # - # @param [Object] dependency - # @return [String] the name for the given `dependency`. - def name_for(dependency) - dependency.to_s - end - - # @return [String] the name of the source of explicit dependencies, i.e. - # those passed to {Resolver#resolve} directly. - def name_for_explicit_dependency_source - 'user-specified dependency' - end - - # @return [String] the name of the source of 'locked' dependencies, i.e. - # those passed to {Resolver#resolve} directly as the `base` - def name_for_locking_dependency_source - 'Lockfile' - end - - # Sort dependencies so that the ones that are easiest to resolve are first. - # Easiest to resolve is (usually) defined by: - # 1) Is this dependency already activated? - # 2) How relaxed are the requirements? - # 3) Are there any conflicts for this dependency? - # 4) How many possibilities are there to satisfy this dependency? - # - # @param [Array<Object>] dependencies - # @param [DependencyGraph] activated the current dependency graph in the - # resolution process. - # @param [{String => Array<Conflict>}] conflicts - # @return [Array<Object>] a sorted copy of `dependencies`. - def sort_dependencies(dependencies, activated, conflicts) - dependencies.sort_by do |dependency| - name = name_for(dependency) - [ - activated.vertex_named(name).payload ? 0 : 1, - conflicts[name] ? 0 : 1, - ] - end - end - - # Returns whether this dependency, which has no possible matching - # specifications, can safely be ignored. - # - # @param [Object] dependency - # @return [Boolean] whether this dependency can safely be skipped. - def allow_missing?(dependency) - false - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb b/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb deleted file mode 100644 index a166bc6991..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -module Bundler::Molinillo - # Conveys information about the resolution process to a user. - module UI - # The {IO} object that should be used to print output. `STDOUT`, by default. - # - # @return [IO] - def output - STDOUT - end - - # Called roughly every {#progress_rate}, this method should convey progress - # to the user. - # - # @return [void] - def indicate_progress - output.print '.' unless debug? - end - - # How often progress should be conveyed to the user via - # {#indicate_progress}, in seconds. A third of a second, by default. - # - # @return [Float] - def progress_rate - 0.33 - end - - # Called before resolution begins. - # - # @return [void] - def before_resolution - output.print 'Resolving dependencies...' - end - - # Called after resolution ends (either successfully or with an error). - # By default, prints a newline. - # - # @return [void] - def after_resolution - output.puts - end - - # Conveys debug information to the user. - # - # @param [Integer] depth the current depth of the resolution process. - # @return [void] - def debug(depth = 0) - if debug? - debug_info = yield - debug_info = debug_info.inspect unless debug_info.is_a?(String) - debug_info = debug_info.split("\n").map { |s| ":#{depth.to_s.rjust 4}: #{s}" } - output.puts debug_info - end - end - - # Whether or not debug messages should be printed. - # By default, whether or not the `MOLINILLO_DEBUG` environment variable is - # set. - # - # @return [Boolean] - def debug? - return @debug_mode if defined?(@debug_mode) - @debug_mode = ENV['MOLINILLO_DEBUG'] - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb b/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb deleted file mode 100644 index 0eb665d17a..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb +++ /dev/null @@ -1,837 +0,0 @@ -# frozen_string_literal: true - -module Bundler::Molinillo - class Resolver - # A specific resolution from a given {Resolver} - class Resolution - # A conflict that the resolution process encountered - # @attr [Object] requirement the requirement that immediately led to the conflict - # @attr [{String,Nil=>[Object]}] requirements the requirements that caused the conflict - # @attr [Object, nil] existing the existing spec that was in conflict with - # the {#possibility} - # @attr [Object] possibility_set the set of specs that was unable to be - # activated due to a conflict. - # @attr [Object] locked_requirement the relevant locking requirement. - # @attr [Array<Array<Object>>] requirement_trees the different requirement - # trees that led to every requirement for the conflicting name. - # @attr [{String=>Object}] activated_by_name the already-activated specs. - # @attr [Object] underlying_error an error that has occurred during resolution, and - # will be raised at the end of it if no resolution is found. - Conflict = Struct.new( - :requirement, - :requirements, - :existing, - :possibility_set, - :locked_requirement, - :requirement_trees, - :activated_by_name, - :underlying_error - ) - - class Conflict - # @return [Object] a spec that was unable to be activated due to a conflict - def possibility - possibility_set && possibility_set.latest_version - end - end - - # A collection of possibility states that share the same dependencies - # @attr [Array] dependencies the dependencies for this set of possibilities - # @attr [Array] possibilities the possibilities - PossibilitySet = Struct.new(:dependencies, :possibilities) - - class PossibilitySet - # String representation of the possibility set, for debugging - def to_s - "[#{possibilities.join(', ')}]" - end - - # @return [Object] most up-to-date dependency in the possibility set - def latest_version - possibilities.last - end - end - - # Details of the state to unwind to when a conflict occurs, and the cause of the unwind - # @attr [Integer] state_index the index of the state to unwind to - # @attr [Object] state_requirement the requirement of the state we're unwinding to - # @attr [Array] requirement_tree for the requirement we're relaxing - # @attr [Array] conflicting_requirements the requirements that combined to cause the conflict - # @attr [Array] requirement_trees for the conflict - # @attr [Array] requirements_unwound_to_instead array of unwind requirements that were chosen over this unwind - UnwindDetails = Struct.new( - :state_index, - :state_requirement, - :requirement_tree, - :conflicting_requirements, - :requirement_trees, - :requirements_unwound_to_instead - ) - - class UnwindDetails - include Comparable - - # We compare UnwindDetails when choosing which state to unwind to. If - # two options have the same state_index we prefer the one most - # removed from a requirement that caused the conflict. Both options - # would unwind to the same state, but a `grandparent` option will - # filter out fewer of its possibilities after doing so - where a state - # is both a `parent` and a `grandparent` to requirements that have - # caused a conflict this is the correct behaviour. - # @param [UnwindDetail] other UnwindDetail to be compared - # @return [Integer] integer specifying ordering - def <=>(other) - if state_index > other.state_index - 1 - elsif state_index == other.state_index - reversed_requirement_tree_index <=> other.reversed_requirement_tree_index - else - -1 - end - end - - # @return [Integer] index of state requirement in reversed requirement tree - # (the conflicting requirement itself will be at position 0) - def reversed_requirement_tree_index - @reversed_requirement_tree_index ||= - if state_requirement - requirement_tree.reverse.index(state_requirement) - else - 999_999 - end - end - - # @return [Boolean] where the requirement of the state we're unwinding - # to directly caused the conflict. Note: in this case, it is - # impossible for the state we're unwinding to to be a parent of - # any of the other conflicting requirements (or we would have - # circularity) - def unwinding_to_primary_requirement? - requirement_tree.last == state_requirement - end - - # @return [Array] array of sub-dependencies to avoid when choosing a - # new possibility for the state we've unwound to. Only relevant for - # non-primary unwinds - def sub_dependencies_to_avoid - @requirements_to_avoid ||= - requirement_trees.map do |tree| - index = tree.index(state_requirement) - tree[index + 1] if index - end.compact - end - - # @return [Array] array of all the requirements that led to the need for - # this unwind - def all_requirements - @all_requirements ||= requirement_trees.flatten(1) - end - end - - # @return [SpecificationProvider] the provider that knows about - # dependencies, requirements, specifications, versions, etc. - attr_reader :specification_provider - - # @return [UI] the UI that knows how to communicate feedback about the - # resolution process back to the user - attr_reader :resolver_ui - - # @return [DependencyGraph] the base dependency graph to which - # dependencies should be 'locked' - attr_reader :base - - # @return [Array] the dependencies that were explicitly required - attr_reader :original_requested - - # Initializes a new resolution. - # @param [SpecificationProvider] specification_provider - # see {#specification_provider} - # @param [UI] resolver_ui see {#resolver_ui} - # @param [Array] requested see {#original_requested} - # @param [DependencyGraph] base see {#base} - def initialize(specification_provider, resolver_ui, requested, base) - @specification_provider = specification_provider - @resolver_ui = resolver_ui - @original_requested = requested - @base = base - @states = [] - @iteration_counter = 0 - @parents_of = Hash.new { |h, k| h[k] = [] } - end - - # Resolves the {#original_requested} dependencies into a full dependency - # graph - # @raise [ResolverError] if successful resolution is impossible - # @return [DependencyGraph] the dependency graph of successfully resolved - # dependencies - def resolve - start_resolution - - while state - break if !state.requirement && state.requirements.empty? - indicate_progress - if state.respond_to?(:pop_possibility_state) # DependencyState - debug(depth) { "Creating possibility state for #{requirement} (#{possibilities.count} remaining)" } - state.pop_possibility_state.tap do |s| - if s - states.push(s) - activated.tag(s) - end - end - end - process_topmost_state - end - - resolve_activated_specs - ensure - end_resolution - end - - # @return [Integer] the number of resolver iterations in between calls to - # {#resolver_ui}'s {UI#indicate_progress} method - attr_accessor :iteration_rate - private :iteration_rate - - # @return [Time] the time at which resolution began - attr_accessor :started_at - private :started_at - - # @return [Array<ResolutionState>] the stack of states for the resolution - attr_accessor :states - private :states - - private - - # Sets up the resolution process - # @return [void] - def start_resolution - @started_at = Time.now - - handle_missing_or_push_dependency_state(initial_state) - - debug { "Starting resolution (#{@started_at})\nUser-requested dependencies: #{original_requested}" } - resolver_ui.before_resolution - end - - def resolve_activated_specs - activated.vertices.each do |_, vertex| - next unless vertex.payload - - latest_version = vertex.payload.possibilities.reverse_each.find do |possibility| - vertex.requirements.all? { |req| requirement_satisfied_by?(req, activated, possibility) } - end - - activated.set_payload(vertex.name, latest_version) - end - activated.freeze - end - - # Ends the resolution process - # @return [void] - def end_resolution - resolver_ui.after_resolution - debug do - "Finished resolution (#{@iteration_counter} steps) " \ - "(Took #{(ended_at = Time.now) - @started_at} seconds) (#{ended_at})" - end - debug { 'Unactivated: ' + Hash[activated.vertices.reject { |_n, v| v.payload }].keys.join(', ') } if state - debug { 'Activated: ' + Hash[activated.vertices.select { |_n, v| v.payload }].keys.join(', ') } if state - end - - require 'bundler/vendor/molinillo/lib/molinillo/state' - require 'bundler/vendor/molinillo/lib/molinillo/modules/specification_provider' - - require 'bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state' - require 'bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider' - - include Bundler::Molinillo::Delegates::ResolutionState - include Bundler::Molinillo::Delegates::SpecificationProvider - - # Processes the topmost available {RequirementState} on the stack - # @return [void] - def process_topmost_state - if possibility - attempt_to_activate - else - create_conflict - unwind_for_conflict - end - rescue CircularDependencyError => underlying_error - create_conflict(underlying_error) - unwind_for_conflict - end - - # @return [Object] the current possibility that the resolution is trying - # to activate - def possibility - possibilities.last - end - - # @return [RequirementState] the current state the resolution is - # operating upon - def state - states.last - end - - # Creates the initial state for the resolution, based upon the - # {#requested} dependencies - # @return [DependencyState] the initial state for the resolution - def initial_state - graph = DependencyGraph.new.tap do |dg| - original_requested.each do |requested| - vertex = dg.add_vertex(name_for(requested), nil, true) - vertex.explicit_requirements << requested - end - dg.tag(:initial_state) - end - - requirements = sort_dependencies(original_requested, graph, {}) - initial_requirement = requirements.shift - DependencyState.new( - initial_requirement && name_for(initial_requirement), - requirements, - graph, - initial_requirement, - possibilities_for_requirement(initial_requirement, graph), - 0, - {}, - [] - ) - end - - # Unwinds the states stack because a conflict has been encountered - # @return [void] - def unwind_for_conflict - details_for_unwind = build_details_for_unwind - unwind_options = unused_unwind_options - debug(depth) { "Unwinding for conflict: #{requirement} to #{details_for_unwind.state_index / 2}" } - conflicts.tap do |c| - sliced_states = states.slice!((details_for_unwind.state_index + 1)..-1) - raise_error_unless_state(c) - activated.rewind_to(sliced_states.first || :initial_state) if sliced_states - state.conflicts = c - state.unused_unwind_options = unwind_options - filter_possibilities_after_unwind(details_for_unwind) - index = states.size - 1 - @parents_of.each { |_, a| a.reject! { |i| i >= index } } - state.unused_unwind_options.reject! { |uw| uw.state_index >= index } - end - end - - # Raises a VersionConflict error, or any underlying error, if there is no - # current state - # @return [void] - def raise_error_unless_state(conflicts) - return if state - - error = conflicts.values.map(&:underlying_error).compact.first - raise error || VersionConflict.new(conflicts, specification_provider) - end - - # @return [UnwindDetails] Details of the nearest index to which we could unwind - def build_details_for_unwind - # Get the possible unwinds for the current conflict - current_conflict = conflicts[name] - binding_requirements = binding_requirements_for_conflict(current_conflict) - unwind_details = unwind_options_for_requirements(binding_requirements) - - last_detail_for_current_unwind = unwind_details.sort.last - current_detail = last_detail_for_current_unwind - - # Look for past conflicts that could be unwound to affect the - # requirement tree for the current conflict - relevant_unused_unwinds = unused_unwind_options.select do |alternative| - intersecting_requirements = - last_detail_for_current_unwind.all_requirements & - alternative.requirements_unwound_to_instead - next if intersecting_requirements.empty? - # Find the highest index unwind whilst looping through - current_detail = alternative if alternative > current_detail - alternative - end - - # Add the current unwind options to the `unused_unwind_options` array. - # The "used" option will be filtered out during `unwind_for_conflict`. - state.unused_unwind_options += unwind_details.reject { |detail| detail.state_index == -1 } - - # Update the requirements_unwound_to_instead on any relevant unused unwinds - relevant_unused_unwinds.each { |d| d.requirements_unwound_to_instead << current_detail.state_requirement } - unwind_details.each { |d| d.requirements_unwound_to_instead << current_detail.state_requirement } - - current_detail - end - - # @param [Array<Object>] array of requirements that combine to create a conflict - # @return [Array<UnwindDetails>] array of UnwindDetails that have a chance - # of resolving the passed requirements - def unwind_options_for_requirements(binding_requirements) - unwind_details = [] - - trees = [] - binding_requirements.reverse_each do |r| - partial_tree = [r] - trees << partial_tree - unwind_details << UnwindDetails.new(-1, nil, partial_tree, binding_requirements, trees, []) - - # If this requirement has alternative possibilities, check if any would - # satisfy the other requirements that created this conflict - requirement_state = find_state_for(r) - if conflict_fixing_possibilities?(requirement_state, binding_requirements) - unwind_details << UnwindDetails.new( - states.index(requirement_state), - r, - partial_tree, - binding_requirements, - trees, - [] - ) - end - - # Next, look at the parent of this requirement, and check if the requirement - # could have been avoided if an alternative PossibilitySet had been chosen - parent_r = parent_of(r) - next if parent_r.nil? - partial_tree.unshift(parent_r) - requirement_state = find_state_for(parent_r) - if requirement_state.possibilities.any? { |set| !set.dependencies.include?(r) } - unwind_details << UnwindDetails.new( - states.index(requirement_state), - parent_r, - partial_tree, - binding_requirements, - trees, - [] - ) - end - - # Finally, look at the grandparent and up of this requirement, looking - # for any possibilities that wouldn't create their parent requirement - grandparent_r = parent_of(parent_r) - until grandparent_r.nil? - partial_tree.unshift(grandparent_r) - requirement_state = find_state_for(grandparent_r) - if requirement_state.possibilities.any? { |set| !set.dependencies.include?(parent_r) } - unwind_details << UnwindDetails.new( - states.index(requirement_state), - grandparent_r, - partial_tree, - binding_requirements, - trees, - [] - ) - end - parent_r = grandparent_r - grandparent_r = parent_of(parent_r) - end - end - - unwind_details - end - - # @param [DependencyState] state - # @param [Array] array of requirements - # @return [Boolean] whether or not the given state has any possibilities - # that could satisfy the given requirements - def conflict_fixing_possibilities?(state, binding_requirements) - return false unless state - - state.possibilities.any? do |possibility_set| - possibility_set.possibilities.any? do |poss| - possibility_satisfies_requirements?(poss, binding_requirements) - end - end - end - - # Filter's a state's possibilities to remove any that would not fix the - # conflict we've just rewound from - # @param [UnwindDetails] details of the conflict just unwound from - # @return [void] - def filter_possibilities_after_unwind(unwind_details) - return unless state && !state.possibilities.empty? - - if unwind_details.unwinding_to_primary_requirement? - filter_possibilities_for_primary_unwind(unwind_details) - else - filter_possibilities_for_parent_unwind(unwind_details) - end - end - - # Filter's a state's possibilities to remove any that would not satisfy - # the requirements in the conflict we've just rewound from - # @param [UnwindDetails] details of the conflict just unwound from - # @return [void] - def filter_possibilities_for_primary_unwind(unwind_details) - unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } - unwinds_to_state << unwind_details - unwind_requirement_sets = unwinds_to_state.map(&:conflicting_requirements) - - state.possibilities.reject! do |possibility_set| - possibility_set.possibilities.none? do |poss| - unwind_requirement_sets.any? do |requirements| - possibility_satisfies_requirements?(poss, requirements) - end - end - end - end - - # @param [Object] possibility a single possibility - # @param [Array] requirements an array of requirements - # @return [Boolean] whether the possibility satisfies all of the - # given requirements - def possibility_satisfies_requirements?(possibility, requirements) - name = name_for(possibility) - - activated.tag(:swap) - activated.set_payload(name, possibility) if activated.vertex_named(name) - satisfied = requirements.all? { |r| requirement_satisfied_by?(r, activated, possibility) } - activated.rewind_to(:swap) - - satisfied - end - - # Filter's a state's possibilities to remove any that would (eventually) - # create a requirement in the conflict we've just rewound from - # @param [UnwindDetails] details of the conflict just unwound from - # @return [void] - def filter_possibilities_for_parent_unwind(unwind_details) - unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } - unwinds_to_state << unwind_details - - primary_unwinds = unwinds_to_state.select(&:unwinding_to_primary_requirement?).uniq - parent_unwinds = unwinds_to_state.uniq - primary_unwinds - - allowed_possibility_sets = Compatibility.flat_map(primary_unwinds) do |unwind| - states[unwind.state_index].possibilities.select do |possibility_set| - possibility_set.possibilities.any? do |poss| - possibility_satisfies_requirements?(poss, unwind.conflicting_requirements) - end - end - end - - requirements_to_avoid = Compatibility.flat_map(parent_unwinds, &:sub_dependencies_to_avoid) - - state.possibilities.reject! do |possibility_set| - !allowed_possibility_sets.include?(possibility_set) && - (requirements_to_avoid - possibility_set.dependencies).empty? - end - end - - # @param [Conflict] conflict - # @return [Array] minimal array of requirements that would cause the passed - # conflict to occur. - def binding_requirements_for_conflict(conflict) - return [conflict.requirement] if conflict.possibility.nil? - - possible_binding_requirements = conflict.requirements.values.flatten(1).uniq - - # When there’s a `CircularDependency` error the conflicting requirement - # (the one causing the circular) won’t be `conflict.requirement` - # (which won’t be for the right state, because we won’t have created it, - # because it’s circular). - # We need to make sure we have that requirement in the conflict’s list, - # otherwise we won’t be able to unwind properly, so we just return all - # the requirements for the conflict. - return possible_binding_requirements if conflict.underlying_error - - possibilities = search_for(conflict.requirement) - - # If all the requirements together don't filter out all possibilities, - # then the only two requirements we need to consider are the initial one - # (where the dependency's version was first chosen) and the last - if binding_requirement_in_set?(nil, possible_binding_requirements, possibilities) - return [conflict.requirement, requirement_for_existing_name(name_for(conflict.requirement))].compact - end - - # Loop through the possible binding requirements, removing each one - # that doesn't bind. Use a `reverse_each` as we want the earliest set of - # binding requirements, and don't use `reject!` as we wish to refine the - # array *on each iteration*. - binding_requirements = possible_binding_requirements.dup - possible_binding_requirements.reverse_each do |req| - next if req == conflict.requirement - unless binding_requirement_in_set?(req, binding_requirements, possibilities) - binding_requirements -= [req] - end - end - - binding_requirements - end - - # @param [Object] requirement we wish to check - # @param [Array] array of requirements - # @param [Array] array of possibilities the requirements will be used to filter - # @return [Boolean] whether or not the given requirement is required to filter - # out all elements of the array of possibilities. - def binding_requirement_in_set?(requirement, possible_binding_requirements, possibilities) - possibilities.any? do |poss| - possibility_satisfies_requirements?(poss, possible_binding_requirements - [requirement]) - end - end - - # @return [Object] the requirement that led to `requirement` being added - # to the list of requirements. - def parent_of(requirement) - return unless requirement - return unless index = @parents_of[requirement].last - return unless parent_state = @states[index] - parent_state.requirement - end - - # @return [Object] the requirement that led to a version of a possibility - # with the given name being activated. - def requirement_for_existing_name(name) - return nil unless vertex = activated.vertex_named(name) - return nil unless vertex.payload - states.find { |s| s.name == name }.requirement - end - - # @return [ResolutionState] the state whose `requirement` is the given - # `requirement`. - def find_state_for(requirement) - return nil unless requirement - states.find { |i| requirement == i.requirement } - end - - # @return [Conflict] a {Conflict} that reflects the failure to activate - # the {#possibility} in conjunction with the current {#state} - def create_conflict(underlying_error = nil) - vertex = activated.vertex_named(name) - locked_requirement = locked_requirement_named(name) - - requirements = {} - unless vertex.explicit_requirements.empty? - requirements[name_for_explicit_dependency_source] = vertex.explicit_requirements - end - requirements[name_for_locking_dependency_source] = [locked_requirement] if locked_requirement - vertex.incoming_edges.each do |edge| - (requirements[edge.origin.payload.latest_version] ||= []).unshift(edge.requirement) - end - - activated_by_name = {} - activated.each { |v| activated_by_name[v.name] = v.payload.latest_version if v.payload } - conflicts[name] = Conflict.new( - requirement, - requirements, - vertex.payload && vertex.payload.latest_version, - possibility, - locked_requirement, - requirement_trees, - activated_by_name, - underlying_error - ) - end - - # @return [Array<Array<Object>>] The different requirement - # trees that led to every requirement for the current spec. - def requirement_trees - vertex = activated.vertex_named(name) - vertex.requirements.map { |r| requirement_tree_for(r) } - end - - # @return [Array<Object>] the list of requirements that led to - # `requirement` being required. - def requirement_tree_for(requirement) - tree = [] - while requirement - tree.unshift(requirement) - requirement = parent_of(requirement) - end - tree - end - - # Indicates progress roughly once every second - # @return [void] - def indicate_progress - @iteration_counter += 1 - @progress_rate ||= resolver_ui.progress_rate - if iteration_rate.nil? - if Time.now - started_at >= @progress_rate - self.iteration_rate = @iteration_counter - end - end - - if iteration_rate && (@iteration_counter % iteration_rate) == 0 - resolver_ui.indicate_progress - end - end - - # Calls the {#resolver_ui}'s {UI#debug} method - # @param [Integer] depth the depth of the {#states} stack - # @param [Proc] block a block that yields a {#to_s} - # @return [void] - def debug(depth = 0, &block) - resolver_ui.debug(depth, &block) - end - - # Attempts to activate the current {#possibility} - # @return [void] - def attempt_to_activate - debug(depth) { 'Attempting to activate ' + possibility.to_s } - existing_vertex = activated.vertex_named(name) - if existing_vertex.payload - debug(depth) { "Found existing spec (#{existing_vertex.payload})" } - attempt_to_filter_existing_spec(existing_vertex) - else - latest = possibility.latest_version - # use reject!(!satisfied) for 1.8.7 compatibility - possibility.possibilities.reject! do |possibility| - !requirement_satisfied_by?(requirement, activated, possibility) - end - if possibility.latest_version.nil? - # ensure there's a possibility for better error messages - possibility.possibilities << latest if latest - create_conflict - unwind_for_conflict - else - activate_new_spec - end - end - end - - # Attempts to update the existing vertex's `PossibilitySet` with a filtered version - # @return [void] - def attempt_to_filter_existing_spec(vertex) - filtered_set = filtered_possibility_set(vertex) - if !filtered_set.possibilities.empty? - activated.set_payload(name, filtered_set) - new_requirements = requirements.dup - push_state_for_requirements(new_requirements, false) - else - create_conflict - debug(depth) { "Unsatisfied by existing spec (#{vertex.payload})" } - unwind_for_conflict - end - end - - # Generates a filtered version of the existing vertex's `PossibilitySet` using the - # current state's `requirement` - # @param [Object] existing vertex - # @return [PossibilitySet] filtered possibility set - def filtered_possibility_set(vertex) - PossibilitySet.new(vertex.payload.dependencies, vertex.payload.possibilities & possibility.possibilities) - end - - # @param [String] requirement_name the spec name to search for - # @return [Object] the locked spec named `requirement_name`, if one - # is found on {#base} - def locked_requirement_named(requirement_name) - vertex = base.vertex_named(requirement_name) - vertex && vertex.payload - end - - # Add the current {#possibility} to the dependency graph of the current - # {#state} - # @return [void] - def activate_new_spec - conflicts.delete(name) - debug(depth) { "Activated #{name} at #{possibility}" } - activated.set_payload(name, possibility) - require_nested_dependencies_for(possibility) - end - - # Requires the dependencies that the recently activated spec has - # @param [Object] activated_possibility the PossibilitySet that has just been - # activated - # @return [void] - def require_nested_dependencies_for(possibility_set) - nested_dependencies = dependencies_for(possibility_set.latest_version) - debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" } - nested_dependencies.each do |d| - activated.add_child_vertex(name_for(d), nil, [name_for(possibility_set.latest_version)], d) - parent_index = states.size - 1 - parents = @parents_of[d] - parents << parent_index if parents.empty? - end - - push_state_for_requirements(requirements + nested_dependencies, !nested_dependencies.empty?) - end - - # Pushes a new {DependencyState} that encapsulates both existing and new - # requirements - # @param [Array] new_requirements - # @return [void] - def push_state_for_requirements(new_requirements, requires_sort = true, new_activated = activated) - new_requirements = sort_dependencies(new_requirements.uniq, new_activated, conflicts) if requires_sort - new_requirement = nil - loop do - new_requirement = new_requirements.shift - break if new_requirement.nil? || states.none? { |s| s.requirement == new_requirement } - end - new_name = new_requirement ? name_for(new_requirement) : ''.freeze - possibilities = possibilities_for_requirement(new_requirement) - handle_missing_or_push_dependency_state DependencyState.new( - new_name, new_requirements, new_activated, - new_requirement, possibilities, depth, conflicts.dup, unused_unwind_options.dup - ) - end - - # Checks a proposed requirement with any existing locked requirement - # before generating an array of possibilities for it. - # @param [Object] the proposed requirement - # @return [Array] possibilities - def possibilities_for_requirement(requirement, activated = self.activated) - return [] unless requirement - if locked_requirement_named(name_for(requirement)) - return locked_requirement_possibility_set(requirement, activated) - end - - group_possibilities(search_for(requirement)) - end - - # @param [Object] the proposed requirement - # @return [Array] possibility set containing only the locked requirement, if any - def locked_requirement_possibility_set(requirement, activated = self.activated) - all_possibilities = search_for(requirement) - locked_requirement = locked_requirement_named(name_for(requirement)) - - # Longwinded way to build a possibilities array with either the locked - # requirement or nothing in it. Required, since the API for - # locked_requirement isn't guaranteed. - locked_possibilities = all_possibilities.select do |possibility| - requirement_satisfied_by?(locked_requirement, activated, possibility) - end - - group_possibilities(locked_possibilities) - end - - # Build an array of PossibilitySets, with each element representing a group of - # dependency versions that all have the same sub-dependency version constraints - # and are contiguous. - # @param [Array] an array of possibilities - # @return [Array] an array of possibility sets - def group_possibilities(possibilities) - possibility_sets = [] - current_possibility_set = nil - - possibilities.reverse_each do |possibility| - dependencies = dependencies_for(possibility) - if current_possibility_set && current_possibility_set.dependencies == dependencies - current_possibility_set.possibilities.unshift(possibility) - else - possibility_sets.unshift(PossibilitySet.new(dependencies, [possibility])) - current_possibility_set = possibility_sets.first - end - end - - possibility_sets - end - - # Pushes a new {DependencyState}. - # If the {#specification_provider} says to - # {SpecificationProvider#allow_missing?} that particular requirement, and - # there are no possibilities for that requirement, then `state` is not - # pushed, and the vertex in {#activated} is removed, and we continue - # resolving the remaining requirements. - # @param [DependencyState] state - # @return [void] - def handle_missing_or_push_dependency_state(state) - if state.requirement && state.possibilities.empty? && allow_missing?(state.requirement) - state.activated.detach_vertex_named(state.name) - push_state_for_requirements(state.requirements.dup, false, state.activated) - else - states.push(state).tap { activated.tag(state) } - end - end - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb b/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb deleted file mode 100644 index 7d36858778..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph' - -module Bundler::Molinillo - # This class encapsulates a dependency resolver. - # The resolver is responsible for determining which set of dependencies to - # activate, with feedback from the {#specification_provider} - # - # - class Resolver - require 'bundler/vendor/molinillo/lib/molinillo/resolution' - - # @return [SpecificationProvider] the specification provider used - # in the resolution process - attr_reader :specification_provider - - # @return [UI] the UI module used to communicate back to the user - # during the resolution process - attr_reader :resolver_ui - - # Initializes a new resolver. - # @param [SpecificationProvider] specification_provider - # see {#specification_provider} - # @param [UI] resolver_ui - # see {#resolver_ui} - def initialize(specification_provider, resolver_ui) - @specification_provider = specification_provider - @resolver_ui = resolver_ui - end - - # Resolves the requested dependencies into a {DependencyGraph}, - # locking to the base dependency graph (if specified) - # @param [Array] requested an array of 'requested' dependencies that the - # {#specification_provider} can understand - # @param [DependencyGraph,nil] base the base dependency graph to which - # dependencies should be 'locked' - def resolve(requested, base = DependencyGraph.new) - Resolution.new(specification_provider, - resolver_ui, - requested, - base). - resolve - end - end -end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/state.rb b/lib/bundler/vendor/molinillo/lib/molinillo/state.rb deleted file mode 100644 index 68fa1f54e3..0000000000 --- a/lib/bundler/vendor/molinillo/lib/molinillo/state.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -module Bundler::Molinillo - # A state that a {Resolution} can be in - # @attr [String] name the name of the current requirement - # @attr [Array<Object>] requirements currently unsatisfied requirements - # @attr [DependencyGraph] activated the graph of activated dependencies - # @attr [Object] requirement the current requirement - # @attr [Object] possibilities the possibilities to satisfy the current requirement - # @attr [Integer] depth the depth of the resolution - # @attr [Hash] conflicts unresolved conflicts, indexed by dependency name - # @attr [Array<UnwindDetails>] unused_unwind_options unwinds for previous conflicts that weren't explored - ResolutionState = Struct.new( - :name, - :requirements, - :activated, - :requirement, - :possibilities, - :depth, - :conflicts, - :unused_unwind_options - ) - - class ResolutionState - # Returns an empty resolution state - # @return [ResolutionState] an empty state - def self.empty - new(nil, [], DependencyGraph.new, nil, nil, 0, {}, []) - end - end - - # A state that encapsulates a set of {#requirements} with an {Array} of - # possibilities - class DependencyState < ResolutionState - # Removes a possibility from `self` - # @return [PossibilityState] a state with a single possibility, - # the possibility that was removed from `self` - def pop_possibility_state - PossibilityState.new( - name, - requirements.dup, - activated, - requirement, - [possibilities.pop], - depth + 1, - conflicts.dup, - unused_unwind_options.dup - ).tap do |state| - state.activated.tag(state) - end - end - end - - # A state that encapsulates a single possibility to fulfill the given - # {#requirement} - class PossibilityState < ResolutionState - end -end diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/faster.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/faster.rb deleted file mode 100644 index e5e09080c2..0000000000 --- a/lib/bundler/vendor/net-http-persistent/lib/net/http/faster.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'net/protocol' - -## -# Aaron Patterson's monkeypatch (accepted into 1.9.1) to fix Net::HTTP's speed -# problems. -# -# http://gist.github.com/251244 - -class Net::BufferedIO #:nodoc: - alias :old_rbuf_fill :rbuf_fill - - def rbuf_fill - if @io.respond_to? :read_nonblock then - begin - @rbuf << @io.read_nonblock(65536) - rescue Errno::EWOULDBLOCK, Errno::EAGAIN => e - retry if IO.select [@io], nil, nil, @read_timeout - raise Timeout::Error, e.message - end - else # SSL sockets do not have read_nonblock - timeout @read_timeout do - @rbuf << @io.sysread(65536) - end - end - end -end if RUBY_VERSION < '1.9' - diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb index 7cbca5bc06..93e403a5bb 100644 --- a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb +++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb @@ -1,88 +1,84 @@ -require 'net/http' +require_relative '../../../../../vendored_net_http' +require_relative '../../../../../vendored_uri' begin - require 'net/https' -rescue LoadError - # net/https or openssl -end if RUBY_VERSION < '1.9' # but only for 1.8 -require 'bundler/vendor/net-http-persistent/lib/net/http/faster' -require 'uri' -require 'cgi' # for escaping - -begin - require 'net/http/pipeline' + require 'cgi/escape' rescue LoadError + require 'cgi/util' # for escaping end +require_relative '../../../../connection_pool/lib/connection_pool' autoload :OpenSSL, 'openssl' ## -# Persistent connections for Net::HTTP +# Persistent connections for Gem::Net::HTTP # -# Bundler::Persistent::Net::HTTP::Persistent maintains persistent connections across all the +# Gem::Net::HTTP::Persistent maintains persistent connections across all the # servers you wish to talk to. For each host:port you communicate with a # single persistent connection is created. # -# Multiple Bundler::Persistent::Net::HTTP::Persistent objects will share the same set of -# connections. -# -# For each thread you start a new connection will be created. A -# Bundler::Persistent::Net::HTTP::Persistent connection will not be shared across threads. +# Connections will be shared across threads through a connection pool to +# increase reuse of connections. # -# You can shut down the HTTP connections when done by calling #shutdown. You -# should name your Bundler::Persistent::Net::HTTP::Persistent object if you intend to call this -# method. +# You can shut down any remaining HTTP connections when done by calling +# #shutdown. # # Example: # # require 'bundler/vendor/net-http-persistent/lib/net/http/persistent' # -# uri = URI 'http://example.com/awesome/web/service' +# uri = Gem::URI 'http://example.com/awesome/web/service' # -# http = Bundler::Persistent::Net::HTTP::Persistent.new 'my_app_name' +# http = Gem::Net::HTTP::Persistent.new # # # perform a GET # response = http.request uri # # # or # -# get = Net::HTTP::Get.new uri.request_uri +# get = Gem::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 +# post = Gem::Net::HTTP::Post.new post_uri.path # post.set_form_data 'some' => 'cool data' # -# # perform the POST, the URI is always required +# # perform the POST, the Gem::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. +# ⚠ Note that for GET, HEAD and other requests that do not have a body, +# it uses Gem::URI#request_uri as default to send query params # -# == SSL +# == TLS/SSL # -# SSL connections are automatically created depending upon the scheme of the -# URI. SSL connections are automatically verified against the default +# TLS connections are automatically created depending upon the scheme of the +# Gem::URI. TLS connections are automatically verified against the default # certificate store for your computer. You can override this by changing # verify_mode or by specifying an alternate cert_store. # -# Here are the SSL settings, see the individual methods for documentation: +# Here are the TLS settings, see the individual methods for documentation: # # #certificate :: This client's certificate -# #ca_file :: The certificate-authority +# #ca_file :: The certificate-authorities +# #ca_path :: Directory with certificate-authorities # #cert_store :: An SSL certificate store +# #ciphers :: List of SSl ciphers allowed +# #extra_chain_cert :: Extra certificates to be added to the certificate chain # #private_key :: The client's SSL private key # #reuse_ssl_sessions :: Reuse a previously opened SSL session for a new # connection +# #ssl_timeout :: Session lifetime # #ssl_version :: Which specific SSL version to use # #verify_callback :: For server certificate verification +# #verify_depth :: Depth of certificate verification # #verify_mode :: How connections should be verified +# #verify_hostname :: Use hostname verification for server certificate +# during the handshake # # == Proxies # # A proxy can be set through #proxy= or at initialization time by providing a -# second argument to ::new. The proxy may be the URI of the proxy server or +# second argument to ::new. The proxy may be the Gem::URI of the proxy server or # <code>:ENV</code> which will consult environment variables. # # See #proxy= and #proxy_from_env for details. @@ -102,14 +98,15 @@ autoload :OpenSSL, 'openssl' # # === Segregation # -# By providing an application name to ::new you can separate your connections -# from the connections of other applications. +# Each Gem::Net::HTTP::Persistent instance has its own pool of connections. There +# is no sharing with other instances (as was true in earlier versions). # # === Idle Timeout # -# If a connection hasn't been used for this number of seconds it will automatically be -# reset upon the next use to avoid attempting to send to a closed connection. -# The default value is 5 seconds. nil means no timeout. Set through #idle_timeout. +# If a connection hasn't been used for this number of seconds it will +# automatically be reset upon the next use to avoid attempting to send to a +# closed connection. The default value is 5 seconds. nil means no timeout. +# Set through #idle_timeout. # # Reducing this value may help avoid the "too many connection resets" error # when sending non-idempotent requests while increasing this value will cause @@ -124,8 +121,9 @@ autoload :OpenSSL, 'openssl' # # 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. +# 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 # @@ -137,48 +135,9 @@ autoload :OpenSSL, 'openssl' # Socket options may be set on newly-created connections. See #socket_options # for details. # -# === Non-Idempotent Requests -# -# By default non-idempotent requests will not be retried per RFC 2616. By -# setting retry_change_requests to true requests will automatically be retried -# once. -# -# Only do this when you know that retrying a POST or other non-idempotent -# request is safe for your application and will not create duplicate -# resources. -# -# The recommended way to handle non-idempotent requests is the following: -# -# require 'bundler/vendor/net-http-persistent/lib/net/http/persistent' -# -# uri = URI 'http://example.com/awesome/web/service' -# post_uri = uri + 'create' -# -# http = Bundler::Persistent::Net::HTTP::Persistent.new 'my_app_name' -# -# post = Net::HTTP::Post.new post_uri.path -# # ... fill in POST request -# -# begin -# response = http.request post_uri, post -# rescue Bundler::Persistent::Net::HTTP::Persistent::Error -# -# # POST failed, make a new request to verify the server did not process -# # the request -# exists_uri = uri + '...' -# response = http.get exists_uri -# -# # Retry if it failed -# retry if response.code == '404' -# end -# -# The method of determining if the resource was created or not is unique to -# the particular service you are using. Of course, you will want to add -# protection from infinite looping. -# # === Connection Termination # -# If you are done using the Bundler::Persistent::Net::HTTP::Persistent instance you may shut down +# If you are done using the Gem::Net::HTTP::Persistent instance you may shut down # all the connections in the current thread with #shutdown. This is not # recommended for normal use, it should only be used when it will be several # minutes before you make another HTTP request. @@ -188,7 +147,7 @@ autoload :OpenSSL, 'openssl' # Ruby will automatically garbage collect and shutdown your HTTP connections # when the thread terminates. -class Bundler::Persistent::Net::HTTP::Persistent +class Gem::Net::HTTP::Persistent ## # The beginning of Time @@ -201,27 +160,30 @@ class Bundler::Persistent::Net::HTTP::Persistent HAVE_OPENSSL = defined? OpenSSL::SSL # :nodoc: ## - # The version of Bundler::Persistent::Net::HTTP::Persistent you are using + # The default connection pool size is 1/4 the allowed open files + # (<code>ulimit -n</code>) or 256 if your OS does not support file handle + # limits (typically windows). - VERSION = '2.9.4' + if Process.const_defined? :RLIMIT_NOFILE + open_file_limits = Process.getrlimit(Process::RLIMIT_NOFILE) + + # Under JRuby on Windows Process responds to `getrlimit` but returns something that does not match docs + if open_file_limits.respond_to?(:first) + DEFAULT_POOL_SIZE = open_file_limits.first / 4 + else + DEFAULT_POOL_SIZE = 256 + end + else + DEFAULT_POOL_SIZE = 256 + end ## - # Exceptions rescued for automatic retry on ruby 2.0.0. This overlaps with - # the exception list for ruby 1.x. + # The version of Gem::Net::HTTP::Persistent you are using - 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 + VERSION = '4.0.6' ## - # Error class for errors raised by Bundler::Persistent::Net::HTTP::Persistent. Various + # Error class for errors raised by Gem::Net::HTTP::Persistent. Various # SystemCallErrors are re-raised with a human-readable message under this # class. @@ -241,38 +203,38 @@ class Bundler::Persistent::Net::HTTP::Persistent # NOTE: This may not work on ruby > 1.9. def self.detect_idle_timeout uri, max = 10 - uri = URI uri unless URI::Generic === uri + uri = Gem::URI uri unless Gem::URI::Generic === uri uri += '/' - req = Net::HTTP::Head.new uri.request_uri + req = Gem::Net::HTTP::Head.new uri.request_uri http = new 'net-http-persistent detect_idle_timeout' - connection = http.connection_for uri + http.connection_for uri do |connection| + sleep_time = 0 - sleep_time = 0 + http = connection.http - loop do - response = connection.request req + loop do + response = http.request req - $stderr.puts "HEAD #{uri} => #{response.code}" if $DEBUG + $stderr.puts "HEAD #{uri} => #{response.code}" if $DEBUG - unless Net::HTTPOK === response then - raise Error, "bad response code #{response.code} detecting idle timeout" - end + unless Gem::Net::HTTPOK === response then + raise Error, "bad response code #{response.code} detecting idle timeout" + end - break if sleep_time >= max + break if sleep_time >= max - sleep_time += 1 + sleep_time += 1 - $stderr.puts "sleeping #{sleep_time}" if $DEBUG - sleep sleep_time + $stderr.puts "sleeping #{sleep_time}" if $DEBUG + sleep sleep_time + end end rescue # ignore StandardErrors, we've probably found the idle timeout. ensure - http.shutdown - return sleep_time unless $! end @@ -281,7 +243,9 @@ class Bundler::Persistent::Net::HTTP::Persistent attr_reader :certificate - # For Net::HTTP parity + ## + # For Gem::Net::HTTP parity + alias cert certificate ## @@ -291,13 +255,29 @@ class Bundler::Persistent::Net::HTTP::Persistent attr_reader :ca_file ## + # A directory of SSL certificates to be used as certificate authorities. + # Setting this will set verify_mode to VERIFY_PEER. + + attr_reader :ca_path + + ## # An SSL certificate store. Setting this will override the default # certificate store. See verify_mode for more information. attr_reader :cert_store ## - # Sends debug_output to this IO via Net::HTTP#set_debug_output. + # The ciphers allowed for SSL connections + + attr_reader :ciphers + + ## + # Extra certificates to be added to the certificate chain + + attr_reader :extra_chain_cert + + ## + # Sends debug_output to this IO via Gem::Net::HTTP#set_debug_output. # # Never use this method in production code, it causes a serious security # hole. @@ -310,12 +290,7 @@ class Bundler::Persistent::Net::HTTP::Persistent attr_reader :generation # :nodoc: ## - # Where this instance's connections live in the thread local variables - - attr_reader :generation_key # :nodoc: - - ## - # Headers that are added to every request using Net::HTTP#add_field + # Headers that are added to every request using Gem::Net::HTTP#add_field attr_reader :headers @@ -338,6 +313,13 @@ class Bundler::Persistent::Net::HTTP::Persistent attr_accessor :max_requests ## + # Number of retries to perform if a request fails. + # + # See also #max_retries=, Gem::Net::HTTP#max_retries=. + + attr_reader :max_retries + + ## # The value sent in the Keep-Alive header. Defaults to 30. Not needed for # HTTP/1.1 servers. # @@ -349,18 +331,17 @@ class Bundler::Persistent::Net::HTTP::Persistent attr_accessor :keep_alive ## - # A name for this connection. Allows you to keep your connections apart - # from everybody else's. + # The name for this collection of persistent connections. attr_reader :name ## - # Seconds to wait until a connection is opened. See Net::HTTP#open_timeout + # Seconds to wait until a connection is opened. See Gem::Net::HTTP#open_timeout attr_accessor :open_timeout ## - # Headers that are added to every request using Net::HTTP#[]= + # Headers that are added to every request using Gem::Net::HTTP#[]= attr_reader :override_headers @@ -369,7 +350,9 @@ class Bundler::Persistent::Net::HTTP::Persistent attr_reader :private_key - # For Net::HTTP parity + ## + # For Gem::Net::HTTP parity + alias key private_key ## @@ -383,14 +366,19 @@ class Bundler::Persistent::Net::HTTP::Persistent attr_reader :no_proxy ## - # Seconds to wait until reading one block. See Net::HTTP#read_timeout + # Test-only accessor for the connection pool + + attr_reader :pool # :nodoc: + + ## + # Seconds to wait until reading one block. See Gem::Net::HTTP#read_timeout attr_accessor :read_timeout ## - # Where this instance's request counts live in the thread local variables + # Seconds to wait until writing one block. See Gem::Net::HTTP#write_timeout - attr_reader :request_key # :nodoc: + attr_accessor :write_timeout ## # By default SSL sessions are reused to avoid extra SSL handshakes. Set @@ -418,17 +406,33 @@ class Bundler::Persistent::Net::HTTP::Persistent attr_reader :ssl_generation # :nodoc: ## - # Where this instance's SSL connections live in the thread local variables + # SSL session lifetime - attr_reader :ssl_generation_key # :nodoc: + attr_reader :ssl_timeout ## # SSL version to use. # # By default, the version will be negotiated automatically between client - # and server. Ruby 1.9 and newer only. + # and server. Ruby 1.9 and newer only. Deprecated since Ruby 2.5. + + attr_reader :ssl_version + + ## + # Minimum SSL version to use, e.g. :TLS1_1 + # + # By default, the version will be negotiated automatically between client + # and server. Ruby 2.5 and newer only. - attr_reader :ssl_version if RUBY_VERSION > '1.9' + attr_reader :min_version + + ## + # Maximum SSL version to use, e.g. :TLS1_2 + # + # By default, the version will be negotiated automatically between client + # and server. Ruby 2.5 and newer only. + + attr_reader :max_version ## # Where this instance's last-use times live in the thread local variables @@ -436,50 +440,63 @@ class Bundler::Persistent::Net::HTTP::Persistent attr_reader :timeout_key # :nodoc: ## - # SSL verification callback. Used when ca_file is set. + # SSL verification callback. Used when ca_file or ca_path is set. attr_reader :verify_callback ## + # Sets the depth of SSL certificate verification + + attr_reader :verify_depth + + ## # HTTPS verify mode. Defaults to OpenSSL::SSL::VERIFY_PEER which verifies # the server certificate. # - # If no ca_file or cert_store is set the default system certificate store is - # used. + # If no ca_file, ca_path or cert_store is set the default system certificate + # store is used. # # You can use +verify_mode+ to override any default values. attr_reader :verify_mode ## - # Enable retries of non-idempotent requests that change data (e.g. POST - # requests) when the server has disconnected. + # HTTPS verify_hostname. + # + # If a client sets this to true and enables SNI with SSLSocket#hostname=, + # the hostname verification on the server certificate is performed + # automatically during the handshake using + # OpenSSL::SSL.verify_certificate_identity(). + # + # You can set +verify_hostname+ as true to use hostname verification + # during the handshake. # - # This will in the worst case lead to multiple requests with the same data, - # but it may be useful for some applications. Take care when enabling - # this option to ensure it is safe to POST or perform other non-idempotent - # requests to the server. + # NOTE: This works with Ruby > 3.0. - attr_accessor :retry_change_requests + attr_reader :verify_hostname ## - # Creates a new Bundler::Persistent::Net::HTTP::Persistent. + # Creates a new Gem::Net::HTTP::Persistent. # - # Set +name+ to keep your connections apart from everybody else's. Not - # required currently, but highly recommended. Your library name should be - # good enough. This parameter will be required in a future version. + # Set a +name+ for fun. Your library name should be good enough, but this + # otherwise has no purpose. # - # +proxy+ may be set to a URI::HTTP or :ENV to pick up proxy options from + # +proxy+ may be set to a Gem::URI::HTTP or :ENV to pick up proxy options from # the environment. See proxy_from_env for details. # - # In order to use a URI for the proxy you may need to do some extra work - # beyond URI parsing if the proxy requires a password: + # In order to use a Gem::URI for the proxy you may need to do some extra work + # beyond Gem::URI parsing if the proxy requires a password: # - # proxy = URI 'http://proxy.example' + # proxy = Gem::URI 'http://proxy.example' # proxy.user = 'AzureDiamond' # proxy.password = 'hunter2' + # + # Set +pool_size+ to limit the maximum number of connections allowed. + # Defaults to 1/4 the number of allowed file handles or 256 if your OS does + # not support a limit on allowed file handles. You can have no more than + # this many threads with active HTTP transactions. - def initialize name = nil, proxy = nil + def initialize name: nil, proxy: nil, pool_size: DEFAULT_POOL_SIZE @name = name @debug_output = nil @@ -491,40 +508,42 @@ class Bundler::Persistent::Net::HTTP::Persistent @keep_alive = 30 @open_timeout = nil @read_timeout = nil + @write_timeout = nil @idle_timeout = 5 @max_requests = nil + @max_retries = 1 @socket_options = [] + @ssl_generation = 0 # incremented when SSL session variables change @socket_options << [Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1] if Socket.const_defined? :TCP_NODELAY - key = ['net_http_persistent', name].compact - @generation_key = [key, 'generations' ].join('_').intern - @ssl_generation_key = [key, 'ssl_generations'].join('_').intern - @request_key = [key, 'requests' ].join('_').intern - @timeout_key = [key, 'timeouts' ].join('_').intern + @pool = Gem::Net::HTTP::Persistent::Pool.new size: pool_size do |http_args| + Gem::Net::HTTP::Persistent::Connection.new Gem::Net::HTTP, http_args, @ssl_generation + end @certificate = nil @ca_file = nil + @ca_path = nil + @ciphers = nil @private_key = nil + @ssl_timeout = nil @ssl_version = nil + @min_version = nil + @max_version = nil @verify_callback = nil + @verify_depth = nil @verify_mode = nil + @verify_hostname = nil @cert_store = nil - @generation = 0 # incremented when proxy URI changes - @ssl_generation = 0 # incremented when SSL session variables change + @generation = 0 # incremented when proxy Gem::URI changes 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 @@ -537,7 +556,7 @@ class Bundler::Persistent::Net::HTTP::Persistent reconnect_ssl end - # For Net::HTTP parity + # For Gem::Net::HTTP parity alias cert= certificate= ## @@ -550,6 +569,15 @@ class Bundler::Persistent::Net::HTTP::Persistent end ## + # Sets the SSL certificate authority path. + + def ca_path= path + @ca_path = path + + reconnect_ssl + end + + ## # Overrides the default SSL certificate store used for verifying # connections. @@ -560,25 +588,26 @@ class Bundler::Persistent::Net::HTTP::Persistent end ## - # Finishes all connections on the given +thread+ that were created before - # the given +generation+ in the threads +generation_key+ list. - # - # See #shutdown for a bunch of scary warning about misusing this method. - - def cleanup(generation, thread = Thread.current, - generation_key = @generation_key) # :nodoc: - timeouts = thread[@timeout_key] + # The ciphers allowed for SSL connections - (0...generation).each do |old_generation| - next unless thread[generation_key] + def ciphers= ciphers + @ciphers = ciphers - conns = thread[generation_key].delete old_generation + reconnect_ssl + end - conns.each_value do |conn| - finish conn, thread + if Gem::Net::HTTP.method_defined?(:extra_chain_cert=) + ## + # Extra certificates to be added to the certificate chain. + # It is only supported starting from Gem::Net::HTTP version 0.1.1 + def extra_chain_cert= extra_chain_cert + @extra_chain_cert = extra_chain_cert - timeouts.delete conn.object_id if timeouts - end if conns + reconnect_ssl + end + else + def extra_chain_cert= _extra_chain_cert + raise "extra_chain_cert= is not supported by this version of Gem::Net::HTTP" end end @@ -586,91 +615,74 @@ class Bundler::Persistent::Net::HTTP::Persistent # Creates a new connection for +uri+ def connection_for uri - Thread.current[@generation_key] ||= Hash.new { |h,k| h[k] = {} } - Thread.current[@ssl_generation_key] ||= Hash.new { |h,k| h[k] = {} } - Thread.current[@request_key] ||= Hash.new 0 - Thread.current[@timeout_key] ||= Hash.new EPOCH - use_ssl = uri.scheme.downcase == 'https' - if use_ssl then - raise Bundler::Persistent::Net::HTTP::Persistent::Error, 'OpenSSL is not available' unless - HAVE_OPENSSL - - ssl_generation = @ssl_generation - - ssl_cleanup ssl_generation - - connections = Thread.current[@ssl_generation_key][ssl_generation] - else - generation = @generation - - cleanup generation - - connections = Thread.current[@generation_key][generation] - end - - net_http_args = [uri.host, uri.port] - connection_id = net_http_args.join ':' + net_http_args = [uri.hostname, uri.port] + # I'm unsure if uri.host or uri.hostname should be checked against + # the proxy bypass list. if @proxy_uri and not proxy_bypass? uri.host, uri.port then - connection_id << @proxy_connection_id net_http_args.concat @proxy_args else net_http_args.concat [nil, nil, nil, nil] end - connection = connections[connection_id] - - unless connection = connections[connection_id] then - connections[connection_id] = http_class.new(*net_http_args) - connection = connections[connection_id] - ssl connection if use_ssl - else - reset connection if expired? connection - end - - start connection unless connection.started? - - connection.read_timeout = @read_timeout if @read_timeout - connection.keep_alive_timeout = @idle_timeout if @idle_timeout && connection.respond_to?(:keep_alive_timeout=) - - connection - rescue Errno::ECONNREFUSED - address = connection.proxy_address || connection.address - port = connection.proxy_port || connection.port + connection = @pool.checkout net_http_args - raise Error, "connection refused: #{address}:#{port}" - rescue Errno::EHOSTDOWN - address = connection.proxy_address || connection.address - port = connection.proxy_port || connection.port + begin + http = connection.http - raise Error, "host down: #{address}:#{port}" - end + connection.ressl @ssl_generation if + connection.ssl_generation != @ssl_generation - ## - # Returns an error message containing the number of requests performed on - # this connection + if not http.started? then + ssl http if use_ssl + start http + elsif expired? connection then + reset connection + end - def error_message connection - requests = Thread.current[@request_key][connection.object_id] - 1 # fixup - last_use = Thread.current[@timeout_key][connection.object_id] + http.keep_alive_timeout = @idle_timeout if @idle_timeout + http.max_retries = @max_retries if http.respond_to?(:max_retries=) + http.read_timeout = @read_timeout if @read_timeout + http.write_timeout = @write_timeout if + @write_timeout && http.respond_to?(:write_timeout=) + + return yield connection + rescue Errno::ECONNREFUSED + if http.proxy? + address = http.proxy_address + port = http.proxy_port + else + address = http.address + port = http.port + end - age = Time.now - last_use + raise Error, "connection refused: #{address}:#{port}" + rescue Errno::EHOSTDOWN + if http.proxy? + address = http.proxy_address + port = http.proxy_port + else + address = http.address + port = http.port + end - "after #{requests} requests on #{connection.object_id}, " \ - "last used #{age} seconds ago" + raise Error, "host down: #{address}:#{port}" + ensure + @pool.checkin net_http_args + end end ## - # URI::escape wrapper + # CGI::escape wrapper def escape str CGI.escape str if str end ## - # URI::unescape wrapper + # CGI::unescape wrapper def unescape str CGI.unescape str if str @@ -682,26 +694,23 @@ class Bundler::Persistent::Net::HTTP::Persistent # 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 true if @max_requests && connection.requests >= @max_requests return false unless @idle_timeout return true if @idle_timeout.zero? - last_used = Thread.current[@timeout_key][connection.object_id] - - Time.now - last_used > @idle_timeout + Time.now - connection.last_use > @idle_timeout end ## - # Starts the Net::HTTP +connection+ + # Starts the Gem::Net::HTTP +connection+ - def start connection - connection.set_debug_output @debug_output if @debug_output - connection.open_timeout = @open_timeout if @open_timeout + def start http + http.set_debug_output @debug_output if @debug_output + http.open_timeout = @open_timeout if @open_timeout - connection.start + http.start - socket = connection.instance_variable_get :@socket + socket = http.instance_variable_get :@socket if socket then # for fakeweb @socket_options.each do |option| @@ -711,98 +720,21 @@ class Bundler::Persistent::Net::HTTP::Persistent end ## - # Finishes the Net::HTTP +connection+ - - def finish connection, thread = Thread.current - if requests = thread[@request_key] then - requests.delete connection.object_id - end + # Finishes the Gem::Net::HTTP +connection+ + def finish connection connection.finish - rescue IOError - end - def http_class # :nodoc: - if RUBY_VERSION > '2.0' then - Net::HTTP - elsif [:Artifice, :FakeWeb, :WebMock].any? { |klass| - Object.const_defined?(klass) - } or not @reuse_ssl_sessions then - Net::HTTP - else - Bundler::Persistent::Net::HTTP::Persistent::SSLReuse - end + connection.http.instance_variable_set :@last_communicated, nil + connection.http.instance_variable_set :@ssl_session, nil unless + @reuse_ssl_sessions end ## # Returns the HTTP protocol version for +uri+ def http_version uri - @http_versions["#{uri.host}:#{uri.port}"] - end - - ## - # Is +req+ idempotent according to RFC 2616? - - def idempotent? req - case req - when Net::HTTP::Delete, Net::HTTP::Get, Net::HTTP::Head, - Net::HTTP::Options, Net::HTTP::Put, Net::HTTP::Trace then - true - end - end - - ## - # 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, 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 - ## - # Workaround for missing Net::HTTPHeader#connection_close? on Ruby 1.8 - - def connection_close? header - header.connection_close? - end - - ## - # Workaround for missing Net::HTTPHeader#connection_keep_alive? on Ruby 1.8 - - def connection_keep_alive? header - header.connection_keep_alive? - end - else - ## - # Workaround for missing Net::HTTPRequest#connection_close? on Ruby 1.8 - - def connection_close? header - header['connection'] =~ /close/ or header['proxy-connection'] =~ /close/ - end - - ## - # Workaround for missing Net::HTTPRequest#connection_keep_alive? on Ruby - # 1.8 - - def connection_keep_alive? header - header['connection'] =~ /keep-alive/ or - header['proxy-connection'] =~ /keep-alive/ - end - end - - ## - # Deprecated in favor of #expired? - - def max_age # :nodoc: - return Time.now + 1 unless @idle_timeout - - Time.now - @idle_timeout + @http_versions["#{uri.hostname}:#{uri.port}"] end ## @@ -813,20 +745,20 @@ class Bundler::Persistent::Net::HTTP::Persistent end ## - # Pipelines +requests+ to the HTTP server at +uri+ yielding responses if a - # block is given. Returns all responses received. + # Set the maximum number of retries for a request. # - # See - # Net::HTTP::Pipeline[http://docs.seattlerb.org/net-http-pipeline/Net/HTTP/Pipeline.html] - # for further details. + # Defaults to one retry. # - # Only if <tt>net-http-pipeline</tt> was required before - # <tt>net-http-persistent</tt> #pipeline will be present. + # Set this to 0 to disable retries. + + def max_retries= retries + retries = retries.to_int - def pipeline uri, requests, &block # :yields: responses - connection = connection_for uri + raise ArgumentError, "max_retries must be positive" if retries < 0 - connection.pipeline requests, &block + @max_retries = retries + + reconnect end ## @@ -838,16 +770,16 @@ class Bundler::Persistent::Net::HTTP::Persistent reconnect_ssl end - # For Net::HTTP parity + # For Gem::Net::HTTP parity alias key= private_key= ## - # Sets the proxy server. The +proxy+ may be the URI of the proxy server, + # Sets the proxy server. The +proxy+ may be the Gem::URI of the proxy server, # the symbol +:ENV+ which will read the proxy from the environment or nil to # disable use of a proxy. See #proxy_from_env for details on setting the # proxy from the environment. # - # If the proxy URI is set after requests have been made, the next request + # If the proxy Gem::URI is set after requests have been made, the next request # will shut-down and re-open all connections. # # The +no_proxy+ query parameter can be used to specify hosts which shouldn't @@ -858,16 +790,16 @@ class Bundler::Persistent::Net::HTTP::Persistent def proxy= proxy @proxy_uri = case proxy when :ENV then proxy_from_env - when URI::HTTP then proxy + when Gem::URI::HTTP then proxy when nil then # ignore - else raise ArgumentError, 'proxy must be :ENV or a URI::HTTP' + else raise ArgumentError, 'proxy must be :ENV or a Gem::URI::HTTP' end @no_proxy.clear if @proxy_uri then @proxy_args = [ - @proxy_uri.host, + @proxy_uri.hostname, @proxy_uri.port, unescape(@proxy_uri.user), unescape(@proxy_uri.password), @@ -876,7 +808,7 @@ class Bundler::Persistent::Net::HTTP::Persistent @proxy_connection_id = [nil, *@proxy_args].join ':' if @proxy_uri.query then - @no_proxy = CGI.parse(@proxy_uri.query)['no_proxy'].join(',').downcase.split(',').map { |x| x.strip }.reject { |x| x.empty? } + @no_proxy = Gem::URI.decode_www_form(@proxy_uri.query).filter_map { |k, v| v if k == 'no_proxy' }.join(',').downcase.split(',').map { |x| x.strip }.reject { |x| x.empty? } end end @@ -885,13 +817,13 @@ class Bundler::Persistent::Net::HTTP::Persistent end ## - # Creates a URI for an HTTP proxy server from ENV variables. + # Creates a Gem::URI for an HTTP proxy server from ENV variables. # # If +HTTP_PROXY+ is set a proxy will be returned. # - # If +HTTP_PROXY_USER+ or +HTTP_PROXY_PASS+ are set the URI is given the + # If +HTTP_PROXY_USER+ or +HTTP_PROXY_PASS+ are set the Gem::URI is given the # indicated user and password unless HTTP_PROXY contains either of these in - # the URI. + # the Gem::URI. # # The +NO_PROXY+ ENV variable can be used to specify hosts which shouldn't # be reached via proxy; if set it should be a comma separated list of @@ -907,7 +839,7 @@ class Bundler::Persistent::Net::HTTP::Persistent return nil if env_proxy.nil? or env_proxy.empty? - uri = URI normalize_uri env_proxy + uri = Gem::URI normalize_uri env_proxy env_no_proxy = ENV['no_proxy'] || ENV['NO_PROXY'] @@ -942,133 +874,90 @@ class Bundler::Persistent::Net::HTTP::Persistent end ## - # Forces reconnection of HTTP connections. + # Forces reconnection of all HTTP connections, including TLS/SSL + # connections. def reconnect @generation += 1 end ## - # Forces reconnection of SSL connections. + # Forces reconnection of only TLS/SSL connections. def reconnect_ssl @ssl_generation += 1 end ## - # Finishes then restarts the Net::HTTP +connection+ + # Finishes then restarts the Gem::Net::HTTP +connection+ def reset connection - Thread.current[@request_key].delete connection.object_id - Thread.current[@timeout_key].delete connection.object_id + http = connection.http finish connection - start connection + start http rescue Errno::ECONNREFUSED - e = Error.new "connection refused: #{connection.address}:#{connection.port}" + e = Error.new "connection refused: #{http.address}:#{http.port}" e.set_backtrace $@ raise e rescue Errno::EHOSTDOWN - e = Error.new "host down: #{connection.address}:#{connection.port}" + e = Error.new "host down: #{http.address}:#{http.port}" e.set_backtrace $@ raise e end ## - # Makes a request on +uri+. If +req+ is nil a Net::HTTP::Get is performed + # Makes a request on +uri+. If +req+ is nil a Gem::Net::HTTP::Get is performed # against +uri+. # - # If a block is passed #request behaves like Net::HTTP#request (the body of + # If a block is passed #request behaves like Gem::Net::HTTP#request (the body of # the response will not have been read). # - # +req+ must be a Net::HTTPRequest subclass (see Net::HTTP for a list). - # - # If there is an error and the request is idempotent according to RFC 2616 - # it will be retried automatically. + # +req+ must be a Gem::Net::HTTPGenericRequest subclass (see Gem::Net::HTTP for a list). def request uri, req = nil, &block - retried = false - bad_response = false - - req = request_setup req || uri - - connection = connection_for uri - connection_id = connection.object_id - - begin - Thread.current[@request_key][connection_id] += 1 - response = connection.request req, &block - - if connection_close?(req) or - (response.http_version <= '1.0' and - not connection_keep_alive?(response)) or - connection_close?(response) then - connection.finish + uri = Gem::URI uri + req = request_setup req || uri + response = nil + + connection_for uri do |connection| + http = connection.http + + begin + connection.requests += 1 + + response = http.request req, &block + + if req.connection_close? or + (response.http_version <= '1.0' and + not response.connection_keep_alive?) or + response.connection_close? then + finish connection + end + rescue Exception # make sure to close the connection when it was interrupted + finish connection + + raise + ensure + connection.last_use = Time.now end - rescue Net::HTTPBadResponse => e - message = error_message connection - - finish connection - - raise Error, "too many bad responses #{message}" if - bad_response or not can_retry? req - - bad_response = true - retry - rescue *RETRIED_EXCEPTIONS => e # retried on ruby 2 - request_failed e, req, connection if - retried or not can_retry? req, @retried_on_ruby_2 - - reset connection - - 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 - @http_versions["#{uri.host}:#{uri.port}"] ||= response.http_version + @http_versions["#{uri.hostname}:#{uri.port}"] ||= response.http_version response 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 + # Creates a GET request if +req_or_uri+ is a Gem::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 + req = if req_or_uri.respond_to? 'request_uri' then + Gem::Net::HTTP::Get.new req_or_uri.request_uri else req_or_uri end @@ -1090,45 +979,27 @@ class Bundler::Persistent::Net::HTTP::Persistent end ## - # Shuts down all connections for +thread+. - # - # Uses the current thread by default. + # Shuts down all connections. Attempting to checkout a connection after + # shutdown will raise an error. # - # If you've used Bundler::Persistent::Net::HTTP::Persistent across multiple threads you should - # call this in each thread when you're done making HTTP requests. + # *NOTE*: Calling shutdown for can be dangerous! # - # *NOTE*: Calling shutdown for another thread can be dangerous! - # - # If the thread is still using the connection it may cause an error! It is - # best to call #shutdown in the thread at the appropriate time instead! - - def shutdown thread = Thread.current - generation = reconnect - cleanup generation, thread, @generation_key + # If any thread is still using a connection it may cause an error! Call + # #shutdown when you are completely done making requests! - ssl_generation = reconnect_ssl - cleanup ssl_generation, thread, @ssl_generation_key - - thread[@request_key] = nil - thread[@timeout_key] = nil + def shutdown + @pool.shutdown { |http| http.finish } end ## - # Shuts down all connections in all threads - # - # *NOTE*: THIS METHOD IS VERY DANGEROUS! + # Discard all existing connections. Subsequent checkouts will create + # new connections as needed. # - # Do not call this method if other threads are still using their - # connections! Call #shutdown at the appropriate time instead! - # - # Use this method only as a last resort! - - def shutdown_in_all_threads - Thread.list.each do |thread| - shutdown thread - end + # If any thread is still using a connection it may cause an error! Call + # #reload when you are completely done making requests! - nil + def reload + @pool.reload { |http| http.finish } end ## @@ -1137,9 +1008,16 @@ class Bundler::Persistent::Net::HTTP::Persistent def ssl connection connection.use_ssl = true + connection.ciphers = @ciphers if @ciphers + connection.ssl_timeout = @ssl_timeout if @ssl_timeout connection.ssl_version = @ssl_version if @ssl_version + connection.min_version = @min_version if @min_version + connection.max_version = @max_version if @max_version - connection.verify_mode = @verify_mode + connection.verify_depth = @verify_depth + connection.verify_mode = @verify_mode + connection.verify_hostname = @verify_hostname if + @verify_hostname != nil && connection.respond_to?(:verify_hostname=) if OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE and not Object.const_defined?(:I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG) then @@ -1168,8 +1046,10 @@ application: WARNING end - if @ca_file then - connection.ca_file = @ca_file + connection.ca_file = @ca_file if @ca_file + connection.ca_path = @ca_path if @ca_path + + if @ca_file or @ca_path then connection.verify_mode = OpenSSL::SSL::VERIFY_PEER connection.verify_callback = @verify_callback if @verify_callback end @@ -1179,6 +1059,10 @@ application: connection.key = @private_key end + if defined?(@extra_chain_cert) and @extra_chain_cert + connection.extra_chain_cert = @extra_chain_cert + end + connection.cert_store = if @cert_store then @cert_store else @@ -1189,11 +1073,12 @@ application: end ## - # Finishes all connections that existed before the given SSL parameter - # +generation+. + # SSL session lifetime - def ssl_cleanup generation # :nodoc: - cleanup generation, Thread.current, @ssl_generation_key + def ssl_timeout= ssl_timeout + @ssl_timeout = ssl_timeout + + reconnect_ssl end ## @@ -1203,7 +1088,34 @@ application: @ssl_version = ssl_version reconnect_ssl - end if RUBY_VERSION > '1.9' + end + + ## + # Minimum SSL version to use + + def min_version= min_version + @min_version = min_version + + reconnect_ssl + end + + ## + # maximum SSL version to use + + def max_version= max_version + @max_version = max_version + + reconnect_ssl + end + + ## + # Sets the depth of SSL certificate verification + + def verify_depth= verify_depth + @verify_depth = verify_depth + + reconnect_ssl + end ## # Sets the HTTPS verify mode. Defaults to OpenSSL::SSL::VERIFY_PEER. @@ -1219,6 +1131,15 @@ application: end ## + # Sets the HTTPS verify_hostname. + + def verify_hostname= verify_hostname + @verify_hostname = verify_hostname + + reconnect_ssl + end + + ## # SSL verification callback. def verify_callback= callback @@ -1226,8 +1147,7 @@ application: reconnect_ssl end - end -require 'bundler/vendor/net-http-persistent/lib/net/http/persistent/ssl_reuse' - +require_relative 'persistent/connection' +require_relative 'persistent/pool' diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb new file mode 100644 index 0000000000..8b9ab5cc78 --- /dev/null +++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb @@ -0,0 +1,41 @@ +## +# A Gem::Net::HTTP connection wrapper that holds extra information for managing the +# connection's lifetime. + +class Gem::Net::HTTP::Persistent::Connection # :nodoc: + + attr_accessor :http + + attr_accessor :last_use + + attr_accessor :requests + + attr_accessor :ssl_generation + + def initialize http_class, http_args, ssl_generation + @http = http_class.new(*http_args) + @ssl_generation = ssl_generation + + reset + end + + def finish + @http.finish + rescue IOError + ensure + reset + end + alias_method :close, :finish + + def reset + @last_use = Gem::Net::HTTP::Persistent::EPOCH + @requests = 0 + end + + def ressl ssl_generation + @ssl_generation = ssl_generation + + finish + end + +end diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb new file mode 100644 index 0000000000..04a1e754bf --- /dev/null +++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb @@ -0,0 +1,65 @@ +class Gem::Net::HTTP::Persistent::Pool < Bundler::ConnectionPool # :nodoc: + + attr_reader :available # :nodoc: + attr_reader :key # :nodoc: + + def initialize(options = {}, &block) + super + + @available = Gem::Net::HTTP::Persistent::TimedStackMulti.new(@size, &block) + @key = "current-#{@available.object_id}" + end + + def checkin net_http_args + if net_http_args.is_a?(Hash) && net_http_args.size == 1 && net_http_args[:force] + # Bundler::ConnectionPool 2.4+ calls `checkin(force: true)` after fork. + # When this happens, we should remove all connections from Thread.current + if stacks = Thread.current[@key] + stacks.each do |http_args, connections| + connections.each do |conn| + @available.push conn, connection_args: http_args + end + connections.clear + end + end + else + stack = Thread.current[@key][net_http_args] ||= [] + + raise Bundler::ConnectionPool::Error, 'no connections are checked out' if + stack.empty? + + conn = stack.pop + + if stack.empty? + @available.push conn, connection_args: net_http_args + + Thread.current[@key].delete(net_http_args) + Thread.current[@key] = nil if Thread.current[@key].empty? + end + end + nil + end + + def checkout net_http_args + stacks = Thread.current[@key] ||= {} + stack = stacks[net_http_args] ||= [] + + if stack.empty? then + conn = @available.pop connection_args: net_http_args + else + conn = stack.last + end + + stack.push conn + + conn + end + + def shutdown + Thread.current[@key] = nil + super + end +end + +require_relative 'timed_stack_multi' + diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/ssl_reuse.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/ssl_reuse.rb deleted file mode 100644 index 1b6b789f6d..0000000000 --- a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/ssl_reuse.rb +++ /dev/null @@ -1,129 +0,0 @@ -## -# This Net::HTTP subclass adds SSL session reuse and Server Name Indication -# (SNI) RFC 3546. -# -# DO NOT DEPEND UPON THIS CLASS -# -# This class is an implementation detail and is subject to change or removal -# at any time. - -class Bundler::Persistent::Net::HTTP::Persistent::SSLReuse < Net::HTTP - - @is_proxy_class = false - @proxy_addr = nil - @proxy_port = nil - @proxy_user = nil - @proxy_pass = nil - - def initialize address, port = nil # :nodoc: - super - - @ssl_session = nil - end - - ## - # From ruby trunk r33086 including http://redmine.ruby-lang.org/issues/5341 - - def connect # :nodoc: - D "opening connection to #{conn_address()}..." - s = timeout(@open_timeout) { TCPSocket.open(conn_address(), conn_port()) } - D "opened" - if use_ssl? - ssl_parameters = Hash.new - iv_list = instance_variables - SSL_ATTRIBUTES.each do |name| - ivname = "@#{name}".intern - if iv_list.include?(ivname) and - value = instance_variable_get(ivname) - ssl_parameters[name] = value - end - end - unless @ssl_context then - @ssl_context = OpenSSL::SSL::SSLContext.new - @ssl_context.set_params(ssl_parameters) - end - s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) - s.sync_close = true - end - @socket = Net::BufferedIO.new(s) - @socket.read_timeout = @read_timeout - @socket.continue_timeout = @continue_timeout if - @socket.respond_to? :continue_timeout - @socket.debug_output = @debug_output - if use_ssl? - begin - if proxy? - @socket.writeline sprintf('CONNECT %s:%s HTTP/%s', - @address, @port, HTTPVersion) - @socket.writeline "Host: #{@address}:#{@port}" - if proxy_user - credential = ["#{proxy_user}:#{proxy_pass}"].pack('m') - credential.delete!("\r\n") - @socket.writeline "Proxy-Authorization: Basic #{credential}" - end - @socket.writeline '' - Net::HTTPResponse.read_new(@socket).value - end - s.session = @ssl_session if @ssl_session - # Server Name Indication (SNI) RFC 3546 - s.hostname = @address if s.respond_to? :hostname= - timeout(@open_timeout) { s.connect } - if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE - s.post_connection_check(@address) - end - @ssl_session = s.session - rescue => exception - D "Conn close because of connect error #{exception}" - @socket.close if @socket and not @socket.closed? - raise exception - end - end - on_connect - end if RUBY_VERSION > '1.9' - - ## - # From ruby_1_8_7 branch r29865 including a modified - # http://redmine.ruby-lang.org/issues/5341 - - def connect # :nodoc: - D "opening connection to #{conn_address()}..." - s = timeout(@open_timeout) { TCPSocket.open(conn_address(), conn_port()) } - D "opened" - if use_ssl? - unless @ssl_context.verify_mode - warn "warning: peer certificate won't be verified in this SSL session" - @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE - end - s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) - s.sync_close = true - end - @socket = Net::BufferedIO.new(s) - @socket.read_timeout = @read_timeout - @socket.debug_output = @debug_output - if use_ssl? - if proxy? - @socket.writeline sprintf('CONNECT %s:%s HTTP/%s', - @address, @port, HTTPVersion) - @socket.writeline "Host: #{@address}:#{@port}" - if proxy_user - credential = ["#{proxy_user}:#{proxy_pass}"].pack('m') - credential.delete!("\r\n") - @socket.writeline "Proxy-Authorization: Basic #{credential}" - end - @socket.writeline '' - Net::HTTPResponse.read_new(@socket).value - end - s.session = @ssl_session if @ssl_session - s.connect - if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE - s.post_connection_check(@address) - end - @ssl_session = s.session - end - on_connect - end if RUBY_VERSION < '1.9' - - private :connect - -end - diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb new file mode 100644 index 0000000000..034fbe39b8 --- /dev/null +++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb @@ -0,0 +1,80 @@ +class Gem::Net::HTTP::Persistent::TimedStackMulti < Bundler::ConnectionPool::TimedStack # :nodoc: + + ## + # Returns a new hash that has arrays for keys + # + # Using a class method to limit the bindings referenced by the hash's + # default_proc + + def self.hash_of_arrays # :nodoc: + Hash.new { |h,k| h[k] = [] } + end + + def initialize(size = 0, &block) + super + + @enqueued = 0 + @ques = self.class.hash_of_arrays + @lru = {} + @key = :"connection_args-#{object_id}" + end + + def empty? + (@created - @enqueued) >= @max + end + + def length + @max - @created + @enqueued + end + + private + + def connection_stored? options = {} # :nodoc: + !@ques[options[:connection_args]].empty? + end + + def fetch_connection options = {} # :nodoc: + connection_args = options[:connection_args] + + @enqueued -= 1 + lru_update connection_args + @ques[connection_args].pop + end + + def lru_update connection_args # :nodoc: + @lru.delete connection_args + @lru[connection_args] = true + end + + def shutdown_connections # :nodoc: + @ques.each_key do |key| + super connection_args: key + end + end + + def store_connection obj, options = {} # :nodoc: + @ques[options[:connection_args]].push obj + @enqueued += 1 + end + + def try_create options = {} # :nodoc: + connection_args = options[:connection_args] + + if @created >= @max && @enqueued >= 1 + oldest, = @lru.first + @lru.delete oldest + connection = @ques[oldest].pop + connection.close if connection.respond_to?(:close) + + @created -= 1 + end + + if @created < @max + @created += 1 + lru_update connection_args + return @create_block.call(connection_args) + end + end + +end + diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub.rb new file mode 100644 index 0000000000..eaaba3fc98 --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub.rb @@ -0,0 +1,31 @@ +require_relative "pub_grub/package" +require_relative "pub_grub/static_package_source" +require_relative "pub_grub/term" +require_relative "pub_grub/version_range" +require_relative "pub_grub/version_constraint" +require_relative "pub_grub/version_union" +require_relative "pub_grub/version_solver" +require_relative "pub_grub/incompatibility" +require_relative 'pub_grub/solve_failure' +require_relative 'pub_grub/failure_writer' +require_relative 'pub_grub/version' + +module Bundler::PubGrub + class << self + attr_writer :logger + + def logger + @logger || default_logger + end + + private + + def default_logger + require "logger" + + logger = ::Logger.new(STDERR) + logger.level = $DEBUG ? ::Logger::DEBUG : ::Logger::WARN + @logger = logger + end + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/assignment.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/assignment.rb new file mode 100644 index 0000000000..2236a97b5b --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/assignment.rb @@ -0,0 +1,20 @@ +module Bundler::PubGrub + class Assignment + attr_reader :term, :cause, :decision_level, :index + def initialize(term, cause, decision_level, index) + @term = term + @cause = cause + @decision_level = decision_level + @index = index + end + + def self.decision(package, version, decision_level, index) + term = Term.new(VersionConstraint.exact(package, version), true) + new(term, :decision, decision_level, index) + end + + def decision? + cause == :decision + end + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/basic_package_source.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/basic_package_source.rb new file mode 100644 index 0000000000..491151ec0b --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/basic_package_source.rb @@ -0,0 +1,169 @@ +require_relative 'version_constraint' +require_relative 'incompatibility' + +module Bundler::PubGrub + # Types: + # + # Where possible, Bundler::PubGrub will accept user-defined types, so long as they quack. + # + # ## "Package": + # + # This class will be used to represent the various packages being solved for. + # .to_s will be called when displaying errors and debugging info, it should + # probably return the package's name. + # It must also have a reasonable definition of #== and #hash + # + # Example classes: String ("rails") + # + # + # ## "Version": + # + # This class will be used to represent a single version number. + # + # Versions don't need to store their associated package, however they will + # only be compared against other versions of the same package. + # + # It must be Comparible (and implement <=> reasonably) + # + # Example classes: Gem::Version, Integer + # + # + # ## "Dependency" + # + # This class represents the requirement one package has on another. It is + # returned by dependencies_for(package, version) and will be passed to + # parse_dependency to convert it to a format Bundler::PubGrub understands. + # + # It must also have a reasonable definition of #== + # + # Example classes: String ("~> 1.0"), Gem::Requirement + # + class BasicPackageSource + # Override me! + # + # This is called per package to find all possible versions of a package. + # + # It is called at most once per-package + # + # Returns: Array of versions for a package, in preferred order of selection + def all_versions_for(package) + raise NotImplementedError + end + + # Override me! + # + # Returns: Hash in the form of { package => requirement, ... } + def dependencies_for(package, version) + raise NotImplementedError + end + + # Override me! + # + # Convert a (user-defined) dependency into a format Bundler::PubGrub understands. + # + # Package is passed to this method but for many implementations is not + # needed. + # + # Returns: either a Bundler::PubGrub::VersionRange, Bundler::PubGrub::VersionUnion, or a + # Bundler::PubGrub::VersionConstraint + def parse_dependency(package, dependency) + raise NotImplementedError + end + + # Override me! + # + # If not overridden, this will call dependencies_for with the root package. + # + # Returns: Hash in the form of { package => requirement, ... } (see dependencies_for) + def root_dependencies + dependencies_for(@root_package, @root_version) + end + + def initialize + @root_package = Package.root + @root_version = Package.root_version + + @sorted_versions = Hash.new do |h,k| + if k == @root_package + h[k] = [@root_version] + else + h[k] = all_versions_for(k).sort + end + end + + @cached_dependencies = Hash.new do |packages, package| + if package == @root_package + packages[package] = { + @root_version => root_dependencies + } + else + packages[package] = Hash.new do |versions, version| + versions[version] = dependencies_for(package, version) + end + end + end + end + + def versions_for(package, range=VersionRange.any) + range.select_versions(@sorted_versions[package]) + end + + def no_versions_incompatibility_for(_package, unsatisfied_term) + cause = Incompatibility::NoVersions.new(unsatisfied_term) + + Incompatibility.new([unsatisfied_term], cause: cause) + end + + def incompatibilities_for(package, version) + package_deps = @cached_dependencies[package] + sorted_versions = @sorted_versions[package] + package_deps[version].map do |dep_package, dep_constraint_name| + low = high = sorted_versions.index(version) + + # find version low such that all >= low share the same dep + while low > 0 && + package_deps[sorted_versions[low - 1]][dep_package] == dep_constraint_name + low -= 1 + end + low = + if low == 0 + nil + else + sorted_versions[low] + end + + # find version high such that all < high share the same dep + while high < sorted_versions.length && + package_deps[sorted_versions[high]][dep_package] == dep_constraint_name + high += 1 + end + high = + if high == sorted_versions.length + nil + else + sorted_versions[high] + end + + range = VersionRange.new(min: low, max: high, include_min: !low.nil?) + + self_constraint = VersionConstraint.new(package, range: range) + + if !@packages.include?(dep_package) + # no such package -> this version is invalid + end + + dep_constraint = parse_dependency(dep_package, dep_constraint_name) + if !dep_constraint + # falsey indicates this dependency was invalid + cause = Bundler::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint_name) + return [Incompatibility.new([Term.new(self_constraint, true)], cause: cause)] + elsif !dep_constraint.is_a?(VersionConstraint) + # Upgrade range/union to VersionConstraint + dep_constraint = VersionConstraint.new(dep_package, range: dep_constraint) + end + + Incompatibility.new([Term.new(self_constraint, true), Term.new(dep_constraint, false)], cause: :dependency) + end + end + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/failure_writer.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/failure_writer.rb new file mode 100644 index 0000000000..ee099b23f4 --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/failure_writer.rb @@ -0,0 +1,182 @@ +module Bundler::PubGrub + class FailureWriter + def initialize(root) + @root = root + + # { Incompatibility => Integer } + @derivations = {} + + # [ [ String, Integer or nil ] ] + @lines = [] + + # { Incompatibility => Integer } + @line_numbers = {} + + count_derivations(root) + end + + def write + return @root.to_s unless @root.conflict? + + visit(@root) + + padding = @line_numbers.empty? ? 0 : "(#{@line_numbers.values.last}) ".length + + @lines.map do |message, number| + next "" if message.empty? + + lead = number ? "(#{number}) " : "" + lead = lead.ljust(padding) + message = message.gsub("\n", "\n" + " " * (padding + 2)) + "#{lead}#{message}" + end.join("\n") + end + + private + + def write_line(incompatibility, message, numbered:) + if numbered + number = @line_numbers.length + 1 + @line_numbers[incompatibility] = number + end + + @lines << [message, number] + end + + def visit(incompatibility, conclusion: false) + raise unless incompatibility.conflict? + + numbered = conclusion || @derivations[incompatibility] > 1; + conjunction = conclusion || incompatibility == @root ? "So," : "And" + + cause = incompatibility.cause + + if cause.conflict.conflict? && cause.other.conflict? + conflict_line = @line_numbers[cause.conflict] + other_line = @line_numbers[cause.other] + + if conflict_line && other_line + write_line( + incompatibility, + "Because #{cause.conflict} (#{conflict_line})\nand #{cause.other} (#{other_line}),\n#{incompatibility}.", + numbered: numbered + ) + elsif conflict_line || other_line + with_line = conflict_line ? cause.conflict : cause.other + without_line = conflict_line ? cause.other : cause.conflict + line = @line_numbers[with_line] + + visit(without_line); + write_line( + incompatibility, + "#{conjunction} because #{with_line} (#{line}),\n#{incompatibility}.", + numbered: numbered + ) + else + single_line_conflict = single_line?(cause.conflict.cause) + single_line_other = single_line?(cause.other.cause) + + if single_line_conflict || single_line_other + first = single_line_other ? cause.conflict : cause.other + second = single_line_other ? cause.other : cause.conflict + visit(first) + visit(second) + write_line( + incompatibility, + "Thus, #{incompatibility}.", + numbered: numbered + ) + else + visit(cause.conflict, conclusion: true) + @lines << ["", nil] + visit(cause.other) + + write_line( + incompatibility, + "#{conjunction} because #{cause.conflict} (#{@line_numbers[cause.conflict]}),\n#{incompatibility}.", + numbered: numbered + ) + end + end + elsif cause.conflict.conflict? || cause.other.conflict? + derived = cause.conflict.conflict? ? cause.conflict : cause.other + ext = cause.conflict.conflict? ? cause.other : cause.conflict + + derived_line = @line_numbers[derived] + if derived_line + write_line( + incompatibility, + "Because #{ext}\nand #{derived} (#{derived_line}),\n#{incompatibility}.", + numbered: numbered + ) + elsif collapsible?(derived) + derived_cause = derived.cause + if derived_cause.conflict.conflict? + collapsed_derived = derived_cause.conflict + collapsed_ext = derived_cause.other + else + collapsed_derived = derived_cause.other + collapsed_ext = derived_cause.conflict + end + + visit(collapsed_derived) + + write_line( + incompatibility, + "#{conjunction} because #{collapsed_ext}\nand #{ext},\n#{incompatibility}.", + numbered: numbered + ) + else + visit(derived) + write_line( + incompatibility, + "#{conjunction} because #{ext},\n#{incompatibility}.", + numbered: numbered + ) + end + else + write_line( + incompatibility, + "Because #{cause.conflict}\nand #{cause.other},\n#{incompatibility}.", + numbered: numbered + ) + end + end + + def single_line?(cause) + !cause.conflict.conflict? && !cause.other.conflict? + end + + def collapsible?(incompatibility) + return false if @derivations[incompatibility] > 1 + + cause = incompatibility.cause + # If incompatibility is derived from two derived incompatibilities, + # there are too many transitive causes to display concisely. + return false if cause.conflict.conflict? && cause.other.conflict? + + # If incompatibility is derived from two external incompatibilities, it + # tends to be confusing to collapse it. + return false unless cause.conflict.conflict? || cause.other.conflict? + + # If incompatibility's internal cause is numbered, collapsing it would + # get too noisy. + complex = cause.conflict.conflict? ? cause.conflict : cause.other + + !@line_numbers.has_key?(complex) + end + + def count_derivations(incompatibility) + if @derivations.has_key?(incompatibility) + @derivations[incompatibility] += 1 + else + @derivations[incompatibility] = 1 + if incompatibility.conflict? + cause = incompatibility.cause + count_derivations(cause.conflict) + count_derivations(cause.other) + end + end + end + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/incompatibility.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/incompatibility.rb new file mode 100644 index 0000000000..239eaf3401 --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/incompatibility.rb @@ -0,0 +1,150 @@ +module Bundler::PubGrub + class Incompatibility + ConflictCause = Struct.new(:incompatibility, :satisfier) do + alias_method :conflict, :incompatibility + alias_method :other, :satisfier + end + + InvalidDependency = Struct.new(:package, :constraint) do + end + + NoVersions = Struct.new(:constraint) do + end + + attr_reader :terms, :cause + + def initialize(terms, cause:, custom_explanation: nil) + @cause = cause + @terms = cleanup_terms(terms) + @custom_explanation = custom_explanation + + if cause == :dependency && @terms.length != 2 + raise ArgumentError, "a dependency Incompatibility must have exactly two terms. Got #{@terms.inspect}" + end + end + + def hash + cause.hash ^ terms.hash + end + + def eql?(other) + cause.eql?(other.cause) && + terms.eql?(other.terms) + end + + def failure? + terms.empty? || (terms.length == 1 && Package.root?(terms[0].package) && terms[0].positive?) + end + + def conflict? + ConflictCause === cause + end + + # Returns all external incompatibilities in this incompatibility's + # derivation graph + def external_incompatibilities + if conflict? + [ + cause.conflict, + cause.other + ].flat_map(&:external_incompatibilities) + else + [this] + end + end + + def to_s + return @custom_explanation if @custom_explanation + + case cause + when :root + "(root dependency)" + when :dependency + "#{terms[0].to_s(allow_every: true)} depends on #{terms[1].invert}" + when Bundler::PubGrub::Incompatibility::InvalidDependency + "#{terms[0].to_s(allow_every: true)} depends on unknown package #{cause.package}" + when Bundler::PubGrub::Incompatibility::NoVersions + "no versions satisfy #{cause.constraint}" + when Bundler::PubGrub::Incompatibility::ConflictCause + if failure? + "version solving has failed" + elsif terms.length == 1 + term = terms[0] + if term.positive? + if term.constraint.any? + "#{term.package} cannot be used" + else + "#{term.to_s(allow_every: true)} cannot be used" + end + else + "#{term.invert} is required" + end + else + if terms.all?(&:positive?) + if terms.length == 2 + "#{terms[0].to_s(allow_every: true)} is incompatible with #{terms[1]}" + else + "one of #{terms.map(&:to_s).join(" or ")} must be false" + end + elsif terms.all?(&:negative?) + if terms.length == 2 + "either #{terms[0].invert} or #{terms[1].invert}" + else + "one of #{terms.map(&:invert).join(" or ")} must be true"; + end + else + positive = terms.select(&:positive?) + negative = terms.select(&:negative?).map(&:invert) + + if positive.length == 1 + "#{positive[0].to_s(allow_every: true)} requires #{negative.join(" or ")}" + else + "if #{positive.join(" and ")} then #{negative.join(" or ")}" + end + end + end + else + raise "unhandled cause: #{cause.inspect}" + end + end + + def inspect + "#<#{self.class} #{to_s}>" + end + + def pretty_print(q) + q.group 2, "#<#{self.class}", ">" do + q.breakable + q.text to_s + + q.breakable + q.text " caused by " + q.pp @cause + end + end + + private + + def cleanup_terms(terms) + terms.each do |term| + raise "#{term.inspect} must be a term" unless term.is_a?(Term) + end + + if terms.length != 1 && ConflictCause === cause + terms = terms.reject do |term| + term.positive? && Package.root?(term.package) + end + end + + # Optimized simple cases + return terms if terms.length <= 1 + return terms if terms.length == 2 && terms[0].package != terms[1].package + + terms.group_by(&:package).map do |package, common_terms| + common_terms.inject do |acc, term| + acc.intersect(term) + end + end + end + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/package.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/package.rb new file mode 100644 index 0000000000..efb9d3da16 --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/package.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Bundler::PubGrub + class Package + + attr_reader :name + + def initialize(name) + @name = name + end + + def inspect + "#<#{self.class} #{name.inspect}>" + end + + def <=>(other) + name <=> other.name + end + + ROOT = Package.new(:root) + ROOT_VERSION = 0 + + def self.root + ROOT + end + + def self.root_version + ROOT_VERSION + end + + def self.root?(package) + if package.respond_to?(:root?) + package.root? + else + package == root + end + end + + def to_s + name.to_s + end + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/partial_solution.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/partial_solution.rb new file mode 100644 index 0000000000..4c4b8ca844 --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/partial_solution.rb @@ -0,0 +1,121 @@ +require_relative 'assignment' + +module Bundler::PubGrub + class PartialSolution + attr_reader :assignments, :decisions + attr_reader :attempted_solutions + + def initialize + reset! + + @attempted_solutions = 1 + @backtracking = false + end + + def decision_level + @decisions.length + end + + def relation(term) + package = term.package + return :overlap if !@terms.key?(package) + + @relation_cache[package][term] ||= + @terms[package].relation(term) + end + + def satisfies?(term) + relation(term) == :subset + end + + def derive(term, cause) + add_assignment(Assignment.new(term, cause, decision_level, assignments.length)) + end + + def satisfier(term) + assignment = + @assignments_by[term.package].bsearch do |assignment_by| + @cumulative_assignments[assignment_by].satisfies?(term) + end + + assignment || raise("#{term} unsatisfied") + end + + # A list of unsatisfied terms + def unsatisfied + @required.keys.reject do |package| + @decisions.key?(package) + end.map do |package| + @terms[package] + end + end + + def decide(package, version) + @attempted_solutions += 1 if @backtracking + @backtracking = false; + + decisions[package] = version + assignment = Assignment.decision(package, version, decision_level, assignments.length) + add_assignment(assignment) + end + + def backtrack(previous_level) + @backtracking = true + + new_assignments = assignments.select do |assignment| + assignment.decision_level <= previous_level + end + + new_decisions = Hash[decisions.first(previous_level)] + + reset! + + @decisions = new_decisions + + new_assignments.each do |assignment| + add_assignment(assignment) + end + end + + private + + def reset! + # { Array<Assignment> } + @assignments = [] + + # { Package => Array<Assignment> } + @assignments_by = Hash.new { |h,k| h[k] = [] } + @cumulative_assignments = {}.compare_by_identity + + # { Package => Package::Version } + @decisions = {} + + # { Package => Term } + @terms = {} + @relation_cache = Hash.new { |h,k| h[k] = {} } + + # { Package => Boolean } + @required = {} + end + + def add_assignment(assignment) + term = assignment.term + package = term.package + + @assignments << assignment + @assignments_by[package] << assignment + + @required[package] = true if term.positive? + + if @terms.key?(package) + old_term = @terms[package] + @terms[package] = old_term.intersect(term) + else + @terms[package] = term + end + @relation_cache[package].clear + + @cumulative_assignments[assignment] = @terms[package] + end + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/rubygems.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/rubygems.rb new file mode 100644 index 0000000000..245c23be22 --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/rubygems.rb @@ -0,0 +1,45 @@ +module Bundler::PubGrub + module RubyGems + extend self + + def requirement_to_range(requirement) + ranges = requirement.requirements.map do |(op, ver)| + case op + when "~>" + name = "~> #{ver}" + bump = ver.class.new(ver.bump.to_s + ".A") + VersionRange.new(name: name, min: ver, max: bump, include_min: true) + when ">" + VersionRange.new(min: ver) + when ">=" + VersionRange.new(min: ver, include_min: true) + when "<" + VersionRange.new(max: ver) + when "<=" + VersionRange.new(max: ver, include_max: true) + when "=" + VersionRange.new(min: ver, max: ver, include_min: true, include_max: true) + when "!=" + VersionRange.new(min: ver, max: ver, include_min: true, include_max: true).invert + else + raise "bad version specifier: #{op}" + end + end + + ranges.inject(&:intersect) + end + + def requirement_to_constraint(package, requirement) + Bundler::PubGrub::VersionConstraint.new(package, range: requirement_to_range(requirement)) + end + + def parse_range(dep) + requirement_to_range(Gem::Requirement.new(dep)) + end + + def parse_constraint(package, dep) + range = parse_range(dep) + Bundler::PubGrub::VersionConstraint.new(package, range: range) + end + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/solve_failure.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/solve_failure.rb new file mode 100644 index 0000000000..961a7a7c0c --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/solve_failure.rb @@ -0,0 +1,19 @@ +require_relative 'failure_writer' + +module Bundler::PubGrub + class SolveFailure < StandardError + attr_reader :incompatibility + + def initialize(incompatibility) + @incompatibility = incompatibility + end + + def to_s + "Could not find compatible versions\n\n#{explanation}" + end + + def explanation + @explanation ||= FailureWriter.new(@incompatibility).write + end + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/static_package_source.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/static_package_source.rb new file mode 100644 index 0000000000..36ab06254d --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/static_package_source.rb @@ -0,0 +1,61 @@ +require_relative 'package' +require_relative 'rubygems' +require_relative 'version_constraint' +require_relative 'incompatibility' +require_relative 'basic_package_source' + +module Bundler::PubGrub + class StaticPackageSource < BasicPackageSource + class DSL + def initialize(packages, root_deps) + @packages = packages + @root_deps = root_deps + end + + def root(deps:) + @root_deps.update(deps) + end + + def add(name, version, deps: {}) + version = Gem::Version.new(version) + @packages[name] ||= {} + raise ArgumentError, "#{name} #{version} declared twice" if @packages[name].key?(version) + @packages[name][version] = clean_deps(name, version, deps) + end + + private + + # Exclude redundant self-referencing dependencies + def clean_deps(name, version, deps) + deps.reject {|dep_name, req| name == dep_name && Bundler::PubGrub::RubyGems.parse_range(req).include?(version) } + end + end + + def initialize + @root_deps = {} + @packages = {} + + yield DSL.new(@packages, @root_deps) + + super() + end + + def all_versions_for(package) + @packages[package].keys + end + + def root_dependencies + @root_deps + end + + def dependencies_for(package, version) + @packages[package][version] + end + + def parse_dependency(package, dependency) + return false unless @packages.key?(package) + + Bundler::PubGrub::RubyGems.parse_constraint(package, dependency) + end + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/strategy.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/strategy.rb new file mode 100644 index 0000000000..6955655ba4 --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/strategy.rb @@ -0,0 +1,42 @@ +module Bundler::PubGrub + class Strategy + def initialize(source) + @source = source + + @root_package = Package.root + @root_version = Package.root_version + + @version_indexes = Hash.new do |h,k| + if k == @root_package + h[k] = { @root_version => 0 } + else + h[k] = @source.all_versions_for(k).each.with_index.to_h + end + end + end + + def next_package_and_version(unsatisfied) + package, range = next_term_to_try_from(unsatisfied) + + [package, most_preferred_version_of(package, range)] + end + + private + + def most_preferred_version_of(package, range) + versions = @source.versions_for(package, range) + + indexes = @version_indexes[package] + versions.min_by { |version| indexes[version] } + end + + def next_term_to_try_from(unsatisfied) + unsatisfied.min_by do |package, range| + matching_versions = @source.versions_for(package, range) + higher_versions = @source.versions_for(package, range.upper_invert) + + [matching_versions.count <= 1 ? 0 : 1, higher_versions.count] + end + end + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/term.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/term.rb new file mode 100644 index 0000000000..1d0f763378 --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/term.rb @@ -0,0 +1,105 @@ +module Bundler::PubGrub + class Term + attr_reader :package, :constraint, :positive + + def initialize(constraint, positive) + @constraint = constraint + @package = @constraint.package + @positive = positive + end + + def to_s(allow_every: false) + if positive + @constraint.to_s(allow_every: allow_every) + else + "not #{@constraint}" + end + end + + def hash + constraint.hash ^ positive.hash + end + + def eql?(other) + positive == other.positive && + constraint.eql?(other.constraint) + end + + def invert + self.class.new(@constraint, !@positive) + end + alias_method :inverse, :invert + + def intersect(other) + raise ArgumentError, "packages must match" if package != other.package + + if positive? && other.positive? + self.class.new(constraint.intersect(other.constraint), true) + elsif negative? && other.negative? + self.class.new(constraint.union(other.constraint), false) + else + positive = positive? ? self : other + negative = negative? ? self : other + self.class.new(positive.constraint.intersect(negative.constraint.invert), true) + end + end + + def difference(other) + intersect(other.invert) + end + + def relation(other) + if positive? && other.positive? + constraint.relation(other.constraint) + elsif negative? && other.positive? + if constraint.allows_all?(other.constraint) + :disjoint + else + :overlap + end + elsif positive? && other.negative? + if !other.constraint.allows_any?(constraint) + :subset + elsif other.constraint.allows_all?(constraint) + :disjoint + else + :overlap + end + elsif negative? && other.negative? + if constraint.allows_all?(other.constraint) + :subset + else + :overlap + end + else + raise + end + end + + def normalized_constraint + @normalized_constraint ||= positive ? constraint : constraint.invert + end + + def satisfies?(other) + raise ArgumentError, "packages must match" unless package == other.package + + relation(other) == :subset + end + + def positive? + @positive + end + + def negative? + !positive? + end + + def empty? + @empty ||= normalized_constraint.empty? + end + + def inspect + "#<#{self.class} #{self}>" + end + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/version.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/version.rb new file mode 100644 index 0000000000..d7984b3863 --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/version.rb @@ -0,0 +1,3 @@ +module Bundler::PubGrub + VERSION = "0.5.0" +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/version_constraint.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_constraint.rb new file mode 100644 index 0000000000..b71f3eaf53 --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_constraint.rb @@ -0,0 +1,129 @@ +require_relative 'version_range' + +module Bundler::PubGrub + class VersionConstraint + attr_reader :package, :range + + # @param package [Bundler::PubGrub::Package] + # @param range [Bundler::PubGrub::VersionRange] + def initialize(package, range: nil) + @package = package + @range = range + end + + def hash + package.hash ^ range.hash + end + + def ==(other) + package == other.package && + range == other.range + end + + def eql?(other) + package.eql?(other.package) && + range.eql?(other.range) + end + + class << self + def exact(package, version) + range = VersionRange.new(min: version, max: version, include_min: true, include_max: true) + new(package, range: range) + end + + def any(package) + new(package, range: VersionRange.any) + end + + def empty(package) + new(package, range: VersionRange.empty) + end + end + + def intersect(other) + unless package == other.package + raise ArgumentError, "Can only intersect between VersionConstraint of the same package" + end + + self.class.new(package, range: range.intersect(other.range)) + end + + def union(other) + unless package == other.package + raise ArgumentError, "Can only intersect between VersionConstraint of the same package" + end + + self.class.new(package, range: range.union(other.range)) + end + + def invert + new_range = range.invert + self.class.new(package, range: new_range) + end + + def difference(other) + intersect(other.invert) + end + + def allows_all?(other) + range.allows_all?(other.range) + end + + def allows_any?(other) + range.intersects?(other.range) + end + + def subset?(other) + other.allows_all?(self) + end + + def overlap?(other) + other.allows_any?(self) + end + + def disjoint?(other) + !overlap?(other) + end + + def relation(other) + if subset?(other) + :subset + elsif overlap?(other) + :overlap + else + :disjoint + end + end + + def to_s(allow_every: false) + if Package.root?(package) + package.to_s + elsif allow_every && any? + "every version of #{package}" + else + "#{package} #{constraint_string}" + end + end + + def constraint_string + if any? + ">= 0" + else + range.to_s + end + end + + def empty? + range.empty? + end + + # Does this match every version of the package + def any? + range.any? + end + + def inspect + "#<#{self.class} #{self}>" + end + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/version_range.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_range.rb new file mode 100644 index 0000000000..49dcf716a3 --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_range.rb @@ -0,0 +1,423 @@ +# frozen_string_literal: true + +module Bundler::PubGrub + class VersionRange + attr_reader :min, :max, :include_min, :include_max + + alias_method :include_min?, :include_min + alias_method :include_max?, :include_max + + class Empty < VersionRange + undef_method :min, :max + undef_method :include_min, :include_min? + undef_method :include_max, :include_max? + + def initialize + end + + def empty? + true + end + + def eql?(other) + other.empty? + end + + def hash + [].hash + end + + def intersects?(_) + false + end + + def intersect(other) + self + end + + def allows_all?(other) + other.empty? + end + + def include?(_) + false + end + + def any? + false + end + + def to_s + "(no versions)" + end + + def ==(other) + other.class == self.class + end + + def invert + VersionRange.any + end + + def select_versions(_) + [] + end + end + + EMPTY = Empty.new + Empty.singleton_class.undef_method(:new) + + def self.empty + EMPTY + end + + def self.any + new + end + + def initialize(min: nil, max: nil, include_min: false, include_max: false, name: nil) + raise ArgumentError, "Ranges without a lower bound cannot have include_min == true" if !min && include_min == true + raise ArgumentError, "Ranges without an upper bound cannot have include_max == true" if !max && include_max == true + + @min = min + @max = max + @include_min = include_min + @include_max = include_max + @name = name + end + + def hash + @hash ||= min.hash ^ max.hash ^ include_min.hash ^ include_max.hash + end + + def eql?(other) + if other.is_a?(VersionRange) + !other.empty? && + min.eql?(other.min) && + max.eql?(other.max) && + include_min.eql?(other.include_min) && + include_max.eql?(other.include_max) + else + ranges.eql?(other.ranges) + end + end + + def ranges + [self] + end + + def include?(version) + compare_version(version) == 0 + end + + # Partitions passed versions into [lower, within, higher] + # + # versions must be sorted + def partition_versions(versions) + min_index = + if !min || versions.empty? + 0 + elsif include_min? + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= min } + else + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > min } + end + + lower = versions.slice(0, min_index) + versions = versions.slice(min_index, versions.size) + + max_index = + if !max || versions.empty? + versions.size + elsif include_max? + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > max } + else + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= max } + end + + [ + lower, + versions.slice(0, max_index), + versions.slice(max_index, versions.size) + ] + end + + # Returns versions which are included by this range. + # + # versions must be sorted + def select_versions(versions) + return versions if any? + + partition_versions(versions)[1] + end + + def compare_version(version) + if min + case version <=> min + when -1 + return -1 + when 0 + return -1 if !include_min + when 1 + end + end + + if max + case version <=> max + when -1 + when 0 + return 1 if !include_max + when 1 + return 1 + end + end + + 0 + end + + def strictly_lower?(other) + return false if !max || !other.min + + case max <=> other.min + when 0 + !include_max || !other.include_min + when -1 + true + when 1 + false + end + end + + def strictly_higher?(other) + other.strictly_lower?(self) + end + + def intersects?(other) + return false if other.empty? + return other.intersects?(self) if other.is_a?(VersionUnion) + !strictly_lower?(other) && !strictly_higher?(other) + end + alias_method :allows_any?, :intersects? + + def intersect(other) + return other if other.empty? + return other.intersect(self) if other.is_a?(VersionUnion) + + min_range = + if !min + other + elsif !other.min + self + else + case min <=> other.min + when 0 + include_min ? other : self + when -1 + other + when 1 + self + end + end + + max_range = + if !max + other + elsif !other.max + self + else + case max <=> other.max + when 0 + include_max ? other : self + when -1 + self + when 1 + other + end + end + + if !min_range.equal?(max_range) && min_range.min && max_range.max + case min_range.min <=> max_range.max + when -1 + when 0 + if !min_range.include_min || !max_range.include_max + return EMPTY + end + when 1 + return EMPTY + end + end + + VersionRange.new( + min: min_range.min, + include_min: min_range.include_min, + max: max_range.max, + include_max: max_range.include_max + ) + end + + # The span covered by two ranges + # + # If self and other are contiguous, this builds a union of the two ranges. + # (if they aren't you are probably calling the wrong method) + def span(other) + return self if other.empty? + + min_range = + if !min + self + elsif !other.min + other + else + case min <=> other.min + when 0 + include_min ? self : other + when -1 + self + when 1 + other + end + end + + max_range = + if !max + self + elsif !other.max + other + else + case max <=> other.max + when 0 + include_max ? self : other + when -1 + other + when 1 + self + end + end + + VersionRange.new( + min: min_range.min, + include_min: min_range.include_min, + max: max_range.max, + include_max: max_range.include_max + ) + end + + def union(other) + return other.union(self) if other.is_a?(VersionUnion) + + if contiguous_to?(other) + span(other) + else + VersionUnion.union([self, other]) + end + end + + def contiguous_to?(other) + return false if other.empty? + return true if any? + + intersects?(other) || contiguous_below?(other) || contiguous_above?(other) + end + + def contiguous_below?(other) + return false if !max || !other.min + + max == other.min && (include_max || other.include_min) + end + + def contiguous_above?(other) + other.contiguous_below?(self) + end + + def allows_all?(other) + return true if other.empty? + + if other.is_a?(VersionUnion) + return VersionUnion.new([self]).allows_all?(other) + end + + return false if max && !other.max + return false if min && !other.min + + if min + case min <=> other.min + when -1 + when 0 + return false if !include_min && other.include_min + when 1 + return false + end + end + + if max + case max <=> other.max + when -1 + return false + when 0 + return false if !include_max && other.include_max + when 1 + end + end + + true + end + + def any? + !min && !max + end + + def empty? + false + end + + def to_s + @name ||= constraints.join(", ") + end + + def inspect + "#<#{self.class} #{to_s}>" + end + + def upper_invert + return self.class.empty unless max + + VersionRange.new(min: max, include_min: !include_max) + end + + def invert + return self.class.empty if any? + + low = -> { VersionRange.new(max: min, include_max: !include_min) } + high = -> { VersionRange.new(min: max, include_min: !include_max) } + + if !min + high.call + elsif !max + low.call + else + low.call.union(high.call) + end + end + + def ==(other) + self.class == other.class && + min == other.min && + max == other.max && + include_min == other.include_min && + include_max == other.include_max + end + + private + + def constraints + return ["any"] if any? + return ["= #{min}"] if min.to_s == max.to_s + + c = [] + c << "#{include_min ? ">=" : ">"} #{min}" if min + c << "#{include_max ? "<=" : "<"} #{max}" if max + c + end + + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb new file mode 100644 index 0000000000..000923e99a --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb @@ -0,0 +1,236 @@ +require_relative 'partial_solution' +require_relative 'term' +require_relative 'incompatibility' +require_relative 'solve_failure' +require_relative 'strategy' + +module Bundler::PubGrub + class VersionSolver + attr_reader :logger + attr_reader :source + attr_reader :solution + attr_reader :strategy + + def initialize(source:, root: Package.root, strategy: Strategy.new(source), logger: Bundler::PubGrub.logger) + @logger = logger + + @source = source + @strategy = strategy + + # { package => [incompatibility, ...]} + @incompatibilities = Hash.new do |h, k| + h[k] = [] + end + + @seen_incompatibilities = {} + + @solution = PartialSolution.new + + add_incompatibility Incompatibility.new([ + Term.new(VersionConstraint.any(root), false) + ], cause: :root) + + propagate(root) + end + + def solved? + solution.unsatisfied.empty? + end + + # Returns true if there is more work to be done, false otherwise + def work + unsatisfied_terms = solution.unsatisfied + if unsatisfied_terms.empty? + logger.info { "Solution found after #{solution.attempted_solutions} attempts:" } + solution.decisions.each do |package, version| + next if Package.root?(package) + logger.info { "* #{package} #{version}" } + end + + return false + end + + next_package = choose_package_version_from(unsatisfied_terms) + propagate(next_package) + + true + end + + def solve + while work; end + + solution.decisions + end + + alias_method :result, :solve + + private + + def propagate(initial_package) + changed = [initial_package] + while package = changed.shift + @incompatibilities[package].reverse_each do |incompatibility| + result = propagate_incompatibility(incompatibility) + if result == :conflict + root_cause = resolve_conflict(incompatibility) + changed.clear + changed << propagate_incompatibility(root_cause) + elsif result # should be a Package + changed << result + end + end + changed.uniq! + end + end + + def propagate_incompatibility(incompatibility) + unsatisfied = nil + incompatibility.terms.each do |term| + relation = solution.relation(term) + if relation == :disjoint + return nil + elsif relation == :overlap + # If more than one term is inconclusive, we can't deduce anything + return nil if unsatisfied + unsatisfied = term + end + end + + if !unsatisfied + return :conflict + end + + logger.debug { "derived: #{unsatisfied.invert}" } + + solution.derive(unsatisfied.invert, incompatibility) + + unsatisfied.package + end + + def choose_package_version_from(unsatisfied_terms) + remaining = unsatisfied_terms.map { |t| [t.package, t.constraint.range] }.to_h + + package, version = strategy.next_package_and_version(remaining) + + logger.debug { "attempting #{package} #{version}" } + + if version.nil? + unsatisfied_term = unsatisfied_terms.find { |t| t.package == package } + add_incompatibility source.no_versions_incompatibility_for(package, unsatisfied_term) + return package + end + + conflict = false + + source.incompatibilities_for(package, version).each do |incompatibility| + if @seen_incompatibilities.include?(incompatibility) + logger.debug { "knew: #{incompatibility}" } + next + end + @seen_incompatibilities[incompatibility] = true + + add_incompatibility incompatibility + + conflict ||= incompatibility.terms.all? do |term| + term.package == package || solution.satisfies?(term) + end + end + + unless conflict + logger.info { "selected #{package} #{version}" } + + solution.decide(package, version) + else + logger.info { "conflict: #{conflict.inspect}" } + end + + package + end + + def resolve_conflict(incompatibility) + logger.info { "conflict: #{incompatibility}" } + + new_incompatibility = nil + + while !incompatibility.failure? + most_recent_term = nil + most_recent_satisfier = nil + difference = nil + + previous_level = 1 + + incompatibility.terms.each do |term| + satisfier = solution.satisfier(term) + + if most_recent_satisfier.nil? + most_recent_term = term + most_recent_satisfier = satisfier + elsif most_recent_satisfier.index < satisfier.index + previous_level = [previous_level, most_recent_satisfier.decision_level].max + most_recent_term = term + most_recent_satisfier = satisfier + difference = nil + else + previous_level = [previous_level, satisfier.decision_level].max + end + + if most_recent_term == term + difference = most_recent_satisfier.term.difference(most_recent_term) + if difference.empty? + difference = nil + else + difference_satisfier = solution.satisfier(difference.inverse) + previous_level = [previous_level, difference_satisfier.decision_level].max + end + end + end + + if previous_level < most_recent_satisfier.decision_level || + most_recent_satisfier.decision? + + logger.info { "backtracking to #{previous_level}" } + solution.backtrack(previous_level) + + if new_incompatibility + add_incompatibility(new_incompatibility) + end + + return incompatibility + end + + new_terms = [] + new_terms += incompatibility.terms - [most_recent_term] + new_terms += most_recent_satisfier.cause.terms.reject { |term| + term.package == most_recent_satisfier.term.package + } + if difference + new_terms << difference.invert + end + + new_incompatibility = Incompatibility.new(new_terms, cause: Incompatibility::ConflictCause.new(incompatibility, most_recent_satisfier.cause)) + + if incompatibility.to_s == new_incompatibility.to_s + logger.info { "!! failed to resolve conflicts, this shouldn't have happened" } + break + end + + incompatibility = new_incompatibility + + partially = difference ? " partially" : "" + logger.info { "! #{most_recent_term} is#{partially} satisfied by #{most_recent_satisfier.term}" } + logger.info { "! which is caused by #{most_recent_satisfier.cause}" } + logger.info { "! thus #{incompatibility}" } + end + + raise SolveFailure.new(incompatibility) + end + + def add_incompatibility(incompatibility) + logger.debug { "fact: #{incompatibility}" } + incompatibility.terms.each do |term| + package = term.package + @incompatibilities[package] << incompatibility + end + end + end +end diff --git a/lib/bundler/vendor/pub_grub/lib/pub_grub/version_union.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_union.rb new file mode 100644 index 0000000000..bbc10c3804 --- /dev/null +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_union.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +module Bundler::PubGrub + class VersionUnion + attr_reader :ranges + + def self.normalize_ranges(ranges) + ranges = ranges.flat_map do |range| + range.ranges + end + + ranges.reject!(&:empty?) + + return [] if ranges.empty? + + mins, ranges = ranges.partition { |r| !r.min } + original_ranges = mins + ranges.sort_by { |r| [r.min, r.include_min ? 0 : 1] } + ranges = [original_ranges.shift] + original_ranges.each do |range| + if ranges.last.contiguous_to?(range) + ranges << ranges.pop.span(range) + else + ranges << range + end + end + + ranges + end + + def self.union(ranges, normalize: true) + ranges = normalize_ranges(ranges) if normalize + + if ranges.size == 0 + VersionRange.empty + elsif ranges.size == 1 + ranges[0] + else + new(ranges) + end + end + + def initialize(ranges) + raise ArgumentError unless ranges.all? { |r| r.instance_of?(VersionRange) } + @ranges = ranges + end + + def hash + ranges.hash + end + + def eql?(other) + ranges.eql?(other.ranges) + end + + def include?(version) + !!ranges.bsearch {|r| r.compare_version(version) } + end + + def select_versions(all_versions) + versions = [] + ranges.inject(all_versions) do |acc, range| + _, matching, higher = range.partition_versions(acc) + versions.concat matching + higher + end + versions + end + + def intersects?(other) + my_ranges = ranges.dup + other_ranges = other.ranges.dup + + my_range = my_ranges.shift + other_range = other_ranges.shift + while my_range && other_range + if my_range.intersects?(other_range) + return true + end + + if !my_range.max || other_range.empty? || (other_range.max && other_range.max < my_range.max) + other_range = other_ranges.shift + else + my_range = my_ranges.shift + end + end + end + alias_method :allows_any?, :intersects? + + def allows_all?(other) + my_ranges = ranges.dup + + my_range = my_ranges.shift + + other.ranges.all? do |other_range| + while my_range + break if my_range.allows_all?(other_range) + my_range = my_ranges.shift + end + + !!my_range + end + end + + def empty? + false + end + + def any? + false + end + + def intersect(other) + my_ranges = ranges.dup + other_ranges = other.ranges.dup + new_ranges = [] + + my_range = my_ranges.shift + other_range = other_ranges.shift + while my_range && other_range + new_ranges << my_range.intersect(other_range) + + if !my_range.max || other_range.empty? || (other_range.max && other_range.max < my_range.max) + other_range = other_ranges.shift + else + my_range = my_ranges.shift + end + end + new_ranges.reject!(&:empty?) + VersionUnion.union(new_ranges, normalize: false) + end + + def upper_invert + ranges.last.upper_invert + end + + def invert + ranges.map(&:invert).inject(:intersect) + end + + def union(other) + VersionUnion.union([self, other]) + end + + def to_s + output = [] + + ranges = self.ranges.dup + while !ranges.empty? + ne = [] + range = ranges.shift + while !ranges.empty? && ranges[0].min.to_s == range.max.to_s + ne << range.max + range = range.span(ranges.shift) + end + + ne.map! {|x| "!= #{x}" } + if ne.empty? + output << range.to_s + elsif range.any? + output << ne.join(', ') + else + output << "#{range}, #{ne.join(', ')}" + end + end + + output.join(" OR ") + end + + def inspect + "#<#{self.class} #{to_s}>" + end + + def ==(other) + self.class == other.class && + self.ranges == other.ranges + end + end +end diff --git a/lib/bundler/vendor/securerandom/lib/securerandom.rb b/lib/bundler/vendor/securerandom/lib/securerandom.rb new file mode 100644 index 0000000000..01b7fa15a6 --- /dev/null +++ b/lib/bundler/vendor/securerandom/lib/securerandom.rb @@ -0,0 +1,102 @@ +# -*- coding: us-ascii -*- +# frozen_string_literal: true + +require 'random/formatter' + +# == Secure random number generator interface. +# +# This library is an interface to secure random number generators which are +# suitable for generating session keys in HTTP cookies, etc. +# +# You can use this library in your application by requiring it: +# +# require 'bundler/vendor/securerandom/lib/securerandom' +# +# It supports the following secure random number generators: +# +# * openssl +# * /dev/urandom +# * Win32 +# +# Bundler::SecureRandom is extended by the Random::Formatter module which +# defines the following methods: +# +# * alphanumeric +# * base64 +# * choose +# * gen_random +# * hex +# * rand +# * random_bytes +# * random_number +# * urlsafe_base64 +# * uuid +# +# These methods are usable as class methods of Bundler::SecureRandom such as +# +Bundler::SecureRandom.hex+. +# +# If a secure random number generator is not available, +# +NotImplementedError+ is raised. + +module Bundler::SecureRandom + + # The version + VERSION = "0.4.1" + + class << self + # Returns a random binary string containing +size+ bytes. + # + # See Random.bytes + def bytes(n) + return gen_random(n) + end + + # Compatibility methods for Ruby 3.2, we can remove this after dropping to support Ruby 3.2 + def alphanumeric(n = nil, chars: ALPHANUMERIC) + n = 16 if n.nil? + choose(chars, n) + end if RUBY_VERSION < '3.3' + + private + + # :stopdoc: + + # Implementation using OpenSSL + def gen_random_openssl(n) + return OpenSSL::Random.random_bytes(n) + end + + # Implementation using system random device + def gen_random_urandom(n) + ret = Random.urandom(n) + unless ret + raise NotImplementedError, "No random device" + end + unless ret.length == n + raise NotImplementedError, "Unexpected partial read from random device: only #{ret.length} for #{n} bytes" + end + ret + end + + begin + # Check if Random.urandom is available + Random.urandom(1) + alias gen_random gen_random_urandom + rescue RuntimeError + begin + require 'openssl' + rescue NoMethodError + raise NotImplementedError, "No random device" + else + alias gen_random gen_random_openssl + end + end + + # :startdoc: + + # Generate random data bytes for Random::Formatter + public :gen_random + end +end + +Bundler::SecureRandom.extend(Random::Formatter) diff --git a/lib/bundler/vendor/thor/lib/thor.rb b/lib/bundler/vendor/thor/lib/thor.rb index 999e8b7e61..945bdbd551 100644 --- a/lib/bundler/vendor/thor/lib/thor.rb +++ b/lib/bundler/vendor/thor/lib/thor.rb @@ -1,7 +1,7 @@ -require "set" -require "bundler/vendor/thor/lib/thor/base" +require_relative "thor/base" class Bundler::Thor + $thor_runner ||= false class << self # Allows for custom "Command" package naming. # @@ -65,8 +65,15 @@ class Bundler::Thor # Defines the long description of the next command. # + # Long description is by default indented, line-wrapped and repeated whitespace merged. + # In order to print long description verbatim, with indentation and spacing exactly + # as found in the code, use the +wrap+ option + # + # long_desc 'your very long description', wrap: false + # # ==== Parameters # long description<String> + # options<Hash> # def long_desc(long_description, options = {}) if options[:for] @@ -74,6 +81,7 @@ class Bundler::Thor command.long_description = long_description if long_description else @long_desc = long_description + @long_desc_wrap = options[:wrap] != false end end @@ -90,9 +98,14 @@ class Bundler::Thor # ==== Parameters # Hash[String|Array => Symbol]:: Maps the string or the strings in the array to the given command. # - def map(mappings = nil) + def map(mappings = nil, **kw) @map ||= from_superclass(:map, {}) + if mappings && !kw.empty? + mappings = kw.merge!(mappings) + else + mappings ||= kw + end if mappings mappings.each do |key, value| if key.respond_to?(:each) @@ -128,7 +141,7 @@ class Bundler::Thor # # magic # end # - # method_option :foo => :bar, :for => :previous_command + # method_option :foo, :for => :previous_command # # def next_command # # magic @@ -148,6 +161,9 @@ class Bundler::Thor # :hide - If you want to hide this option from the help. # def method_option(name, options = {}) + unless [ Symbol, String ].any? { |klass| name.is_a?(klass) } + raise ArgumentError, "Expected a Symbol or String, got #{name.inspect}" + end scope = if options[:for] find_and_refresh_command(options[:for]).options else @@ -158,6 +174,81 @@ class Bundler::Thor end alias_method :option, :method_option + # Adds and declares option group for exclusive options in the + # block and arguments. You can declare options as the outside of the block. + # + # If :for is given as option, it allows you to change the options from + # a previous defined command. + # + # ==== Parameters + # Array[Bundler::Thor::Option.name] + # options<Hash>:: :for is applied for previous defined command. + # + # ==== Examples + # + # exclusive do + # option :one + # option :two + # end + # + # Or + # + # option :one + # option :two + # exclusive :one, :two + # + # If you give "--one" and "--two" at the same time ExclusiveArgumentsError + # will be raised. + # + def method_exclusive(*args, &block) + register_options_relation_for(:method_options, + :method_exclusive_option_names, *args, &block) + end + alias_method :exclusive, :method_exclusive + + # Adds and declares option group for required at least one of options in the + # block of arguments. You can declare options as the outside of the block. + # + # If :for is given as option, it allows you to change the options from + # a previous defined command. + # + # ==== Parameters + # Array[Bundler::Thor::Option.name] + # options<Hash>:: :for is applied for previous defined command. + # + # ==== Examples + # + # at_least_one do + # option :one + # option :two + # end + # + # Or + # + # option :one + # option :two + # at_least_one :one, :two + # + # If you do not give "--one" and "--two" AtLeastOneRequiredArgumentError + # will be raised. + # + # You can use at_least_one and exclusive at the same time. + # + # exclusive do + # at_least_one do + # option :one + # option :two + # end + # end + # + # Then it is required either only one of "--one" or "--two". + # + def method_at_least_one(*args, &block) + register_options_relation_for(:method_options, + :method_at_least_one_option_names, *args, &block) + end + alias_method :at_least_one, :method_at_least_one + # Prints help information for the given command. # # ==== Parameters @@ -170,12 +261,19 @@ class Bundler::Thor handle_no_command_error(meth) unless command shell.say "Usage:" - shell.say " #{banner(command)}" + shell.say " #{banner(command).split("\n").join("\n ")}" shell.say class_options_help(shell, nil => command.options.values) + print_exclusive_options(shell, command) + print_at_least_one_required_options(shell, command) + if command.long_description shell.say "Description:" - shell.print_wrapped(command.long_description, :indent => 2) + if command.wrap_long_description + shell.print_wrapped(command.long_description, indent: 2) + else + shell.say command.long_description + end else shell.say command.description end @@ -192,7 +290,7 @@ class Bundler::Thor Bundler::Thor::Util.thor_classes_in(self).each do |klass| list += klass.printable_commands(false) end - list.sort! { |a, b| a[0] <=> b[0] } + sort_commands!(list) if defined?(@package_name) && @package_name shell.say "#{@package_name} commands:" @@ -200,9 +298,11 @@ class Bundler::Thor shell.say "Commands:" end - shell.print_table(list, :indent => 2, :truncate => true) + shell.print_table(list, indent: 2, truncate: true) shell.say class_options_help(shell) + print_exclusive_options(shell) + print_at_least_one_required_options(shell) end # Returns commands ready to be printed. @@ -233,7 +333,7 @@ class Bundler::Thor define_method(subcommand) do |*args| args, opts = Bundler::Thor::Arguments.split(args) - invoke_args = [args, opts, {:invoked_via_subcommand => true, :class_options => options}] + invoke_args = [args, opts, {invoked_via_subcommand: true, class_options: options}] invoke_args.unshift "help" if opts.delete("--help") || opts.delete("-h") invoke subcommand_class, *invoke_args end @@ -318,7 +418,7 @@ class Bundler::Thor # ==== Parameters # Symbol ...:: A list of commands that should be affected. def stop_on_unknown_option!(*command_names) - stop_on_unknown_option.merge(command_names) + @stop_on_unknown_option = stop_on_unknown_option | command_names end def stop_on_unknown_option?(command) #:nodoc: @@ -332,26 +432,77 @@ class Bundler::Thor # ==== Parameters # Symbol ...:: A list of commands that should be affected. def disable_required_check!(*command_names) - disable_required_check.merge(command_names) + @disable_required_check = disable_required_check | command_names end def disable_required_check?(command) #:nodoc: command && disable_required_check.include?(command.name.to_sym) end + # Checks if a specified command exists. + # + # ==== Parameters + # command_name<String>:: The name of the command to check for existence. + # + # ==== Returns + # Boolean:: +true+ if the command exists, +false+ otherwise. + def command_exists?(command_name) #:nodoc: + commands.keys.include?(normalize_command_name(command_name)) + end + protected + # Returns this class exclusive options array set. + # + # ==== Returns + # Array[Array[Bundler::Thor::Option.name]] + # + def method_exclusive_option_names #:nodoc: + @method_exclusive_option_names ||= [] + end + + # Returns this class at least one of required options array set. + # + # ==== Returns + # Array[Array[Bundler::Thor::Option.name]] + # + def method_at_least_one_option_names #:nodoc: + @method_at_least_one_option_names ||= [] + end + def stop_on_unknown_option #:nodoc: - @stop_on_unknown_option ||= Set.new + @stop_on_unknown_option ||= [] end # help command has the required check disabled by default. def disable_required_check #:nodoc: - @disable_required_check ||= Set.new([:help]) + @disable_required_check ||= [:help] + end + + def print_exclusive_options(shell, command = nil) # :nodoc: + opts = [] + opts = command.method_exclusive_option_names unless command.nil? + opts += class_exclusive_option_names + unless opts.empty? + shell.say "Exclusive Options:" + shell.print_table(opts.map{ |ex| ex.map{ |e| "--#{e}"}}, indent: 2 ) + shell.say + end + end + + def print_at_least_one_required_options(shell, command = nil) # :nodoc: + opts = [] + opts = command.method_at_least_one_option_names unless command.nil? + opts += class_at_least_one_option_names + unless opts.empty? + shell.say "Required At Least One:" + shell.print_table(opts.map{ |ex| ex.map{ |e| "--#{e}"}}, indent: 2 ) + shell.say + end end # The method responsible for dispatching given the args. - def dispatch(meth, given_args, given_opts, config) #:nodoc: # rubocop:disable MethodLength + def dispatch(meth, given_args, given_opts, config) #:nodoc: meth ||= retrieve_command_name(given_args) command = all_commands[normalize_command_name(meth)] @@ -393,7 +544,9 @@ class Bundler::Thor # the namespace should be displayed as arguments. # def banner(command, namespace = nil, subcommand = false) - "#{basename} #{command.formatted_usage(self, $thor_runner, subcommand)}" + command.formatted_usage(self, $thor_runner, subcommand).split("\n").map do |formatted_usage| + "#{basename} #{formatted_usage}" + end.join("\n") end def baseclass #:nodoc: @@ -408,12 +561,16 @@ class Bundler::Thor @usage ||= nil @desc ||= nil @long_desc ||= nil + @long_desc_wrap ||= nil @hide ||= nil if @usage && @desc base_class = @hide ? Bundler::Thor::HiddenCommand : Bundler::Thor::Command - commands[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options) - @usage, @desc, @long_desc, @method_options, @hide = nil + relations = {exclusive_option_names: method_exclusive_option_names, + at_least_one_option_names: method_at_least_one_option_names} + commands[meth] = base_class.new(meth, @desc, @long_desc, @long_desc_wrap, @usage, method_options, relations) + @usage, @desc, @long_desc, @long_desc_wrap, @method_options, @hide = nil + @method_exclusive_option_names, @method_at_least_one_option_names = nil true elsif all_commands[meth] || meth == "method_missing" true @@ -468,7 +625,7 @@ class Bundler::Thor # 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 + possibilities = all_commands.reject { |_k, c| c.hidden? }.merge(map).keys.select { |n| meth == n[0, len] }.sort unique_possibilities = possibilities.map { |k| map[k] || k }.uniq if possibilities.include?(meth) @@ -488,6 +645,14 @@ class Bundler::Thor " end alias_method :subtask_help, :subcommand_help + + # Sort the commands, lexicographically by default. + # + # Can be overridden in the subclass to change the display order of the + # commands. + def sort_commands!(list) + list.sort! { |a, b| a[0] <=> b[0] } + end end include Bundler::Thor::Base diff --git a/lib/bundler/vendor/thor/lib/thor/actions.rb b/lib/bundler/vendor/thor/lib/thor/actions.rb index e6698572a9..ca58182691 100644 --- a/lib/bundler/vendor/thor/lib/thor/actions.rb +++ b/lib/bundler/vendor/thor/lib/thor/actions.rb @@ -1,17 +1,16 @@ -require "uri" -require "bundler/vendor/thor/lib/thor/core_ext/io_binary_read" -require "bundler/vendor/thor/lib/thor/actions/create_file" -require "bundler/vendor/thor/lib/thor/actions/create_link" -require "bundler/vendor/thor/lib/thor/actions/directory" -require "bundler/vendor/thor/lib/thor/actions/empty_directory" -require "bundler/vendor/thor/lib/thor/actions/file_manipulation" -require "bundler/vendor/thor/lib/thor/actions/inject_into_file" +require_relative "actions/create_file" +require_relative "actions/create_link" +require_relative "actions/directory" +require_relative "actions/empty_directory" +require_relative "actions/file_manipulation" +require_relative "actions/inject_into_file" class Bundler::Thor module Actions attr_accessor :behavior def self.included(base) #:nodoc: + super(base) base.extend ClassMethods end @@ -47,17 +46,17 @@ class Bundler::Thor # Add runtime options that help actions execution. # def add_runtime_options! - class_option :force, :type => :boolean, :aliases => "-f", :group => :runtime, - :desc => "Overwrite files that already exist" + class_option :force, type: :boolean, aliases: "-f", group: :runtime, + desc: "Overwrite files that already exist" - class_option :pretend, :type => :boolean, :aliases => "-p", :group => :runtime, - :desc => "Run but do not make any changes" + class_option :pretend, type: :boolean, aliases: "-p", group: :runtime, + desc: "Run but do not make any changes" - class_option :quiet, :type => :boolean, :aliases => "-q", :group => :runtime, - :desc => "Suppress status output" + class_option :quiet, type: :boolean, aliases: "-q", group: :runtime, + desc: "Suppress status output" - class_option :skip, :type => :boolean, :aliases => "-s", :group => :runtime, - :desc => "Skip files that already exist" + class_option :skip, type: :boolean, aliases: "-s", group: :runtime, + desc: "Skip files that already exist" end end @@ -113,8 +112,10 @@ class Bundler::Thor # the script started). # def relative_to_original_destination_root(path, remove_dot = true) - path = path.dup - if path.gsub!(@destination_stack[0], ".") + root = @destination_stack[0] + if path.start_with?(root) && [File::SEPARATOR, File::ALT_SEPARATOR, nil, ""].include?(path[root.size..root.size]) + path = path.dup + path[0...root.size] = "." remove_dot ? (path[2..-1] || "") : path else path @@ -160,6 +161,8 @@ class Bundler::Thor # to the block you provide. The path is set back to the previous path when # the method exits. # + # Returns the value yielded by the block. + # # ==== Parameters # dir<String>:: the directory to move to. # config<Hash>:: give :verbose => true to log and use padding. @@ -172,22 +175,24 @@ class Bundler::Thor shell.padding += 1 if verbose @destination_stack.push File.expand_path(dir, destination_root) - # If the directory doesnt exist and we're not pretending + # If the directory doesn't exist and we're not pretending if !File.exist?(destination_root) && !pretend require "fileutils" FileUtils.mkdir_p(destination_root) end + result = nil if pretend # In pretend mode, just yield down to the block - block.arity == 1 ? yield(destination_root) : yield + result = block.arity == 1 ? yield(destination_root) : yield else require "fileutils" - FileUtils.cd(destination_root) { block.arity == 1 ? yield(destination_root) : yield } + FileUtils.cd(destination_root) { result = block.arity == 1 ? yield(destination_root) : yield } end @destination_stack.pop shell.padding -= 1 if verbose + result end # Goes to the root and execute the given block. @@ -217,9 +222,10 @@ class Bundler::Thor shell.padding += 1 if verbose contents = if is_uri - open(path, "Accept" => "application/x-thor-template", &:read) + require "open-uri" + URI.open(path, "Accept" => "application/x-thor-template", &:read) else - open(path, &:read) + File.open(path, &:read) end instance_eval(contents, path) @@ -252,9 +258,22 @@ class Bundler::Thor say_status :run, desc, config.fetch(:verbose, true) - unless options[:pretend] - config[:capture] ? `#{command}` : system(command.to_s) + return if options[:pretend] + + env_splat = [config[:env]] if config[:env] + + if config[:capture] + require "open3" + result, status = Open3.capture2e(*env_splat, command.to_s) + success = status.success? + else + result = system(*env_splat, command.to_s) + success = result end + + abort if !success && config.fetch(:abort_on_failure, self.class.exit_on_failure?) + + result end # Executes a ruby script (taking into account WIN32 platform quirks). @@ -265,7 +284,7 @@ class Bundler::Thor # def run_ruby_script(command, config = {}) return unless behavior == :invoke - run command, config.merge(:with => Bundler::Thor::Util.ruby_command) + run command, config.merge(with: Bundler::Thor::Util.ruby_command) end # Run a thor command. A hash of options can be given and it's converted to @@ -296,7 +315,7 @@ class Bundler::Thor args.push Bundler::Thor::Options.to_switches(config) command = args.join(" ").strip - run command, :with => :thor, :verbose => verbose, :pretend => pretend, :capture => capture + run command, with: :thor, verbose: verbose, pretend: pretend, capture: capture end protected @@ -304,7 +323,7 @@ class Bundler::Thor # Allow current root to be shared between invocations. # def _shared_configuration #:nodoc: - super.merge!(:destination_root => destination_root) + super.merge!(destination_root: destination_root) end def _cleanup_options_and_set(options, key) #:nodoc: diff --git a/lib/bundler/vendor/thor/lib/thor/actions/create_file.rb b/lib/bundler/vendor/thor/lib/thor/actions/create_file.rb index 97d22d9bbd..6724835b01 100644 --- a/lib/bundler/vendor/thor/lib/thor/actions/create_file.rb +++ b/lib/bundler/vendor/thor/lib/thor/actions/create_file.rb @@ -1,4 +1,4 @@ -require "bundler/vendor/thor/lib/thor/actions/empty_directory" +require_relative "empty_directory" class Bundler::Thor module Actions @@ -43,7 +43,8 @@ class Bundler::Thor # Boolean:: true if it is identical, false otherwise. # def identical? - exists? && File.binread(destination) == render + # binread uses ASCII-8BIT, so to avoid false negatives, the string must use the same + exists? && File.binread(destination) == String.new(render).force_encoding("ASCII-8BIT") end # Holds the content to be added to the file. @@ -60,7 +61,7 @@ class Bundler::Thor invoke_with_conflict_check do require "fileutils" FileUtils.mkdir_p(File.dirname(destination)) - File.open(destination, "wb") { |f| f.write render } + File.open(destination, "wb", config[:perm]) { |f| f.write render } end given_destination end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/create_link.rb b/lib/bundler/vendor/thor/lib/thor/actions/create_link.rb index 3a664401b4..fb76fcdbe9 100644 --- a/lib/bundler/vendor/thor/lib/thor/actions/create_link.rb +++ b/lib/bundler/vendor/thor/lib/thor/actions/create_link.rb @@ -1,4 +1,4 @@ -require "bundler/vendor/thor/lib/thor/actions/create_file" +require_relative "create_file" class Bundler::Thor module Actions @@ -33,7 +33,8 @@ class Bundler::Thor # Boolean:: true if it is identical, false otherwise. # def identical? - exists? && File.identical?(render, destination) + source = File.expand_path(render, File.dirname(destination)) + exists? && File.identical?(source, destination) end def invoke! diff --git a/lib/bundler/vendor/thor/lib/thor/actions/directory.rb b/lib/bundler/vendor/thor/lib/thor/actions/directory.rb index f555f7b7e0..2f9687c0a5 100644 --- a/lib/bundler/vendor/thor/lib/thor/actions/directory.rb +++ b/lib/bundler/vendor/thor/lib/thor/actions/directory.rb @@ -1,4 +1,4 @@ -require "bundler/vendor/thor/lib/thor/actions/empty_directory" +require_relative "empty_directory" class Bundler::Thor module Actions @@ -56,9 +56,9 @@ class Bundler::Thor attr_reader :source def initialize(base, source, destination = nil, config = {}, &block) - @source = File.expand_path(base.find_in_source_paths(source.to_s)) + @source = File.expand_path(Dir[Util.escape_globs(base.find_in_source_paths(source.to_s))].first) @block = block - super(base, destination, {:recursive => true}.merge(config)) + super(base, destination, {recursive: true}.merge(config)) end def invoke! @@ -96,22 +96,12 @@ class Bundler::Thor 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 file_level_lookup(previous_lookup) + File.join(previous_lookup, "*") + end - def files(lookup) - Dir.glob(lookup, File::FNM_DOTMATCH) - end + def files(lookup) + Dir.glob(lookup, File::FNM_DOTMATCH) end end end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb b/lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb index 284d92c19a..c0bca78525 100644 --- a/lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb +++ b/lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb @@ -33,7 +33,7 @@ class Bundler::Thor # def initialize(base, destination, config = {}) @base = base - @config = {:verbose => true}.merge(config) + @config = {verbose: true}.merge(config) self.destination = destination end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb b/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb index 4c83bebc86..d8c9863054 100644 --- a/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb +++ b/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb @@ -10,7 +10,6 @@ class Bundler::Thor # destination<String>:: the relative path to the destination root. # config<Hash>:: give :verbose => false to not log the status, and # :mode => :preserve, to preserve the file mode from the source. - # # ==== Examples # @@ -23,14 +22,14 @@ class Bundler::Thor destination = args.first || source source = File.expand_path(find_in_source_paths(source.to_s)) - create_file destination, nil, config do + resulting_destination = create_file destination, nil, config do content = File.binread(source) content = yield(content) if block content end if config[:mode] == :preserve mode = File.stat(source).mode - chmod(destination, mode, config) + chmod(resulting_destination, mode, config) end end @@ -60,15 +59,21 @@ class Bundler::Thor # destination. If a block is given instead of destination, the content of # the url is yielded and used as location. # + # +get+ relies on open-uri, so passing application user input would provide + # a command injection attack vector. + # # ==== Parameters # source<String>:: the address of the given content. # destination<String>:: the relative path to the destination root. - # config<Hash>:: give :verbose => false to not log the status. + # config<Hash>:: give :verbose => false to not log the status, and + # :http_headers => <Hash> to add headers to an http request. # # ==== Examples # # get "http://gist.github.com/103208", "doc/README" # + # get "http://gist.github.com/103208", "doc/README", :http_headers => {"Content-Type" => "application/json"} + # # get "http://gist.github.com/103208" do |content| # content.split("\n").first # end @@ -77,14 +82,14 @@ class Bundler::Thor config = args.last.is_a?(Hash) ? args.pop : {} destination = args.first - if source =~ %r{^https?\://} + render = if source =~ %r{^https?\://} require "open-uri" + URI.send(:open, source, config.fetch(:http_headers, {})) { |input| input.binmode.read } else source = File.expand_path(find_in_source_paths(source.to_s)) + File.open(source) { |input| input.binmode.read } end - render = open(source) { |input| input.binmode.read } - destination ||= if block_given? block.arity == 1 ? yield(render) : yield else @@ -117,7 +122,8 @@ class Bundler::Thor context = config.delete(:context) || instance_eval("binding") create_file destination, nil, config do - content = CapturableERB.new(::File.binread(source), nil, "-", "@output_buffer").tap do |erb| + capturable_erb = CapturableERB.new(::File.binread(source), trim_mode: "-", eoutvar: "@output_buffer") + content = capturable_erb.tap do |erb| erb.filename = source end.result(context) content = yield(content) if block @@ -201,9 +207,9 @@ class Bundler::Thor # # ==== Examples # - # inject_into_class "app/controllers/application_controller.rb", ApplicationController, " filter_parameter :password\n" + # inject_into_class "app/controllers/application_controller.rb", "ApplicationController", " filter_parameter :password\n" # - # inject_into_class "app/controllers/application_controller.rb", ApplicationController do + # inject_into_class "app/controllers/application_controller.rb", "ApplicationController" do # " filter_parameter :password\n" # end # @@ -224,9 +230,9 @@ class Bundler::Thor # # ==== Examples # - # inject_into_module "app/helpers/application_helper.rb", ApplicationHelper, " def help; 'help'; end\n" + # inject_into_module "app/helpers/application_helper.rb", "ApplicationHelper", " def help; 'help'; end\n" # - # inject_into_module "app/helpers/application_helper.rb", ApplicationHelper do + # inject_into_module "app/helpers/application_helper.rb", "ApplicationHelper" do # " def help; 'help'; end\n" # end # @@ -236,13 +242,43 @@ class Bundler::Thor insert_into_file(path, *(args << config), &block) end + # Run a regular expression replacement on a file, raising an error if the + # contents of the file are not changed. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # flag<Regexp|String>:: the regexp or string to be replaced + # replacement<String>:: the replacement, can be also given as a block + # config<Hash>:: give :verbose => false to not log the status, and + # :force => true, to force the replacement regardless of runner behavior. + # + # ==== Example + # + # gsub_file! 'app/controllers/application_controller.rb', /#\s*(filter_parameter_logging :password)/, '\1' + # + # gsub_file! 'README', /rake/, :green do |match| + # match << " no more. Use thor!" + # end + # + def gsub_file!(path, flag, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + + return unless behavior == :invoke || config.fetch(:force, false) + + path = File.expand_path(path, destination_root) + say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true) + + actually_gsub_file(path, flag, args, true, &block) unless options[:pretend] + end + # Run a regular expression replacement on a file. # # ==== Parameters # path<String>:: path of the file to be changed # flag<Regexp|String>:: the regexp or string to be replaced # replacement<String>:: the replacement, can be also given as a block - # config<Hash>:: give :verbose => false to not log the status. + # config<Hash>:: give :verbose => false to not log the status, and + # :force => true, to force the replacement regardless of runner behavior. # # ==== Example # @@ -253,22 +289,18 @@ class Bundler::Thor # end # def gsub_file(path, flag, *args, &block) - return unless behavior == :invoke config = args.last.is_a?(Hash) ? args.pop : {} + return unless behavior == :invoke || config.fetch(:force, false) + path = File.expand_path(path, destination_root) say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true) - unless options[:pretend] - content = File.binread(path) - content.gsub!(flag, *args, &block) - File.open(path, "wb") { |file| file.write(content) } - end + actually_gsub_file(path, flag, args, false, &block) unless options[:pretend] end - # Uncomment all lines matching a given regex. It will leave the space - # which existed before the comment hash in tact but will remove any spacing - # between the comment hash and the beginning of the line. + # Uncomment all lines matching a given regex. Preserves indentation before + # the comment hash and removes the hash and any immediate following space. # # ==== Parameters # path<String>:: path of the file to be changed @@ -282,7 +314,7 @@ class Bundler::Thor def uncomment_lines(path, flag, *args) flag = flag.respond_to?(:source) ? flag.source : flag - gsub_file(path, /^(\s*)#[[:blank:]]*(.*#{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 @@ -301,7 +333,7 @@ class Bundler::Thor def comment_lines(path, flag, *args) flag = flag.respond_to?(:source) ? flag.source : flag - gsub_file(path, /^(\s*)([^#|\n]*#{flag})/, '\1# \2', *args) + gsub_file(path, /^(\s*)([^#\n]*#{flag})/, '\1# \2', *args) end # Removes a file at the given location. @@ -320,7 +352,7 @@ class Bundler::Thor path = File.expand_path(path, destination_root) say_status :remove, relative_to_original_destination_root(path), config.fetch(:verbose, true) - if !options[:pretend] && File.exist?(path) + if !options[:pretend] && (File.exist?(path) || File.symlink?(path)) require "fileutils" ::FileUtils.rm_rf(path) end @@ -341,7 +373,7 @@ class Bundler::Thor end def with_output_buffer(buf = "".dup) #:nodoc: - raise ArgumentError, "Buffer can not be a frozen object" if buf.frozen? + raise ArgumentError, "Buffer cannot be a frozen object" if buf.frozen? old_buffer = output_buffer self.output_buffer = buf yield @@ -350,6 +382,17 @@ class Bundler::Thor self.output_buffer = old_buffer end + def actually_gsub_file(path, flag, args, error_on_no_change, &block) + content = File.binread(path) + success = content.gsub!(flag, *args, &block) + + if success.nil? && error_on_no_change + raise Bundler::Thor::Error, "The content of #{path} did not change" + end + + File.open(path, "wb") { |file| file.write(content) } + end + # Bundler::Thor::Actions#capture depends on what kind of buffer is used in ERB. # Thus CapturableERB fixes ERB to use String buffer. class CapturableERB < ERB diff --git a/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb b/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb index 349b26ff65..70526e615f 100644 --- a/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb +++ b/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb @@ -1,4 +1,4 @@ -require "bundler/vendor/thor/lib/thor/actions/empty_directory" +require_relative "empty_directory" class Bundler::Thor module Actions @@ -21,9 +21,14 @@ class Bundler::Thor # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n") # end # + WARNINGS = {unchanged_no_flag: "File unchanged! Either the supplied flag value not found or the content has already been inserted!"} + def insert_into_file(destination, *args, &block) data = block_given? ? block : args.shift - config = args.shift + + config = args.shift || {} + config[:after] = /\z/ unless config.key?(:before) || config.key?(:after) + action InjectIntoFile.new(self, destination, data, config) end alias_method :inject_into_file, :insert_into_file @@ -32,7 +37,7 @@ class Bundler::Thor attr_reader :replacement, :flag, :behavior def initialize(base, destination, data, config) - super(base, destination, {:verbose => true}.merge(config)) + super(base, destination, {verbose: true}.merge(config)) @behavior, @flag = if @config.key?(:after) [:after, @config.delete(:after)] @@ -45,8 +50,6 @@ class Bundler::Thor end def invoke! - say_status :invoke - content = if @behavior == :after '\0' + replacement else @@ -54,7 +57,13 @@ class Bundler::Thor end if exists? - replace!(/#{flag}/, content, config[:force]) + if replace!(/#{flag}/, content, config[:force]) + say_status(:invoke) + elsif replacement_present? + say_status(:unchanged, color: :blue) + else + say_status(:unchanged, warning: WARNINGS[:unchanged_no_flag], color: :red) + end else unless pretend? raise Bundler::Thor::Error, "The file #{ destination } does not appear to exist" @@ -78,7 +87,7 @@ class Bundler::Thor protected - def say_status(behavior) + def say_status(behavior, warning: nil, color: nil) status = if behavior == :invoke if flag == /\A/ :prepend @@ -87,21 +96,33 @@ class Bundler::Thor else :insert end + elsif warning + warning + elsif behavior == :unchanged + :unchanged else :subtract end - super(status, config[:verbose]) + super(status, (color || config[:verbose])) + end + + def content + @content ||= File.read(destination) + end + + def replacement_present? + content.include?(replacement) end # Adds the content to the file. # def replace!(regexp, string, force) - return if pretend? - content = File.read(destination) - if force || !content.include?(replacement) - content.gsub!(regexp, string) - File.open(destination, "wb") { |file| file.write(content) } + if force || !replacement_present? + success = content.gsub!(regexp, string) + + File.open(destination, "wb") { |file| file.write(content) } unless pretend? + success end end end diff --git a/lib/bundler/vendor/thor/lib/thor/base.rb b/lib/bundler/vendor/thor/lib/thor/base.rb index 9bd1077170..b156899c1e 100644 --- a/lib/bundler/vendor/thor/lib/thor/base.rb +++ b/lib/bundler/vendor/thor/lib/thor/base.rb @@ -1,17 +1,17 @@ -require "bundler/vendor/thor/lib/thor/command" -require "bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access" -require "bundler/vendor/thor/lib/thor/core_ext/ordered_hash" -require "bundler/vendor/thor/lib/thor/error" -require "bundler/vendor/thor/lib/thor/invocation" -require "bundler/vendor/thor/lib/thor/parser" -require "bundler/vendor/thor/lib/thor/shell" -require "bundler/vendor/thor/lib/thor/line_editor" -require "bundler/vendor/thor/lib/thor/util" +require_relative "command" +require_relative "core_ext/hash_with_indifferent_access" +require_relative "error" +require_relative "invocation" +require_relative "nested_context" +require_relative "parser" +require_relative "shell" +require_relative "line_editor" +require_relative "util" class Bundler::Thor - autoload :Actions, "bundler/vendor/thor/lib/thor/actions" - autoload :RakeCompat, "bundler/vendor/thor/lib/thor/rake_compat" - autoload :Group, "bundler/vendor/thor/lib/thor/group" + autoload :Actions, File.expand_path("actions", __dir__) + autoload :RakeCompat, File.expand_path("rake_compat", __dir__) + autoload :Group, File.expand_path("group", __dir__) # Shortcuts for help. HELP_MAPPINGS = %w(-h -? --help -D) @@ -22,6 +22,15 @@ class Bundler::Thor TEMPLATE_EXTNAME = ".tt" + class << self + def deprecation_warning(message) #:nodoc: + unless ENV["THOR_SILENCE_DEPRECATION"] + warn "Deprecation warning: #{message}\n" + + "You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION." + end + end + end + module Base attr_accessor :options, :parent_options, :args @@ -51,6 +60,7 @@ class Bundler::Thor command_options = config.delete(:command_options) # hook for start parse_options = parse_options.merge(command_options) if command_options + if local_options.is_a?(Array) array_options = local_options hash_options = {} @@ -64,9 +74,24 @@ class Bundler::Thor # Let Bundler::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_command] - disable_required_check = self.class.disable_required_check? config[:current_command] - opts = Bundler::Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check) + current_command = config[:current_command] + stop_on_unknown = self.class.stop_on_unknown_option? current_command + + # Give a relation of options. + # After parsing, Bundler::Thor::Options check whether right relations are kept + relations = if current_command.nil? + {exclusive_option_names: [], at_least_one_option_names: []} + else + current_command.options_relation + end + + self.class.class_exclusive_option_names.map { |n| relations[:exclusive_option_names] << n } + self.class.class_at_least_one_option_names.map { |n| relations[:at_least_one_option_names] << n } + + disable_required_check = self.class.disable_required_check? current_command + + opts = Bundler::Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check, relations) + self.options = opts.parse(array_options) self.options = config[:class_options].merge(options) if config[:class_options] @@ -89,6 +114,7 @@ class Bundler::Thor class << self def included(base) #:nodoc: + super(base) base.extend ClassMethods base.send :include, Invocation base.send :include, Shell @@ -113,7 +139,7 @@ class Bundler::Thor end # Whenever a class inherits from Bundler::Thor or Bundler::Thor::Group, we should track the - # class and the file on Bundler::Thor::Base. This is the method responsable for it. + # class and the file on Bundler::Thor::Base. This is the method responsible for it. # def register_klass_file(klass) #:nodoc: file = caller[1].match(/(.*):\d+/)[1] @@ -153,17 +179,20 @@ class Bundler::Thor # If you want to raise an error when the default value of an option does not match # the type call check_default_type! - # This is disabled by default for compatibility. + # This will be the default; for compatibility a deprecation warning is issued if necessary. def check_default_type! @check_default_type = true end - def check_default_type #:nodoc: - @check_default_type ||= from_superclass(:check_default_type, false) + # If you want to use defaults that don't match the type of an option, + # either specify `check_default_type: false` or call `allow_incompatible_default_type!` + def allow_incompatible_default_type! + @check_default_type = false end - def check_default_type? #:nodoc: - !!check_default_type + def check_default_type #:nodoc: + @check_default_type = from_superclass(:check_default_type, nil) unless defined?(@check_default_type) + @check_default_type end # If true, option parsing is suspended as soon as an unknown option or a @@ -297,9 +326,92 @@ class Bundler::Thor # :hide:: -- If you want to hide this option from the help. # def class_option(name, options = {}) + unless [ Symbol, String ].any? { |klass| name.is_a?(klass) } + raise ArgumentError, "Expected a Symbol or String, got #{name.inspect}" + end build_option(name, options, class_options) end + # Adds and declares option group for exclusive options in the + # block and arguments. You can declare options as the outside of the block. + # + # ==== Parameters + # Array[Bundler::Thor::Option.name] + # + # ==== Examples + # + # class_exclusive do + # class_option :one + # class_option :two + # end + # + # Or + # + # class_option :one + # class_option :two + # class_exclusive :one, :two + # + # If you give "--one" and "--two" at the same time ExclusiveArgumentsError + # will be raised. + # + def class_exclusive(*args, &block) + register_options_relation_for(:class_options, + :class_exclusive_option_names, *args, &block) + end + + # Adds and declares option group for required at least one of options in the + # block and arguments. You can declare options as the outside of the block. + # + # ==== Examples + # + # class_at_least_one do + # class_option :one + # class_option :two + # end + # + # Or + # + # class_option :one + # class_option :two + # class_at_least_one :one, :two + # + # If you do not give "--one" and "--two" AtLeastOneRequiredArgumentError + # will be raised. + # + # You can use class_at_least_one and class_exclusive at the same time. + # + # class_exclusive do + # class_at_least_one do + # class_option :one + # class_option :two + # end + # end + # + # Then it is required either only one of "--one" or "--two". + # + def class_at_least_one(*args, &block) + register_options_relation_for(:class_options, + :class_at_least_one_option_names, *args, &block) + end + + # Returns this class exclusive options array set, looking up in the ancestors chain. + # + # ==== Returns + # Array[Array[Bundler::Thor::Option.name]] + # + def class_exclusive_option_names + @class_exclusive_option_names ||= from_superclass(:class_exclusive_option_names, []) + end + + # Returns this class at least one of required options array set, looking up in the ancestors chain. + # + # ==== Returns + # Array[Array[Bundler::Thor::Option.name]] + # + def class_at_least_one_option_names + @class_at_least_one_option_names ||= from_superclass(:class_at_least_one_option_names, []) + end + # Removes a previous defined argument. If :undefine is given, undefine # accessors as well. # @@ -353,22 +465,22 @@ class Bundler::Thor # Returns the commands for this Bundler::Thor class. # # ==== Returns - # OrderedHash:: An ordered hash with commands names as keys and Bundler::Thor::Command - # objects as values. + # Hash:: An ordered hash with commands names as keys and Bundler::Thor::Command + # objects as values. # def commands - @commands ||= Bundler::Thor::CoreExt::OrderedHash.new + @commands ||= Hash.new end alias_method :tasks, :commands # Returns the commands for this Bundler::Thor class and all subclasses. # # ==== Returns - # OrderedHash:: An ordered hash with commands names as keys and Bundler::Thor::Command - # objects as values. + # Hash:: An ordered hash with commands names as keys and Bundler::Thor::Command + # objects as values. # def all_commands - @all_commands ||= from_superclass(:all_commands, Bundler::Thor::CoreExt::OrderedHash.new) + @all_commands ||= from_superclass(:all_commands, Hash.new) @all_commands.merge!(commands) end alias_method :all_tasks, :all_commands @@ -415,14 +527,20 @@ class Bundler::Thor # remove_command :this_is_not_a_command # end # - def no_commands - @no_commands = true - yield - ensure - @no_commands = false + def no_commands(&block) + no_commands_context.enter(&block) end + alias_method :no_tasks, :no_commands + def no_commands_context + @no_commands_context ||= NestedContext.new + end + + def no_commands? + no_commands_context.entered? + end + # Sets the namespace for the Bundler::Thor or Bundler::Thor::Group class. By default the # namespace is retrieved from the class name. If your Bundler::Thor class is named # Scripts::MyScript, the help method, for example, will be called as: @@ -466,13 +584,13 @@ class Bundler::Thor dispatch(nil, given_args.dup, nil, config) rescue Bundler::Thor::Error => e config[:debug] || ENV["THOR_DEBUG"] == "1" ? (raise e) : config[:shell].error(e.message) - exit(1) if exit_on_failure? + exit(false) if exit_on_failure? rescue Errno::EPIPE # 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) + exit(true) end # Allows to use private methods from parent in child classes as commands. @@ -487,14 +605,13 @@ class Bundler::Thor # def public_command(*names) names.each do |name| - class_eval "def #{name}(*); super end" + class_eval "def #{name}(*); super end", __FILE__, __LINE__ end end alias_method :public_task, :public_command def handle_no_command_error(command, has_namespace = $thor_runner) #:nodoc: - raise UndefinedCommandError, "Could not find command #{command.inspect} in #{namespace.inspect} namespace." if has_namespace - raise UndefinedCommandError, "Could not find command #{command.inspect}." + raise UndefinedCommandError.new(command, all_commands.keys, (namespace if has_namespace)) end alias_method :handle_no_task_error, :handle_no_command_error @@ -503,10 +620,16 @@ class Bundler::Thor msg = "ERROR: \"#{basename} #{name}\" was called with ".dup msg << "no arguments" if args.empty? msg << "arguments " << args.inspect unless args.empty? - msg << "\nUsage: #{banner(command).inspect}" + msg << "\nUsage: \"#{banner(command).split("\n").join("\"\n \"")}\"" raise InvocationError, msg end + # A flag that makes the process exit with status 1 if any error happens. + def exit_on_failure? + Bundler::Thor.deprecation_warning "Bundler::Thor exit with status 0 on errors. To keep this behavior, you must define `exit_on_failure?` in `#{self.name}`" + false + end + protected # Prints the class options per group. If an option does not belong to @@ -534,20 +657,19 @@ class Bundler::Thor return if options.empty? list = [] - padding = options.map { |o| o.aliases.size }.max.to_i * 4 - + padding = options.map { |o| o.aliases_for_usage.size }.max.to_i options.each do |option| next if option.hide item = [option.usage(padding)] item.push(option.description ? "# #{option.description}" : "") list << item - list << ["", "# Default: #{option.default}"] if option.show_default? - list << ["", "# Possible values: #{option.enum.join(', ')}"] if option.enum + list << ["", "# Default: #{option.print_default}"] if option.show_default? + list << ["", "# Possible values: #{option.enum_to_s}"] if option.enum end shell.say(group_name ? "#{group_name} options:" : "Options:") - shell.print_table(list, :indent => 2) + shell.print_table(list, indent: 2) shell.say "" end @@ -564,7 +686,7 @@ class Bundler::Thor # options<Hash>:: Described in both class_option and method_option. # scope<Hash>:: Options hash that is being built up def build_option(name, options, scope) #:nodoc: - scope[name] = Bundler::Thor::Option.new(name, options.merge(:check_default_type => check_default_type?)) + scope[name] = Bundler::Thor::Option.new(name, {check_default_type: check_default_type}.merge!(options)) end # Receives a hash of options, parse them and add to the scope. This is a @@ -586,7 +708,7 @@ class Bundler::Thor def find_and_refresh_command(name) #:nodoc: if commands[name.to_s] commands[name.to_s] - elsif command = all_commands[name.to_s] # rubocop:disable AssignmentInCondition + elsif command = all_commands[name.to_s] # rubocop:disable Lint/AssignmentInCondition commands[name.to_s] = command.clone else raise ArgumentError, "You supplied :for => #{name.inspect}, but the command #{name.inspect} could not be found." @@ -594,16 +716,18 @@ class Bundler::Thor end alias_method :find_and_refresh_task, :find_and_refresh_command - # Everytime someone inherits from a Bundler::Thor class, register the klass + # Every time someone inherits from a Bundler::Thor class, register the klass # and file into baseclass. def inherited(klass) + super(klass) Bundler::Thor::Base.register_klass_file(klass) - klass.instance_variable_set(:@no_commands, false) + klass.instance_variable_set(:@no_commands, 0) end # Fire this callback whenever a method is added. Added methods are # tracked as commands by invoking the create_command method. def method_added(meth) + super(meth) meth = meth.to_s if meth == "initialize" @@ -614,8 +738,7 @@ class Bundler::Thor # Return if it's not a public instance method return unless public_method_defined?(meth.to_sym) - @no_commands ||= false - return if @no_commands || !create_command(meth) + return if no_commands? || !create_command(meth) is_thor_reserved_word?(meth, :command) Bundler::Thor::Base.register_klass_file(self) @@ -642,11 +765,6 @@ class Bundler::Thor end end - # A flag that makes the process exit with status 1 if any error happens. - def exit_on_failure? - false - end - # # The basename of the program invoking the thor class. # @@ -674,6 +792,34 @@ class Bundler::Thor def dispatch(command, given_args, given_opts, config) #:nodoc: raise NotImplementedError end + + # Register a relation of options for target(method_option/class_option) + # by args and block. + def register_options_relation_for(target, relation, *args, &block) # :nodoc: + opt = args.pop if args.last.is_a? Hash + opt ||= {} + names = args.map{ |arg| arg.to_s } + names += built_option_names(target, opt, &block) if block_given? + command_scope_member(relation, opt) << names + end + + # Get target(method_options or class_options) options + # of before and after by block evaluation. + def built_option_names(target, opt = {}, &block) # :nodoc: + before = command_scope_member(target, opt).map{ |k,v| v.name } + instance_eval(&block) + after = command_scope_member(target, opt).map{ |k,v| v.name } + after - before + end + + # Get command scope member by name. + def command_scope_member(name, options = {}) # :nodoc: + if options[:for] + find_and_refresh_command(options[:for]).send(name) + else + send(name) + end + end end end end diff --git a/lib/bundler/vendor/thor/lib/thor/command.rb b/lib/bundler/vendor/thor/lib/thor/command.rb index c636948e5d..68c8fffedb 100644 --- a/lib/bundler/vendor/thor/lib/thor/command.rb +++ b/lib/bundler/vendor/thor/lib/thor/command.rb @@ -1,14 +1,15 @@ class Bundler::Thor - class Command < Struct.new(:name, :description, :long_description, :usage, :options, :ancestor_name) + class Command < Struct.new(:name, :description, :long_description, :wrap_long_description, :usage, :options, :options_relation, :ancestor_name) FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/ - def initialize(name, description, long_description, usage, options = nil) - super(name.to_s, description, long_description, usage, options || {}) + def initialize(name, description, long_description, wrap_long_description, usage, options = nil, options_relation = nil) + super(name.to_s, description, long_description, wrap_long_description, usage, options || {}, options_relation || {}) end def initialize_copy(other) #:nodoc: super(other) self.options = other.options.dup if other.options + self.options_relation = other.options_relation.dup if other.options_relation end def hidden? @@ -49,24 +50,40 @@ class Bundler::Thor formatted ||= "".dup - # Add usage with required arguments - formatted << if klass && !klass.arguments.empty? - usage.to_s.gsub(/^#{name}/) do |match| - match << " " << klass.arguments.map(&:usage).compact.join(" ") - end - else - usage.to_s - end + Array(usage).map do |specific_usage| + formatted_specific_usage = formatted - # Add required options - formatted << " #{required_options}" + formatted_specific_usage += required_arguments_for(klass, specific_usage) - # Strip and go! - formatted.strip + # Add required options + formatted_specific_usage += " #{required_options}" + + # Strip and go! + formatted_specific_usage.strip + end.join("\n") + end + + def method_exclusive_option_names #:nodoc: + self.options_relation[:exclusive_option_names] || [] + end + + def method_at_least_one_option_names #:nodoc: + self.options_relation[:at_least_one_option_names] || [] end protected + # Add usage with required arguments + def required_arguments_for(klass, usage) + if klass && !klass.arguments.empty? + usage.to_s.gsub(/^#{name}/) do |match| + match << " " << klass.arguments.map(&:usage).compact.join(" ") + end + else + usage.to_s + end + end + def not_debugging?(instance) !(instance.class.respond_to?(:debugging) && instance.class.debugging) end @@ -97,8 +114,7 @@ class Bundler::Thor def handle_argument_error?(instance, error, caller) not_debugging?(instance) && (error.message =~ /wrong number of arguments/ || error.message =~ /given \d*, expected \d*/) && begin saned = sans_backtrace(error.backtrace, caller) - # Ruby 1.9 always include the called method in the backtrace - saned.empty? || (saned.size == 1 && RUBY_VERSION >= "1.9") + saned.empty? || saned.size == 1 end end @@ -120,7 +136,7 @@ class Bundler::Thor # A dynamic command that handles method missing scenarios. class DynamicCommand < Command def initialize(name, options = nil) - super(name.to_s, "A dynamically-generated command", name.to_s, name.to_s, options) + super(name.to_s, "A dynamically-generated command", name.to_s, nil, name.to_s, options) end def run(instance, args = []) diff --git a/lib/bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb b/lib/bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb index c167aa33b8..b16a98f782 100644 --- a/lib/bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb +++ b/lib/bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb @@ -28,10 +28,20 @@ class Bundler::Thor super(convert_key(key)) end + def except(*keys) + dup.tap do |hash| + keys.each { |key| hash.delete(convert_key(key)) } + end + end + def fetch(key, *args) super(convert_key(key), *args) end + def slice(*keys) + super(*keys.map{ |key| convert_key(key) }) + end + def key?(key) super(convert_key(key)) end diff --git a/lib/bundler/vendor/thor/lib/thor/core_ext/io_binary_read.rb b/lib/bundler/vendor/thor/lib/thor/core_ext/io_binary_read.rb deleted file mode 100644 index 0f6e2e0af2..0000000000 --- a/lib/bundler/vendor/thor/lib/thor/core_ext/io_binary_read.rb +++ /dev/null @@ -1,12 +0,0 @@ -class IO #:nodoc: - class << self - unless method_defined? :binread - 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 - end - end -end diff --git a/lib/bundler/vendor/thor/lib/thor/core_ext/ordered_hash.rb b/lib/bundler/vendor/thor/lib/thor/core_ext/ordered_hash.rb deleted file mode 100644 index 76f1e43c65..0000000000 --- a/lib/bundler/vendor/thor/lib/thor/core_ext/ordered_hash.rb +++ /dev/null @@ -1,129 +0,0 @@ -class Bundler::Thor - module CoreExt - class OrderedHash < ::Hash - if RUBY_VERSION < "1.9" - def initialize(*args, &block) - super - @keys = [] - end - - def initialize_copy(other) - super - # make a deep copy of keys - @keys = other.keys - end - - def []=(key, value) - @keys << key unless key?(key) - super - end - - def delete(key) - if key? key - index = @keys.index(key) - @keys.delete_at index - end - super - end - - def delete_if - super - sync_keys! - self - end - - alias_method :reject!, :delete_if - - def reject(&block) - dup.reject!(&block) - end - - def keys - @keys.dup - end - - def values - @keys.map { |key| self[key] } - end - - def to_hash - self - end - - def to_a - @keys.map { |key| [key, self[key]] } - end - - def each_key - return to_enum(:each_key) unless block_given? - @keys.each { |key| yield(key) } - self - end - - def each_value - return to_enum(:each_value) unless block_given? - @keys.each { |key| yield(self[key]) } - self - end - - def each - return to_enum(:each) unless block_given? - @keys.each { |key| yield([key, self[key]]) } - self - end - - def each_pair - return to_enum(:each_pair) unless block_given? - @keys.each { |key| yield(key, self[key]) } - self - end - - alias_method :select, :find_all - - def clear - super - @keys.clear - self - end - - def shift - k = @keys.first - v = delete(k) - [k, v] - end - - def merge!(other_hash) - if block_given? - other_hash.each { |k, v| self[k] = key?(k) ? yield(k, self[k], v) : v } - else - other_hash.each { |k, v| self[k] = v } - end - self - end - - alias_method :update, :merge! - - def merge(other_hash, &block) - dup.merge!(other_hash, &block) - end - - # When replacing with another hash, the initial order of our keys must come from the other hash -ordered or not. - def replace(other) - super - @keys = other.keys - self - end - - def inspect - "#<#{self.class} #{super}>" - end - - private - - def sync_keys! - @keys.delete_if { |k| !key?(k) } - end - end - end - end -end diff --git a/lib/bundler/vendor/thor/lib/thor/error.rb b/lib/bundler/vendor/thor/lib/thor/error.rb index 2f816081f3..928646e501 100644 --- a/lib/bundler/vendor/thor/lib/thor/error.rb +++ b/lib/bundler/vendor/thor/lib/thor/error.rb @@ -1,4 +1,16 @@ class Bundler::Thor + Correctable = if defined?(DidYouMean::SpellChecker) && defined?(DidYouMean::Correctable) # rubocop:disable Naming/ConstantName + Module.new do + def to_s + super + DidYouMean.formatter.message_for(corrections) + end + + def corrections + @corrections ||= self.class.const_get(:SpellChecker).new(self).corrections + end + end + end + # Bundler::Thor::Error is raised when it's caused by wrong usage of thor classes. Those # errors have their backtrace suppressed and are nicely shown to the user. # @@ -10,6 +22,35 @@ class Bundler::Thor # Raised when a command was not found. class UndefinedCommandError < Error + class SpellChecker + attr_reader :error + + def initialize(error) + @error = error + end + + def corrections + @corrections ||= spell_checker.correct(error.command).map(&:inspect) + end + + def spell_checker + DidYouMean::SpellChecker.new(dictionary: error.all_commands) + end + end + + attr_reader :command, :all_commands + + def initialize(command, all_commands, namespace) + @command = command + @all_commands = all_commands + + message = "Could not find command #{command.inspect}" + message = namespace ? "#{message} in #{namespace.inspect} namespace." : "#{message}." + + super(message) + end + + prepend Correctable if Correctable end UndefinedTaskError = UndefinedCommandError @@ -22,6 +63,33 @@ class Bundler::Thor end class UnknownArgumentError < Error + class SpellChecker + attr_reader :error + + def initialize(error) + @error = error + end + + def corrections + @corrections ||= + error.unknown.flat_map { |unknown| spell_checker.correct(unknown) }.uniq.map(&:inspect) + end + + def spell_checker + @spell_checker ||= DidYouMean::SpellChecker.new(dictionary: error.switches) + end + end + + attr_reader :switches, :unknown + + def initialize(switches, unknown) + @switches = switches + @unknown = unknown + + super("Unknown switches #{unknown.map(&:inspect).join(', ')}") + end + + prepend Correctable if Correctable end class RequiredArgumentMissingError < InvocationError @@ -29,4 +97,10 @@ class Bundler::Thor class MalformattedArgumentError < InvocationError end + + class ExclusiveArgumentError < InvocationError + end + + class AtLeastOneRequiredArgumentError < InvocationError + end end diff --git a/lib/bundler/vendor/thor/lib/thor/group.rb b/lib/bundler/vendor/thor/lib/thor/group.rb index 05ddc10cd3..30bc311294 100644 --- a/lib/bundler/vendor/thor/lib/thor/group.rb +++ b/lib/bundler/vendor/thor/lib/thor/group.rb @@ -1,4 +1,4 @@ -require "bundler/vendor/thor/lib/thor/base" +require_relative "base" # Bundler::Thor has a special class called Bundler::Thor::Group. The main difference to Bundler::Thor class # is that it invokes all commands at once. It also include some methods that allows @@ -61,7 +61,7 @@ class Bundler::Thor::Group invocations[name] = false invocation_blocks[name] = block if block_given? - class_eval <<-METHOD, __FILE__, __LINE__ + class_eval <<-METHOD, __FILE__, __LINE__ + 1 def _invoke_#{name.to_s.gsub(/\W/, '_')} klass, command = self.class.prepare_for_invocation(nil, #{name.inspect}) @@ -120,7 +120,7 @@ class Bundler::Thor::Group invocations[name] = true invocation_blocks[name] = block if block_given? - class_eval <<-METHOD, __FILE__, __LINE__ + class_eval <<-METHOD, __FILE__, __LINE__ + 1 def _invoke_from_option_#{name.to_s.gsub(/\W/, '_')} return unless options[#{name.inspect}] @@ -169,7 +169,7 @@ class Bundler::Thor::Group # options are added to group_options hash. Options that already exists # in base_options are not added twice. # - def get_options_from_invocations(group_options, base_options) #:nodoc: # rubocop:disable MethodLength + def get_options_from_invocations(group_options, base_options) #:nodoc: invocations.each do |name, from_option| value = if from_option option = class_options[name] @@ -211,6 +211,17 @@ class Bundler::Thor::Group raise error, msg end + # Checks if a specified command exists. + # + # ==== Parameters + # command_name<String>:: The name of the command to check for existence. + # + # ==== Returns + # Boolean:: +true+ if the command exists, +false+ otherwise. + def command_exists?(command_name) #:nodoc: + commands.keys.include?(command_name) + end + protected # The method responsible for dispatching given the args. diff --git a/lib/bundler/vendor/thor/lib/thor/invocation.rb b/lib/bundler/vendor/thor/lib/thor/invocation.rb index 866d2212a7..5ce74710ba 100644 --- a/lib/bundler/vendor/thor/lib/thor/invocation.rb +++ b/lib/bundler/vendor/thor/lib/thor/invocation.rb @@ -1,6 +1,7 @@ class Bundler::Thor module Invocation def self.included(base) #:nodoc: + super(base) base.extend ClassMethods end @@ -142,7 +143,7 @@ class Bundler::Thor # Configuration values that are shared between invocations. def _shared_configuration #:nodoc: - {:invocations => @_invocations} + {invocations: @_invocations} end # This method simply retrieves the class and command to be invoked. diff --git a/lib/bundler/vendor/thor/lib/thor/line_editor.rb b/lib/bundler/vendor/thor/lib/thor/line_editor.rb index ce81a17484..5c0c336e7a 100644 --- a/lib/bundler/vendor/thor/lib/thor/line_editor.rb +++ b/lib/bundler/vendor/thor/lib/thor/line_editor.rb @@ -1,5 +1,5 @@ -require "bundler/vendor/thor/lib/thor/line_editor/basic" -require "bundler/vendor/thor/lib/thor/line_editor/readline" +require_relative "line_editor/basic" +require_relative "line_editor/readline" class Bundler::Thor module LineEditor diff --git a/lib/bundler/vendor/thor/lib/thor/line_editor/basic.rb b/lib/bundler/vendor/thor/lib/thor/line_editor/basic.rb index 0adb2b3137..fe3d7c998f 100644 --- a/lib/bundler/vendor/thor/lib/thor/line_editor/basic.rb +++ b/lib/bundler/vendor/thor/lib/thor/line_editor/basic.rb @@ -24,7 +24,7 @@ class Bundler::Thor $stdin.gets else # Lazy-load io/console since it is gem-ified as of 2.3 - require "io/console" if RUBY_VERSION > "1.9.2" + require "io/console" $stdin.noecho(&:gets) end end diff --git a/lib/bundler/vendor/thor/lib/thor/line_editor/readline.rb b/lib/bundler/vendor/thor/lib/thor/line_editor/readline.rb index dd39cff35d..120eadd06a 100644 --- a/lib/bundler/vendor/thor/lib/thor/line_editor/readline.rb +++ b/lib/bundler/vendor/thor/lib/thor/line_editor/readline.rb @@ -1,19 +1,19 @@ -begin - require "readline" -rescue LoadError -end - class Bundler::Thor module LineEditor class Readline < Basic def self.available? + begin + require "readline" + rescue LoadError + end + Object.const_defined?(:Readline) end def readline if echo? ::Readline.completion_append_character = nil - # Ruby 1.8.7 does not allow Readline.completion_proc= to receive nil. + # rb-readline does not allow Readline.completion_proc= to receive nil. if complete = completion_proc ::Readline.completion_proc = complete end diff --git a/lib/bundler/vendor/thor/lib/thor/nested_context.rb b/lib/bundler/vendor/thor/lib/thor/nested_context.rb new file mode 100644 index 0000000000..7d60cb1c12 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/nested_context.rb @@ -0,0 +1,29 @@ +class Bundler::Thor + class NestedContext + def initialize + @depth = 0 + end + + def enter + push + + yield + ensure + pop + end + + def entered? + @depth.positive? + end + + private + + def push + @depth += 1 + end + + def pop + @depth -= 1 + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/parser.rb b/lib/bundler/vendor/thor/lib/thor/parser.rb index 08f80e565d..45394732ca 100644 --- a/lib/bundler/vendor/thor/lib/thor/parser.rb +++ b/lib/bundler/vendor/thor/lib/thor/parser.rb @@ -1,4 +1,4 @@ -require "bundler/vendor/thor/lib/thor/parser/argument" -require "bundler/vendor/thor/lib/thor/parser/arguments" -require "bundler/vendor/thor/lib/thor/parser/option" -require "bundler/vendor/thor/lib/thor/parser/options" +require_relative "parser/argument" +require_relative "parser/arguments" +require_relative "parser/option" +require_relative "parser/options" diff --git a/lib/bundler/vendor/thor/lib/thor/parser/argument.rb b/lib/bundler/vendor/thor/lib/thor/parser/argument.rb index dfe7398583..ee9db4ad8a 100644 --- a/lib/bundler/vendor/thor/lib/thor/parser/argument.rb +++ b/lib/bundler/vendor/thor/lib/thor/parser/argument.rb @@ -24,6 +24,14 @@ class Bundler::Thor validate! # Trigger specific validations end + def print_default + if @type == :array and @default.is_a?(Array) + @default.map(&:dump).join(" ") + else + @default + end + end + def usage required? ? banner : "[#{banner}]" end @@ -41,11 +49,19 @@ class Bundler::Thor end end + def enum_to_s + if enum.respond_to? :join + enum.join(", ") + else + "#{enum.first}..#{enum.last}" + end + end + protected def validate! raise ArgumentError, "An argument cannot be required and have default value." if required? && !default.nil? - raise ArgumentError, "An argument cannot have an enum other than an array." if @enum && !@enum.is_a?(Array) + raise ArgumentError, "An argument cannot have an enum other than an enumerable." if @enum && !@enum.is_a?(Enumerable) end def valid_type?(type) diff --git a/lib/bundler/vendor/thor/lib/thor/parser/arguments.rb b/lib/bundler/vendor/thor/lib/thor/parser/arguments.rb index 1fd790f4b7..b6f9c9a37a 100644 --- a/lib/bundler/vendor/thor/lib/thor/parser/arguments.rb +++ b/lib/bundler/vendor/thor/lib/thor/parser/arguments.rb @@ -1,5 +1,5 @@ class Bundler::Thor - class Arguments #:nodoc: # rubocop:disable ClassLength + class Arguments #:nodoc: NUMERIC = /[-+]?(\d*\.\d+|\d+)/ # Receives an array of args and returns two arrays, one with arguments @@ -9,7 +9,7 @@ class Bundler::Thor arguments = [] args.each do |item| - break if item =~ /^-/ + break if item.is_a?(String) && item =~ /^-/ arguments << item end @@ -30,7 +30,7 @@ class Bundler::Thor arguments.each do |argument| if !argument.default.nil? - @assigns[argument.human_name] = argument.default + @assigns[argument.human_name] = argument.default.dup elsif argument.required? @non_assigned_required << argument end @@ -82,7 +82,7 @@ class Bundler::Thor end def current_is_value? - peek && peek.to_s !~ /^-/ + peek && peek.to_s !~ /^-{1,2}\S+/ end # Runs through the argument array getting strings that contains ":" and @@ -117,8 +117,18 @@ class Bundler::Thor # def parse_array(name) return shift if peek.is_a?(Array) + array = [] - array << shift while current_is_value? + + while current_is_value? + value = shift + + if !value.empty? + validate_enum_value!(name, value, "Expected all values of '%s' to be one of %s; got %s") + end + + array << value + end array end @@ -134,11 +144,9 @@ class Bundler::Thor end value = $&.index(".") ? shift.to_f : shift.to_i - if @switches.is_a?(Hash) && switch = @switches[name] - if switch.enum && !switch.enum.include?(value) - raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}" - end - end + + validate_enum_value!(name, value, "Expected '%s' to be one of %s; got %s") + value end @@ -152,15 +160,27 @@ class Bundler::Thor nil else value = shift - if @switches.is_a?(Hash) && switch = @switches[name] - if switch.enum && !switch.enum.include?(value) - raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}" - end - end + + validate_enum_value!(name, value, "Expected '%s' to be one of %s; got %s") + value end end + # Raises an error if the switch is an enum and the values aren't included on it. + # + def validate_enum_value!(name, value, message) + return unless @switches.is_a?(Hash) + + switch = @switches[name] + + return unless switch + + if switch.enum && !switch.enum.include?(value) + raise MalformattedArgumentError, message % [name, switch.enum_to_s, value] + end + end + # Raises an error if @non_assigned_required array is not empty. # def check_requirement! diff --git a/lib/bundler/vendor/thor/lib/thor/parser/option.rb b/lib/bundler/vendor/thor/lib/thor/parser/option.rb index 85169b56c8..72617c7e34 100644 --- a/lib/bundler/vendor/thor/lib/thor/parser/option.rb +++ b/lib/bundler/vendor/thor/lib/thor/parser/option.rb @@ -1,17 +1,18 @@ class Bundler::Thor class Option < Argument #:nodoc: - attr_reader :aliases, :group, :lazy_default, :hide + attr_reader :aliases, :group, :lazy_default, :hide, :repeatable VALID_TYPES = [:boolean, :numeric, :hash, :array, :string] def initialize(name, options = {}) @check_default_type = options[:check_default_type] options[:required] = false unless options.key?(:required) + @repeatable = options.fetch(:repeatable, false) super - @lazy_default = options[:lazy_default] - @group = options[:group].to_s.capitalize if options[:group] - @aliases = Array(options[:aliases]) - @hide = options[:hide] + @lazy_default = options[:lazy_default] + @group = options[:group].to_s.capitalize if options[:group] + @aliases = normalize_aliases(options[:aliases]) + @hide = options[:hide] end # This parse quick options given as method_options. It makes several @@ -57,7 +58,7 @@ class Bundler::Thor default = nil if VALID_TYPES.include?(value) value - elsif required = (value == :required) # rubocop:disable AssignmentInCondition + elsif required = (value == :required) # rubocop:disable Lint/AssignmentInCondition :string end when TrueClass, FalseClass @@ -68,7 +69,7 @@ class Bundler::Thor value.class.name.downcase.to_sym end - new(name.to_s, :required => required, :type => type, :default => default, :aliases => aliases) + new(name.to_s, required: required, type: type, default: default, aliases: aliases) end def switch_name @@ -88,14 +89,27 @@ class Bundler::Thor sample = "[#{sample}]".dup unless required? - if boolean? - sample << ", [#{dasherize('no-' + human_name)}]" unless (name == "force") || name.start_with?("no-") + if boolean? && name != "force" && !name.match(/\A(no|skip)[\-_]/) + sample << ", [#{dasherize('no-' + human_name)}], [#{dasherize('skip-' + human_name)}]" end + aliases_for_usage.ljust(padding) + sample + end + + def aliases_for_usage if aliases.empty? - (" " * padding) << sample + "" + else + "#{aliases.join(', ')}, " + end + end + + def show_default? + case default + when TrueClass, FalseClass + true else - "#{aliases.join(', ')}, #{sample}" + super end end @@ -111,7 +125,7 @@ class Bundler::Thor def validate! raise ArgumentError, "An option cannot be boolean and required." if boolean? && required? - validate_default_type! if @check_default_type + validate_default_type! end def validate_default_type! @@ -128,7 +142,19 @@ class Bundler::Thor @default.class.name.downcase.to_sym end - raise ArgumentError, "Expected #{@type} default value for '#{switch_name}'; got #{@default.inspect} (#{default_type})" unless default_type == @type + expected_type = (@repeatable && @type != :hash) ? :array : @type + + if default_type != expected_type + err = "Expected #{expected_type} default value for '#{switch_name}'; got #{@default.inspect} (#{default_type})" + + if @check_default_type + raise ArgumentError, err + elsif @check_default_type == nil + Bundler::Thor.deprecation_warning "#{err}.\n" + + "This will be rejected in the future unless you explicitly pass the options `check_default_type: false`" + + " or call `allow_incompatible_default_type!` in your code" + end + end end def dasherized? @@ -142,5 +168,11 @@ class Bundler::Thor def dasherize(str) (str.length > 1 ? "--" : "-") + str.tr("_", "-") end + + private + + def normalize_aliases(aliases) + Array(aliases).map { |short| short.to_s.sub(/^(?!\-)/, "-") } + end end end diff --git a/lib/bundler/vendor/thor/lib/thor/parser/options.rb b/lib/bundler/vendor/thor/lib/thor/parser/options.rb index 70f6366842..fe22d989e5 100644 --- a/lib/bundler/vendor/thor/lib/thor/parser/options.rb +++ b/lib/bundler/vendor/thor/lib/thor/parser/options.rb @@ -1,5 +1,5 @@ class Bundler::Thor - class Options < Arguments #:nodoc: # rubocop:disable ClassLength + class Options < Arguments #:nodoc: LONG_RE = /^(--\w+(?:-\w+)*)$/ SHORT_RE = /^(-[a-z])$/i EQ_RE = /^(--\w+(?:-\w+)*|-[a-z])=(.*)$/i @@ -29,8 +29,10 @@ class Bundler::Thor # # If +stop_on_unknown+ is true, #parse will stop as soon as it encounters # an unknown option or a regular argument. - def initialize(hash_options = {}, defaults = {}, stop_on_unknown = false, disable_required_check = false) + def initialize(hash_options = {}, defaults = {}, stop_on_unknown = false, disable_required_check = false, relations = {}) @stop_on_unknown = stop_on_unknown + @exclusives = (relations[:exclusive_option_names] || []).select{|array| !array.empty?} + @at_least_ones = (relations[:at_least_one_option_names] || []).select{|array| !array.empty?} @disable_required_check = disable_required_check options = hash_options.values super(options) @@ -44,12 +46,13 @@ class Bundler::Thor @shorts = {} @switches = {} @extra = [] + @stopped_parsing_after_extra_index = nil + @is_treated_as_value = false options.each do |option| @switches[option.switch_name] = option - option.aliases.each do |short| - name = short.to_s.sub(/^(?!\-)/, "-") + option.aliases.each do |name| @shorts[name] ||= option.switch_name end end @@ -66,14 +69,26 @@ class Bundler::Thor if result == OPTS_END shift @parsing_options = false + @stopped_parsing_after_extra_index ||= @extra.size super else result end end - def parse(args) # rubocop:disable MethodLength + def shift + @is_treated_as_value = false + super + end + + def unshift(arg, is_value: false) + @is_treated_as_value = is_value + super(arg) + end + + def parse(args) # rubocop:disable Metrics/MethodLength @pile = args.dup + @is_treated_as_value = false @parsing_options = true while peek @@ -86,7 +101,10 @@ class Bundler::Thor when SHORT_SQ_RE unshift($1.split("").map { |f| "-#{f}" }) next - when EQ_RE, SHORT_NUM + when EQ_RE + unshift($2, is_value: true) + switch = $1 + when SHORT_NUM unshift($2) switch = $1 when LONG_RE, SHORT_RE @@ -95,10 +113,12 @@ class Bundler::Thor switch = normalize_switch(switch) option = switch_option(switch) - @assigns[option.human_name] = parse_peek(switch, option) + result = parse_peek(switch, option) + assign_result!(option, result) elsif @stop_on_unknown @parsing_options = false @extra << shifted + @stopped_parsing_after_extra_index ||= @extra.size @extra << shift while peek break elsif match @@ -113,25 +133,75 @@ class Bundler::Thor end check_requirement! unless @disable_required_check + check_exclusive! + check_at_least_one! assigns = Bundler::Thor::CoreExt::HashWithIndifferentAccess.new(@assigns) assigns.freeze assigns end + def check_exclusive! + opts = @assigns.keys + # When option A and B are exclusive, if A and B are given at the same time, + # the difference of argument array size will decrease. + found = @exclusives.find{ |ex| (ex - opts).size < ex.size - 1 } + if found + names = names_to_switch_names(found & opts).map{|n| "'#{n}'"} + class_name = self.class.name.split("::").last.downcase + fail ExclusiveArgumentError, "Found exclusive #{class_name} #{names.join(", ")}" + end + end + + def check_at_least_one! + opts = @assigns.keys + # When at least one is required of the options A and B, + # if the both options were not given, none? would be true. + found = @at_least_ones.find{ |one_reqs| one_reqs.none?{ |o| opts.include? o} } + if found + names = names_to_switch_names(found).map{|n| "'#{n}'"} + class_name = self.class.name.split("::").last.downcase + fail AtLeastOneRequiredArgumentError, "Not found at least one of required #{class_name} #{names.join(", ")}" + end + end + def check_unknown! + to_check = @stopped_parsing_after_extra_index ? @extra[0...@stopped_parsing_after_extra_index] : @extra + # an unknown option starts with - or -- and has no more --'s afterward. - unknown = @extra.select { |str| str =~ /^--?(?:(?!--).)*$/ } - raise UnknownArgumentError, "Unknown switches '#{unknown.join(', ')}'" unless unknown.empty? + unknown = to_check.select { |str| str =~ /^--?(?:(?!--).)*$/ } + raise UnknownArgumentError.new(@switches.keys, unknown) unless unknown.empty? end protected + # Option names changes to swith name or human name + def names_to_switch_names(names = []) + @switches.map do |_, o| + if names.include? o.name + o.respond_to?(:switch_name) ? o.switch_name : o.human_name + else + nil + end + end.compact + end + + def assign_result!(option, result) + if option.repeatable && option.type == :hash + (@assigns[option.human_name] ||= {}).merge!(result) + elsif option.repeatable + (@assigns[option.human_name] ||= []) << result + else + @assigns[option.human_name] = result + end + end + # Check if the current value in peek is a registered switch. # # Two booleans are returned. The first is true if the current value # starts with a hyphen; the second is true if it is a registered switch. def current_is_switch? + return [false, false] if @is_treated_as_value case peek when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM [true, switch?($1)] @@ -143,6 +213,7 @@ class Bundler::Thor end def current_is_switch_formatted? + return false if @is_treated_as_value case peek when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM, SHORT_SQ_RE true @@ -152,15 +223,16 @@ class Bundler::Thor end def current_is_value? + return true if @is_treated_as_value peek && (!parsing_options? || super) end def switch?(arg) - switch_option(normalize_switch(arg)) + !switch_option(normalize_switch(arg)).nil? end def switch_option(arg) - if match = no_or_skip?(arg) # rubocop:disable AssignmentInCondition + if match = no_or_skip?(arg) # rubocop:disable Lint/AssignmentInCondition @switches[arg] || @switches["--#{match}"] else @switches[arg] @@ -178,7 +250,8 @@ class Bundler::Thor @parsing_options end - # Parse boolean values which can be given as --foo=true, --foo or --no-foo. + # Parse boolean values which can be given as --foo=true or --foo for true values, or + # --foo=false, --no-foo or --skip-foo for false values. # def parse_boolean(switch) if current_is_value? @@ -189,7 +262,7 @@ class Bundler::Thor shift false else - !no_or_skip?(switch) + @switches.key?(switch) || !no_or_skip?(switch) end else @switches.key?(switch) || !no_or_skip?(switch) diff --git a/lib/bundler/vendor/thor/lib/thor/rake_compat.rb b/lib/bundler/vendor/thor/lib/thor/rake_compat.rb index 60282e2991..c6a4653fc1 100644 --- a/lib/bundler/vendor/thor/lib/thor/rake_compat.rb +++ b/lib/bundler/vendor/thor/lib/thor/rake_compat.rb @@ -25,6 +25,7 @@ class Bundler::Thor end def self.included(base) + super(base) # Hack. Make rakefile point to invoker, so rdoc task is generated properly. rakefile = File.basename(caller[0].match(/(.*):\d+/)[1]) Rake.application.instance_variable_set(:@rakefile, rakefile) @@ -40,7 +41,7 @@ instance_eval do def task(*) task = super - if klass = Bundler::Thor::RakeCompat.rake_classes.last # rubocop:disable AssignmentInCondition + if klass = Bundler::Thor::RakeCompat.rake_classes.last # rubocop:disable Lint/AssignmentInCondition non_namespaced_name = task.name.split(":").last description = non_namespaced_name @@ -58,7 +59,7 @@ instance_eval do end def namespace(name) - if klass = Bundler::Thor::RakeCompat.rake_classes.last # rubocop:disable AssignmentInCondition + if klass = Bundler::Thor::RakeCompat.rake_classes.last # rubocop:disable Lint/AssignmentInCondition const_name = Bundler::Thor::Util.camel_case(name.to_s).to_sym klass.const_set(const_name, Class.new(Bundler::Thor)) new_klass = klass.const_get(const_name) diff --git a/lib/bundler/vendor/thor/lib/thor/runner.rb b/lib/bundler/vendor/thor/lib/thor/runner.rb index b110b8d478..f0ce6df96c 100644 --- a/lib/bundler/vendor/thor/lib/thor/runner.rb +++ b/lib/bundler/vendor/thor/lib/thor/runner.rb @@ -1,12 +1,10 @@ -require "bundler/vendor/thor/lib/thor" -require "bundler/vendor/thor/lib/thor/group" -require "bundler/vendor/thor/lib/thor/core_ext/io_binary_read" +require_relative "../thor" +require_relative "group" -require "yaml" -require "digest" -require "pathname" +require "digest/sha2" +require "pathname" unless defined?(Pathname) -class Bundler::Thor::Runner < Bundler::Thor #:nodoc: # rubocop:disable ClassLength +class Bundler::Thor::Runner < Bundler::Thor #:nodoc: map "-T" => :list, "-i" => :install, "-u" => :update, "-v" => :version def self.banner(command, all = false, subcommand = false) @@ -24,7 +22,7 @@ class Bundler::Thor::Runner < Bundler::Thor #:nodoc: # rubocop:disable ClassLeng initialize_thorfiles(meth) klass, command = Bundler::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 => shell) + klass.start(["-h", command].compact, shell: shell) else super end @@ -39,33 +37,45 @@ class Bundler::Thor::Runner < Bundler::Thor #:nodoc: # rubocop:disable ClassLeng klass, command = Bundler::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 => shell) + klass.start(args, shell: shell) end desc "install NAME", "Install an optionally named Bundler::Thor file into your system commands" - method_options :as => :string, :relative => :boolean, :force => :boolean - def install(name) # rubocop:disable MethodLength + method_options as: :string, relative: :boolean, force: :boolean + def install(name) # rubocop:disable Metrics/MethodLength initialize_thorfiles - # If a directory name is provided as the argument, look for a 'main.thor' - # command in said directory. - begin - if File.directory?(File.expand_path(name)) - base = File.join(name, "main.thor") - package = :directory - contents = open(base, &:read) - else - base = name - package = :file - contents = open(name, &:read) + is_uri = name =~ %r{^https?\://} + + if is_uri + base = name + package = :file + require "open-uri" + begin + contents = URI.open(name, &:read) + rescue OpenURI::HTTPError + raise Error, "Error opening URI '#{name}'" + end + else + # If a directory name is provided as the argument, look for a 'main.thor' + # command in said directory. + begin + if File.directory?(File.expand_path(name)) + base = File.join(name, "main.thor") + package = :directory + contents = File.open(base, &:read) + else + base = name + package = :file + require "open-uri" + contents = URI.open(name, &:read) + end + rescue Errno::ENOENT + raise Error, "Error opening file '#{name}'" end - rescue OpenURI::HTTPError - raise Error, "Error opening URI '#{name}'" - rescue Errno::ENOENT - raise Error, "Error opening file '#{name}'" end - say "Your Bundler::Thorfile contains:" + say "Your Thorfile contains:" say contents unless options["force"] @@ -83,16 +93,16 @@ class Bundler::Thor::Runner < Bundler::Thor #:nodoc: # rubocop:disable ClassLeng as = basename if as.empty? end - location = if options[:relative] || name =~ %r{^https?://} + location = if options[:relative] || is_uri name else File.expand_path(name) end thor_yaml[as] = { - :filename => Digest(:MD5).hexdigest(name + as), - :location => location, - :namespaces => Bundler::Thor::Util.namespaces_in_content(contents, base) + filename: Digest::SHA256.hexdigest(name + as), + location: location, + namespaces: Bundler::Thor::Util.namespaces_in_content(contents, base) } save_yaml(thor_yaml) @@ -111,7 +121,7 @@ class Bundler::Thor::Runner < Bundler::Thor #:nodoc: # rubocop:disable ClassLeng desc "version", "Show Bundler::Thor version" def version - require "bundler/vendor/thor/lib/thor/version" + require_relative "version" say "Bundler::Thor #{Bundler::Thor::VERSION}" end @@ -153,14 +163,14 @@ class Bundler::Thor::Runner < Bundler::Thor #:nodoc: # rubocop:disable ClassLeng end desc "installed", "List the installed Bundler::Thor modules and commands" - method_options :internal => :boolean + method_options internal: :boolean def installed initialize_thorfiles(nil, true) display_klasses(true, options["internal"]) end desc "list [SEARCH]", "List the available thor commands (--substring means .*SEARCH)" - method_options :substring => :boolean, :group => :string, :all => :boolean, :debug => :boolean + method_options substring: :boolean, group: :string, all: :boolean, debug: :boolean def list(search = "") initialize_thorfiles @@ -184,6 +194,7 @@ private def thor_yaml @thor_yaml ||= begin yaml_file = File.join(thor_root, "thor.yml") + require "yaml" yaml = YAML.load_file(yaml_file) if File.exist?(yaml_file) yaml || {} end @@ -204,7 +215,7 @@ private File.open(yaml_file, "w") { |f| f.puts yaml.to_yaml } end - # Load the Bundler::Thorfiles. If relevant_to is supplied, looks for specific files + # Load the Thorfiles. If relevant_to is supplied, looks for specific files # in the thor_root instead of loading them all. # # By default, it also traverses the current path until find Bundler::Thor files, as @@ -217,11 +228,11 @@ private end end - # Finds Bundler::Thorfiles by traversing from your current directory down to the root + # Finds Thorfiles by traversing from your current directory down to the root # directory of your system. If at any time we find a Bundler::Thor file, we stop. # - # We also ensure that system-wide Bundler::Thorfiles are loaded first, so local - # Bundler::Thorfiles can override them. + # We also ensure that system-wide Thorfiles are loaded first, so local + # Thorfiles can override them. # # ==== Example # @@ -229,7 +240,7 @@ private # # 1. /Users/wycats/dev/thor # 2. /Users/wycats/dev - # 3. /Users/wycats <-- we find a Bundler::Thorfile here, so we stop + # 3. /Users/wycats <-- we find a Thorfile here, so we stop # # Suppose we start at c:\Documents and Settings\james\dev\thor ... # @@ -237,7 +248,7 @@ private # 2. c:\Documents and Settings\james\dev # 3. c:\Documents and Settings\james # 4. c:\Documents and Settings - # 5. c:\ <-- no Bundler::Thorfiles found! + # 5. c:\ <-- no Thorfiles found! # def thorfiles(relevant_to = nil, skip_lookup = false) thorfiles = [] @@ -258,7 +269,7 @@ private end end - # Load Bundler::Thorfiles relevant to the given method. If you provide "foo:bar" it + # Load Thorfiles relevant to the given method. If you provide "foo:bar" it # will load all thor files in the thor.yaml that has "foo" e "foo:bar" # namespaces registered. # @@ -302,7 +313,7 @@ private say shell.set_color(namespace, :blue, true) say "-" * namespace.size - print_table(list, :truncate => true) + print_table(list, truncate: true) say end alias_method :display_tasks, :display_commands diff --git a/lib/bundler/vendor/thor/lib/thor/shell.rb b/lib/bundler/vendor/thor/lib/thor/shell.rb index e945549324..265f3ba046 100644 --- a/lib/bundler/vendor/thor/lib/thor/shell.rb +++ b/lib/bundler/vendor/thor/lib/thor/shell.rb @@ -21,12 +21,12 @@ class Bundler::Thor end module Shell - SHELL_DELEGATED_METHODS = [:ask, :error, :set_color, :yes?, :no?, :say, :say_status, :print_in_columns, :print_table, :print_wrapped, :file_collision, :terminal_width] + SHELL_DELEGATED_METHODS = [:ask, :error, :set_color, :yes?, :no?, :say, :say_error, :say_status, :print_in_columns, :print_table, :print_wrapped, :file_collision, :terminal_width] attr_writer :shell - autoload :Basic, "bundler/vendor/thor/lib/thor/shell/basic" - autoload :Color, "bundler/vendor/thor/lib/thor/shell/color" - autoload :HTML, "bundler/vendor/thor/lib/thor/shell/html" + autoload :Basic, File.expand_path("shell/basic", __dir__) + autoload :Color, File.expand_path("shell/color", __dir__) + autoload :HTML, File.expand_path("shell/html", __dir__) # Add shell to initialize config values. # @@ -55,7 +55,7 @@ class Bundler::Thor # Common methods that are delegated to the shell. SHELL_DELEGATED_METHODS.each do |method| - module_eval <<-METHOD, __FILE__, __LINE__ + module_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{method}(*args,&block) shell.#{method}(*args,&block) end @@ -75,7 +75,7 @@ class Bundler::Thor # Allow shell to be shared between invocations. # def _shared_configuration #:nodoc: - super.merge!(:shell => shell) + super.merge!(shell: shell) end end end diff --git a/lib/bundler/vendor/thor/lib/thor/shell/basic.rb b/lib/bundler/vendor/thor/lib/thor/shell/basic.rb index 5162390efd..da02b94227 100644 --- a/lib/bundler/vendor/thor/lib/thor/shell/basic.rb +++ b/lib/bundler/vendor/thor/lib/thor/shell/basic.rb @@ -1,3 +1,7 @@ +require_relative "column_printer" +require_relative "table_printer" +require_relative "wrapped_printer" + class Bundler::Thor module Shell class Basic @@ -45,6 +49,10 @@ class Bundler::Thor # Asks something to the user and receives a response. # + # If a default value is specified it will be presented to the user + # and allows them to select that value with an empty response. This + # option is ignored when limited answers are supplied. + # # If asked to limit the correct responses, you can pass in an # array of acceptable answers. If one of those is not supplied, # they will be shown a message stating that one of those answers @@ -59,13 +67,15 @@ class Bundler::Thor # Readline. # # ==== Example - # ask("What is your name?") + # ask("What is your name?") + # + # ask("What is the planet furthest from the sun?", :default => "Neptune") # - # ask("What is your favorite Neopolitan flavor?", :limited_to => ["strawberry", "chocolate", "vanilla"]) + # ask("What is your favorite Neopolitan flavor?", :limited_to => ["strawberry", "chocolate", "vanilla"]) # - # ask("What is your password?", :echo => false) + # ask("What is your password?", :echo => false) # - # ask("Where should the file be saved?", :path => true) + # ask("Where should the file be saved?", :path => true) # def ask(statement, *args) options = args.last.is_a?(Hash) ? args.pop : {} @@ -83,9 +93,11 @@ class Bundler::Thor # are passed straight to puts (behavior got from Highline). # # ==== Example - # say("I know you knew that.") + # say("I know you knew that.") # def say(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/)) + return if quiet? + buffer = prepare_message(message, *color) buffer << "\n" if force_new_line && !message.to_s.end_with?("\n") @@ -93,6 +105,23 @@ class Bundler::Thor stdout.flush end + # Say (print) an error to the user. If the sentence ends with a whitespace + # or tab character, a new line is not appended (print + flush). Otherwise + # are passed straight to puts (behavior got from Highline). + # + # ==== Example + # say_error("error: something went wrong") + # + def say_error(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/)) + return if quiet? + + buffer = prepare_message(message, *color) + buffer << "\n" if force_new_line && !message.to_s.end_with?("\n") + + stderr.print(buffer) + stderr.flush + end + # Say a status with the given color and appends the message. Since this # method is used frequently by actions, it allows nil or false to be given # in log_status, avoiding the message from being shown. If a Symbol is @@ -101,30 +130,31 @@ class Bundler::Thor def say_status(status, message, log_status = true) return if quiet? || log_status == false spaces = " " * (padding + 1) - color = log_status.is_a?(Symbol) ? log_status : :green - status = status.to_s.rjust(12) + margin = " " * status.length + spaces + + color = log_status.is_a?(Symbol) ? log_status : :green status = set_color status, color, true if color - buffer = "#{status}#{spaces}#{message}" - buffer = "#{buffer}\n" unless buffer.end_with?("\n") + message = message.to_s.chomp.gsub(/(?<!\A)^/, margin) + buffer = "#{status}#{spaces}#{message}\n" stdout.print(buffer) stdout.flush end - # Make a question the to user and returns true if the user replies "y" or + # Asks the user a question and returns true if the user replies "y" or # "yes". # def yes?(statement, color = nil) - !!(ask(statement, color, :add_to_history => false) =~ is?(:yes)) + !!(ask(statement, color, add_to_history: false) =~ is?(:yes)) end - # Make a question the to user and returns true if the user replies "n" or + # Asks the user a question and returns true if the user replies "n" or # "no". # def no?(statement, color = nil) - !!(ask(statement, color, :add_to_history => false) =~ is?(:no)) + !!(ask(statement, color, add_to_history: false) =~ is?(:no)) end # Prints values in columns @@ -133,16 +163,8 @@ class Bundler::Thor # Array[String, String, ...] # def print_in_columns(array) - return if array.empty? - colwidth = (array.map { |el| el.to_s.size }.max || 0) + 2 - array.each_with_index do |value, index| - # Don't output trailing spaces when printing the last column - if ((((index + 1) % (terminal_width / colwidth))).zero? && !index.zero?) || index + 1 == array.length - stdout.puts value - else - stdout.printf("%-#{colwidth}s", value) - end - end + printer = ColumnPrinter.new(stdout) + printer.print(array) end # Prints a table. @@ -153,58 +175,11 @@ class Bundler::Thor # ==== Options # indent<Integer>:: Indent the first column by indent value. # colwidth<Integer>:: Force the first column to colwidth spaces wide. + # borders<Boolean>:: Adds ascii borders. # - def print_table(array, options = {}) # rubocop:disable MethodLength - return if array.empty? - - formats = [] - indent = options[:indent].to_i - colwidth = options[:colwidth] - options[:truncate] = terminal_width if options[:truncate] == true - - formats << "%-#{colwidth + 2}s".dup if colwidth - start = colwidth ? 1 : 0 - - colcount = array.max { |a, b| a.size <=> b.size }.size - - maximas = [] - - start.upto(colcount - 1) do |index| - maxima = array.map { |row| row[index] ? row[index].to_s.size : 0 }.max - maximas << maxima - formats << if index == colcount - 1 - # Don't output 2 trailing spaces when printing the last column - "%-s".dup - else - "%-#{maxima + 2}s".dup - end - end - - formats[0] = formats[0].insert(0, " " * indent) - formats << "%s" - - array.each do |row| - sentence = "".dup - - row.each_with_index do |column, index| - maxima = maximas[index] - - f = if column.is_a?(Numeric) - if index == row.size - 1 - # Don't output 2 trailing spaces when printing the last column - "%#{maxima}s" - else - "%#{maxima}s " - end - else - formats[index] - end - sentence << f % column.to_s - end - - sentence = truncate(sentence, options[:truncate]) if options[:truncate] - stdout.puts sentence - end + def print_table(array, options = {}) # rubocop:disable Metrics/MethodLength + printer = TablePrinter.new(stdout, options) + printer.print(array) end # Prints a long string, word-wrapping the text to the current width of the @@ -217,20 +192,8 @@ class Bundler::Thor # indent<Integer>:: Indent each line of the printed paragraph by indent value. # def print_wrapped(message, options = {}) - indent = options[:indent] || 0 - width = terminal_width - indent - paras = message.split("\n\n") - - paras.map! do |unwrapped| - unwrapped.strip.tr("\n", " ").squeeze(" ").gsub(/.{1,#{width}}(?:\s|\Z)/) { ($& + 5.chr).gsub(/\n\005/, "\n").gsub(/\005/, "\n") } - end - - paras.each do |para| - para.split("\n").each do |line| - stdout.puts line.insert(0, " " * indent) - end - stdout.puts unless para == paras.last - end + printer = WrappedPrinter.new(stdout, options) + printer.print(message) end # Deals with file collision and returns true if the file should be @@ -239,16 +202,16 @@ class Bundler::Thor # # ==== Parameters # destination<String>:: the destination file to solve conflicts - # block<Proc>:: an optional block that returns the value to be used in diff + # block<Proc>:: an optional block that returns the value to be used in diff and merge # def file_collision(destination) return true if @always_force - options = block_given? ? "[Ynaqdh]" : "[Ynaqh]" + options = block_given? ? "[Ynaqdhm]" : "[Ynaqh]" loop do answer = ask( %[Overwrite #{destination}? (enter "h" for help) #{options}], - :add_to_history => false + add_to_history: false ) case answer @@ -267,25 +230,19 @@ class Bundler::Thor when is?(:diff) show_diff(destination, yield) if block_given? say "Retrying..." + when is?(:merge) + if block_given? && !merge_tool.empty? + merge(destination, yield) + return nil + end + + say "Please specify merge tool to `THOR_MERGE` env." else - say file_collision_help + say file_collision_help(block_given?) end end end - # This code was copied from Rake, available under MIT-LICENSE - # Copyright (c) 2003, 2004 Jim Weirich - def terminal_width - result = if ENV["THOR_COLUMNS"] - ENV["THOR_COLUMNS"].to_i - else - unix? ? dynamic_width : 80 - end - result < 10 ? 80 : result - rescue - 80 - end - # Called if something goes wrong during the execution. This is used by Bundler::Thor # internally and should not be used inside your scripts. If something went # wrong, you can always raise an exception. If you raise a Bundler::Thor::Error, it @@ -336,22 +293,28 @@ class Bundler::Thor end end - def file_collision_help #:nodoc: - <<-HELP + def file_collision_help(block_given) #:nodoc: + help = <<-HELP Y - yes, overwrite n - no, do not overwrite a - all, overwrite this and all others q - quit, abort - d - diff, show the differences between the old and the new h - help, show this help HELP + if block_given + help << <<-HELP + d - diff, show the differences between the old and the new + m - merge, run merge tool + HELP + end + help end def show_diff(destination, content) #:nodoc: diff_cmd = ENV["THOR_DIFF"] || ENV["RAILS_DIFF"] || "diff -u" require "tempfile" - Tempfile.open(File.basename(destination), File.dirname(destination)) do |temp| + Tempfile.open(File.basename(destination), File.dirname(destination), binmode: true) do |temp| temp.write content temp.rewind system %(#{diff_cmd} "#{destination}" "#{temp.path}") @@ -362,46 +325,8 @@ class Bundler::Thor mute? || (base && base.options[:quiet]) end - # Calculate the dynamic width of the terminal - def dynamic_width - @dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput) - end - - def dynamic_width_stty - `stty size 2>/dev/null`.split[1].to_i - end - - def dynamic_width_tput - `tput cols 2>/dev/null`.to_i - end - def unix? - RUBY_PLATFORM =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris|irix|hpux)/i - end - - def truncate(string, width) - as_unicode do - chars = string.chars.to_a - if chars.length <= width - chars.join - else - chars[0, width - 3].join + "..." - end - end - end - - if "".respond_to?(:encode) - def as_unicode - yield - end - else - def as_unicode - old = $KCODE - $KCODE = "U" - yield - ensure - $KCODE = old - end + Terminal.unix? end def ask_simply(statement, color, options) @@ -423,15 +348,37 @@ class Bundler::Thor def ask_filtered(statement, color, options) answer_set = options[:limited_to] + case_insensitive = options.fetch(:case_insensitive, false) correct_answer = nil until correct_answer answers = answer_set.join(", ") answer = ask_simply("#{statement} [#{answers}]", color, options) - correct_answer = answer_set.include?(answer) ? answer : nil + correct_answer = answer_match(answer_set, answer, case_insensitive) say("Your response must be one of: [#{answers}]. Please try again.") unless correct_answer end correct_answer end + + def answer_match(possibilities, answer, case_insensitive) + if case_insensitive + possibilities.detect{ |possibility| possibility.downcase == answer.downcase } + else + possibilities.detect{ |possibility| possibility == answer } + end + end + + def merge(destination, content) #:nodoc: + require "tempfile" + Tempfile.open([File.basename(destination), File.extname(destination)], File.dirname(destination)) do |temp| + temp.write content + temp.rewind + system(merge_tool, temp.path, destination) + end + end + + def merge_tool #:nodoc: + @merge_tool ||= ENV["THOR_MERGE"] || "git difftool --no-index" + end end end end diff --git a/lib/bundler/vendor/thor/lib/thor/shell/color.rb b/lib/bundler/vendor/thor/lib/thor/shell/color.rb index da289cb50c..5d708fadca 100644 --- a/lib/bundler/vendor/thor/lib/thor/shell/color.rb +++ b/lib/bundler/vendor/thor/lib/thor/shell/color.rb @@ -1,4 +1,4 @@ -require "bundler/vendor/thor/lib/thor/shell/basic" +require_relative "basic" class Bundler::Thor module Shell @@ -97,52 +97,15 @@ class Bundler::Thor protected def can_display_colors? - stdout.tty? + are_colors_supported? && !are_colors_disabled? end - # Overwrite show_diff to show diff with colors if Diff::LCS is - # available. - # - def show_diff(destination, content) #:nodoc: - if diff_lcs_loaded? && ENV["THOR_DIFF"].nil? && ENV["RAILS_DIFF"].nil? - actual = File.binread(destination).to_s.split("\n") - content = content.to_s.split("\n") - - Diff::LCS.sdiff(actual, content).each do |diff| - output_diff_line(diff) - end - else - super - end + def are_colors_supported? + stdout.tty? && ENV["TERM"] != "dumb" end - 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 - end - end - - # Check if Diff::LCS is loaded. If it is, use it to create pretty output - # for diff. - # - def diff_lcs_loaded? #:nodoc: - return true if defined?(Diff::LCS) - return @diff_lcs_loaded unless @diff_lcs_loaded.nil? - - @diff_lcs_loaded = begin - require "diff/lcs" - true - rescue LoadError - false - end + def are_colors_disabled? + !ENV["NO_COLOR"].nil? && !ENV["NO_COLOR"].empty? end end end diff --git a/lib/bundler/vendor/thor/lib/thor/shell/column_printer.rb b/lib/bundler/vendor/thor/lib/thor/shell/column_printer.rb new file mode 100644 index 0000000000..56a9e6181b --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/shell/column_printer.rb @@ -0,0 +1,29 @@ +require_relative "terminal" + +class Bundler::Thor + module Shell + class ColumnPrinter + attr_reader :stdout, :options + + def initialize(stdout, options = {}) + @stdout = stdout + @options = options + @indent = options[:indent].to_i + end + + def print(array) + return if array.empty? + colwidth = (array.map { |el| el.to_s.size }.max || 0) + 2 + array.each_with_index do |value, index| + # Don't output trailing spaces when printing the last column + if ((((index + 1) % (Terminal.terminal_width / colwidth))).zero? && !index.zero?) || index + 1 == array.length + stdout.puts value + else + stdout.printf("%-#{colwidth}s", value) + end + end + end + end + end +end + diff --git a/lib/bundler/vendor/thor/lib/thor/shell/html.rb b/lib/bundler/vendor/thor/lib/thor/shell/html.rb index 83d2054988..a0a8520e5c 100644 --- a/lib/bundler/vendor/thor/lib/thor/shell/html.rb +++ b/lib/bundler/vendor/thor/lib/thor/shell/html.rb @@ -1,4 +1,4 @@ -require "bundler/vendor/thor/lib/thor/shell/basic" +require_relative "basic" class Bundler::Thor module Shell @@ -51,20 +51,20 @@ class Bundler::Thor def set_color(string, *colors) if colors.all? { |color| color.is_a?(Symbol) || color.is_a?(String) } html_colors = colors.map { |color| lookup_color(color) } - "<span style=\"#{html_colors.join('; ')};\">#{string}</span>" + "<span style=\"#{html_colors.join('; ')};\">#{Bundler::Thor::Util.escape_html(string)}</span>" else color, bold = colors html_color = self.class.const_get(color.to_s.upcase) if color.is_a?(Symbol) styles = [html_color] styles << BOLD if bold - "<span style=\"#{styles.join('; ')};\">#{string}</span>" + "<span style=\"#{styles.join('; ')};\">#{Bundler::Thor::Util.escape_html(string)}</span>" end end # Ask something to the user and receives a response. # # ==== Example - # ask("What is your name?") + # ask("What is your name?") # # TODO: Implement #ask for Bundler::Thor::Shell::HTML def ask(statement, color = nil) @@ -76,51 +76,6 @@ class Bundler::Thor def can_display_colors? true end - - # Overwrite show_diff to show diff with colors if Diff::LCS is - # available. - # - def show_diff(destination, content) #:nodoc: - if diff_lcs_loaded? && ENV["THOR_DIFF"].nil? && ENV["RAILS_DIFF"].nil? - actual = File.binread(destination).to_s.split("\n") - content = content.to_s.split("\n") - - Diff::LCS.sdiff(actual, content).each do |diff| - output_diff_line(diff) - end - else - super - end - end - - 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 - end - end - - # Check if Diff::LCS is loaded. If it is, use it to create pretty output - # for diff. - # - def diff_lcs_loaded? #:nodoc: - return true if defined?(Diff::LCS) - return @diff_lcs_loaded unless @diff_lcs_loaded.nil? - - @diff_lcs_loaded = begin - require "diff/lcs" - true - rescue LoadError - false - end - end end end end diff --git a/lib/bundler/vendor/thor/lib/thor/shell/table_printer.rb b/lib/bundler/vendor/thor/lib/thor/shell/table_printer.rb new file mode 100644 index 0000000000..dee3614753 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/shell/table_printer.rb @@ -0,0 +1,118 @@ +require_relative "column_printer" +require_relative "terminal" + +class Bundler::Thor + module Shell + class TablePrinter < ColumnPrinter + BORDER_SEPARATOR = :separator + + def initialize(stdout, options = {}) + super + @formats = [] + @maximas = [] + @colwidth = options[:colwidth] + @truncate = options[:truncate] == true ? Terminal.terminal_width : options[:truncate] + @padding = 1 + end + + def print(array) + return if array.empty? + + prepare(array) + + print_border_separator if options[:borders] + + array.each do |row| + if options[:borders] && row == BORDER_SEPARATOR + print_border_separator + next + end + + sentence = "".dup + + row.each_with_index do |column, index| + sentence << format_cell(column, row.size, index) + end + + sentence = truncate(sentence) + sentence << "|" if options[:borders] + stdout.puts indentation + sentence + + end + print_border_separator if options[:borders] + end + + private + + def prepare(array) + array = array.reject{|row| row == BORDER_SEPARATOR } + + @formats << "%-#{@colwidth + 2}s".dup if @colwidth + start = @colwidth ? 1 : 0 + + colcount = array.max { |a, b| a.size <=> b.size }.size + + start.upto(colcount - 1) do |index| + maxima = array.map { |row| row[index] ? row[index].to_s.size : 0 }.max + + @maximas << maxima + @formats << if options[:borders] + "%-#{maxima}s".dup + elsif index == colcount - 1 + # Don't output 2 trailing spaces when printing the last column + "%-s".dup + else + "%-#{maxima + 2}s".dup + end + end + + @formats << "%s" + end + + def format_cell(column, row_size, index) + maxima = @maximas[index] + + f = if column.is_a?(Numeric) + if options[:borders] + # With borders we handle padding separately + "%#{maxima}s" + elsif index == row_size - 1 + # Don't output 2 trailing spaces when printing the last column + "%#{maxima}s" + else + "%#{maxima}s " + end + else + @formats[index] + end + + cell = "".dup + cell << "|" + " " * @padding if options[:borders] + cell << f % column.to_s + cell << " " * @padding if options[:borders] + cell + end + + def print_border_separator + separator = @maximas.map do |maxima| + "+" + "-" * (maxima + 2 * @padding) + end + stdout.puts indentation + separator.join + "+" + end + + def truncate(string) + return string unless @truncate + chars = string.chars.to_a + if chars.length <= @truncate + chars.join + else + chars[0, @truncate - 3 - @indent].join + "..." + end + end + + def indentation + " " * @indent + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/shell/terminal.rb b/lib/bundler/vendor/thor/lib/thor/shell/terminal.rb new file mode 100644 index 0000000000..2c60684308 --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/shell/terminal.rb @@ -0,0 +1,42 @@ +class Bundler::Thor + module Shell + module Terminal + DEFAULT_TERMINAL_WIDTH = 80 + + class << self + # This code was copied from Rake, available under MIT-LICENSE + # Copyright (c) 2003, 2004 Jim Weirich + def terminal_width + result = if ENV["THOR_COLUMNS"] + ENV["THOR_COLUMNS"].to_i + else + unix? ? dynamic_width : DEFAULT_TERMINAL_WIDTH + end + result < 10 ? DEFAULT_TERMINAL_WIDTH : result + rescue + DEFAULT_TERMINAL_WIDTH + end + + def unix? + RUBY_PLATFORM =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris)/i + end + + private + + # Calculate the dynamic width of the terminal + def dynamic_width + @dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput) + end + + def dynamic_width_stty + `stty size 2>/dev/null`.split[1].to_i + end + + def dynamic_width_tput + `tput cols 2>/dev/null`.to_i + end + + end + end + end +end diff --git a/lib/bundler/vendor/thor/lib/thor/shell/wrapped_printer.rb b/lib/bundler/vendor/thor/lib/thor/shell/wrapped_printer.rb new file mode 100644 index 0000000000..ba88e952db --- /dev/null +++ b/lib/bundler/vendor/thor/lib/thor/shell/wrapped_printer.rb @@ -0,0 +1,38 @@ +require_relative "column_printer" +require_relative "terminal" + +class Bundler::Thor + module Shell + class WrappedPrinter < ColumnPrinter + def print(message) + width = Terminal.terminal_width - @indent + paras = message.split("\n\n") + + paras.map! do |unwrapped| + words = unwrapped.split(" ") + counter = words.first.length + words.inject do |memo, word| + word = word.gsub(/\n\005/, "\n").gsub(/\005/, "\n") + counter = 0 if word.include? "\n" + if (counter + word.length + 1) < width + memo = "#{memo} #{word}" + counter += (word.length + 1) + else + memo = "#{memo}\n#{word}" + counter = word.length + end + memo + end + end.compact! + + paras.each do |para| + para.split("\n").each do |line| + stdout.puts line.insert(0, " " * @indent) + end + stdout.puts unless para == paras.last + end + end + end + end +end + diff --git a/lib/bundler/vendor/thor/lib/thor/util.rb b/lib/bundler/vendor/thor/lib/thor/util.rb index 5d03177a28..cd8f9ece87 100644 --- a/lib/bundler/vendor/thor/lib/thor/util.rb +++ b/lib/bundler/vendor/thor/lib/thor/util.rb @@ -27,7 +27,7 @@ class Bundler::Thor end # Receives a constant and converts it to a Bundler::Thor namespace. Since Bundler::Thor - # commands can be added to a sandbox, this method is also responsable for + # commands can be added to a sandbox, this method is also responsible for # removing the sandbox namespace. # # This method should not be used in general because it's used to deal with @@ -90,7 +90,7 @@ class Bundler::Thor def snake_case(str) return str.downcase if str =~ /^[A-Z_]+$/ str.gsub(/\B[A-Z]/, '_\&').squeeze("_") =~ /_*(.*)/ - $+.downcase + Regexp.last_match(-1).downcase end # Receives a string and convert it to camel case. camel_case returns CamelCase. @@ -130,9 +130,10 @@ class Bundler::Thor # 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 = Bundler::Thor::Util.find_by_namespace(pieces.join(":")) + *pieces, command = namespace.split(":") + namespace = pieces.join(":") + namespace = "default" if namespace.empty? + klass = Bundler::Thor::Base.subclasses.detect { |thor| thor.namespace == namespace && thor.command_exists?(command) } end unless klass # look for a Bundler::Thor::Group with the right name klass = Bundler::Thor::Util.find_by_namespace(namespace) @@ -150,7 +151,7 @@ class Bundler::Thor # inside the sandbox to avoid namespacing conflicts. # def load_thorfile(path, content = nil, debug = false) - content ||= File.binread(path) + content ||= File.read(path) begin Bundler::Thor::Sandbox.class_eval(content, path) @@ -189,7 +190,7 @@ class Bundler::Thor # Returns the root where thor files are located, depending on the OS. # def thor_root - File.join(user_home, ".thor").tr('\\', "/") + File.join(user_home, ".thor").tr("\\", "/") end # Returns the files in the thor root. On Windows thor_root will be something @@ -211,7 +212,7 @@ class Bundler::Thor # def globs_for(path) path = escape_globs(path) - ["#{path}/Bundler::Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"] + ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/**/*.thor"] end # Return the path to the ruby interpreter taking into account multiple @@ -236,7 +237,7 @@ class Bundler::Thor # symlink points to 'ruby_install_name' ruby = alternate_ruby if linked_ruby == ruby_name || linked_ruby == ruby end - rescue NotImplementedError # rubocop:disable HandleExceptions + rescue NotImplementedError # rubocop:disable Lint/HandleExceptions # just ignore on windows end end @@ -263,6 +264,22 @@ class Bundler::Thor def escape_globs(path) path.to_s.gsub(/[*?{}\[\]]/, '\\\\\\&') end + + # Returns a string that has had any HTML characters escaped. + # + # ==== Examples + # + # Bundler::Thor::Util.escape_html('<div>') # => "<div>" + # + # ==== Parameters + # String + # + # ==== Returns + # String + # + def escape_html(string) + CGI.escapeHTML(string) + end end end end diff --git a/lib/bundler/vendor/thor/lib/thor/version.rb b/lib/bundler/vendor/thor/lib/thor/version.rb index df8f18821a..5474a2f71b 100644 --- a/lib/bundler/vendor/thor/lib/thor/version.rb +++ b/lib/bundler/vendor/thor/lib/thor/version.rb @@ -1,3 +1,3 @@ class Bundler::Thor - VERSION = "0.20.0" + VERSION = "1.4.0" end diff --git a/lib/bundler/vendor/tsort/lib/tsort.rb b/lib/bundler/vendor/tsort/lib/tsort.rb new file mode 100644 index 0000000000..cf8731f760 --- /dev/null +++ b/lib/bundler/vendor/tsort/lib/tsort.rb @@ -0,0 +1,455 @@ +# frozen_string_literal: true + +#-- +# tsort.rb - provides a module for topological sorting and strongly connected components. +#++ +# + +# +# Bundler::TSort implements topological sorting using Tarjan's algorithm for +# strongly connected components. +# +# Bundler::TSort is designed to be able to be used with any object which can be +# interpreted as a directed graph. +# +# Bundler::TSort requires two methods to interpret an object as a graph, +# tsort_each_node and tsort_each_child. +# +# * tsort_each_node is used to iterate for all nodes over a graph. +# * tsort_each_child is used to iterate for child nodes of a given node. +# +# The equality of nodes are defined by eql? and hash since +# Bundler::TSort uses Hash internally. +# +# == A Simple Example +# +# The following example demonstrates how to mix the Bundler::TSort module into an +# existing class (in this case, Hash). Here, we're treating each key in +# the hash as a node in the graph, and so we simply alias the required +# #tsort_each_node method to Hash's #each_key method. For each key in the +# hash, the associated value is an array of the node's child nodes. This +# choice in turn leads to our implementation of the required #tsort_each_child +# method, which fetches the array of child nodes and then iterates over that +# array using the user-supplied block. +# +# require 'bundler/vendor/tsort/lib/tsort' +# +# class Hash +# include Bundler::TSort +# alias tsort_each_node each_key +# def tsort_each_child(node, &block) +# fetch(node).each(&block) +# end +# end +# +# {1=>[2, 3], 2=>[3], 3=>[], 4=>[]}.tsort +# #=> [3, 2, 1, 4] +# +# {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}.strongly_connected_components +# #=> [[4], [2, 3], [1]] +# +# == A More Realistic Example +# +# A very simple `make' like tool can be implemented as follows: +# +# require 'bundler/vendor/tsort/lib/tsort' +# +# class Make +# def initialize +# @dep = {} +# @dep.default = [] +# end +# +# def rule(outputs, inputs=[], &block) +# triple = [outputs, inputs, block] +# outputs.each {|f| @dep[f] = [triple]} +# @dep[triple] = inputs +# end +# +# def build(target) +# each_strongly_connected_component_from(target) {|ns| +# if ns.length != 1 +# fs = ns.delete_if {|n| Array === n} +# raise Bundler::TSort::Cyclic.new("cyclic dependencies: #{fs.join ', '}") +# end +# n = ns.first +# if Array === n +# outputs, inputs, block = n +# inputs_time = inputs.map {|f| File.mtime f}.max +# begin +# outputs_time = outputs.map {|f| File.mtime f}.min +# rescue Errno::ENOENT +# outputs_time = nil +# end +# if outputs_time == nil || +# inputs_time != nil && outputs_time <= inputs_time +# sleep 1 if inputs_time != nil && inputs_time.to_i == Time.now.to_i +# block.call +# end +# end +# } +# end +# +# def tsort_each_child(node, &block) +# @dep[node].each(&block) +# end +# include Bundler::TSort +# end +# +# def command(arg) +# print arg, "\n" +# system arg +# end +# +# m = Make.new +# m.rule(%w[t1]) { command 'date > t1' } +# m.rule(%w[t2]) { command 'date > t2' } +# m.rule(%w[t3]) { command 'date > t3' } +# m.rule(%w[t4], %w[t1 t3]) { command 'cat t1 t3 > t4' } +# m.rule(%w[t5], %w[t4 t2]) { command 'cat t4 t2 > t5' } +# m.build('t5') +# +# == Bugs +# +# * 'tsort.rb' is wrong name because this library uses +# Tarjan's algorithm for strongly connected components. +# Although 'strongly_connected_components.rb' is correct but too long. +# +# == References +# +# R. E. Tarjan, "Depth First Search and Linear Graph Algorithms", +# <em>SIAM Journal on Computing</em>, Vol. 1, No. 2, pp. 146-160, June 1972. +# + +module Bundler::TSort + + VERSION = "0.2.0" + + class Cyclic < StandardError + end + + # Returns a topologically sorted array of nodes. + # The array is sorted from children to parents, i.e. + # the first element has no child and the last node has no parent. + # + # If there is a cycle, Bundler::TSort::Cyclic is raised. + # + # class G + # include Bundler::TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # p graph.tsort #=> [4, 2, 3, 1] + # + # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}) + # p graph.tsort # raises Bundler::TSort::Cyclic + # + def tsort + each_node = method(:tsort_each_node) + each_child = method(:tsort_each_child) + Bundler::TSort.tsort(each_node, each_child) + end + + # Returns a topologically sorted array of nodes. + # The array is sorted from children to parents, i.e. + # the first element has no child and the last node has no parent. + # + # The graph is represented by _each_node_ and _each_child_. + # _each_node_ should have +call+ method which yields for each node in the graph. + # _each_child_ should have +call+ method which takes a node argument and yields for each child node. + # + # If there is a cycle, Bundler::TSort::Cyclic is raised. + # + # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # p Bundler::TSort.tsort(each_node, each_child) #=> [4, 2, 3, 1] + # + # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # p Bundler::TSort.tsort(each_node, each_child) # raises Bundler::TSort::Cyclic + # + def self.tsort(each_node, each_child) + tsort_each(each_node, each_child).to_a + end + + # The iterator version of the #tsort method. + # <tt><em>obj</em>.tsort_each</tt> is similar to <tt><em>obj</em>.tsort.each</tt>, but + # modification of _obj_ during the iteration may lead to unexpected results. + # + # #tsort_each returns +nil+. + # If there is a cycle, Bundler::TSort::Cyclic is raised. + # + # class G + # include Bundler::TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # graph.tsort_each {|n| p n } + # #=> 4 + # # 2 + # # 3 + # # 1 + # + def tsort_each(&block) # :yields: node + each_node = method(:tsort_each_node) + each_child = method(:tsort_each_child) + Bundler::TSort.tsort_each(each_node, each_child, &block) + end + + # The iterator version of the Bundler::TSort.tsort method. + # + # The graph is represented by _each_node_ and _each_child_. + # _each_node_ should have +call+ method which yields for each node in the graph. + # _each_child_ should have +call+ method which takes a node argument and yields for each child node. + # + # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # Bundler::TSort.tsort_each(each_node, each_child) {|n| p n } + # #=> 4 + # # 2 + # # 3 + # # 1 + # + def self.tsort_each(each_node, each_child) # :yields: node + return to_enum(__method__, each_node, each_child) unless block_given? + + each_strongly_connected_component(each_node, each_child) {|component| + if component.size == 1 + yield component.first + else + raise Cyclic.new("topological sort failed: #{component.inspect}") + end + } + end + + # Returns strongly connected components as an array of arrays of nodes. + # The array is sorted from children to parents. + # Each elements of the array represents a strongly connected component. + # + # class G + # include Bundler::TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # p graph.strongly_connected_components #=> [[4], [2], [3], [1]] + # + # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}) + # p graph.strongly_connected_components #=> [[4], [2, 3], [1]] + # + def strongly_connected_components + each_node = method(:tsort_each_node) + each_child = method(:tsort_each_child) + Bundler::TSort.strongly_connected_components(each_node, each_child) + end + + # Returns strongly connected components as an array of arrays of nodes. + # The array is sorted from children to parents. + # Each elements of the array represents a strongly connected component. + # + # The graph is represented by _each_node_ and _each_child_. + # _each_node_ should have +call+ method which yields for each node in the graph. + # _each_child_ should have +call+ method which takes a node argument and yields for each child node. + # + # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # p Bundler::TSort.strongly_connected_components(each_node, each_child) + # #=> [[4], [2], [3], [1]] + # + # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # p Bundler::TSort.strongly_connected_components(each_node, each_child) + # #=> [[4], [2, 3], [1]] + # + def self.strongly_connected_components(each_node, each_child) + each_strongly_connected_component(each_node, each_child).to_a + end + + # The iterator version of the #strongly_connected_components method. + # <tt><em>obj</em>.each_strongly_connected_component</tt> is similar to + # <tt><em>obj</em>.strongly_connected_components.each</tt>, but + # modification of _obj_ during the iteration may lead to unexpected results. + # + # #each_strongly_connected_component returns +nil+. + # + # class G + # include Bundler::TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # graph.each_strongly_connected_component {|scc| p scc } + # #=> [4] + # # [2] + # # [3] + # # [1] + # + # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}) + # graph.each_strongly_connected_component {|scc| p scc } + # #=> [4] + # # [2, 3] + # # [1] + # + def each_strongly_connected_component(&block) # :yields: nodes + each_node = method(:tsort_each_node) + each_child = method(:tsort_each_child) + Bundler::TSort.each_strongly_connected_component(each_node, each_child, &block) + end + + # The iterator version of the Bundler::TSort.strongly_connected_components method. + # + # The graph is represented by _each_node_ and _each_child_. + # _each_node_ should have +call+ method which yields for each node in the graph. + # _each_child_ should have +call+ method which takes a node argument and yields for each child node. + # + # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # Bundler::TSort.each_strongly_connected_component(each_node, each_child) {|scc| p scc } + # #=> [4] + # # [2] + # # [3] + # # [1] + # + # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # Bundler::TSort.each_strongly_connected_component(each_node, each_child) {|scc| p scc } + # #=> [4] + # # [2, 3] + # # [1] + # + def self.each_strongly_connected_component(each_node, each_child) # :yields: nodes + return to_enum(__method__, each_node, each_child) unless block_given? + + id_map = {} + stack = [] + each_node.call {|node| + unless id_map.include? node + each_strongly_connected_component_from(node, each_child, id_map, stack) {|c| + yield c + } + end + } + nil + end + + # Iterates over strongly connected component in the subgraph reachable from + # _node_. + # + # Return value is unspecified. + # + # #each_strongly_connected_component_from doesn't call #tsort_each_node. + # + # class G + # include Bundler::TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # graph.each_strongly_connected_component_from(2) {|scc| p scc } + # #=> [4] + # # [2] + # + # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}) + # graph.each_strongly_connected_component_from(2) {|scc| p scc } + # #=> [4] + # # [2, 3] + # + def each_strongly_connected_component_from(node, id_map={}, stack=[], &block) # :yields: nodes + Bundler::TSort.each_strongly_connected_component_from(node, method(:tsort_each_child), id_map, stack, &block) + end + + # Iterates over strongly connected components in a graph. + # The graph is represented by _node_ and _each_child_. + # + # _node_ is the first node. + # _each_child_ should have +call+ method which takes a node argument + # and yields for each child node. + # + # Return value is unspecified. + # + # #Bundler::TSort.each_strongly_connected_component_from is a class method and + # it doesn't need a class to represent a graph which includes Bundler::TSort. + # + # graph = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]} + # each_child = lambda {|n, &b| graph[n].each(&b) } + # Bundler::TSort.each_strongly_connected_component_from(1, each_child) {|scc| + # p scc + # } + # #=> [4] + # # [2, 3] + # # [1] + # + def self.each_strongly_connected_component_from(node, each_child, id_map={}, stack=[]) # :yields: nodes + return to_enum(__method__, node, each_child, id_map, stack) unless block_given? + + minimum_id = node_id = id_map[node] = id_map.size + stack_length = stack.length + stack << node + + each_child.call(node) {|child| + if id_map.include? child + child_id = id_map[child] + minimum_id = child_id if child_id && child_id < minimum_id + else + sub_minimum_id = + each_strongly_connected_component_from(child, each_child, id_map, stack) {|c| + yield c + } + minimum_id = sub_minimum_id if sub_minimum_id < minimum_id + end + } + + if node_id == minimum_id + component = stack.slice!(stack_length .. -1) + component.each {|n| id_map[n] = nil} + yield component + end + + minimum_id + end + + # Should be implemented by a extended class. + # + # #tsort_each_node is used to iterate for all nodes over a graph. + # + def tsort_each_node # :yields: node + raise NotImplementedError.new + end + + # Should be implemented by a extended class. + # + # #tsort_each_child is used to iterate for child nodes of _node_. + # + def tsort_each_child(node) # :yields: child + raise NotImplementedError.new + end +end diff --git a/lib/bundler/vendor/uri/lib/uri.rb b/lib/bundler/vendor/uri/lib/uri.rb new file mode 100644 index 0000000000..57b380c480 --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: false +# Bundler::URI is a module providing classes to handle Uniform Resource Identifiers +# (RFC2396[https://www.rfc-editor.org/rfc/rfc2396]). +# +# == Features +# +# * Uniform way of handling URIs. +# * Flexibility to introduce custom Bundler::URI schemes. +# * Flexibility to have an alternate Bundler::URI::Parser (or just different patterns +# and regexp's). +# +# == Basic example +# +# require 'bundler/vendor/uri/lib/uri' +# +# uri = Bundler::URI("http://foo.com/posts?id=30&limit=5#time=1305298413") +# #=> #<Bundler::URI::HTTP http://foo.com/posts?id=30&limit=5#time=1305298413> +# +# uri.scheme #=> "http" +# uri.host #=> "foo.com" +# uri.path #=> "/posts" +# uri.query #=> "id=30&limit=5" +# uri.fragment #=> "time=1305298413" +# +# uri.to_s #=> "http://foo.com/posts?id=30&limit=5#time=1305298413" +# +# == Adding custom URIs +# +# module Bundler::URI +# class RSYNC < Generic +# DEFAULT_PORT = 873 +# end +# register_scheme 'RSYNC', RSYNC +# end +# #=> Bundler::URI::RSYNC +# +# Bundler::URI.scheme_list +# #=> {"FILE"=>Bundler::URI::File, "FTP"=>Bundler::URI::FTP, "HTTP"=>Bundler::URI::HTTP, +# # "HTTPS"=>Bundler::URI::HTTPS, "LDAP"=>Bundler::URI::LDAP, "LDAPS"=>Bundler::URI::LDAPS, +# # "MAILTO"=>Bundler::URI::MailTo, "RSYNC"=>Bundler::URI::RSYNC} +# +# uri = Bundler::URI("rsync://rsync.foo.com") +# #=> #<Bundler::URI::RSYNC rsync://rsync.foo.com> +# +# == RFC References +# +# A good place to view an RFC spec is http://www.ietf.org/rfc.html. +# +# Here is a list of all related RFC's: +# - RFC822[https://www.rfc-editor.org/rfc/rfc822] +# - RFC1738[https://www.rfc-editor.org/rfc/rfc1738] +# - RFC2255[https://www.rfc-editor.org/rfc/rfc2255] +# - RFC2368[https://www.rfc-editor.org/rfc/rfc2368] +# - RFC2373[https://www.rfc-editor.org/rfc/rfc2373] +# - RFC2396[https://www.rfc-editor.org/rfc/rfc2396] +# - RFC2732[https://www.rfc-editor.org/rfc/rfc2732] +# - RFC3986[https://www.rfc-editor.org/rfc/rfc3986] +# +# == Class tree +# +# - Bundler::URI::Generic (in uri/generic.rb) +# - Bundler::URI::File - (in uri/file.rb) +# - Bundler::URI::FTP - (in uri/ftp.rb) +# - Bundler::URI::HTTP - (in uri/http.rb) +# - Bundler::URI::HTTPS - (in uri/https.rb) +# - Bundler::URI::LDAP - (in uri/ldap.rb) +# - Bundler::URI::LDAPS - (in uri/ldaps.rb) +# - Bundler::URI::MailTo - (in uri/mailto.rb) +# - Bundler::URI::Parser - (in uri/common.rb) +# - Bundler::URI::REGEXP - (in uri/common.rb) +# - Bundler::URI::REGEXP::PATTERN - (in uri/common.rb) +# - Bundler::URI::Util - (in uri/common.rb) +# - Bundler::URI::Error - (in uri/common.rb) +# - Bundler::URI::InvalidURIError - (in uri/common.rb) +# - Bundler::URI::InvalidComponentError - (in uri/common.rb) +# - Bundler::URI::BadURIError - (in uri/common.rb) +# +# == Copyright Info +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# Documentation:: +# Akira Yamada <akira@ruby-lang.org> +# Dmitry V. Sabanin <sdmitry@lrn.ru> +# Vincent Batts <vbatts@hashbangbash.com> +# License:: +# Copyright (c) 2001 akira yamada <akira@ruby-lang.org> +# You can redistribute it and/or modify it under the same term as Ruby. +# + +module Bundler::URI +end + +require_relative 'uri/version' +require_relative 'uri/common' +require_relative 'uri/generic' +require_relative 'uri/file' +require_relative 'uri/ftp' +require_relative 'uri/http' +require_relative 'uri/https' +require_relative 'uri/ldap' +require_relative 'uri/ldaps' +require_relative 'uri/mailto' +require_relative 'uri/ws' +require_relative 'uri/wss' diff --git a/lib/bundler/vendor/uri/lib/uri/common.rb b/lib/bundler/vendor/uri/lib/uri/common.rb new file mode 100644 index 0000000000..38339119c5 --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri/common.rb @@ -0,0 +1,922 @@ +# frozen_string_literal: true +#-- +# = uri/common.rb +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# License:: +# You can redistribute it and/or modify it under the same term as Ruby. +# +# See Bundler::URI for general documentation +# + +require_relative "rfc2396_parser" +require_relative "rfc3986_parser" + +module Bundler::URI + # The default parser instance for RFC 2396. + RFC2396_PARSER = RFC2396_Parser.new + Ractor.make_shareable(RFC2396_PARSER) if defined?(Ractor) + + # The default parser instance for RFC 3986. + RFC3986_PARSER = RFC3986_Parser.new + Ractor.make_shareable(RFC3986_PARSER) if defined?(Ractor) + + # The default parser instance. + DEFAULT_PARSER = RFC3986_PARSER + Ractor.make_shareable(DEFAULT_PARSER) if defined?(Ractor) + + # Set the default parser instance. + def self.parser=(parser = RFC3986_PARSER) + remove_const(:Parser) if defined?(::Bundler::URI::Parser) + const_set("Parser", parser.class) + + remove_const(:PARSER) if defined?(::Bundler::URI::PARSER) + const_set("PARSER", parser) + + remove_const(:REGEXP) if defined?(::Bundler::URI::REGEXP) + remove_const(:PATTERN) if defined?(::Bundler::URI::PATTERN) + if Parser == RFC2396_Parser + const_set("REGEXP", Bundler::URI::RFC2396_REGEXP) + const_set("PATTERN", Bundler::URI::RFC2396_REGEXP::PATTERN) + end + + Parser.new.regexp.each_pair do |sym, str| + remove_const(sym) if const_defined?(sym, false) + const_set(sym, str) + end + end + self.parser = RFC3986_PARSER + + def self.const_missing(const) # :nodoc: + if const == :REGEXP + warn "Bundler::URI::REGEXP is obsolete. Use Bundler::URI::RFC2396_REGEXP explicitly.", uplevel: 1 if $VERBOSE + Bundler::URI::RFC2396_REGEXP + elsif value = RFC2396_PARSER.regexp[const] + warn "Bundler::URI::#{const} is obsolete. Use Bundler::URI::RFC2396_PARSER.regexp[#{const.inspect}] explicitly.", uplevel: 1 if $VERBOSE + value + elsif value = RFC2396_Parser.const_get(const) + warn "Bundler::URI::#{const} is obsolete. Use Bundler::URI::RFC2396_Parser::#{const} explicitly.", uplevel: 1 if $VERBOSE + value + else + super + end + end + + module Util # :nodoc: + def make_components_hash(klass, array_hash) + tmp = {} + if array_hash.kind_of?(Array) && + array_hash.size == klass.component.size - 1 + klass.component[1..-1].each_index do |i| + begin + tmp[klass.component[i + 1]] = array_hash[i].clone + rescue TypeError + tmp[klass.component[i + 1]] = array_hash[i] + end + end + + elsif array_hash.kind_of?(Hash) + array_hash.each do |key, value| + begin + tmp[key] = value.clone + rescue TypeError + tmp[key] = value + end + end + else + raise ArgumentError, + "expected Array of or Hash of components of #{klass} (#{klass.component[1..-1].join(', ')})" + end + tmp[:scheme] = klass.to_s.sub(/\A.*::/, '').downcase + + return tmp + end + module_function :make_components_hash + end + + module Schemes # :nodoc: + class << self + ReservedChars = ".+-" + EscapedChars = "\u01C0\u01C1\u01C2" + # Use Lo category chars as escaped chars for TruffleRuby, which + # does not allow Symbol categories as identifiers. + + def escape(name) + unless name and name.ascii_only? + return nil + end + name.upcase.tr(ReservedChars, EscapedChars) + end + + def unescape(name) + name.tr(EscapedChars, ReservedChars).encode(Encoding::US_ASCII).upcase + end + + def find(name) + const_get(name, false) if name and const_defined?(name, false) + end + + def register(name, klass) + unless scheme = escape(name) + raise ArgumentError, "invalid character as scheme - #{name}" + end + const_set(scheme, klass) + end + + def list + constants.map { |name| + [unescape(name.to_s), const_get(name)] + }.to_h + end + end + end + private_constant :Schemes + + # Registers the given +klass+ as the class to be instantiated + # when parsing a \Bundler::URI with the given +scheme+: + # + # Bundler::URI.register_scheme('MS_SEARCH', Bundler::URI::Generic) # => Bundler::URI::Generic + # Bundler::URI.scheme_list['MS_SEARCH'] # => Bundler::URI::Generic + # + # Note that after calling String#upcase on +scheme+, it must be a valid + # constant name. + def self.register_scheme(scheme, klass) + Schemes.register(scheme, klass) + end + + # Returns a hash of the defined schemes: + # + # Bundler::URI.scheme_list + # # => + # {"MAILTO"=>Bundler::URI::MailTo, + # "LDAPS"=>Bundler::URI::LDAPS, + # "WS"=>Bundler::URI::WS, + # "HTTP"=>Bundler::URI::HTTP, + # "HTTPS"=>Bundler::URI::HTTPS, + # "LDAP"=>Bundler::URI::LDAP, + # "FILE"=>Bundler::URI::File, + # "FTP"=>Bundler::URI::FTP} + # + # Related: Bundler::URI.register_scheme. + def self.scheme_list + Schemes.list + end + + # :stopdoc: + INITIAL_SCHEMES = scheme_list + private_constant :INITIAL_SCHEMES + Ractor.make_shareable(INITIAL_SCHEMES) if defined?(Ractor) + # :startdoc: + + # Returns a new object constructed from the given +scheme+, +arguments+, + # and +default+: + # + # - The new object is an instance of <tt>Bundler::URI.scheme_list[scheme.upcase]</tt>. + # - The object is initialized by calling the class initializer + # using +scheme+ and +arguments+. + # See Bundler::URI::Generic.new. + # + # Examples: + # + # values = ['john.doe', 'www.example.com', '123', nil, '/forum/questions/', nil, 'tag=networking&order=newest', 'top'] + # Bundler::URI.for('https', *values) + # # => #<Bundler::URI::HTTPS https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top> + # Bundler::URI.for('foo', *values, default: Bundler::URI::HTTP) + # # => #<Bundler::URI::HTTP foo://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top> + # + def self.for(scheme, *arguments, default: Generic) + const_name = Schemes.escape(scheme) + + uri_class = INITIAL_SCHEMES[const_name] + uri_class ||= Schemes.find(const_name) + uri_class ||= default + + return uri_class.new(scheme, *arguments) + end + + # + # Base class for all Bundler::URI exceptions. + # + class Error < StandardError; end + # + # Not a Bundler::URI. + # + class InvalidURIError < Error; end + # + # Not a Bundler::URI component. + # + class InvalidComponentError < Error; end + # + # Bundler::URI is valid, bad usage is not. + # + class BadURIError < Error; end + + # Returns a 9-element array representing the parts of the \Bundler::URI + # formed from the string +uri+; + # each array element is a string or +nil+: + # + # names = %w[scheme userinfo host port registry path opaque query fragment] + # values = Bundler::URI.split('https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top') + # names.zip(values) + # # => + # [["scheme", "https"], + # ["userinfo", "john.doe"], + # ["host", "www.example.com"], + # ["port", "123"], + # ["registry", nil], + # ["path", "/forum/questions/"], + # ["opaque", nil], + # ["query", "tag=networking&order=newest"], + # ["fragment", "top"]] + # + def self.split(uri) + PARSER.split(uri) + end + + # Returns a new \Bundler::URI object constructed from the given string +uri+: + # + # Bundler::URI.parse('https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top') + # # => #<Bundler::URI::HTTPS https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top> + # Bundler::URI.parse('http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top') + # # => #<Bundler::URI::HTTP http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top> + # + # It's recommended to first Bundler::URI::RFC2396_PARSER.escape string +uri+ + # if it may contain invalid Bundler::URI characters. + # + def self.parse(uri) + PARSER.parse(uri) + end + + # Merges the given Bundler::URI strings +str+ + # per {RFC 2396}[https://www.rfc-editor.org/rfc/rfc2396.html]. + # + # Each string in +str+ is converted to an + # {RFC3986 Bundler::URI}[https://www.rfc-editor.org/rfc/rfc3986.html] before being merged. + # + # Examples: + # + # Bundler::URI.join("http://example.com/","main.rbx") + # # => #<Bundler::URI::HTTP http://example.com/main.rbx> + # + # Bundler::URI.join('http://example.com', 'foo') + # # => #<Bundler::URI::HTTP http://example.com/foo> + # + # Bundler::URI.join('http://example.com', '/foo', '/bar') + # # => #<Bundler::URI::HTTP http://example.com/bar> + # + # Bundler::URI.join('http://example.com', '/foo', 'bar') + # # => #<Bundler::URI::HTTP http://example.com/bar> + # + # Bundler::URI.join('http://example.com', '/foo/', 'bar') + # # => #<Bundler::URI::HTTP http://example.com/foo/bar> + # + def self.join(*str) + DEFAULT_PARSER.join(*str) + end + + # + # == Synopsis + # + # Bundler::URI::extract(str[, schemes][,&blk]) + # + # == Args + # + # +str+:: + # String to extract URIs from. + # +schemes+:: + # Limit Bundler::URI matching to specific schemes. + # + # == Description + # + # Extracts URIs from a string. If block given, iterates through all matched URIs. + # Returns nil if block given or array with matches. + # + # == Usage + # + # require "bundler/vendor/uri/lib/uri" + # + # Bundler::URI.extract("text here http://foo.example.org/bla and here mailto:test@example.com and here also.") + # # => ["http://foo.example.com/bla", "mailto:test@example.com"] + # + def self.extract(str, schemes = nil, &block) # :nodoc: + warn "Bundler::URI.extract is obsolete", uplevel: 1 if $VERBOSE + PARSER.extract(str, schemes, &block) + end + + # + # == Synopsis + # + # Bundler::URI::regexp([match_schemes]) + # + # == Args + # + # +match_schemes+:: + # Array of schemes. If given, resulting regexp matches to URIs + # whose scheme is one of the match_schemes. + # + # == Description + # + # Returns a Regexp object which matches to Bundler::URI-like strings. + # The Regexp object returned by this method includes arbitrary + # number of capture group (parentheses). Never rely on its number. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # # extract first Bundler::URI from html_string + # html_string.slice(Bundler::URI.regexp) + # + # # remove ftp URIs + # html_string.sub(Bundler::URI.regexp(['ftp']), '') + # + # # You should not rely on the number of parentheses + # html_string.scan(Bundler::URI.regexp) do |*matches| + # p $& + # end + # + def self.regexp(schemes = nil)# :nodoc: + warn "Bundler::URI.regexp is obsolete", uplevel: 1 if $VERBOSE + PARSER.make_regexp(schemes) + end + + TBLENCWWWCOMP_ = {} # :nodoc: + 256.times do |i| + TBLENCWWWCOMP_[-i.chr] = -('%%%02X' % i) + end + TBLENCURICOMP_ = TBLENCWWWCOMP_.dup.freeze # :nodoc: + TBLENCWWWCOMP_[' '] = '+' + TBLENCWWWCOMP_.freeze + TBLDECWWWCOMP_ = {} # :nodoc: + 256.times do |i| + h, l = i>>4, i&15 + TBLDECWWWCOMP_[-('%%%X%X' % [h, l])] = -i.chr + TBLDECWWWCOMP_[-('%%%x%X' % [h, l])] = -i.chr + TBLDECWWWCOMP_[-('%%%X%x' % [h, l])] = -i.chr + TBLDECWWWCOMP_[-('%%%x%x' % [h, l])] = -i.chr + end + TBLDECWWWCOMP_['+'] = ' ' + TBLDECWWWCOMP_.freeze + + # Returns a URL-encoded string derived from the given string +str+. + # + # The returned string: + # + # - Preserves: + # + # - Characters <tt>'*'</tt>, <tt>'.'</tt>, <tt>'-'</tt>, and <tt>'_'</tt>. + # - Character in ranges <tt>'a'..'z'</tt>, <tt>'A'..'Z'</tt>, + # and <tt>'0'..'9'</tt>. + # + # Example: + # + # Bundler::URI.encode_www_form_component('*.-_azAZ09') + # # => "*.-_azAZ09" + # + # - Converts: + # + # - Character <tt>' '</tt> to character <tt>'+'</tt>. + # - Any other character to "percent notation"; + # the percent notation for character <i>c</i> is <tt>'%%%X' % c.ord</tt>. + # + # Example: + # + # Bundler::URI.encode_www_form_component('Here are some punctuation characters: ,;?:') + # # => "Here+are+some+punctuation+characters%3A+%2C%3B%3F%3A" + # + # Encoding: + # + # - If +str+ has encoding Encoding::ASCII_8BIT, argument +enc+ is ignored. + # - Otherwise +str+ is converted first to Encoding::UTF_8 + # (with suitable character replacements), + # and then to encoding +enc+. + # + # In either case, the returned string has forced encoding Encoding::US_ASCII. + # + # Related: Bundler::URI.encode_uri_component (encodes <tt>' '</tt> as <tt>'%20'</tt>). + def self.encode_www_form_component(str, enc=nil) + _encode_uri_component(/[^*\-.0-9A-Z_a-z]/, TBLENCWWWCOMP_, str, enc) + end + + # Returns a string decoded from the given \URL-encoded string +str+. + # + # The given string is first encoded as Encoding::ASCII-8BIT (using String#b), + # then decoded (as below), and finally force-encoded to the given encoding +enc+. + # + # The returned string: + # + # - Preserves: + # + # - Characters <tt>'*'</tt>, <tt>'.'</tt>, <tt>'-'</tt>, and <tt>'_'</tt>. + # - Character in ranges <tt>'a'..'z'</tt>, <tt>'A'..'Z'</tt>, + # and <tt>'0'..'9'</tt>. + # + # Example: + # + # Bundler::URI.decode_www_form_component('*.-_azAZ09') + # # => "*.-_azAZ09" + # + # - Converts: + # + # - Character <tt>'+'</tt> to character <tt>' '</tt>. + # - Each "percent notation" to an ASCII character. + # + # Example: + # + # Bundler::URI.decode_www_form_component('Here+are+some+punctuation+characters%3A+%2C%3B%3F%3A') + # # => "Here are some punctuation characters: ,;?:" + # + # Related: Bundler::URI.decode_uri_component (preserves <tt>'+'</tt>). + def self.decode_www_form_component(str, enc=Encoding::UTF_8) + _decode_uri_component(/\+|%\h\h/, str, enc) + end + + # Like Bundler::URI.encode_www_form_component, except that <tt>' '</tt> (space) + # is encoded as <tt>'%20'</tt> (instead of <tt>'+'</tt>). + def self.encode_uri_component(str, enc=nil) + _encode_uri_component(/[^*\-.0-9A-Z_a-z]/, TBLENCURICOMP_, str, enc) + end + + # Like Bundler::URI.decode_www_form_component, except that <tt>'+'</tt> is preserved. + def self.decode_uri_component(str, enc=Encoding::UTF_8) + _decode_uri_component(/%\h\h/, str, enc) + end + + # Returns a string derived from the given string +str+ with + # Bundler::URI-encoded characters matching +regexp+ according to +table+. + def self._encode_uri_component(regexp, table, str, enc) + str = str.to_s.dup + if str.encoding != Encoding::ASCII_8BIT + if enc && enc != Encoding::ASCII_8BIT + str.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace) + str.encode!(enc, fallback: ->(x){"&##{x.ord};"}) + end + str.force_encoding(Encoding::ASCII_8BIT) + end + str.gsub!(regexp, table) + str.force_encoding(Encoding::US_ASCII) + end + private_class_method :_encode_uri_component + + # Returns a string decoding characters matching +regexp+ from the + # given \URL-encoded string +str+. + def self._decode_uri_component(regexp, str, enc) + raise ArgumentError, "invalid %-encoding (#{str})" if /%(?!\h\h)/.match?(str) + str.b.gsub(regexp, TBLDECWWWCOMP_).force_encoding(enc) + end + private_class_method :_decode_uri_component + + # Returns a URL-encoded string derived from the given + # {Enumerable}[rdoc-ref:Enumerable@Enumerable+in+Ruby+Classes] + # +enum+. + # + # The result is suitable for use as form data + # for an \HTTP request whose <tt>Content-Type</tt> is + # <tt>'application/x-www-form-urlencoded'</tt>. + # + # The returned string consists of the elements of +enum+, + # each converted to one or more URL-encoded strings, + # and all joined with character <tt>'&'</tt>. + # + # Simple examples: + # + # Bundler::URI.encode_www_form([['foo', 0], ['bar', 1], ['baz', 2]]) + # # => "foo=0&bar=1&baz=2" + # Bundler::URI.encode_www_form({foo: 0, bar: 1, baz: 2}) + # # => "foo=0&bar=1&baz=2" + # + # The returned string is formed using method Bundler::URI.encode_www_form_component, + # which converts certain characters: + # + # Bundler::URI.encode_www_form('f#o': '/', 'b-r': '$', 'b z': '@') + # # => "f%23o=%2F&b-r=%24&b+z=%40" + # + # When +enum+ is Array-like, each element +ele+ is converted to a field: + # + # - If +ele+ is an array of two or more elements, + # the field is formed from its first two elements + # (and any additional elements are ignored): + # + # name = Bundler::URI.encode_www_form_component(ele[0], enc) + # value = Bundler::URI.encode_www_form_component(ele[1], enc) + # "#{name}=#{value}" + # + # Examples: + # + # Bundler::URI.encode_www_form([%w[foo bar], %w[baz bat bah]]) + # # => "foo=bar&baz=bat" + # Bundler::URI.encode_www_form([['foo', 0], ['bar', :baz, 'bat']]) + # # => "foo=0&bar=baz" + # + # - If +ele+ is an array of one element, + # the field is formed from <tt>ele[0]</tt>: + # + # Bundler::URI.encode_www_form_component(ele[0]) + # + # Example: + # + # Bundler::URI.encode_www_form([['foo'], [:bar], [0]]) + # # => "foo&bar&0" + # + # - Otherwise the field is formed from +ele+: + # + # Bundler::URI.encode_www_form_component(ele) + # + # Example: + # + # Bundler::URI.encode_www_form(['foo', :bar, 0]) + # # => "foo&bar&0" + # + # The elements of an Array-like +enum+ may be mixture: + # + # Bundler::URI.encode_www_form([['foo', 0], ['bar', 1, 2], ['baz'], :bat]) + # # => "foo=0&bar=1&baz&bat" + # + # When +enum+ is Hash-like, + # each +key+/+value+ pair is converted to one or more fields: + # + # - If +value+ is + # {Array-convertible}[rdoc-ref:implicit_conversion.rdoc@Array-Convertible+Objects], + # each element +ele+ in +value+ is paired with +key+ to form a field: + # + # name = Bundler::URI.encode_www_form_component(key, enc) + # value = Bundler::URI.encode_www_form_component(ele, enc) + # "#{name}=#{value}" + # + # Example: + # + # Bundler::URI.encode_www_form({foo: [:bar, 1], baz: [:bat, :bam, 2]}) + # # => "foo=bar&foo=1&baz=bat&baz=bam&baz=2" + # + # - Otherwise, +key+ and +value+ are paired to form a field: + # + # name = Bundler::URI.encode_www_form_component(key, enc) + # value = Bundler::URI.encode_www_form_component(value, enc) + # "#{name}=#{value}" + # + # Example: + # + # Bundler::URI.encode_www_form({foo: 0, bar: 1, baz: 2}) + # # => "foo=0&bar=1&baz=2" + # + # The elements of a Hash-like +enum+ may be mixture: + # + # Bundler::URI.encode_www_form({foo: [0, 1], bar: 2}) + # # => "foo=0&foo=1&bar=2" + # + def self.encode_www_form(enum, enc=nil) + enum.map do |k,v| + if v.nil? + encode_www_form_component(k, enc) + elsif v.respond_to?(:to_ary) + v.to_ary.map do |w| + str = encode_www_form_component(k, enc) + unless w.nil? + str << '=' + str << encode_www_form_component(w, enc) + end + end.join('&') + else + str = encode_www_form_component(k, enc) + str << '=' + str << encode_www_form_component(v, enc) + end + end.join('&') + end + + # Returns name/value pairs derived from the given string +str+, + # which must be an ASCII string. + # + # The method may be used to decode the body of Net::HTTPResponse object +res+ + # for which <tt>res['Content-Type']</tt> is <tt>'application/x-www-form-urlencoded'</tt>. + # + # The returned data is an array of 2-element subarrays; + # each subarray is a name/value pair (both are strings). + # Each returned string has encoding +enc+, + # and has had invalid characters removed via + # {String#scrub}[rdoc-ref:String#scrub]. + # + # A simple example: + # + # Bundler::URI.decode_www_form('foo=0&bar=1&baz') + # # => [["foo", "0"], ["bar", "1"], ["baz", ""]] + # + # The returned strings have certain conversions, + # similar to those performed in Bundler::URI.decode_www_form_component: + # + # Bundler::URI.decode_www_form('f%23o=%2F&b-r=%24&b+z=%40') + # # => [["f#o", "/"], ["b-r", "$"], ["b z", "@"]] + # + # The given string may contain consecutive separators: + # + # Bundler::URI.decode_www_form('foo=0&&bar=1&&baz=2') + # # => [["foo", "0"], ["", ""], ["bar", "1"], ["", ""], ["baz", "2"]] + # + # A different separator may be specified: + # + # Bundler::URI.decode_www_form('foo=0--bar=1--baz', separator: '--') + # # => [["foo", "0"], ["bar", "1"], ["baz", ""]] + # + def self.decode_www_form(str, enc=Encoding::UTF_8, separator: '&', use__charset_: false, isindex: false) + raise ArgumentError, "the input of #{self.name}.#{__method__} must be ASCII only string" unless str.ascii_only? + ary = [] + return ary if str.empty? + enc = Encoding.find(enc) + str.b.each_line(separator) do |string| + string.chomp!(separator) + key, sep, val = string.partition('=') + if isindex + if sep.empty? + val = key + key = +'' + end + isindex = false + end + + if use__charset_ and key == '_charset_' and e = get_encoding(val) + enc = e + use__charset_ = false + end + + key.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_) + if val + val.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_) + else + val = +'' + end + + ary << [key, val] + end + ary.each do |k, v| + k.force_encoding(enc) + k.scrub! + v.force_encoding(enc) + v.scrub! + end + ary + end + + private +=begin command for WEB_ENCODINGS_ + curl https://encoding.spec.whatwg.org/encodings.json| + ruby -rjson -e 'H={} + h={ + "shift_jis"=>"Windows-31J", + "euc-jp"=>"cp51932", + "iso-2022-jp"=>"cp50221", + "x-mac-cyrillic"=>"macCyrillic", + } + JSON($<.read).map{|x|x["encodings"]}.flatten.each{|x| + Encoding.find(n=h.fetch(n=x["name"].downcase,n))rescue next + x["labels"].each{|y|H[y]=n} + } + puts "{" + H.each{|k,v|puts %[ #{k.dump}=>#{v.dump},]} + puts "}" +' +=end + WEB_ENCODINGS_ = { + "unicode-1-1-utf-8"=>"utf-8", + "utf-8"=>"utf-8", + "utf8"=>"utf-8", + "866"=>"ibm866", + "cp866"=>"ibm866", + "csibm866"=>"ibm866", + "ibm866"=>"ibm866", + "csisolatin2"=>"iso-8859-2", + "iso-8859-2"=>"iso-8859-2", + "iso-ir-101"=>"iso-8859-2", + "iso8859-2"=>"iso-8859-2", + "iso88592"=>"iso-8859-2", + "iso_8859-2"=>"iso-8859-2", + "iso_8859-2:1987"=>"iso-8859-2", + "l2"=>"iso-8859-2", + "latin2"=>"iso-8859-2", + "csisolatin3"=>"iso-8859-3", + "iso-8859-3"=>"iso-8859-3", + "iso-ir-109"=>"iso-8859-3", + "iso8859-3"=>"iso-8859-3", + "iso88593"=>"iso-8859-3", + "iso_8859-3"=>"iso-8859-3", + "iso_8859-3:1988"=>"iso-8859-3", + "l3"=>"iso-8859-3", + "latin3"=>"iso-8859-3", + "csisolatin4"=>"iso-8859-4", + "iso-8859-4"=>"iso-8859-4", + "iso-ir-110"=>"iso-8859-4", + "iso8859-4"=>"iso-8859-4", + "iso88594"=>"iso-8859-4", + "iso_8859-4"=>"iso-8859-4", + "iso_8859-4:1988"=>"iso-8859-4", + "l4"=>"iso-8859-4", + "latin4"=>"iso-8859-4", + "csisolatincyrillic"=>"iso-8859-5", + "cyrillic"=>"iso-8859-5", + "iso-8859-5"=>"iso-8859-5", + "iso-ir-144"=>"iso-8859-5", + "iso8859-5"=>"iso-8859-5", + "iso88595"=>"iso-8859-5", + "iso_8859-5"=>"iso-8859-5", + "iso_8859-5:1988"=>"iso-8859-5", + "arabic"=>"iso-8859-6", + "asmo-708"=>"iso-8859-6", + "csiso88596e"=>"iso-8859-6", + "csiso88596i"=>"iso-8859-6", + "csisolatinarabic"=>"iso-8859-6", + "ecma-114"=>"iso-8859-6", + "iso-8859-6"=>"iso-8859-6", + "iso-8859-6-e"=>"iso-8859-6", + "iso-8859-6-i"=>"iso-8859-6", + "iso-ir-127"=>"iso-8859-6", + "iso8859-6"=>"iso-8859-6", + "iso88596"=>"iso-8859-6", + "iso_8859-6"=>"iso-8859-6", + "iso_8859-6:1987"=>"iso-8859-6", + "csisolatingreek"=>"iso-8859-7", + "ecma-118"=>"iso-8859-7", + "elot_928"=>"iso-8859-7", + "greek"=>"iso-8859-7", + "greek8"=>"iso-8859-7", + "iso-8859-7"=>"iso-8859-7", + "iso-ir-126"=>"iso-8859-7", + "iso8859-7"=>"iso-8859-7", + "iso88597"=>"iso-8859-7", + "iso_8859-7"=>"iso-8859-7", + "iso_8859-7:1987"=>"iso-8859-7", + "sun_eu_greek"=>"iso-8859-7", + "csiso88598e"=>"iso-8859-8", + "csisolatinhebrew"=>"iso-8859-8", + "hebrew"=>"iso-8859-8", + "iso-8859-8"=>"iso-8859-8", + "iso-8859-8-e"=>"iso-8859-8", + "iso-ir-138"=>"iso-8859-8", + "iso8859-8"=>"iso-8859-8", + "iso88598"=>"iso-8859-8", + "iso_8859-8"=>"iso-8859-8", + "iso_8859-8:1988"=>"iso-8859-8", + "visual"=>"iso-8859-8", + "csisolatin6"=>"iso-8859-10", + "iso-8859-10"=>"iso-8859-10", + "iso-ir-157"=>"iso-8859-10", + "iso8859-10"=>"iso-8859-10", + "iso885910"=>"iso-8859-10", + "l6"=>"iso-8859-10", + "latin6"=>"iso-8859-10", + "iso-8859-13"=>"iso-8859-13", + "iso8859-13"=>"iso-8859-13", + "iso885913"=>"iso-8859-13", + "iso-8859-14"=>"iso-8859-14", + "iso8859-14"=>"iso-8859-14", + "iso885914"=>"iso-8859-14", + "csisolatin9"=>"iso-8859-15", + "iso-8859-15"=>"iso-8859-15", + "iso8859-15"=>"iso-8859-15", + "iso885915"=>"iso-8859-15", + "iso_8859-15"=>"iso-8859-15", + "l9"=>"iso-8859-15", + "iso-8859-16"=>"iso-8859-16", + "cskoi8r"=>"koi8-r", + "koi"=>"koi8-r", + "koi8"=>"koi8-r", + "koi8-r"=>"koi8-r", + "koi8_r"=>"koi8-r", + "koi8-ru"=>"koi8-u", + "koi8-u"=>"koi8-u", + "dos-874"=>"windows-874", + "iso-8859-11"=>"windows-874", + "iso8859-11"=>"windows-874", + "iso885911"=>"windows-874", + "tis-620"=>"windows-874", + "windows-874"=>"windows-874", + "cp1250"=>"windows-1250", + "windows-1250"=>"windows-1250", + "x-cp1250"=>"windows-1250", + "cp1251"=>"windows-1251", + "windows-1251"=>"windows-1251", + "x-cp1251"=>"windows-1251", + "ansi_x3.4-1968"=>"windows-1252", + "ascii"=>"windows-1252", + "cp1252"=>"windows-1252", + "cp819"=>"windows-1252", + "csisolatin1"=>"windows-1252", + "ibm819"=>"windows-1252", + "iso-8859-1"=>"windows-1252", + "iso-ir-100"=>"windows-1252", + "iso8859-1"=>"windows-1252", + "iso88591"=>"windows-1252", + "iso_8859-1"=>"windows-1252", + "iso_8859-1:1987"=>"windows-1252", + "l1"=>"windows-1252", + "latin1"=>"windows-1252", + "us-ascii"=>"windows-1252", + "windows-1252"=>"windows-1252", + "x-cp1252"=>"windows-1252", + "cp1253"=>"windows-1253", + "windows-1253"=>"windows-1253", + "x-cp1253"=>"windows-1253", + "cp1254"=>"windows-1254", + "csisolatin5"=>"windows-1254", + "iso-8859-9"=>"windows-1254", + "iso-ir-148"=>"windows-1254", + "iso8859-9"=>"windows-1254", + "iso88599"=>"windows-1254", + "iso_8859-9"=>"windows-1254", + "iso_8859-9:1989"=>"windows-1254", + "l5"=>"windows-1254", + "latin5"=>"windows-1254", + "windows-1254"=>"windows-1254", + "x-cp1254"=>"windows-1254", + "cp1255"=>"windows-1255", + "windows-1255"=>"windows-1255", + "x-cp1255"=>"windows-1255", + "cp1256"=>"windows-1256", + "windows-1256"=>"windows-1256", + "x-cp1256"=>"windows-1256", + "cp1257"=>"windows-1257", + "windows-1257"=>"windows-1257", + "x-cp1257"=>"windows-1257", + "cp1258"=>"windows-1258", + "windows-1258"=>"windows-1258", + "x-cp1258"=>"windows-1258", + "x-mac-cyrillic"=>"macCyrillic", + "x-mac-ukrainian"=>"macCyrillic", + "chinese"=>"gbk", + "csgb2312"=>"gbk", + "csiso58gb231280"=>"gbk", + "gb2312"=>"gbk", + "gb_2312"=>"gbk", + "gb_2312-80"=>"gbk", + "gbk"=>"gbk", + "iso-ir-58"=>"gbk", + "x-gbk"=>"gbk", + "gb18030"=>"gb18030", + "big5"=>"big5", + "big5-hkscs"=>"big5", + "cn-big5"=>"big5", + "csbig5"=>"big5", + "x-x-big5"=>"big5", + "cseucpkdfmtjapanese"=>"cp51932", + "euc-jp"=>"cp51932", + "x-euc-jp"=>"cp51932", + "csiso2022jp"=>"cp50221", + "iso-2022-jp"=>"cp50221", + "csshiftjis"=>"Windows-31J", + "ms932"=>"Windows-31J", + "ms_kanji"=>"Windows-31J", + "shift-jis"=>"Windows-31J", + "shift_jis"=>"Windows-31J", + "sjis"=>"Windows-31J", + "windows-31j"=>"Windows-31J", + "x-sjis"=>"Windows-31J", + "cseuckr"=>"euc-kr", + "csksc56011987"=>"euc-kr", + "euc-kr"=>"euc-kr", + "iso-ir-149"=>"euc-kr", + "korean"=>"euc-kr", + "ks_c_5601-1987"=>"euc-kr", + "ks_c_5601-1989"=>"euc-kr", + "ksc5601"=>"euc-kr", + "ksc_5601"=>"euc-kr", + "windows-949"=>"euc-kr", + "utf-16be"=>"utf-16be", + "utf-16"=>"utf-16le", + "utf-16le"=>"utf-16le", + } # :nodoc: + Ractor.make_shareable(WEB_ENCODINGS_) if defined?(Ractor) + + # :nodoc: + # return encoding or nil + # http://encoding.spec.whatwg.org/#concept-encoding-get + def self.get_encoding(label) + Encoding.find(WEB_ENCODINGS_[label.to_str.strip.downcase]) rescue nil + end +end # module Bundler::URI + +module Bundler + + # + # Returns a \Bundler::URI object derived from the given +uri+, + # which may be a \Bundler::URI string or an existing \Bundler::URI object: + # + # require 'bundler/vendor/uri/lib/uri' + # # Returns a new Bundler::URI. + # uri = Bundler::URI('http://github.com/ruby/ruby') + # # => #<Bundler::URI::HTTP http://github.com/ruby/ruby> + # # Returns the given Bundler::URI. + # Bundler::URI(uri) + # # => #<Bundler::URI::HTTP http://github.com/ruby/ruby> + # + # You must require 'bundler/vendor/uri/lib/uri' to use this method. + # + def URI(uri) + if uri.is_a?(Bundler::URI::Generic) + uri + elsif uri = String.try_convert(uri) + Bundler::URI.parse(uri) + else + raise ArgumentError, + "bad argument (expected Bundler::URI object or Bundler::URI string)" + end + end + module_function :URI +end diff --git a/lib/bundler/vendor/uri/lib/uri/file.rb b/lib/bundler/vendor/uri/lib/uri/file.rb new file mode 100644 index 0000000000..21dd9ee535 --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri/file.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require_relative 'generic' + +module Bundler::URI + + # + # The "file" Bundler::URI is defined by RFC8089. + # + class File < Generic + # A Default port of nil for Bundler::URI::File. + DEFAULT_PORT = nil + + # + # An Array of the available components for Bundler::URI::File. + # + COMPONENT = [ + :scheme, + :host, + :path + ].freeze + + # + # == Description + # + # Creates a new Bundler::URI::File object from components, with syntax checking. + # + # The components accepted are +host+ and +path+. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the + # order <code>[host, path]</code>. + # + # A path from e.g. the File class should be escaped before + # being passed. + # + # Examples: + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri1 = Bundler::URI::File.build(['host.example.com', '/path/file.zip']) + # uri1.to_s # => "file://host.example.com/path/file.zip" + # + # uri2 = Bundler::URI::File.build({:host => 'host.example.com', + # :path => '/ruby/src'}) + # uri2.to_s # => "file://host.example.com/ruby/src" + # + # uri3 = Bundler::URI::File.build({:path => Bundler::URI::RFC2396_PARSER.escape('/path/my file.txt')}) + # uri3.to_s # => "file:///path/my%20file.txt" + # + def self.build(args) + tmp = Util::make_components_hash(self, args) + super(tmp) + end + + # Protected setter for the host component +v+. + # + # See also Bundler::URI::Generic.host=. + # + def set_host(v) + v = "" if v.nil? || v == "localhost" + @host = v + end + + # do nothing + def set_port(v) + end + + # raise InvalidURIError + def check_userinfo(user) + raise Bundler::URI::InvalidURIError, "cannot set userinfo for file Bundler::URI" + end + + # raise InvalidURIError + def check_user(user) + raise Bundler::URI::InvalidURIError, "cannot set user for file Bundler::URI" + end + + # raise InvalidURIError + def check_password(user) + raise Bundler::URI::InvalidURIError, "cannot set password for file Bundler::URI" + end + + # do nothing + def set_userinfo(v) + end + + # do nothing + def set_user(v) + end + + # do nothing + def set_password(v) + end + end + + register_scheme 'FILE', File +end diff --git a/lib/bundler/vendor/uri/lib/uri/ftp.rb b/lib/bundler/vendor/uri/lib/uri/ftp.rb new file mode 100644 index 0000000000..f83985fd3d --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri/ftp.rb @@ -0,0 +1,267 @@ +# frozen_string_literal: false +# = uri/ftp.rb +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Bundler::URI for general documentation +# + +require_relative 'generic' + +module Bundler::URI + + # + # FTP Bundler::URI syntax is defined by RFC1738 section 3.2. + # + # This class will be redesigned because of difference of implementations; + # the structure of its path. draft-hoffman-ftp-uri-04 is a draft but it + # is a good summary about the de facto spec. + # https://datatracker.ietf.org/doc/html/draft-hoffman-ftp-uri-04 + # + class FTP < Generic + # A Default port of 21 for Bundler::URI::FTP. + DEFAULT_PORT = 21 + + # + # An Array of the available components for Bundler::URI::FTP. + # + COMPONENT = [ + :scheme, + :userinfo, :host, :port, + :path, :typecode + ].freeze + + # + # Typecode is "a", "i", or "d". + # + # * "a" indicates a text file (the FTP command was ASCII) + # * "i" indicates a binary file (FTP command IMAGE) + # * "d" indicates the contents of a directory should be displayed + # + TYPECODE = ['a', 'i', 'd'].freeze + + # Typecode prefix ";type=". + TYPECODE_PREFIX = ';type='.freeze + + def self.new2(user, password, host, port, path, + typecode = nil, arg_check = true) # :nodoc: + # Do not use this method! Not tested. [Bug #7301] + # This methods remains just for compatibility, + # Keep it undocumented until the active maintainer is assigned. + typecode = nil if typecode.size == 0 + if typecode && !TYPECODE.include?(typecode) + raise ArgumentError, + "bad typecode is specified: #{typecode}" + end + + # do escape + + self.new('ftp', + [user, password], + host, port, nil, + typecode ? path + TYPECODE_PREFIX + typecode : path, + nil, nil, nil, arg_check) + end + + # + # == Description + # + # Creates a new Bundler::URI::FTP object from components, with syntax checking. + # + # The components accepted are +userinfo+, +host+, +port+, +path+, and + # +typecode+. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the + # order <code>[userinfo, host, port, path, typecode]</code>. + # + # If the path supplied is absolute, it will be escaped in order to + # make it absolute in the Bundler::URI. + # + # Examples: + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri1 = Bundler::URI::FTP.build(['user:password', 'ftp.example.com', nil, + # '/path/file.zip', 'i']) + # uri1.to_s # => "ftp://user:password@ftp.example.com/%2Fpath/file.zip;type=i" + # + # uri2 = Bundler::URI::FTP.build({:host => 'ftp.example.com', + # :path => 'ruby/src'}) + # uri2.to_s # => "ftp://ftp.example.com/ruby/src" + # + def self.build(args) + + # Fix the incoming path to be generic URL syntax + # FTP path -> URL path + # foo/bar /foo/bar + # /foo/bar /%2Ffoo/bar + # + if args.kind_of?(Array) + args[3] = '/' + args[3].sub(/^\//, '%2F') + else + args[:path] = '/' + args[:path].sub(/^\//, '%2F') + end + + tmp = Util::make_components_hash(self, args) + + if tmp[:typecode] + if tmp[:typecode].size == 1 + tmp[:typecode] = TYPECODE_PREFIX + tmp[:typecode] + end + tmp[:path] << tmp[:typecode] + end + + return super(tmp) + end + + # + # == Description + # + # Creates a new Bundler::URI::FTP object from generic URL components with no + # syntax checking. + # + # Unlike build(), this method does not escape the path component as + # required by RFC1738; instead it is treated as per RFC2396. + # + # Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+, + # +opaque+, +query+, and +fragment+, in that order. + # + def initialize(scheme, + userinfo, host, port, registry, + path, opaque, + query, + fragment, + parser = nil, + arg_check = false) + raise InvalidURIError unless path + path = path.sub(/^\//,'') + path.sub!(/^%2F/,'/') + super(scheme, userinfo, host, port, registry, path, opaque, + query, fragment, parser, arg_check) + @typecode = nil + if tmp = @path.index(TYPECODE_PREFIX) + typecode = @path[tmp + TYPECODE_PREFIX.size..-1] + @path = @path[0..tmp - 1] + + if arg_check + self.typecode = typecode + else + self.set_typecode(typecode) + end + end + end + + # typecode accessor. + # + # See Bundler::URI::FTP::COMPONENT. + attr_reader :typecode + + # Validates typecode +v+, + # returns +true+ or +false+. + # + def check_typecode(v) + if TYPECODE.include?(v) + return true + else + raise InvalidComponentError, + "bad typecode(expected #{TYPECODE.join(', ')}): #{v}" + end + end + private :check_typecode + + # Private setter for the typecode +v+. + # + # See also Bundler::URI::FTP.typecode=. + # + def set_typecode(v) + @typecode = v + end + protected :set_typecode + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the typecode +v+ + # (with validation). + # + # See also Bundler::URI::FTP.check_typecode. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse("ftp://john@ftp.example.com/my_file.img") + # #=> #<Bundler::URI::FTP ftp://john@ftp.example.com/my_file.img> + # uri.typecode = "i" + # uri + # #=> #<Bundler::URI::FTP ftp://john@ftp.example.com/my_file.img;type=i> + # + def typecode=(typecode) + check_typecode(typecode) + set_typecode(typecode) + typecode + end + + def merge(oth) # :nodoc: + tmp = super(oth) + if self != tmp + tmp.set_typecode(oth.typecode) + end + + return tmp + end + + # Returns the path from an FTP Bundler::URI. + # + # RFC 1738 specifically states that the path for an FTP Bundler::URI does not + # include the / which separates the Bundler::URI path from the Bundler::URI host. Example: + # + # <code>ftp://ftp.example.com/pub/ruby</code> + # + # The above Bundler::URI indicates that the client should connect to + # ftp.example.com then cd to pub/ruby from the initial login directory. + # + # If you want to cd to an absolute directory, you must include an + # escaped / (%2F) in the path. Example: + # + # <code>ftp://ftp.example.com/%2Fpub/ruby</code> + # + # This method will then return "/pub/ruby". + # + def path + return @path.sub(/^\//,'').sub(/^%2F/,'/') + end + + # Private setter for the path of the Bundler::URI::FTP. + def set_path(v) + super("/" + v.sub(/^\//, "%2F")) + end + protected :set_path + + # Returns a String representation of the Bundler::URI::FTP. + def to_s + save_path = nil + if @typecode + save_path = @path + @path = @path + TYPECODE_PREFIX + @typecode + end + str = super + if @typecode + @path = save_path + end + + return str + end + end + + register_scheme 'FTP', FTP +end diff --git a/lib/bundler/vendor/uri/lib/uri/generic.rb b/lib/bundler/vendor/uri/lib/uri/generic.rb new file mode 100644 index 0000000000..30dab60903 --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri/generic.rb @@ -0,0 +1,1592 @@ +# frozen_string_literal: true + +# = uri/generic.rb +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Bundler::URI for general documentation +# + +require_relative 'common' +autoload :IPSocket, 'socket' +autoload :IPAddr, 'ipaddr' + +module Bundler::URI + + # + # Base class for all Bundler::URI classes. + # Implements generic Bundler::URI syntax as per RFC 2396. + # + class Generic + include Bundler::URI + + # + # A Default port of nil for Bundler::URI::Generic. + # + DEFAULT_PORT = nil + + # + # Returns default port. + # + def self.default_port + self::DEFAULT_PORT + end + + # + # Returns default port. + # + def default_port + self.class.default_port + end + + # + # An Array of the available components for Bundler::URI::Generic. + # + COMPONENT = [ + :scheme, + :userinfo, :host, :port, :registry, + :path, :opaque, + :query, + :fragment + ].freeze + + # + # Components of the Bundler::URI in the order. + # + def self.component + self::COMPONENT + end + + USE_REGISTRY = false # :nodoc: + + def self.use_registry # :nodoc: + self::USE_REGISTRY + end + + # + # == Synopsis + # + # See ::new. + # + # == Description + # + # At first, tries to create a new Bundler::URI::Generic instance using + # Bundler::URI::Generic::build. But, if exception Bundler::URI::InvalidComponentError is raised, + # then it does Bundler::URI::RFC2396_PARSER.escape all Bundler::URI components and tries again. + # + def self.build2(args) + begin + return self.build(args) + rescue InvalidComponentError + if args.kind_of?(Array) + return self.build(args.collect{|x| + if x.is_a?(String) + Bundler::URI::RFC2396_PARSER.escape(x) + else + x + end + }) + elsif args.kind_of?(Hash) + tmp = {} + args.each do |key, value| + tmp[key] = if value + Bundler::URI::RFC2396_PARSER.escape(value) + else + value + end + end + return self.build(tmp) + end + end + end + + # + # == Synopsis + # + # See ::new. + # + # == Description + # + # Creates a new Bundler::URI::Generic instance from components of Bundler::URI::Generic + # with check. Components are: scheme, userinfo, host, port, registry, path, + # opaque, query, and fragment. You can provide arguments either by an Array or a Hash. + # See ::new for hash keys to use or for order of array items. + # + def self.build(args) + if args.kind_of?(Array) && + args.size == ::Bundler::URI::Generic::COMPONENT.size + tmp = args.dup + elsif args.kind_of?(Hash) + tmp = ::Bundler::URI::Generic::COMPONENT.collect do |c| + if args.include?(c) + args[c] + else + nil + end + end + else + component = self.component rescue ::Bundler::URI::Generic::COMPONENT + raise ArgumentError, + "expected Array of or Hash of components of #{self} (#{component.join(', ')})" + end + + tmp << nil + tmp << true + return self.new(*tmp) + end + + # + # == Args + # + # +scheme+:: + # Protocol scheme, i.e. 'http','ftp','mailto' and so on. + # +userinfo+:: + # User name and password, i.e. 'sdmitry:bla'. + # +host+:: + # Server host name. + # +port+:: + # Server port. + # +registry+:: + # Registry of naming authorities. + # +path+:: + # Path on server. + # +opaque+:: + # Opaque part. + # +query+:: + # Query data. + # +fragment+:: + # Part of the Bundler::URI after '#' character. + # +parser+:: + # Parser for internal use [Bundler::URI::DEFAULT_PARSER by default]. + # +arg_check+:: + # Check arguments [false by default]. + # + # == Description + # + # Creates a new Bundler::URI::Generic instance from ``generic'' components without check. + # + def initialize(scheme, + userinfo, host, port, registry, + path, opaque, + query, + fragment, + parser = DEFAULT_PARSER, + arg_check = false) + @scheme = nil + @user = nil + @password = nil + @host = nil + @port = nil + @path = nil + @query = nil + @opaque = nil + @fragment = nil + @parser = parser == DEFAULT_PARSER ? nil : parser + + if arg_check + self.scheme = scheme + self.hostname = host + self.port = port + self.userinfo = userinfo + self.path = path + self.query = query + self.opaque = opaque + self.fragment = fragment + else + self.set_scheme(scheme) + self.set_host(host) + self.set_port(port) + self.set_userinfo(userinfo) + self.set_path(path) + self.query = query + self.set_opaque(opaque) + self.fragment=(fragment) + end + if registry + raise InvalidURIError, + "the scheme #{@scheme} does not accept registry part: #{registry} (or bad hostname?)" + end + + @scheme&.freeze + self.set_path('') if !@path && !@opaque # (see RFC2396 Section 5.2) + self.set_port(self.default_port) if self.default_port && !@port + end + + # + # Returns the scheme component of the Bundler::URI. + # + # Bundler::URI("http://foo/bar/baz").scheme #=> "http" + # + attr_reader :scheme + + # Returns the host component of the Bundler::URI. + # + # Bundler::URI("http://foo/bar/baz").host #=> "foo" + # + # It returns nil if no host component exists. + # + # Bundler::URI("mailto:foo@example.org").host #=> nil + # + # The component does not contain the port number. + # + # Bundler::URI("http://foo:8080/bar/baz").host #=> "foo" + # + # Since IPv6 addresses are wrapped with brackets in URIs, + # this method returns IPv6 addresses wrapped with brackets. + # This form is not appropriate to pass to socket methods such as TCPSocket.open. + # If unwrapped host names are required, use the #hostname method. + # + # Bundler::URI("http://[::1]/bar/baz").host #=> "[::1]" + # Bundler::URI("http://[::1]/bar/baz").hostname #=> "::1" + # + attr_reader :host + + # Returns the port component of the Bundler::URI. + # + # Bundler::URI("http://foo/bar/baz").port #=> 80 + # Bundler::URI("http://foo:8080/bar/baz").port #=> 8080 + # + attr_reader :port + + def registry # :nodoc: + nil + end + + # Returns the path component of the Bundler::URI. + # + # Bundler::URI("http://foo/bar/baz").path #=> "/bar/baz" + # + attr_reader :path + + # Returns the query component of the Bundler::URI. + # + # Bundler::URI("http://foo/bar/baz?search=FooBar").query #=> "search=FooBar" + # + attr_reader :query + + # Returns the opaque part of the Bundler::URI. + # + # Bundler::URI("mailto:foo@example.org").opaque #=> "foo@example.org" + # Bundler::URI("http://foo/bar/baz").opaque #=> nil + # + # The portion of the path that does not make use of the slash '/'. + # The path typically refers to an absolute path or an opaque part. + # (See RFC2396 Section 3 and 5.2.) + # + attr_reader :opaque + + # Returns the fragment component of the Bundler::URI. + # + # Bundler::URI("http://foo/bar/baz?search=FooBar#ponies").fragment #=> "ponies" + # + attr_reader :fragment + + # Returns the parser to be used. + # + # Unless the +parser+ is defined, DEFAULT_PARSER is used. + # + def parser + if !defined?(@parser) || !@parser + DEFAULT_PARSER + else + @parser || DEFAULT_PARSER + end + end + + # Replaces self by other Bundler::URI object. + # + def replace!(oth) + if self.class != oth.class + raise ArgumentError, "expected #{self.class} object" + end + + component.each do |c| + self.__send__("#{c}=", oth.__send__(c)) + end + end + private :replace! + + # + # Components of the Bundler::URI in the order. + # + def component + self.class.component + end + + # + # Checks the scheme +v+ component against the +parser+ Regexp for :SCHEME. + # + def check_scheme(v) + if v && parser.regexp[:SCHEME] !~ v + raise InvalidComponentError, + "bad component(expected scheme component): #{v}" + end + + return true + end + private :check_scheme + + # Protected setter for the scheme component +v+. + # + # See also Bundler::URI::Generic.scheme=. + # + def set_scheme(v) + @scheme = v&.downcase + end + protected :set_scheme + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the scheme component +v+ + # (with validation). + # + # See also Bundler::URI::Generic.check_scheme. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse("http://my.example.com") + # uri.scheme = "https" + # uri.to_s #=> "https://my.example.com" + # + def scheme=(v) + check_scheme(v) + set_scheme(v) + v + end + + # + # Checks the +user+ and +password+. + # + # If +password+ is not provided, then +user+ is + # split, using Bundler::URI::Generic.split_userinfo, to + # pull +user+ and +password. + # + # See also Bundler::URI::Generic.check_user, Bundler::URI::Generic.check_password. + # + def check_userinfo(user, password = nil) + if !password + user, password = split_userinfo(user) + end + check_user(user) + check_password(password, user) + + return true + end + private :check_userinfo + + # + # Checks the user +v+ component for RFC2396 compliance + # and against the +parser+ Regexp for :USERINFO. + # + # Can not have a registry or opaque component defined, + # with a user component defined. + # + def check_user(v) + if @opaque + raise InvalidURIError, + "cannot set user with opaque" + end + + return v unless v + + if parser.regexp[:USERINFO] !~ v + raise InvalidComponentError, + "bad component(expected userinfo component or user component): #{v}" + end + + return true + end + private :check_user + + # + # Checks the password +v+ component for RFC2396 compliance + # and against the +parser+ Regexp for :USERINFO. + # + # Can not have a registry or opaque component defined, + # with a user component defined. + # + def check_password(v, user = @user) + if @opaque + raise InvalidURIError, + "cannot set password with opaque" + end + return v unless v + + if !user + raise InvalidURIError, + "password component depends user component" + end + + if parser.regexp[:USERINFO] !~ v + raise InvalidComponentError, + "bad password component" + end + + return true + end + private :check_password + + # + # Sets userinfo, argument is string like 'name:pass'. + # + def userinfo=(userinfo) + if userinfo.nil? + return nil + end + check_userinfo(*userinfo) + set_userinfo(*userinfo) + # returns userinfo + end + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the +user+ component + # (with validation). + # + # See also Bundler::URI::Generic.check_user. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse("http://john:S3nsit1ve@my.example.com") + # uri.user = "sam" + # uri.to_s #=> "http://sam:V3ry_S3nsit1ve@my.example.com" + # + def user=(user) + check_user(user) + set_user(user) + # returns user + end + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the +password+ component + # (with validation). + # + # See also Bundler::URI::Generic.check_password. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse("http://john:S3nsit1ve@my.example.com") + # uri.password = "V3ry_S3nsit1ve" + # uri.to_s #=> "http://john:V3ry_S3nsit1ve@my.example.com" + # + def password=(password) + check_password(password) + set_password(password) + # returns password + end + + # Protected setter for the +user+ component, and +password+ if available + # (with validation). + # + # See also Bundler::URI::Generic.userinfo=. + # + def set_userinfo(user, password = nil) + unless password + user, password = split_userinfo(user) + end + @user = user + @password = password + + [@user, @password] + end + protected :set_userinfo + + # Protected setter for the user component +v+. + # + # See also Bundler::URI::Generic.user=. + # + def set_user(v) + set_userinfo(v, nil) + v + end + protected :set_user + + # Protected setter for the password component +v+. + # + # See also Bundler::URI::Generic.password=. + # + def set_password(v) + @password = v + # returns v + end + protected :set_password + + # Returns the userinfo +ui+ as <code>[user, password]</code> + # if properly formatted as 'user:password'. + def split_userinfo(ui) + return nil, nil unless ui + user, password = ui.split(':', 2) + + return user, password + end + private :split_userinfo + + # Escapes 'user:password' +v+ based on RFC 1738 section 3.1. + def escape_userpass(v) + parser.escape(v, /[@:\/]/o) # RFC 1738 section 3.1 #/ + end + private :escape_userpass + + # Returns the userinfo, either as 'user' or 'user:password'. + def userinfo + if @user.nil? + nil + elsif @password.nil? + @user + else + @user + ':' + @password + end + end + + # Returns the user component (without Bundler::URI decoding). + def user + @user + end + + # Returns the password component (without Bundler::URI decoding). + def password + @password + end + + # Returns the authority info (array of user, password, host and + # port), if any is set. Or returns +nil+. + def authority + return @user, @password, @host, @port if @user || @password || @host || @port + end + + # Returns the user component after Bundler::URI decoding. + def decoded_user + Bundler::URI.decode_uri_component(@user) if @user + end + + # Returns the password component after Bundler::URI decoding. + def decoded_password + Bundler::URI.decode_uri_component(@password) if @password + end + + # + # Checks the host +v+ component for RFC2396 compliance + # and against the +parser+ Regexp for :HOST. + # + # Can not have a registry or opaque component defined, + # with a host component defined. + # + def check_host(v) + return v unless v + + if @opaque + raise InvalidURIError, + "cannot set host with registry or opaque" + elsif parser.regexp[:HOST] !~ v + raise InvalidComponentError, + "bad component(expected host component): #{v}" + end + + return true + end + private :check_host + + # Protected setter for the host component +v+. + # + # See also Bundler::URI::Generic.host=. + # + def set_host(v) + @host = v + end + protected :set_host + + # Protected setter for the authority info (+user+, +password+, +host+ + # and +port+). If +port+ is +nil+, +default_port+ will be set. + # + protected def set_authority(user, password, host, port = nil) + @user, @password, @host, @port = user, password, host, port || self.default_port + end + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the host component +v+ + # (with validation). + # + # See also Bundler::URI::Generic.check_host. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse("http://my.example.com") + # uri.host = "foo.com" + # uri.to_s #=> "http://foo.com" + # + def host=(v) + check_host(v) + set_host(v) + set_userinfo(nil) + v + end + + # Extract the host part of the Bundler::URI and unwrap brackets for IPv6 addresses. + # + # This method is the same as Bundler::URI::Generic#host except + # brackets for IPv6 (and future IP) addresses are removed. + # + # uri = Bundler::URI("http://[::1]/bar") + # uri.hostname #=> "::1" + # uri.host #=> "[::1]" + # + def hostname + v = self.host + v&.start_with?('[') && v.end_with?(']') ? v[1..-2] : v + end + + # Sets the host part of the Bundler::URI as the argument with brackets for IPv6 addresses. + # + # This method is the same as Bundler::URI::Generic#host= except + # the argument can be a bare IPv6 address. + # + # uri = Bundler::URI("http://foo/bar") + # uri.hostname = "::1" + # uri.to_s #=> "http://[::1]/bar" + # + # If the argument seems to be an IPv6 address, + # it is wrapped with brackets. + # + def hostname=(v) + v = "[#{v}]" if !(v&.start_with?('[') && v&.end_with?(']')) && v&.index(':') + self.host = v + end + + # + # Checks the port +v+ component for RFC2396 compliance + # and against the +parser+ Regexp for :PORT. + # + # Can not have a registry or opaque component defined, + # with a port component defined. + # + def check_port(v) + return v unless v + + if @opaque + raise InvalidURIError, + "cannot set port with registry or opaque" + elsif !v.kind_of?(Integer) && parser.regexp[:PORT] !~ v + raise InvalidComponentError, + "bad component(expected port component): #{v.inspect}" + end + + return true + end + private :check_port + + # Protected setter for the port component +v+. + # + # See also Bundler::URI::Generic.port=. + # + def set_port(v) + v = v.empty? ? nil : v.to_i unless !v || v.kind_of?(Integer) + @port = v + end + protected :set_port + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the port component +v+ + # (with validation). + # + # See also Bundler::URI::Generic.check_port. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse("http://my.example.com") + # uri.port = 8080 + # uri.to_s #=> "http://my.example.com:8080" + # + def port=(v) + check_port(v) + set_port(v) + set_userinfo(nil) + port + end + + def check_registry(v) # :nodoc: + raise InvalidURIError, "cannot set registry" + end + private :check_registry + + def set_registry(v) # :nodoc: + raise InvalidURIError, "cannot set registry" + end + protected :set_registry + + def registry=(v) # :nodoc: + raise InvalidURIError, "cannot set registry" + end + + # + # Checks the path +v+ component for RFC2396 compliance + # and against the +parser+ Regexp + # for :ABS_PATH and :REL_PATH. + # + # Can not have a opaque component defined, + # with a path component defined. + # + def check_path(v) + # raise if both hier and opaque are not nil, because: + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + # hier_part = ( net_path | abs_path ) [ "?" query ] + if v && @opaque + raise InvalidURIError, + "path conflicts with opaque" + end + + # If scheme is ftp, path may be relative. + # See RFC 1738 section 3.2.2, and RFC 2396. + if @scheme && @scheme != "ftp" + if v && v != '' && parser.regexp[:ABS_PATH] !~ v + raise InvalidComponentError, + "bad component(expected absolute path component): #{v}" + end + else + if v && v != '' && parser.regexp[:ABS_PATH] !~ v && + parser.regexp[:REL_PATH] !~ v + raise InvalidComponentError, + "bad component(expected relative path component): #{v}" + end + end + + return true + end + private :check_path + + # Protected setter for the path component +v+. + # + # See also Bundler::URI::Generic.path=. + # + def set_path(v) + @path = v + end + protected :set_path + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the path component +v+ + # (with validation). + # + # See also Bundler::URI::Generic.check_path. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse("http://my.example.com/pub/files") + # uri.path = "/faq/" + # uri.to_s #=> "http://my.example.com/faq/" + # + def path=(v) + check_path(v) + set_path(v) + v + end + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the query component +v+. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse("http://my.example.com/?id=25") + # uri.query = "id=1" + # uri.to_s #=> "http://my.example.com/?id=1" + # + def query=(v) + return @query = nil unless v + raise InvalidURIError, "query conflicts with opaque" if @opaque + + x = v.to_str + v = x.dup if x.equal? v + v.encode!(Encoding::UTF_8) rescue nil + v.delete!("\t\r\n") + v.force_encoding(Encoding::ASCII_8BIT) + raise InvalidURIError, "invalid percent escape: #{$1}" if /(%\H\H)/n.match(v) + v.gsub!(/(?!%\h\h|[!$-&(-;=?-_a-~])./n.freeze){'%%%02X' % $&.ord} + v.force_encoding(Encoding::US_ASCII) + @query = v + end + + # + # Checks the opaque +v+ component for RFC2396 compliance and + # against the +parser+ Regexp for :OPAQUE. + # + # Can not have a host, port, user, or path component defined, + # with an opaque component defined. + # + def check_opaque(v) + return v unless v + + # raise if both hier and opaque are not nil, because: + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + # hier_part = ( net_path | abs_path ) [ "?" query ] + if @host || @port || @user || @path # userinfo = @user + ':' + @password + raise InvalidURIError, + "cannot set opaque with host, port, userinfo or path" + elsif v && parser.regexp[:OPAQUE] !~ v + raise InvalidComponentError, + "bad component(expected opaque component): #{v}" + end + + return true + end + private :check_opaque + + # Protected setter for the opaque component +v+. + # + # See also Bundler::URI::Generic.opaque=. + # + def set_opaque(v) + @opaque = v + end + protected :set_opaque + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the opaque component +v+ + # (with validation). + # + # See also Bundler::URI::Generic.check_opaque. + # + def opaque=(v) + check_opaque(v) + set_opaque(v) + v + end + + # + # Checks the fragment +v+ component against the +parser+ Regexp for :FRAGMENT. + # + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the fragment component +v+ + # (with validation). + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse("http://my.example.com/?id=25#time=1305212049") + # uri.fragment = "time=1305212086" + # uri.to_s #=> "http://my.example.com/?id=25#time=1305212086" + # + def fragment=(v) + return @fragment = nil unless v + + x = v.to_str + v = x.dup if x.equal? v + v.encode!(Encoding::UTF_8) rescue nil + v.delete!("\t\r\n") + v.force_encoding(Encoding::ASCII_8BIT) + v.gsub!(/(?!%\h\h|[!-~])./n){'%%%02X' % $&.ord} + v.force_encoding(Encoding::US_ASCII) + @fragment = v + end + + # + # Returns true if Bundler::URI is hierarchical. + # + # == Description + # + # Bundler::URI has components listed in order of decreasing significance from left to right, + # see RFC3986 https://www.rfc-editor.org/rfc/rfc3986 1.2.3. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse("http://my.example.com/") + # uri.hierarchical? + # #=> true + # uri = Bundler::URI.parse("mailto:joe@example.com") + # uri.hierarchical? + # #=> false + # + def hierarchical? + if @path + true + else + false + end + end + + # + # Returns true if Bundler::URI has a scheme (e.g. http:// or https://) specified. + # + def absolute? + if @scheme + true + else + false + end + end + alias absolute absolute? + + # + # Returns true if Bundler::URI does not have a scheme (e.g. http:// or https://) specified. + # + def relative? + !absolute? + end + + # + # Returns an Array of the path split on '/'. + # + def split_path(path) + path.split("/", -1) + end + private :split_path + + # + # Merges a base path +base+, with relative path +rel+, + # returns a modified base path. + # + def merge_path(base, rel) + + # RFC2396, Section 5.2, 5) + # RFC2396, Section 5.2, 6) + base_path = split_path(base) + rel_path = split_path(rel) + + # RFC2396, Section 5.2, 6), a) + base_path << '' if base_path.last == '..' + while i = base_path.index('..') + base_path.slice!(i - 1, 2) + end + + if (first = rel_path.first) and first.empty? + base_path.clear + rel_path.shift + end + + # RFC2396, Section 5.2, 6), c) + # RFC2396, Section 5.2, 6), d) + rel_path.push('') if rel_path.last == '.' || rel_path.last == '..' + rel_path.delete('.') + + # RFC2396, Section 5.2, 6), e) + tmp = [] + rel_path.each do |x| + if x == '..' && + !(tmp.empty? || tmp.last == '..') + tmp.pop + else + tmp << x + end + end + + add_trailer_slash = !tmp.empty? + if base_path.empty? + base_path = [''] # keep '/' for root directory + elsif add_trailer_slash + base_path.pop + end + while x = tmp.shift + if x == '..' + # RFC2396, Section 4 + # a .. or . in an absolute path has no special meaning + base_path.pop if base_path.size > 1 + else + # if x == '..' + # valid absolute (but abnormal) path "/../..." + # else + # valid absolute path + # end + base_path << x + tmp.each {|t| base_path << t} + add_trailer_slash = false + break + end + end + base_path.push('') if add_trailer_slash + + return base_path.join('/') + end + private :merge_path + + # + # == Args + # + # +oth+:: + # Bundler::URI or String + # + # == Description + # + # Destructive form of #merge. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse("http://my.example.com") + # uri.merge!("/main.rbx?page=1") + # uri.to_s # => "http://my.example.com/main.rbx?page=1" + # + def merge!(oth) + t = merge(oth) + if self == t + nil + else + replace!(t) + self + end + end + + # + # == Args + # + # +oth+:: + # Bundler::URI or String + # + # == Description + # + # Merges two URIs. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse("http://my.example.com") + # uri.merge("/main.rbx?page=1") + # # => "http://my.example.com/main.rbx?page=1" + # + def merge(oth) + rel = parser.__send__(:convert_to_uri, oth) + + if rel.absolute? + #raise BadURIError, "both Bundler::URI are absolute" if absolute? + # hmm... should return oth for usability? + return rel + end + + unless self.absolute? + raise BadURIError, "both Bundler::URI are relative" + end + + base = self.dup + + authority = rel.authority + + # RFC2396, Section 5.2, 2) + if (rel.path.nil? || rel.path.empty?) && !authority && !rel.query + base.fragment=(rel.fragment) if rel.fragment + return base + end + + base.query = nil + base.fragment=(nil) + + # RFC2396, Section 5.2, 4) + if authority + base.set_authority(*authority) + base.set_path(rel.path) + elsif base.path && rel.path + base.set_path(merge_path(base.path, rel.path)) + end + + # RFC2396, Section 5.2, 7) + base.query = rel.query if rel.query + base.fragment=(rel.fragment) if rel.fragment + + return base + end # merge + alias + merge + + # :stopdoc: + def route_from_path(src, dst) + case dst + when src + # RFC2396, Section 4.2 + return '' + when %r{(?:\A|/)\.\.?(?:/|\z)} + # dst has abnormal absolute path, + # like "/./", "/../", "/x/../", ... + return dst.dup + end + + src_path = src.scan(%r{[^/]*/}) + dst_path = dst.scan(%r{[^/]*/?}) + + # discard same parts + while !dst_path.empty? && dst_path.first == src_path.first + src_path.shift + dst_path.shift + end + + tmp = dst_path.join + + # calculate + if src_path.empty? + if tmp.empty? + return './' + elsif dst_path.first.include?(':') # (see RFC2396 Section 5) + return './' + tmp + else + return tmp + end + end + + return '../' * src_path.size + tmp + end + private :route_from_path + # :startdoc: + + # :stopdoc: + def route_from0(oth) + oth = parser.__send__(:convert_to_uri, oth) + if self.relative? + raise BadURIError, + "relative Bundler::URI: #{self}" + end + if oth.relative? + raise BadURIError, + "relative Bundler::URI: #{oth}" + end + + if self.scheme != oth.scheme + return self, self.dup + end + rel = Bundler::URI::Generic.new(nil, # it is relative Bundler::URI + self.userinfo, self.host, self.port, + nil, self.path, self.opaque, + self.query, self.fragment, parser) + + if rel.userinfo != oth.userinfo || + rel.host.to_s.downcase != oth.host.to_s.downcase || + rel.port != oth.port + + if self.userinfo.nil? && self.host.nil? + return self, self.dup + end + + rel.set_port(nil) if rel.port == oth.default_port + return rel, rel + end + rel.set_userinfo(nil) + rel.set_host(nil) + rel.set_port(nil) + + if rel.path && rel.path == oth.path + rel.set_path('') + rel.query = nil if rel.query == oth.query + return rel, rel + elsif rel.opaque && rel.opaque == oth.opaque + rel.set_opaque('') + rel.query = nil if rel.query == oth.query + return rel, rel + end + + # you can modify `rel', but cannot `oth'. + return oth, rel + end + private :route_from0 + # :startdoc: + + # + # == Args + # + # +oth+:: + # Bundler::URI or String + # + # == Description + # + # Calculates relative path from oth to self. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse('http://my.example.com/main.rbx?page=1') + # uri.route_from('http://my.example.com') + # #=> #<Bundler::URI::Generic /main.rbx?page=1> + # + def route_from(oth) + # you can modify `rel', but cannot `oth'. + begin + oth, rel = route_from0(oth) + rescue + raise $!.class, $!.message + end + if oth == rel + return rel + end + + rel.set_path(route_from_path(oth.path, self.path)) + if rel.path == './' && self.query + # "./?foo" -> "?foo" + rel.set_path('') + end + + return rel + end + + alias - route_from + + # + # == Args + # + # +oth+:: + # Bundler::URI or String + # + # == Description + # + # Calculates relative path to oth from self. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse('http://my.example.com') + # uri.route_to('http://my.example.com/main.rbx?page=1') + # #=> #<Bundler::URI::Generic /main.rbx?page=1> + # + def route_to(oth) + parser.__send__(:convert_to_uri, oth).route_from(self) + end + + # + # Returns normalized Bundler::URI. + # + # require 'bundler/vendor/uri/lib/uri' + # + # Bundler::URI("HTTP://my.EXAMPLE.com").normalize + # #=> #<Bundler::URI::HTTP http://my.example.com/> + # + # Normalization here means: + # + # * scheme and host are converted to lowercase, + # * an empty path component is set to "/". + # + def normalize + uri = dup + uri.normalize! + uri + end + + # + # Destructive version of #normalize. + # + def normalize! + if path&.empty? + set_path('/') + end + if scheme && scheme != scheme.downcase + set_scheme(self.scheme.downcase) + end + if host && host != host.downcase + set_host(self.host.downcase) + end + end + + # + # Constructs String from Bundler::URI. + # + def to_s + str = ''.dup + if @scheme + str << @scheme + str << ':' + end + + if @opaque + str << @opaque + else + if @host || %w[file postgres].include?(@scheme) + str << '//' + end + if self.userinfo + str << self.userinfo + str << '@' + end + if @host + str << @host + end + if @port && @port != self.default_port + str << ':' + str << @port.to_s + end + if (@host || @port) && !@path.empty? && !@path.start_with?('/') + str << '/' + end + str << @path + if @query + str << '?' + str << @query + end + end + if @fragment + str << '#' + str << @fragment + end + str + end + alias to_str to_s + + # + # Compares two URIs. + # + def ==(oth) + if self.class == oth.class + self.normalize.component_ary == oth.normalize.component_ary + else + false + end + end + + # Returns the hash value. + def hash + self.component_ary.hash + end + + # Compares with _oth_ for Hash. + def eql?(oth) + self.class == oth.class && + parser == oth.parser && + self.component_ary.eql?(oth.component_ary) + end + + # Returns an Array of the components defined from the COMPONENT Array. + def component_ary + component.collect do |x| + self.__send__(x) + end + end + protected :component_ary + + # == Args + # + # +components+:: + # Multiple Symbol arguments defined in Bundler::URI::HTTP. + # + # == Description + # + # Selects specified components from Bundler::URI. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse('http://myuser:mypass@my.example.com/test.rbx') + # uri.select(:userinfo, :host, :path) + # # => ["myuser:mypass", "my.example.com", "/test.rbx"] + # + def select(*components) + components.collect do |c| + if component.include?(c) + self.__send__(c) + else + raise ArgumentError, + "expected of components of #{self.class} (#{self.class.component.join(', ')})" + end + end + end + + def inspect # :nodoc: + "#<#{self.class} #{self}>" + end + + # + # == Args + # + # +v+:: + # Bundler::URI or String + # + # == Description + # + # Attempts to parse other Bundler::URI +oth+, + # returns [parsed_oth, self]. + # + # == Usage + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse("http://my.example.com") + # uri.coerce("http://foo.com") + # #=> [#<Bundler::URI::HTTP http://foo.com>, #<Bundler::URI::HTTP http://my.example.com>] + # + def coerce(oth) + case oth + when String + oth = parser.parse(oth) + else + super + end + + return oth, self + end + + # Returns a proxy Bundler::URI. + # The proxy Bundler::URI is obtained from environment variables such as http_proxy, + # ftp_proxy, no_proxy, etc. + # If there is no proper proxy, nil is returned. + # + # If the optional parameter +env+ is specified, it is used instead of ENV. + # + # Note that capitalized variables (HTTP_PROXY, FTP_PROXY, NO_PROXY, etc.) + # are examined, too. + # + # But http_proxy and HTTP_PROXY is treated specially under CGI environment. + # It's because HTTP_PROXY may be set by Proxy: header. + # So HTTP_PROXY is not used. + # http_proxy is not used too if the variable is case insensitive. + # CGI_HTTP_PROXY can be used instead. + def find_proxy(env=ENV) + raise BadURIError, "relative Bundler::URI: #{self}" if self.relative? + name = self.scheme.downcase + '_proxy' + proxy_uri = nil + if name == 'http_proxy' && env.include?('REQUEST_METHOD') # CGI? + # HTTP_PROXY conflicts with *_proxy for proxy settings and + # HTTP_* for header information in CGI. + # So it should be careful to use it. + pairs = env.reject {|k, v| /\Ahttp_proxy\z/i !~ k } + case pairs.length + when 0 # no proxy setting anyway. + proxy_uri = nil + when 1 + k, _ = pairs.shift + if k == 'http_proxy' && env[k.upcase] == nil + # http_proxy is safe to use because ENV is case sensitive. + proxy_uri = env[name] + else + proxy_uri = nil + end + else # http_proxy is safe to use because ENV is case sensitive. + proxy_uri = env.to_hash[name] + end + if !proxy_uri + # Use CGI_HTTP_PROXY. cf. libwww-perl. + proxy_uri = env["CGI_#{name.upcase}"] + end + elsif name == 'http_proxy' + if RUBY_ENGINE == 'jruby' && p_addr = ENV_JAVA['http.proxyHost'] + p_port = ENV_JAVA['http.proxyPort'] + if p_user = ENV_JAVA['http.proxyUser'] + p_pass = ENV_JAVA['http.proxyPass'] + proxy_uri = "http://#{p_user}:#{p_pass}@#{p_addr}:#{p_port}" + else + proxy_uri = "http://#{p_addr}:#{p_port}" + end + else + unless proxy_uri = env[name] + if proxy_uri = env[name.upcase] + warn 'The environment variable HTTP_PROXY is discouraged. Please use http_proxy instead.', uplevel: 1 + end + end + end + else + proxy_uri = env[name] || env[name.upcase] + end + + if proxy_uri.nil? || proxy_uri.empty? + return nil + end + + if self.hostname + begin + addr = IPSocket.getaddress(self.hostname) + return nil if /\A127\.|\A::1\z/ =~ addr + rescue SocketError + end + end + + name = 'no_proxy' + if no_proxy = env[name] || env[name.upcase] + return nil unless Bundler::URI::Generic.use_proxy?(self.hostname, addr, self.port, no_proxy) + end + Bundler::URI.parse(proxy_uri) + end + + def self.use_proxy?(hostname, addr, port, no_proxy) # :nodoc: + hostname = hostname.downcase + dothostname = ".#{hostname}" + no_proxy.scan(/([^:,\s]+)(?::(\d+))?/) {|p_host, p_port| + if !p_port || port == p_port.to_i + if p_host.start_with?('.') + return false if hostname.end_with?(p_host.downcase) + else + return false if dothostname.end_with?(".#{p_host.downcase}") + end + if addr + begin + return false if IPAddr.new(p_host).include?(addr) + rescue IPAddr::InvalidAddressError + next + end + end + end + } + true + end + end +end diff --git a/lib/bundler/vendor/uri/lib/uri/http.rb b/lib/bundler/vendor/uri/lib/uri/http.rb new file mode 100644 index 0000000000..9b217ee266 --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri/http.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: false +# = uri/http.rb +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Bundler::URI for general documentation +# + +require_relative 'generic' + +module Bundler::URI + + # + # The syntax of HTTP URIs is defined in RFC1738 section 3.3. + # + # Note that the Ruby Bundler::URI library allows HTTP URLs containing usernames and + # passwords. This is not legal as per the RFC, but used to be + # supported in Internet Explorer 5 and 6, before the MS04-004 security + # update. See <URL:http://support.microsoft.com/kb/834489>. + # + class HTTP < Generic + # A Default port of 80 for Bundler::URI::HTTP. + DEFAULT_PORT = 80 + + # An Array of the available components for Bundler::URI::HTTP. + COMPONENT = %i[ + scheme + userinfo host port + path + query + fragment + ].freeze + + # + # == Description + # + # Creates a new Bundler::URI::HTTP object from components, with syntax checking. + # + # The components accepted are userinfo, host, port, path, query, and + # fragment. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the + # order <code>[userinfo, host, port, path, query, fragment]</code>. + # + # Example: + # + # uri = Bundler::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar') + # + # uri = Bundler::URI::HTTP.build([nil, "www.example.com", nil, "/path", + # "query", 'fragment']) + # + # Currently, if passed userinfo components this method generates + # invalid HTTP URIs as per RFC 1738. + # + def self.build(args) + tmp = Util.make_components_hash(self, args) + super(tmp) + end + + # Do not allow empty host names, as they are not allowed by RFC 3986. + def check_host(v) + ret = super + + if ret && v.empty? + raise InvalidComponentError, + "bad component(expected host component): #{v}" + end + + ret + end + + # + # == Description + # + # Returns the full path for an HTTP request, as required by Net::HTTP::Get. + # + # If the Bundler::URI contains a query, the full path is Bundler::URI#path + '?' + Bundler::URI#query. + # Otherwise, the path is simply Bundler::URI#path. + # + # Example: + # + # uri = Bundler::URI::HTTP.build(path: '/foo/bar', query: 'test=true') + # uri.request_uri # => "/foo/bar?test=true" + # + def request_uri + return unless @path + + url = @query ? "#@path?#@query" : @path.dup + url.start_with?(?/.freeze) ? url : ?/ + url + end + + # + # == Description + # + # Returns the authority for an HTTP uri, as defined in + # https://www.rfc-editor.org/rfc/rfc3986#section-3.2. + # + # + # Example: + # + # Bundler::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar').authority #=> "www.example.com" + # Bundler::URI::HTTP.build(host: 'www.example.com', port: 8000, path: '/foo/bar').authority #=> "www.example.com:8000" + # Bundler::URI::HTTP.build(host: 'www.example.com', port: 80, path: '/foo/bar').authority #=> "www.example.com" + # + def authority + if port == default_port + host + else + "#{host}:#{port}" + end + end + + # + # == Description + # + # Returns the origin for an HTTP uri, as defined in + # https://www.rfc-editor.org/rfc/rfc6454. + # + # + # Example: + # + # Bundler::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar').origin #=> "http://www.example.com" + # Bundler::URI::HTTP.build(host: 'www.example.com', port: 8000, path: '/foo/bar').origin #=> "http://www.example.com:8000" + # Bundler::URI::HTTP.build(host: 'www.example.com', port: 80, path: '/foo/bar').origin #=> "http://www.example.com" + # Bundler::URI::HTTPS.build(host: 'www.example.com', path: '/foo/bar').origin #=> "https://www.example.com" + # + def origin + "#{scheme}://#{authority}" + end + end + + register_scheme 'HTTP', HTTP +end diff --git a/lib/bundler/vendor/uri/lib/uri/https.rb b/lib/bundler/vendor/uri/lib/uri/https.rb new file mode 100644 index 0000000000..e4556e3ecb --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri/https.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: false +# = uri/https.rb +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Bundler::URI for general documentation +# + +require_relative 'http' + +module Bundler::URI + + # The default port for HTTPS URIs is 443, and the scheme is 'https:' rather + # than 'http:'. Other than that, HTTPS URIs are identical to HTTP URIs; + # see Bundler::URI::HTTP. + class HTTPS < HTTP + # A Default port of 443 for Bundler::URI::HTTPS + DEFAULT_PORT = 443 + end + + register_scheme 'HTTPS', HTTPS +end diff --git a/lib/bundler/vendor/uri/lib/uri/ldap.rb b/lib/bundler/vendor/uri/lib/uri/ldap.rb new file mode 100644 index 0000000000..9811b6e2f5 --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri/ldap.rb @@ -0,0 +1,261 @@ +# frozen_string_literal: false +# = uri/ldap.rb +# +# Author:: +# Takaaki Tateishi <ttate@jaist.ac.jp> +# Akira Yamada <akira@ruby-lang.org> +# License:: +# Bundler::URI::LDAP is copyrighted free software by Takaaki Tateishi and Akira Yamada. +# You can redistribute it and/or modify it under the same term as Ruby. +# +# See Bundler::URI for general documentation +# + +require_relative 'generic' + +module Bundler::URI + + # + # LDAP Bundler::URI SCHEMA (described in RFC2255). + #-- + # ldap://<host>/<dn>[?<attrs>[?<scope>[?<filter>[?<extensions>]]]] + #++ + class LDAP < Generic + + # A Default port of 389 for Bundler::URI::LDAP. + DEFAULT_PORT = 389 + + # An Array of the available components for Bundler::URI::LDAP. + COMPONENT = [ + :scheme, + :host, :port, + :dn, + :attributes, + :scope, + :filter, + :extensions, + ].freeze + + # Scopes available for the starting point. + # + # * SCOPE_BASE - the Base DN + # * SCOPE_ONE - one level under the Base DN, not including the base DN and + # not including any entries under this + # * SCOPE_SUB - subtrees, all entries at all levels + # + SCOPE = [ + SCOPE_ONE = 'one', + SCOPE_SUB = 'sub', + SCOPE_BASE = 'base', + ].freeze + + # + # == Description + # + # Creates a new Bundler::URI::LDAP object from components, with syntax checking. + # + # The components accepted are host, port, dn, attributes, + # scope, filter, and extensions. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the + # order <code>[host, port, dn, attributes, scope, filter, extensions]</code>. + # + # Example: + # + # uri = Bundler::URI::LDAP.build({:host => 'ldap.example.com', + # :dn => '/dc=example'}) + # + # uri = Bundler::URI::LDAP.build(["ldap.example.com", nil, + # "/dc=example;dc=com", "query", nil, nil, nil]) + # + def self.build(args) + tmp = Util::make_components_hash(self, args) + + if tmp[:dn] + tmp[:path] = tmp[:dn] + end + + query = [] + [:extensions, :filter, :scope, :attributes].collect do |x| + next if !tmp[x] && query.size == 0 + query.unshift(tmp[x]) + end + + tmp[:query] = query.join('?') + + return super(tmp) + end + + # + # == Description + # + # Creates a new Bundler::URI::LDAP object from generic Bundler::URI components as per + # RFC 2396. No LDAP-specific syntax checking is performed. + # + # Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+, + # +opaque+, +query+, and +fragment+, in that order. + # + # Example: + # + # uri = Bundler::URI::LDAP.new("ldap", nil, "ldap.example.com", nil, nil, + # "/dc=example;dc=com", nil, "query", nil) + # + # See also Bundler::URI::Generic.new. + # + def initialize(*arg) + super(*arg) + + if @fragment + raise InvalidURIError, 'bad LDAP URL' + end + + parse_dn + parse_query + end + + # Private method to cleanup +dn+ from using the +path+ component attribute. + def parse_dn + raise InvalidURIError, 'bad LDAP URL' unless @path + @dn = @path[1..-1] + end + private :parse_dn + + # Private method to cleanup +attributes+, +scope+, +filter+, and +extensions+ + # from using the +query+ component attribute. + def parse_query + @attributes = nil + @scope = nil + @filter = nil + @extensions = nil + + if @query + attrs, scope, filter, extensions = @query.split('?') + + @attributes = attrs if attrs && attrs.size > 0 + @scope = scope if scope && scope.size > 0 + @filter = filter if filter && filter.size > 0 + @extensions = extensions if extensions && extensions.size > 0 + end + end + private :parse_query + + # Private method to assemble +query+ from +attributes+, +scope+, +filter+, and +extensions+. + def build_path_query + @path = '/' + @dn + + query = [] + [@extensions, @filter, @scope, @attributes].each do |x| + next if !x && query.size == 0 + query.unshift(x) + end + @query = query.join('?') + end + private :build_path_query + + # Returns dn. + def dn + @dn + end + + # Private setter for dn +val+. + def set_dn(val) + @dn = val + build_path_query + @dn + end + protected :set_dn + + # Setter for dn +val+. + def dn=(val) + set_dn(val) + val + end + + # Returns attributes. + def attributes + @attributes + end + + # Private setter for attributes +val+. + def set_attributes(val) + @attributes = val + build_path_query + @attributes + end + protected :set_attributes + + # Setter for attributes +val+. + def attributes=(val) + set_attributes(val) + val + end + + # Returns scope. + def scope + @scope + end + + # Private setter for scope +val+. + def set_scope(val) + @scope = val + build_path_query + @scope + end + protected :set_scope + + # Setter for scope +val+. + def scope=(val) + set_scope(val) + val + end + + # Returns filter. + def filter + @filter + end + + # Private setter for filter +val+. + def set_filter(val) + @filter = val + build_path_query + @filter + end + protected :set_filter + + # Setter for filter +val+. + def filter=(val) + set_filter(val) + val + end + + # Returns extensions. + def extensions + @extensions + end + + # Private setter for extensions +val+. + def set_extensions(val) + @extensions = val + build_path_query + @extensions + end + protected :set_extensions + + # Setter for extensions +val+. + def extensions=(val) + set_extensions(val) + val + end + + # Checks if Bundler::URI has a path. + # For Bundler::URI::LDAP this will return +false+. + def hierarchical? + false + end + end + + register_scheme 'LDAP', LDAP +end diff --git a/lib/bundler/vendor/uri/lib/uri/ldaps.rb b/lib/bundler/vendor/uri/lib/uri/ldaps.rb new file mode 100644 index 0000000000..c786168450 --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri/ldaps.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: false +# = uri/ldap.rb +# +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Bundler::URI for general documentation +# + +require_relative 'ldap' + +module Bundler::URI + + # The default port for LDAPS URIs is 636, and the scheme is 'ldaps:' rather + # than 'ldap:'. Other than that, LDAPS URIs are identical to LDAP URIs; + # see Bundler::URI::LDAP. + class LDAPS < LDAP + # A Default port of 636 for Bundler::URI::LDAPS + DEFAULT_PORT = 636 + end + + register_scheme 'LDAPS', LDAPS +end diff --git a/lib/bundler/vendor/uri/lib/uri/mailto.rb b/lib/bundler/vendor/uri/lib/uri/mailto.rb new file mode 100644 index 0000000000..ff2e30be86 --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri/mailto.rb @@ -0,0 +1,293 @@ +# frozen_string_literal: false +# = uri/mailto.rb +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Bundler::URI for general documentation +# + +require_relative 'generic' + +module Bundler::URI + + # + # RFC6068, the mailto URL scheme. + # + class MailTo < Generic + include RFC2396_REGEXP + + # A Default port of nil for Bundler::URI::MailTo. + DEFAULT_PORT = nil + + # An Array of the available components for Bundler::URI::MailTo. + COMPONENT = [ :scheme, :to, :headers ].freeze + + # :stopdoc: + # "hname" and "hvalue" are encodings of an RFC 822 header name and + # value, respectively. As with "to", all URL reserved characters must + # be encoded. + # + # "#mailbox" is as specified in RFC 822 [RFC822]. This means that it + # consists of zero or more comma-separated mail addresses, possibly + # including "phrase" and "comment" components. Note that all URL + # reserved characters in "to" must be encoded: in particular, + # parentheses, commas, and the percent sign ("%"), which commonly occur + # in the "mailbox" syntax. + # + # Within mailto URLs, the characters "?", "=", "&" are reserved. + + # ; RFC 6068 + # hfields = "?" hfield *( "&" hfield ) + # hfield = hfname "=" hfvalue + # hfname = *qchar + # hfvalue = *qchar + # qchar = unreserved / pct-encoded / some-delims + # some-delims = "!" / "$" / "'" / "(" / ")" / "*" + # / "+" / "," / ";" / ":" / "@" + # + # ; RFC3986 + # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + # pct-encoded = "%" HEXDIG HEXDIG + HEADER_REGEXP = /\A(?<hfield>(?:%\h\h|[!$'-.0-;@-Z_a-z~])*=(?:%\h\h|[!$'-.0-;@-Z_a-z~])*)(?:&\g<hfield>)*\z/ + # practical regexp for email address + # https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address + EMAIL_REGEXP = /\A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\z/ + # :startdoc: + + # + # == Description + # + # Creates a new Bundler::URI::MailTo object from components, with syntax checking. + # + # Components can be provided as an Array or Hash. If an Array is used, + # the components must be supplied as <code>[to, headers]</code>. + # + # If a Hash is used, the keys are the component names preceded by colons. + # + # The headers can be supplied as a pre-encoded string, such as + # <code>"subject=subscribe&cc=address"</code>, or as an Array of Arrays + # like <code>[['subject', 'subscribe'], ['cc', 'address']]</code>. + # + # Examples: + # + # require 'bundler/vendor/uri/lib/uri' + # + # m1 = Bundler::URI::MailTo.build(['joe@example.com', 'subject=Ruby']) + # m1.to_s # => "mailto:joe@example.com?subject=Ruby" + # + # m2 = Bundler::URI::MailTo.build(['john@example.com', [['Subject', 'Ruby'], ['Cc', 'jack@example.com']]]) + # m2.to_s # => "mailto:john@example.com?Subject=Ruby&Cc=jack@example.com" + # + # m3 = Bundler::URI::MailTo.build({:to => 'listman@example.com', :headers => [['subject', 'subscribe']]}) + # m3.to_s # => "mailto:listman@example.com?subject=subscribe" + # + def self.build(args) + tmp = Util.make_components_hash(self, args) + + case tmp[:to] + when Array + tmp[:opaque] = tmp[:to].join(',') + when String + tmp[:opaque] = tmp[:to].dup + else + tmp[:opaque] = '' + end + + if tmp[:headers] + query = + case tmp[:headers] + when Array + tmp[:headers].collect { |x| + if x.kind_of?(Array) + x[0] + '=' + x[1..-1].join + else + x.to_s + end + }.join('&') + when Hash + tmp[:headers].collect { |h,v| + h + '=' + v + }.join('&') + else + tmp[:headers].to_s + end + unless query.empty? + tmp[:opaque] << '?' << query + end + end + + super(tmp) + end + + # + # == Description + # + # Creates a new Bundler::URI::MailTo object from generic URL components with + # no syntax checking. + # + # This method is usually called from Bundler::URI::parse, which checks + # the validity of each component. + # + def initialize(*arg) + super(*arg) + + @to = nil + @headers = [] + + # The RFC3986 parser does not normally populate opaque + @opaque = "?#{@query}" if @query && !@opaque + + unless @opaque + raise InvalidComponentError, + "missing opaque part for mailto URL" + end + to, header = @opaque.split('?', 2) + # allow semicolon as a addr-spec separator + # http://support.microsoft.com/kb/820868 + unless /\A(?:[^@,;]+@[^@,;]+(?:\z|[,;]))*\z/ =~ to + raise InvalidComponentError, + "unrecognised opaque part for mailtoURL: #{@opaque}" + end + + if arg[10] # arg_check + self.to = to + self.headers = header + else + set_to(to) + set_headers(header) + end + end + + # The primary e-mail address of the URL, as a String. + attr_reader :to + + # E-mail headers set by the URL, as an Array of Arrays. + attr_reader :headers + + # Checks the to +v+ component. + def check_to(v) + return true unless v + return true if v.size == 0 + + v.split(/[,;]/).each do |addr| + # check url safety as path-rootless + if /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*\z/ !~ addr + raise InvalidComponentError, + "an address in 'to' is invalid as Bundler::URI #{addr.dump}" + end + + # check addr-spec + # don't s/\+/ /g + addr.gsub!(/%\h\h/, Bundler::URI::TBLDECWWWCOMP_) + if EMAIL_REGEXP !~ addr + raise InvalidComponentError, + "an address in 'to' is invalid as uri-escaped addr-spec #{addr.dump}" + end + end + + true + end + private :check_to + + # Private setter for to +v+. + def set_to(v) + @to = v + end + protected :set_to + + # Setter for to +v+. + def to=(v) + check_to(v) + set_to(v) + v + end + + # Checks the headers +v+ component against either + # * HEADER_REGEXP + def check_headers(v) + return true unless v + return true if v.size == 0 + if HEADER_REGEXP !~ v + raise InvalidComponentError, + "bad component(expected opaque component): #{v}" + end + + true + end + private :check_headers + + # Private setter for headers +v+. + def set_headers(v) + @headers = [] + if v + v.split('&').each do |x| + @headers << x.split(/=/, 2) + end + end + end + protected :set_headers + + # Setter for headers +v+. + def headers=(v) + check_headers(v) + set_headers(v) + v + end + + # Constructs String from Bundler::URI. + def to_s + @scheme + ':' + + if @to + @to + else + '' + end + + if @headers.size > 0 + '?' + @headers.collect{|x| x.join('=')}.join('&') + else + '' + end + + if @fragment + '#' + @fragment + else + '' + end + end + + # Returns the RFC822 e-mail text equivalent of the URL, as a String. + # + # Example: + # + # require 'bundler/vendor/uri/lib/uri' + # + # uri = Bundler::URI.parse("mailto:ruby-list@ruby-lang.org?Subject=subscribe&cc=myaddr") + # uri.to_mailtext + # # => "To: ruby-list@ruby-lang.org\nSubject: subscribe\nCc: myaddr\n\n\n" + # + def to_mailtext + to = Bundler::URI.decode_www_form_component(@to) + head = '' + body = '' + @headers.each do |x| + case x[0] + when 'body' + body = Bundler::URI.decode_www_form_component(x[1]) + when 'to' + to << ', ' + Bundler::URI.decode_www_form_component(x[1]) + else + head << Bundler::URI.decode_www_form_component(x[0]).capitalize + ': ' + + Bundler::URI.decode_www_form_component(x[1]) + "\n" + end + end + + "To: #{to} +#{head} +#{body} +" + end + alias to_rfc822text to_mailtext + end + + register_scheme 'MAILTO', MailTo +end diff --git a/lib/bundler/vendor/uri/lib/uri/rfc2396_parser.rb b/lib/bundler/vendor/uri/lib/uri/rfc2396_parser.rb new file mode 100644 index 0000000000..522113fe67 --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri/rfc2396_parser.rb @@ -0,0 +1,547 @@ +# frozen_string_literal: false +#-- +# = uri/common.rb +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# License:: +# You can redistribute it and/or modify it under the same term as Ruby. +# +# See Bundler::URI for general documentation +# + +module Bundler::URI + # + # Includes Bundler::URI::REGEXP::PATTERN + # + module RFC2396_REGEXP + # + # Patterns used to parse Bundler::URI's + # + module PATTERN + # :stopdoc: + + # RFC 2396 (Bundler::URI Generic Syntax) + # RFC 2732 (IPv6 Literal Addresses in URL's) + # RFC 2373 (IPv6 Addressing Architecture) + + # alpha = lowalpha | upalpha + ALPHA = "a-zA-Z" + # alphanum = alpha | digit + ALNUM = "#{ALPHA}\\d" + + # hex = digit | "A" | "B" | "C" | "D" | "E" | "F" | + # "a" | "b" | "c" | "d" | "e" | "f" + HEX = "a-fA-F\\d" + # escaped = "%" hex hex + ESCAPED = "%[#{HEX}]{2}" + # mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | + # "(" | ")" + # unreserved = alphanum | mark + UNRESERVED = "\\-_.!~*'()#{ALNUM}" + # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | + # "$" | "," + # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | + # "$" | "," | "[" | "]" (RFC 2732) + RESERVED = ";/?:@&=+$,\\[\\]" + + # domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum + DOMLABEL = "(?:[#{ALNUM}](?:[-#{ALNUM}]*[#{ALNUM}])?)" + # toplabel = alpha | alpha *( alphanum | "-" ) alphanum + TOPLABEL = "(?:[#{ALPHA}](?:[-#{ALNUM}]*[#{ALNUM}])?)" + # hostname = *( domainlabel "." ) toplabel [ "." ] + HOSTNAME = "(?:#{DOMLABEL}\\.)*#{TOPLABEL}\\.?" + + # :startdoc: + end # PATTERN + + # :startdoc: + end # REGEXP + + # Class that parses String's into Bundler::URI's. + # + # It contains a Hash set of patterns and Regexp's that match and validate. + # + class RFC2396_Parser + include RFC2396_REGEXP + + # + # == Synopsis + # + # Bundler::URI::RFC2396_Parser.new([opts]) + # + # == Args + # + # The constructor accepts a hash as options for parser. + # Keys of options are pattern names of Bundler::URI components + # and values of options are pattern strings. + # The constructor generates set of regexps for parsing URIs. + # + # You can use the following keys: + # + # * :ESCAPED (Bundler::URI::PATTERN::ESCAPED in default) + # * :UNRESERVED (Bundler::URI::PATTERN::UNRESERVED in default) + # * :DOMLABEL (Bundler::URI::PATTERN::DOMLABEL in default) + # * :TOPLABEL (Bundler::URI::PATTERN::TOPLABEL in default) + # * :HOSTNAME (Bundler::URI::PATTERN::HOSTNAME in default) + # + # == Examples + # + # p = Bundler::URI::RFC2396_Parser.new(:ESCAPED => "(?:%[a-fA-F0-9]{2}|%u[a-fA-F0-9]{4})") + # u = p.parse("http://example.jp/%uABCD") #=> #<Bundler::URI::HTTP http://example.jp/%uABCD> + # Bundler::URI.parse(u.to_s) #=> raises Bundler::URI::InvalidURIError + # + # s = "http://example.com/ABCD" + # u1 = p.parse(s) #=> #<Bundler::URI::HTTP http://example.com/ABCD> + # u2 = Bundler::URI.parse(s) #=> #<Bundler::URI::HTTP http://example.com/ABCD> + # u1 == u2 #=> true + # u1.eql?(u2) #=> false + # + def initialize(opts = {}) + @pattern = initialize_pattern(opts) + @pattern.each_value(&:freeze) + @pattern.freeze + + @regexp = initialize_regexp(@pattern) + @regexp.each_value(&:freeze) + @regexp.freeze + end + + # The Hash of patterns. + # + # See also #initialize_pattern. + attr_reader :pattern + + # The Hash of Regexp. + # + # See also #initialize_regexp. + attr_reader :regexp + + # Returns a split Bundler::URI against +regexp[:ABS_URI]+. + def split(uri) + case uri + when '' + # null uri + + when @regexp[:ABS_URI] + scheme, opaque, userinfo, host, port, + registry, path, query, fragment = $~[1..-1] + + # Bundler::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + # hier_part = ( net_path | abs_path ) [ "?" query ] + # opaque_part = uric_no_slash *uric + + # abs_path = "/" path_segments + # net_path = "//" authority [ abs_path ] + + # authority = server | reg_name + # server = [ [ userinfo "@" ] hostport ] + + if !scheme + raise InvalidURIError, + "bad Bundler::URI (absolute but no scheme): #{uri}" + end + if !opaque && (!path && (!host && !registry)) + raise InvalidURIError, + "bad Bundler::URI (absolute but no path): #{uri}" + end + + when @regexp[:REL_URI] + scheme = nil + opaque = nil + + userinfo, host, port, registry, + rel_segment, abs_path, query, fragment = $~[1..-1] + if rel_segment && abs_path + path = rel_segment + abs_path + elsif rel_segment + path = rel_segment + elsif abs_path + path = abs_path + end + + # Bundler::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + + # relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + + # net_path = "//" authority [ abs_path ] + # abs_path = "/" path_segments + # rel_path = rel_segment [ abs_path ] + + # authority = server | reg_name + # server = [ [ userinfo "@" ] hostport ] + + else + raise InvalidURIError, "bad Bundler::URI (is not Bundler::URI?): #{uri}" + end + + path = '' if !path && !opaque # (see RFC2396 Section 5.2) + ret = [ + scheme, + userinfo, host, port, # X + registry, # X + path, # Y + opaque, # Y + query, + fragment + ] + return ret + end + + # + # == Args + # + # +uri+:: + # String + # + # == Description + # + # Parses +uri+ and constructs either matching Bundler::URI scheme object + # (File, FTP, HTTP, HTTPS, LDAP, LDAPS, or MailTo) or Bundler::URI::Generic. + # + # == Usage + # + # Bundler::URI::RFC2396_PARSER.parse("ldap://ldap.example.com/dc=example?user=john") + # #=> #<Bundler::URI::LDAP ldap://ldap.example.com/dc=example?user=john> + # + def parse(uri) + Bundler::URI.for(*self.split(uri), self) + end + + # + # == Args + # + # +uris+:: + # an Array of Strings + # + # == Description + # + # Attempts to parse and merge a set of URIs. + # + def join(*uris) + uris[0] = convert_to_uri(uris[0]) + uris.inject :merge + end + + # + # :call-seq: + # extract( str ) + # extract( str, schemes ) + # extract( str, schemes ) {|item| block } + # + # == Args + # + # +str+:: + # String to search + # +schemes+:: + # Patterns to apply to +str+ + # + # == Description + # + # Attempts to parse and merge a set of URIs. + # If no +block+ given, then returns the result, + # else it calls +block+ for each element in result. + # + # See also #make_regexp. + # + def extract(str, schemes = nil) + if block_given? + str.scan(make_regexp(schemes)) { yield $& } + nil + else + result = [] + str.scan(make_regexp(schemes)) { result.push $& } + result + end + end + + # Returns Regexp that is default +self.regexp[:ABS_URI_REF]+, + # unless +schemes+ is provided. Then it is a Regexp.union with +self.pattern[:X_ABS_URI]+. + def make_regexp(schemes = nil) + unless schemes + @regexp[:ABS_URI_REF] + else + /(?=(?i:#{Regexp.union(*schemes).source}):)#{@pattern[:X_ABS_URI]}/x + end + end + + # + # :call-seq: + # escape( str ) + # escape( str, unsafe ) + # + # == Args + # + # +str+:: + # String to make safe + # +unsafe+:: + # Regexp to apply. Defaults to +self.regexp[:UNSAFE]+ + # + # == Description + # + # Constructs a safe String from +str+, removing unsafe characters, + # replacing them with codes. + # + def escape(str, unsafe = @regexp[:UNSAFE]) + unless unsafe.kind_of?(Regexp) + # perhaps unsafe is String object + unsafe = Regexp.new("[#{Regexp.quote(unsafe)}]", false) + end + str.gsub(unsafe) do + us = $& + tmp = '' + us.each_byte do |uc| + tmp << sprintf('%%%02X', uc) + end + tmp + end.force_encoding(Encoding::US_ASCII) + end + + # + # :call-seq: + # unescape( str ) + # unescape( str, escaped ) + # + # == Args + # + # +str+:: + # String to remove escapes from + # +escaped+:: + # Regexp to apply. Defaults to +self.regexp[:ESCAPED]+ + # + # == Description + # + # Removes escapes from +str+. + # + def unescape(str, escaped = @regexp[:ESCAPED]) + enc = str.encoding + enc = Encoding::UTF_8 if enc == Encoding::US_ASCII + str.gsub(escaped) { [$&[1, 2]].pack('H2').force_encoding(enc) } + end + + TO_S = Kernel.instance_method(:to_s) # :nodoc: + if TO_S.respond_to?(:bind_call) + def inspect # :nodoc: + TO_S.bind_call(self) + end + else + def inspect # :nodoc: + TO_S.bind(self).call + end + end + + private + + # Constructs the default Hash of patterns. + def initialize_pattern(opts = {}) + ret = {} + ret[:ESCAPED] = escaped = (opts.delete(:ESCAPED) || PATTERN::ESCAPED) + ret[:UNRESERVED] = unreserved = opts.delete(:UNRESERVED) || PATTERN::UNRESERVED + ret[:RESERVED] = reserved = opts.delete(:RESERVED) || PATTERN::RESERVED + ret[:DOMLABEL] = opts.delete(:DOMLABEL) || PATTERN::DOMLABEL + ret[:TOPLABEL] = opts.delete(:TOPLABEL) || PATTERN::TOPLABEL + ret[:HOSTNAME] = hostname = opts.delete(:HOSTNAME) + + # RFC 2396 (Bundler::URI Generic Syntax) + # RFC 2732 (IPv6 Literal Addresses in URL's) + # RFC 2373 (IPv6 Addressing Architecture) + + # uric = reserved | unreserved | escaped + ret[:URIC] = uric = "(?:[#{unreserved}#{reserved}]|#{escaped})" + # uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" | + # "&" | "=" | "+" | "$" | "," + ret[:URIC_NO_SLASH] = uric_no_slash = "(?:[#{unreserved};?:@&=+$,]|#{escaped})" + # query = *uric + ret[:QUERY] = query = "#{uric}*" + # fragment = *uric + ret[:FRAGMENT] = fragment = "#{uric}*" + + # hostname = *( domainlabel "." ) toplabel [ "." ] + # reg-name = *( unreserved / pct-encoded / sub-delims ) # RFC3986 + unless hostname + ret[:HOSTNAME] = hostname = "(?:[a-zA-Z0-9\\-.]|%\\h\\h)+" + end + + # RFC 2373, APPENDIX B: + # IPv6address = hexpart [ ":" IPv4address ] + # IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT + # hexpart = hexseq | hexseq "::" [ hexseq ] | "::" [ hexseq ] + # hexseq = hex4 *( ":" hex4) + # hex4 = 1*4HEXDIG + # + # XXX: This definition has a flaw. "::" + IPv4address must be + # allowed too. Here is a replacement. + # + # IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT + ret[:IPV4ADDR] = ipv4addr = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}" + # hex4 = 1*4HEXDIG + hex4 = "[#{PATTERN::HEX}]{1,4}" + # lastpart = hex4 | IPv4address + lastpart = "(?:#{hex4}|#{ipv4addr})" + # hexseq1 = *( hex4 ":" ) hex4 + hexseq1 = "(?:#{hex4}:)*#{hex4}" + # hexseq2 = *( hex4 ":" ) lastpart + hexseq2 = "(?:#{hex4}:)*#{lastpart}" + # IPv6address = hexseq2 | [ hexseq1 ] "::" [ hexseq2 ] + ret[:IPV6ADDR] = ipv6addr = "(?:#{hexseq2}|(?:#{hexseq1})?::(?:#{hexseq2})?)" + + # IPv6prefix = ( hexseq1 | [ hexseq1 ] "::" [ hexseq1 ] ) "/" 1*2DIGIT + # unused + + # ipv6reference = "[" IPv6address "]" (RFC 2732) + ret[:IPV6REF] = ipv6ref = "\\[#{ipv6addr}\\]" + + # host = hostname | IPv4address + # host = hostname | IPv4address | IPv6reference (RFC 2732) + ret[:HOST] = host = "(?:#{hostname}|#{ipv4addr}|#{ipv6ref})" + # port = *digit + ret[:PORT] = port = '\d*' + # hostport = host [ ":" port ] + ret[:HOSTPORT] = hostport = "#{host}(?::#{port})?" + + # userinfo = *( unreserved | escaped | + # ";" | ":" | "&" | "=" | "+" | "$" | "," ) + ret[:USERINFO] = userinfo = "(?:[#{unreserved};:&=+$,]|#{escaped})*" + + # pchar = unreserved | escaped | + # ":" | "@" | "&" | "=" | "+" | "$" | "," + pchar = "(?:[#{unreserved}:@&=+$,]|#{escaped})" + # param = *pchar + param = "#{pchar}*" + # segment = *pchar *( ";" param ) + segment = "#{pchar}*(?:;#{param})*" + # path_segments = segment *( "/" segment ) + ret[:PATH_SEGMENTS] = path_segments = "#{segment}(?:/#{segment})*" + + # server = [ [ userinfo "@" ] hostport ] + server = "(?:#{userinfo}@)?#{hostport}" + # reg_name = 1*( unreserved | escaped | "$" | "," | + # ";" | ":" | "@" | "&" | "=" | "+" ) + ret[:REG_NAME] = reg_name = "(?:[#{unreserved}$,;:@&=+]|#{escaped})+" + # authority = server | reg_name + authority = "(?:#{server}|#{reg_name})" + + # rel_segment = 1*( unreserved | escaped | + # ";" | "@" | "&" | "=" | "+" | "$" | "," ) + ret[:REL_SEGMENT] = rel_segment = "(?:[#{unreserved};@&=+$,]|#{escaped})+" + + # scheme = alpha *( alpha | digit | "+" | "-" | "." ) + ret[:SCHEME] = scheme = "[#{PATTERN::ALPHA}][\\-+.#{PATTERN::ALPHA}\\d]*" + + # abs_path = "/" path_segments + ret[:ABS_PATH] = abs_path = "/#{path_segments}" + # rel_path = rel_segment [ abs_path ] + ret[:REL_PATH] = rel_path = "#{rel_segment}(?:#{abs_path})?" + # net_path = "//" authority [ abs_path ] + ret[:NET_PATH] = net_path = "//#{authority}(?:#{abs_path})?" + + # hier_part = ( net_path | abs_path ) [ "?" query ] + ret[:HIER_PART] = hier_part = "(?:#{net_path}|#{abs_path})(?:\\?(?:#{query}))?" + # opaque_part = uric_no_slash *uric + ret[:OPAQUE_PART] = opaque_part = "#{uric_no_slash}#{uric}*" + + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + ret[:ABS_URI] = abs_uri = "#{scheme}:(?:#{hier_part}|#{opaque_part})" + # relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + ret[:REL_URI] = rel_uri = "(?:#{net_path}|#{abs_path}|#{rel_path})(?:\\?#{query})?" + + # Bundler::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + ret[:URI_REF] = "(?:#{abs_uri}|#{rel_uri})?(?:##{fragment})?" + + ret[:X_ABS_URI] = " + (#{scheme}): (?# 1: scheme) + (?: + (#{opaque_part}) (?# 2: opaque) + | + (?:(?: + //(?: + (?:(?:(#{userinfo})@)? (?# 3: userinfo) + (?:(#{host})(?::(\\d*))?))? (?# 4: host, 5: port) + | + (#{reg_name}) (?# 6: registry) + ) + | + (?!//)) (?# XXX: '//' is the mark for hostport) + (#{abs_path})? (?# 7: path) + )(?:\\?(#{query}))? (?# 8: query) + ) + (?:\\#(#{fragment}))? (?# 9: fragment) + " + + ret[:X_REL_URI] = " + (?: + (?: + // + (?: + (?:(#{userinfo})@)? (?# 1: userinfo) + (#{host})?(?::(\\d*))? (?# 2: host, 3: port) + | + (#{reg_name}) (?# 4: registry) + ) + ) + | + (#{rel_segment}) (?# 5: rel_segment) + )? + (#{abs_path})? (?# 6: abs_path) + (?:\\?(#{query}))? (?# 7: query) + (?:\\#(#{fragment}))? (?# 8: fragment) + " + + ret + end + + # Constructs the default Hash of Regexp's. + def initialize_regexp(pattern) + ret = {} + + # for Bundler::URI::split + ret[:ABS_URI] = Regexp.new('\A\s*+' + pattern[:X_ABS_URI] + '\s*\z', Regexp::EXTENDED) + ret[:REL_URI] = Regexp.new('\A\s*+' + pattern[:X_REL_URI] + '\s*\z', Regexp::EXTENDED) + + # for Bundler::URI::extract + ret[:URI_REF] = Regexp.new(pattern[:URI_REF]) + ret[:ABS_URI_REF] = Regexp.new(pattern[:X_ABS_URI], Regexp::EXTENDED) + ret[:REL_URI_REF] = Regexp.new(pattern[:X_REL_URI], Regexp::EXTENDED) + + # for Bundler::URI::escape/unescape + ret[:ESCAPED] = Regexp.new(pattern[:ESCAPED]) + ret[:UNSAFE] = Regexp.new("[^#{pattern[:UNRESERVED]}#{pattern[:RESERVED]}]") + + # for Generic#initialize + ret[:SCHEME] = Regexp.new("\\A#{pattern[:SCHEME]}\\z") + ret[:USERINFO] = Regexp.new("\\A#{pattern[:USERINFO]}\\z") + ret[:HOST] = Regexp.new("\\A#{pattern[:HOST]}\\z") + ret[:PORT] = Regexp.new("\\A#{pattern[:PORT]}\\z") + ret[:OPAQUE] = Regexp.new("\\A#{pattern[:OPAQUE_PART]}\\z") + ret[:REGISTRY] = Regexp.new("\\A#{pattern[:REG_NAME]}\\z") + ret[:ABS_PATH] = Regexp.new("\\A#{pattern[:ABS_PATH]}\\z") + ret[:REL_PATH] = Regexp.new("\\A#{pattern[:REL_PATH]}\\z") + ret[:QUERY] = Regexp.new("\\A#{pattern[:QUERY]}\\z") + ret[:FRAGMENT] = Regexp.new("\\A#{pattern[:FRAGMENT]}\\z") + + ret + end + + # Returns +uri+ as-is if it is Bundler::URI, or convert it to Bundler::URI if it is + # a String. + def convert_to_uri(uri) + if uri.is_a?(Bundler::URI::Generic) + uri + elsif uri = String.try_convert(uri) + parse(uri) + else + raise ArgumentError, + "bad argument (expected Bundler::URI object or Bundler::URI string)" + end + end + + end # class Parser + + # Backward compatibility for Bundler::URI::REGEXP::PATTERN::* + RFC2396_Parser.new.pattern.each_pair do |sym, str| + unless RFC2396_REGEXP::PATTERN.const_defined?(sym, false) + RFC2396_REGEXP::PATTERN.const_set(sym, str) + end + end +end # module Bundler::URI diff --git a/lib/bundler/vendor/uri/lib/uri/rfc3986_parser.rb b/lib/bundler/vendor/uri/lib/uri/rfc3986_parser.rb new file mode 100644 index 0000000000..d1ff28df23 --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri/rfc3986_parser.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true +module Bundler::URI + class RFC3986_Parser # :nodoc: + # Bundler::URI defined in RFC3986 + HOST = %r[ + (?<IP-literal>\[(?: + (?<IPv6address> + (?:\h{1,4}:){6} + (?<ls32>\h{1,4}:\h{1,4} + | (?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d) + \.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>) + ) + | ::(?:\h{1,4}:){5}\g<ls32> + | \h{1,4}?::(?:\h{1,4}:){4}\g<ls32> + | (?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g<ls32> + | (?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32> + | (?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32> + | (?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32> + | (?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4} + | (?:(?:\h{1,4}:){,6}\h{1,4})?:: + ) + | (?<IPvFuture>v\h++\.[!$&-.0-9:;=A-Z_a-z~]++) + )\]) + | \g<IPv4address> + | (?<reg-name>(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])*+) + ]x + + USERINFO = /(?:%\h\h|[!$&-.0-9:;=A-Z_a-z~])*+/ + + SCHEME = %r[[A-Za-z][+\-.0-9A-Za-z]*+].source + SEG = %r[(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/])].source + SEG_NC = %r[(?:%\h\h|[!$&-.0-9;=@A-Z_a-z~])].source + FRAGMENT = %r[(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/?])*+].source + + RFC3986_URI = %r[\A + (?<seg>#{SEG}){0} + (?<Bundler::URI> + (?<scheme>#{SCHEME}): + (?<hier-part>// + (?<authority> + (?:(?<userinfo>#{USERINFO.source})@)? + (?<host>#{HOST.source.delete(" \n")}) + (?::(?<port>\d*+))? + ) + (?<path-abempty>(?:/\g<seg>*+)?) + | (?<path-absolute>/((?!/)\g<seg>++)?) + | (?<path-rootless>(?!/)\g<seg>++) + | (?<path-empty>) + ) + (?:\?(?<query>[^\#]*+))? + (?:\#(?<fragment>#{FRAGMENT}))? + )\z]x + + RFC3986_relative_ref = %r[\A + (?<seg>#{SEG}){0} + (?<relative-ref> + (?<relative-part>// + (?<authority> + (?:(?<userinfo>#{USERINFO.source})@)? + (?<host>#{HOST.source.delete(" \n")}(?<!/))? + (?::(?<port>\d*+))? + ) + (?<path-abempty>(?:/\g<seg>*+)?) + | (?<path-absolute>/\g<seg>*+) + | (?<path-noscheme>#{SEG_NC}++(?:/\g<seg>*+)?) + | (?<path-empty>) + ) + (?:\?(?<query>[^#]*+))? + (?:\#(?<fragment>#{FRAGMENT}))? + )\z]x + attr_reader :regexp + + def initialize + @regexp = default_regexp.each_value(&:freeze).freeze + end + + def split(uri) #:nodoc: + begin + uri = uri.to_str + rescue NoMethodError + raise InvalidURIError, "bad Bundler::URI (is not Bundler::URI?): #{uri.inspect}" + end + uri.ascii_only? or + raise InvalidURIError, "Bundler::URI must be ascii only #{uri.dump}" + if m = RFC3986_URI.match(uri) + query = m["query"] + scheme = m["scheme"] + opaque = m["path-rootless"] + if opaque + opaque << "?#{query}" if query + [ scheme, + nil, # userinfo + nil, # host + nil, # port + nil, # registry + nil, # path + opaque, + nil, # query + m["fragment"] + ] + else # normal + [ scheme, + m["userinfo"], + m["host"], + m["port"], + nil, # registry + (m["path-abempty"] || + m["path-absolute"] || + m["path-empty"]), + nil, # opaque + query, + m["fragment"] + ] + end + elsif m = RFC3986_relative_ref.match(uri) + [ nil, # scheme + m["userinfo"], + m["host"], + m["port"], + nil, # registry, + (m["path-abempty"] || + m["path-absolute"] || + m["path-noscheme"] || + m["path-empty"]), + nil, # opaque + m["query"], + m["fragment"] + ] + else + raise InvalidURIError, "bad Bundler::URI (is not Bundler::URI?): #{uri.inspect}" + end + end + + def parse(uri) # :nodoc: + Bundler::URI.for(*self.split(uri), self) + end + + def join(*uris) # :nodoc: + uris[0] = convert_to_uri(uris[0]) + uris.inject :merge + end + + # Compatibility for RFC2396 parser + def extract(str, schemes = nil, &block) # :nodoc: + warn "Bundler::URI::RFC3986_PARSER.extract is obsolete. Use Bundler::URI::RFC2396_PARSER.extract explicitly.", uplevel: 1 if $VERBOSE + RFC2396_PARSER.extract(str, schemes, &block) + end + + # Compatibility for RFC2396 parser + def make_regexp(schemes = nil) # :nodoc: + warn "Bundler::URI::RFC3986_PARSER.make_regexp is obsolete. Use Bundler::URI::RFC2396_PARSER.make_regexp explicitly.", uplevel: 1 if $VERBOSE + RFC2396_PARSER.make_regexp(schemes) + end + + # Compatibility for RFC2396 parser + def escape(str, unsafe = nil) # :nodoc: + warn "Bundler::URI::RFC3986_PARSER.escape is obsolete. Use Bundler::URI::RFC2396_PARSER.escape explicitly.", uplevel: 1 if $VERBOSE + unsafe ? RFC2396_PARSER.escape(str, unsafe) : RFC2396_PARSER.escape(str) + end + + # Compatibility for RFC2396 parser + def unescape(str, escaped = nil) # :nodoc: + warn "Bundler::URI::RFC3986_PARSER.unescape is obsolete. Use Bundler::URI::RFC2396_PARSER.unescape explicitly.", uplevel: 1 if $VERBOSE + escaped ? RFC2396_PARSER.unescape(str, escaped) : RFC2396_PARSER.unescape(str) + end + + @@to_s = Kernel.instance_method(:to_s) + if @@to_s.respond_to?(:bind_call) + def inspect + @@to_s.bind_call(self) + end + else + def inspect + @@to_s.bind(self).call + end + end + + private + + def default_regexp # :nodoc: + { + SCHEME: %r[\A#{SCHEME}\z]o, + USERINFO: %r[\A#{USERINFO}\z]o, + HOST: %r[\A#{HOST}\z]o, + ABS_PATH: %r[\A/#{SEG}*+\z]o, + REL_PATH: %r[\A(?!/)#{SEG}++\z]o, + QUERY: %r[\A(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/?])*+\z], + FRAGMENT: %r[\A#{FRAGMENT}\z]o, + OPAQUE: %r[\A(?:[^/].*)?\z], + PORT: /\A[\x09\x0a\x0c\x0d ]*+\d*[\x09\x0a\x0c\x0d ]*\z/, + } + end + + def convert_to_uri(uri) + if uri.is_a?(Bundler::URI::Generic) + uri + elsif uri = String.try_convert(uri) + parse(uri) + else + raise ArgumentError, + "bad argument (expected Bundler::URI object or Bundler::URI string)" + end + end + + end # class Parser +end # module Bundler::URI diff --git a/lib/bundler/vendor/uri/lib/uri/version.rb b/lib/bundler/vendor/uri/lib/uri/version.rb new file mode 100644 index 0000000000..ad76308e81 --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri/version.rb @@ -0,0 +1,6 @@ +module Bundler::URI + # :stopdoc: + VERSION = '1.1.1'.freeze + VERSION_CODE = VERSION.split('.').map{|s| s.rjust(2, '0')}.join.freeze + # :startdoc: +end diff --git a/lib/bundler/vendor/uri/lib/uri/ws.rb b/lib/bundler/vendor/uri/lib/uri/ws.rb new file mode 100644 index 0000000000..10ae6f5834 --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri/ws.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: false +# = uri/ws.rb +# +# Author:: Matt Muller <mamuller@amazon.com> +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Bundler::URI for general documentation +# + +require_relative 'generic' + +module Bundler::URI + + # + # The syntax of WS URIs is defined in RFC6455 section 3. + # + # Note that the Ruby Bundler::URI library allows WS URLs containing usernames and + # passwords. This is not legal as per the RFC, but used to be + # supported in Internet Explorer 5 and 6, before the MS04-004 security + # update. See <URL:http://support.microsoft.com/kb/834489>. + # + class WS < Generic + # A Default port of 80 for Bundler::URI::WS. + DEFAULT_PORT = 80 + + # An Array of the available components for Bundler::URI::WS. + COMPONENT = %i[ + scheme + userinfo host port + path + query + ].freeze + + # + # == Description + # + # Creates a new Bundler::URI::WS object from components, with syntax checking. + # + # The components accepted are userinfo, host, port, path, and query. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the + # order <code>[userinfo, host, port, path, query]</code>. + # + # Example: + # + # uri = Bundler::URI::WS.build(host: 'www.example.com', path: '/foo/bar') + # + # uri = Bundler::URI::WS.build([nil, "www.example.com", nil, "/path", "query"]) + # + # Currently, if passed userinfo components this method generates + # invalid WS URIs as per RFC 1738. + # + def self.build(args) + tmp = Util.make_components_hash(self, args) + super(tmp) + end + + # + # == Description + # + # Returns the full path for a WS Bundler::URI, as required by Net::HTTP::Get. + # + # If the Bundler::URI contains a query, the full path is Bundler::URI#path + '?' + Bundler::URI#query. + # Otherwise, the path is simply Bundler::URI#path. + # + # Example: + # + # uri = Bundler::URI::WS.build(path: '/foo/bar', query: 'test=true') + # uri.request_uri # => "/foo/bar?test=true" + # + def request_uri + return unless @path + + url = @query ? "#@path?#@query" : @path.dup + url.start_with?(?/.freeze) ? url : ?/ + url + end + end + + register_scheme 'WS', WS +end diff --git a/lib/bundler/vendor/uri/lib/uri/wss.rb b/lib/bundler/vendor/uri/lib/uri/wss.rb new file mode 100644 index 0000000000..e8db1ceabf --- /dev/null +++ b/lib/bundler/vendor/uri/lib/uri/wss.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: false +# = uri/wss.rb +# +# Author:: Matt Muller <mamuller@amazon.com> +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Bundler::URI for general documentation +# + +require_relative 'ws' + +module Bundler::URI + + # The default port for WSS URIs is 443, and the scheme is 'wss:' rather + # than 'ws:'. Other than that, WSS URIs are identical to WS URIs; + # see Bundler::URI::WS. + class WSS < WS + # A Default port of 443 for Bundler::URI::WSS + DEFAULT_PORT = 443 + end + + register_scheme 'WSS', WSS +end diff --git a/lib/bundler/vendored_fileutils.rb b/lib/bundler/vendored_fileutils.rb index d14e98baf7..1be1138ce2 100644 --- a/lib/bundler/vendored_fileutils.rb +++ b/lib/bundler/vendored_fileutils.rb @@ -1,9 +1,4 @@ # frozen_string_literal: true module Bundler; end -if RUBY_VERSION >= "2.4" - require "bundler/vendor/fileutils/lib/fileutils" -else - # the version we vendor is 2.4+ - require "fileutils" -end +require_relative "vendor/fileutils/lib/fileutils" diff --git a/lib/bundler/vendored_net_http.rb b/lib/bundler/vendored_net_http.rb new file mode 100644 index 0000000000..8ff2ccd1fe --- /dev/null +++ b/lib/bundler/vendored_net_http.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# This defined? guard can be removed once RubyGems 3.4 support is dropped. +# +# Bundler specs load this code from `spec/support/vendored_net_http.rb` to avoid +# activating the Bundler gem too early. Without this guard, we get redefinition +# warnings once Bundler is actually activated and +# `lib/bundler/vendored_net_http.rb` is required. This is not an issue in +# RubyGems versions including `rubygems/vendored_net_http` since `require` takes +# care of avoiding the double load. +# +unless defined?(Gem::Net) + begin + require "rubygems/vendored_net_http" + rescue LoadError + begin + require "rubygems/net/http" + rescue LoadError + require "net/http" + Gem::Net = Net + end + end +end diff --git a/lib/bundler/vendored_persistent.rb b/lib/bundler/vendored_persistent.rb index de9c42fcc1..ab985c267f 100644 --- a/lib/bundler/vendored_persistent.rb +++ b/lib/bundler/vendored_persistent.rb @@ -1,12 +1,5 @@ # frozen_string_literal: true -# We forcibly require OpenSSL, because net/http/persistent will only autoload -# it. On some Rubies, autoload fails but explicit require succeeds. -begin - require "openssl" -rescue LoadError - # some Ruby builds don't have OpenSSL -end module Bundler module Persistent module Net @@ -15,38 +8,4 @@ module Bundler end end end -require "bundler/vendor/net-http-persistent/lib/net/http/persistent" - -module Bundler - class PersistentHTTP < Persistent::Net::HTTP::Persistent - def connection_for(uri) - connection = super - warn_old_tls_version_rubygems_connection(uri, connection) - connection - end - - def warn_old_tls_version_rubygems_connection(uri, connection) - return unless connection.use_ssl? - return unless (uri.host || "").end_with?("rubygems.org") - - socket = connection.instance_variable_get(:@socket) - return unless socket - socket_io = socket.io - return unless socket_io.respond_to?(:ssl_version) - ssl_version = socket_io.ssl_version - - case ssl_version - when /TLSv([\d\.]+)/ - version = Gem::Version.new($1) - if version < Gem::Version.new("1.2") - Bundler.ui.warn \ - "Warning: Your Ruby version is compiled against a copy of OpenSSL that is very old. " \ - "Starting in January 2018, RubyGems.org will refuse connection requests from these " \ - "very old versions of OpenSSL. If you will need to continue installing gems after " \ - "January 2018, please follow this guide to upgrade: http://ruby.to/tls-outdated.", - :wrap => true - end - end - end - end -end +require_relative "vendor/net-http-persistent/lib/net/http/persistent" diff --git a/lib/bundler/vendored_molinillo.rb b/lib/bundler/vendored_pub_grub.rb index 061b634f72..b36a996b29 100644 --- a/lib/bundler/vendored_molinillo.rb +++ b/lib/bundler/vendored_pub_grub.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true module Bundler; end -require "bundler/vendor/molinillo/lib/molinillo" +require_relative "vendor/pub_grub/lib/pub_grub" diff --git a/lib/bundler/vendored_securerandom.rb b/lib/bundler/vendored_securerandom.rb new file mode 100644 index 0000000000..6a704dbd40 --- /dev/null +++ b/lib/bundler/vendored_securerandom.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Use RubyGems vendored copy when available. Otherwise fallback to Bundler +# vendored copy. The vendored copy in Bundler can be removed once support for +# RubyGems 3.5.18 is dropped. + +begin + require "rubygems/vendored_securerandom" +rescue LoadError + require_relative "vendor/securerandom/lib/securerandom" + Gem::SecureRandom = Bundler::SecureRandom +end diff --git a/lib/bundler/vendored_thor.rb b/lib/bundler/vendored_thor.rb index 8cca090f55..0666cfc9b9 100644 --- a/lib/bundler/vendored_thor.rb +++ b/lib/bundler/vendored_thor.rb @@ -2,7 +2,7 @@ module Bundler def self.require_thor_actions - Kernel.send(:require, "bundler/vendor/thor/lib/thor/actions") + require_relative "vendor/thor/lib/thor/actions" end end -require "bundler/vendor/thor/lib/thor" +require_relative "vendor/thor/lib/thor" diff --git a/lib/bundler/vendored_timeout.rb b/lib/bundler/vendored_timeout.rb new file mode 100644 index 0000000000..9b2507c0cc --- /dev/null +++ b/lib/bundler/vendored_timeout.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +begin + require "rubygems/vendored_timeout" +rescue LoadError + begin + require "rubygems/timeout" + rescue LoadError + require "timeout" + Gem::Timeout = Timeout + end +end diff --git a/lib/bundler/vendored_tsort.rb b/lib/bundler/vendored_tsort.rb new file mode 100644 index 0000000000..38aed0b5de --- /dev/null +++ b/lib/bundler/vendored_tsort.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +module Bundler; end +require_relative "vendor/tsort/lib/tsort" diff --git a/lib/bundler/vendored_uri.rb b/lib/bundler/vendored_uri.rb new file mode 100644 index 0000000000..2efddc65f9 --- /dev/null +++ b/lib/bundler/vendored_uri.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Bundler; end + +# Use RubyGems vendored copy when available. Otherwise fallback to Bundler +# vendored copy. The vendored copy in Bundler can be removed once support for +# RubyGems 3.5 is dropped. + +begin + require "rubygems/vendor/uri/lib/uri" +rescue LoadError + require_relative "vendor/uri/lib/uri" + Gem::URI = Bundler::URI + + module Gem + def URI(uri) # rubocop:disable Naming/MethodName + Bundler::URI(uri) + end + module_function :URI + end +end diff --git a/lib/bundler/version.rb b/lib/bundler/version.rb index c2355ff2c8..ca7bb0719a 100644 --- a/lib/bundler/version.rb +++ b/lib/bundler/version.rb @@ -1,28 +1,21 @@ # frozen_string_literal: false -# Ruby 1.9.3 and old RubyGems don't play nice with frozen version strings -# rubocop:disable MutableConstant - module Bundler - # We're doing this because we might write tests that deal - # with other versions of bundler and we are unsure how to - # handle this better. - VERSION = "1.16.0" unless defined?(::Bundler::VERSION) + VERSION = "4.1.0.dev".freeze - def self.overwrite_loaded_gem_version - begin - require "rubygems" - rescue LoadError - return - end - return unless bundler_spec = Gem.loaded_specs["bundler"] - return if bundler_spec.version == VERSION - bundler_spec.version = Bundler::VERSION + def self.bundler_major_version + @bundler_major_version ||= gem_version.segments.first end - private_class_method :overwrite_loaded_gem_version - overwrite_loaded_gem_version - def self.bundler_major_version - @bundler_major_version ||= VERSION.split(".").first.to_i + def self.gem_version + @gem_version ||= Gem::Version.create(VERSION) + end + + def self.verbose_version + @verbose_version ||= "#{VERSION}#{simulated_version ? " (simulating Bundler #{simulated_version})" : ""}" + end + + def self.simulated_version + @simulated_version ||= Bundler.settings[:simulate_version] end end diff --git a/lib/bundler/version_ranges.rb b/lib/bundler/version_ranges.rb deleted file mode 100644 index ec25716cde..0000000000 --- a/lib/bundler/version_ranges.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: true - -module Bundler - module VersionRanges - NEq = Struct.new(:version) - ReqR = Struct.new(:left, :right) - class ReqR - Endpoint = Struct.new(:version, :inclusive) - def to_s - "#{left.inclusive ? "[" : "("}#{left.version}, #{right.version}#{right.inclusive ? "]" : ")"}" - end - INFINITY = Object.new.freeze - ZERO = Gem::Version.new("0.a") - - def cover?(v) - return false if left.inclusive && left.version > v - return false if !left.inclusive && left.version >= v - - if right.version != INFINITY - return false if right.inclusive && right.version < v - return false if !right.inclusive && right.version <= v - end - - true - end - - def empty? - left.version == right.version && !(left.inclusive && right.inclusive) - end - - def single? - left.version == right.version - end - - UNIVERSAL = ReqR.new(ReqR::Endpoint.new(Gem::Version.new("0.a"), true), ReqR::Endpoint.new(ReqR::INFINITY, false)).freeze - end - - def self.for_many(requirements) - requirements = requirements.map(&:requirements).flatten(1).map {|r| r.join(" ") } - requirements << ">= 0.a" if requirements.empty? - requirement = Gem::Requirement.new(requirements) - self.for(requirement) - end - - def self.for(requirement) - ranges = requirement.requirements.map do |op, v| - case op - when "=" then ReqR.new(ReqR::Endpoint.new(v, true), ReqR::Endpoint.new(v, true)) - when "!=" then NEq.new(v) - when ">=" then ReqR.new(ReqR::Endpoint.new(v, true), ReqR::Endpoint.new(ReqR::INFINITY, false)) - when ">" then ReqR.new(ReqR::Endpoint.new(v, false), ReqR::Endpoint.new(ReqR::INFINITY, false)) - when "<" then ReqR.new(ReqR::Endpoint.new(ReqR::ZERO, true), ReqR::Endpoint.new(v, false)) - when "<=" then ReqR.new(ReqR::Endpoint.new(ReqR::ZERO, true), ReqR::Endpoint.new(v, true)) - when "~>" then ReqR.new(ReqR::Endpoint.new(v, true), ReqR::Endpoint.new(v.bump, false)) - else raise "unknown version op #{op} in requirement #{requirement}" - end - end.uniq - ranges, neqs = ranges.partition {|r| !r.is_a?(NEq) } - - [ranges.sort_by {|range| [range.left.version, range.left.inclusive ? 0 : 1] }, neqs.map(&:version)] - end - - def self.empty?(ranges, neqs) - !ranges.reduce(ReqR::UNIVERSAL) do |last_range, curr_range| - next false unless last_range - next false if curr_range.single? && neqs.include?(curr_range.left.version) - next curr_range if last_range.right.version == ReqR::INFINITY - case last_range.right.version <=> curr_range.left.version - when 1 then next curr_range - when 0 then next(last_range.right.inclusive && curr_range.left.inclusive && !neqs.include?(curr_range.left.version) && curr_range) - when -1 then next false - end - end - end - end -end diff --git a/lib/bundler/vlad.rb b/lib/bundler/vlad.rb index 68181e7db8..c3a3d949a6 100644 --- a/lib/bundler/vlad.rb +++ b/lib/bundler/vlad.rb @@ -1,17 +1,4 @@ # frozen_string_literal: true -require "bundler/shared_helpers" -Bundler::SharedHelpers.major_deprecation 2, - "The Bundler task for Vlad" - -# Vlad task for Bundler. -# -# Add "require 'bundler/vlad'" in your Vlad deploy.rb, and -# include the vlad:bundle:install task in your vlad:deploy task. -require "bundler/deployment" - -include Rake::DSL if defined? Rake::DSL - -namespace :vlad do - Bundler::Deployment.define_task(Rake::RemoteTask, :remote_task, :roles => :app) -end +require_relative "shared_helpers" +Bundler::SharedHelpers.feature_removed! "The Bundler task for Vlad" diff --git a/lib/bundler/worker.rb b/lib/bundler/worker.rb index e91cfa7805..77f4f004aa 100644 --- a/lib/bundler/worker.rb +++ b/lib/bundler/worker.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "thread" - module Bundler class Worker POISON = Object.new @@ -23,20 +21,22 @@ module Bundler # @param func [Proc] job to run in inside the worker pool def initialize(size, name, func) @name = name - @request_queue = Queue.new - @response_queue = Queue.new + @request_queue = Thread::Queue.new + @request_queue_with_priority = Thread::Queue.new + @response_queue = Thread::Queue.new @func = func @size = size @threads = nil - SharedHelpers.trap("INT") { abort_threads } + @previous_interrupt_handler = nil end # Enqueue 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) + def enq(obj, priority: false) + queue = priority ? @request_queue_with_priority : @request_queue create_threads unless @threads - @request_queue.enq obj + queue.enq obj end # Retrieves results of job function being executed in worker pool @@ -50,11 +50,17 @@ module Bundler stop_threads end - private + private def process_queue(i) loop do - obj = @request_queue.deq + obj = begin + @request_queue_with_priority.deq(true) + rescue ThreadError + @request_queue.deq(false, timeout: 0.05) + end + + next if obj.nil? break if obj.equal? POISON @response_queue.enq apply_func(obj, i) end @@ -62,7 +68,7 @@ module Bundler def apply_func(obj, i) @func.call(obj, i) - rescue Exception => e + rescue Exception => e # rubocop:disable Lint/RescueException WrappedException.new(e) end @@ -70,13 +76,16 @@ module Bundler # so as worker threads after retrieving it, shut themselves down def stop_threads return unless @threads + @threads.each { @request_queue.enq POISON } @threads.each(&:join) + + remove_interrupt_handler + @threads = nil end def abort_threads - return unless @threads Bundler.ui.debug("\n#{caller.join("\n")}") @threads.each(&:exit) exit 1 @@ -86,21 +95,31 @@ module Bundler creation_errors = [] @threads = Array.new(@size) do |i| - begin - Thread.start { process_queue(i) }.tap do |thread| - thread.name = "#{name} Worker ##{i}" if thread.respond_to?(:name=) - end - rescue ThreadError => e - creation_errors << e - nil + Thread.start { process_queue(i) }.tap do |thread| + thread.name = "#{name} Worker ##{i}" end + rescue ThreadError => e + creation_errors << e + nil end.compact + add_interrupt_handler unless @threads.empty? + return if creation_errors.empty? message = "Failed to create threads for the #{name} worker: #{creation_errors.map(&:to_s).uniq.join(", ")}" raise ThreadCreationError, message if @threads.empty? Bundler.ui.info message end + + def add_interrupt_handler + @previous_interrupt_handler = trap("INT") { abort_threads } + end + + def remove_interrupt_handler + return unless @previous_interrupt_handler + + trap "INT", @previous_interrupt_handler + end end end diff --git a/lib/bundler/yaml_serializer.rb b/lib/bundler/yaml_serializer.rb index 0fd81c40ef..ab1eb6dbcf 100644 --- a/lib/bundler/yaml_serializer.rb +++ b/lib/bundler/yaml_serializer.rb @@ -3,7 +3,7 @@ module Bundler # A stub yaml serializer that can handle only hashes and strings (as of now). module YAMLSerializer - module_function + module_function def dump(hash) yaml = String.new("---") @@ -17,7 +17,11 @@ module Bundler if v.is_a?(Hash) yaml << dump_hash(v).gsub(/^(?!$)/, " ") # indent all non-empty lines elsif v.is_a?(Array) # Expected to be array of strings - yaml << "\n- " << v.map {|s| s.to_s.gsub(/\s+/, " ").inspect }.join("\n- ") << "\n" + if v.empty? + yaml << " []\n" + else + yaml << "\n- " << v.map {|s| s.to_s.gsub(/\s+/, " ").inspect }.join("\n- ") << "\n" + end else yaml << " " << v.to_s.gsub(/\s+/, " ").inspect << "\n" end @@ -37,10 +41,9 @@ module Bundler HASH_REGEX = / ^ ([ ]*) # indentations - (.+) # key + ([^#]+) # key excludes comment char '#' (?::(?=(?:\s|$))) # : (without the lookahead the #key includes this when : is present in value) [ ]? - (?: !\s)? # optional exclamation mark found with ruby 1.9.3 (['"]?) # optional opening quote (.*) # value \3 # matching closing quote @@ -52,11 +55,12 @@ module Bundler stack = [res] last_hash = nil last_empty_key = nil - str.split(/\r?\n/).each do |line| + str.split(/\r?\n/) do |line| if match = HASH_REGEX.match(line) indent, key, quote, val = match.captures - key = convert_to_backward_compatible_key(key) - depth = indent.scan(/ /).length + val = strip_comment(val) + + depth = indent.size / 2 if quote.empty? && val.empty? new_hash = {} stack[depth][key] = new_hash @@ -64,10 +68,13 @@ module Bundler last_empty_key = key last_hash = stack[depth] else + val = [] if val == "[]" # empty array stack[depth][key] = val end elsif match = ARRAY_REGEX.match(line) _, val = match.captures + val = strip_comment(val) + last_hash[last_empty_key] = [] unless last_hash[last_empty_key].is_a?(Array) last_hash[last_empty_key].push(val) @@ -76,15 +83,16 @@ module Bundler res end - # for settings' keys - def convert_to_backward_compatible_key(key) - key = "#{key}/" if key =~ /https?:/i && key !~ %r{/\Z} - key = key.gsub(".", "__") if key.include?(".") - key + def strip_comment(val) + if val.include?("#") && !val.start_with?("#") + val.split("#", 2).first.strip + else + val + end end class << self - private :dump_hash, :convert_to_backward_compatible_key + private :dump_hash end end end |
