diff options
Diffstat (limited to 'lib/bundler')
214 files changed, 5284 insertions, 3907 deletions
diff --git a/lib/bundler/build_metadata.rb b/lib/bundler/build_metadata.rb index 5d2a8b53bb..49d2518078 100644 --- a/lib/bundler/build_metadata.rb +++ b/lib/bundler/build_metadata.rb @@ -4,21 +4,26 @@ 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. @@ -34,10 +39,5 @@ module Bundler @git_commit_sha ||= "unknown" end - - # Whether this is an official release build of Bundler. - def self.release? - @release - end end end diff --git a/lib/bundler/bundler.gemspec b/lib/bundler/bundler.gemspec index 88411f295d..49319e81b4 100644 --- a/lib/bundler/bundler.gemspec +++ b/lib/bundler/bundler.gemspec @@ -23,16 +23,16 @@ Gem::Specification.new do |s| 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/rubygems/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3ABundler", - "changelog_uri" => "https://github.com/rubygems/rubygems/blob/master/bundler/CHANGELOG.md", + "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/rubygems/rubygems/tree/master/bundler", + "source_code_uri" => "https://github.com/ruby/rubygems/tree/master/bundler", } - s.required_ruby_version = ">= 3.1.0" + s.required_ruby_version = ">= 3.2.0" # It should match the RubyGems version shipped with `required_ruby_version` above - s.required_rubygems_version = ">= 3.3.3" + s.required_rubygems_version = ">= 3.4.1" s.files = Dir.glob("lib/bundler{.rb,/**/*}", File::FNM_DOTMATCH).reject {|f| File.directory?(f) } diff --git a/lib/bundler/capistrano.rb b/lib/bundler/capistrano.rb index 705840143f..6d2437d895 100644 --- a/lib/bundler/capistrano.rb +++ b/lib/bundler/capistrano.rb @@ -1,22 +1,4 @@ # frozen_string_literal: true require_relative "shared_helpers" -Bundler::SharedHelpers.major_deprecation 2, - "The Bundler task for Capistrano. Please use https://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_relative "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 https://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 +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 index 60ba93417c..ce05818bb0 100644 --- a/lib/bundler/checksum.rb +++ b/lib/bundler/checksum.rb @@ -126,7 +126,7 @@ module Bundler end def removable? - type == :lock || type == :gem + [:lock, :gem].include?(type) end def ==(other) @@ -190,7 +190,7 @@ module Bundler def replace(spec, checksum) return unless checksum - lock_name = spec.name_tuple.lock_name + lock_name = spec.lock_name @store_mutex.synchronize do existing = fetch_checksum(lock_name, checksum.algo) if !existing || existing.same_source?(checksum) @@ -201,10 +201,18 @@ module Bundler end end - def register(spec, checksum) - return unless checksum + 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 - register_checksum(spec.name_tuple.lock_name, checksum) + def register(spec, checksum) + register_checksum(spec.lock_name, checksum) end def merge!(other) @@ -216,9 +224,9 @@ module Bundler end def to_lock(spec) - lock_name = spec.name_tuple.lock_name + lock_name = spec.lock_name checksums = @store[lock_name] - if checksums + if checksums&.any? "#{lock_name} #{checksums.values.map(&:to_lock).sort.join(",")}" else lock_name @@ -229,11 +237,15 @@ module Bundler def register_checksum(lock_name, checksum) @store_mutex.synchronize do - existing = fetch_checksum(lock_name, checksum.algo) - if existing - merge_checksum(lock_name, checksum, existing) + 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 - store_checksum(lock_name, checksum) + init_checksum(lock_name) end end end @@ -243,7 +255,11 @@ module Bundler end def store_checksum(lock_name, checksum) - (@store[lock_name] ||= {})[checksum.algo] = checksum + init_checksum(lock_name)[checksum.algo] = checksum + end + + def init_checksum(lock_name) + @store[lock_name] ||= {} end def fetch_checksum(lock_name, algo) diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 91c31651da..9d8a68fff9 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -11,7 +11,7 @@ module 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 - EXTENSIONS = ["c", "rust"].freeze + EXTENSIONS = ["c", "rust", "go"].freeze COMMAND_ALIASES = { "check" => "c", @@ -24,7 +24,7 @@ module Bundler }.freeze def self.start(*) - check_deprecated_ext_option(ARGV) if ARGV.include?("--ext") + check_invalid_ext_option(ARGV) if ARGV.include?("--ext") super ensure @@ -59,17 +59,29 @@ module Bundler 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_settings_and_root! + 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 Bundler.auto_install if AUTO_INSTALL_CMDS.include?(current_cmd) rescue UnknownArgumentError => e raise InvalidOption, e.message @@ -77,7 +89,7 @@ module Bundler 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) } end @@ -92,7 +104,7 @@ module Bundler 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 } @@ -107,7 +119,33 @@ module Bundler shell.say self.class.send(:class_options_help, shell) end - default_task(Bundler.feature_flag.default_cli_command) + + 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", @@ -117,6 +155,10 @@ module Bundler 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" @@ -130,7 +172,7 @@ module Bundler if man_pages.include?(command) man_page = man_pages[command] - if Bundler.which("man") && !man_path.match?(%r{^file:/.+!/META-INF/jruby.home/.+}) + 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_page)}.ronn") @@ -143,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 @@ -173,7 +215,7 @@ module Bundler 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?}" + method_option "path", type: :string, banner: "Specify a different path than the system default, namely, $BUNDLE_PATH or $GEM_HOME (removed)" def check remembered_flag_deprecation("path") @@ -187,12 +229,11 @@ module Bundler 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" + method_option "install", type: :boolean, banner: "Runs 'bundle install' after removing the gems from the Gemfile (removed)" def remove(*gems) if ARGV.include?("--install") - message = "The `--install` flag has been deprecated. `bundle install` is triggered by default." removed_message = "The `--install` flag has been removed. `bundle install` is triggered by default." - SharedHelpers.major_deprecation(2, message, removed_message: removed_message) + raise InvalidOption, removed_message end require_relative "cli/remove" @@ -210,42 +251,53 @@ module Bundler If the bundle has already been installed, bundler will tell you so and then exit. D - method_option "binstubs", type: :string, lazy_default: "bin", banner: "Generate bin stubs for bundled gems to ./bin" - method_option "clean", type: :boolean, banner: "Run bundle clean automatically after install" - method_option "deployment", type: :boolean, banner: "Install using defaults tuned for deployment environments" - method_option "frozen", type: :boolean, banner: "Do not allow the Gemfile.lock to be updated after this 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 "redownload", type: :boolean, aliases: "--force", banner: "Force downloading every gem." - method_option "no-prune", type: :boolean, banner: "Don't remove stale gems from the cache." - method_option "path", type: :string, banner: "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?}" + 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')" + 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" + 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." - method_option "with", type: :array, banner: "Include gems that are part of the specified named group." + 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 - SharedHelpers.major_deprecation(2, "The `--force` option has been renamed to `--redownload`") if ARGV.include?("--force") - %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_negative_flag_deprecation("no-deployment") + 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.dup).run + 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") @@ -263,7 +315,7 @@ module Bundler 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 "redownload", type: :boolean, aliases: "--force", banner: "Force downloading every gem." + 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" @@ -273,8 +325,8 @@ module Bundler 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) - SharedHelpers.major_deprecation(2, "The `--force` option has been renamed to `--redownload`") if ARGV.include?("--force") require_relative "cli/update" Bundler.settings.temporary(no_install: false) do Update.new(options, gems).run @@ -287,12 +339,11 @@ module Bundler 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 "outdated", type: :boolean, banner: "Show verbose output including whether gems are outdated (removed)." def show(gem_name = nil) if ARGV.include?("--outdated") - message = "the `--outdated` flag to `bundle show` was undocumented and will be removed without replacement" - removed_message = "the `--outdated` flag to `bundle show` was undocumented and has been removed without replacement" - SharedHelpers.major_deprecation(2, message, removed_message: removed_message) + 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 @@ -302,6 +353,7 @@ module Bundler 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" @@ -325,12 +377,14 @@ module Bundler 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 "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) + remembered_flag_deprecation("path", option_name: "bin") + require_relative "cli/binstubs" Binstubs.new(options, gems).run end @@ -351,8 +405,10 @@ module Bundler 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: "Adds optimistic declaration of version to gem" + 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 @@ -383,6 +439,7 @@ module Bundler 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_relative "cli/outdated" Outdated.new(options, gems).run @@ -396,15 +453,15 @@ module Bundler end desc "cache [OPTIONS]", "Locks and then caches all of the gems into vendor/cache" - method_option "all", type: :boolean, default: Bundler.feature_flag.cache_all?, banner: "Include all sources (including path and git)." + 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." - 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?}" + 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" + 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 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 @@ -413,18 +470,19 @@ module Bundler D 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") - if ARGV.include?("--path") - message = - "The `--path` flag is deprecated because its semantics are 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" + %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." - SharedHelpers.major_deprecation 2, message, removed_message: removed_message + "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" @@ -434,7 +492,7 @@ module Bundler map aliases_for("cache") desc "exec [OPTIONS]", "Run the command in context of the bundle" - 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 deprecated" + 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 @@ -443,9 +501,8 @@ module Bundler D def exec(*args) if ARGV.include?("--no-keep-file-descriptors") - message = "The `--no-keep-file-descriptors` has been deprecated. `bundle exec` no longer mess with your file descriptors. Close them in the exec'd script if you need to" 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" - SharedHelpers.major_deprecation(2, message, removed_message: removed_message) + raise InvalidOption, removed_message end require_relative "cli/exec" @@ -486,13 +543,13 @@ module Bundler 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 @@ -512,41 +569,32 @@ module Bundler end end - unless Bundler.feature_flag.bundler_3_mode? - 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." - def viz - SharedHelpers.major_deprecation 2, "The `viz` command has been renamed to `graph` and moved to a plugin. See https://github.com/rubygems/bundler-graph" - require_relative "cli/viz" - Viz.new(options.dup).run - end + desc "viz [OPTIONS]", "Generates a visual dependency graph", hide: true + def viz + SharedHelpers.feature_removed! "The `viz` command has been renamed to `graph` and moved to a plugin. See https://github.com/rubygems/bundler-graph" end 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 set --global 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: :string, desc: "Generate the boilerplate for C extension code.", enum: EXTENSIONS - method_option :git, type: :boolean, default: true, desc: "Initialize a git repo inside your library." - method_option :mit, type: :boolean, desc: "Generate an MIT license file. Set a default with `bundle config set --global gem.mit true`." - method_option :rubocop, type: :boolean, desc: "Add rubocop to the generated Rakefile and gemspec. Set a default with `bundle config set --global gem.rubocop true`." - method_option :changelog, type: :boolean, desc: "Generate changelog file. Set a default with `bundle config set --global gem.changelog true`." + 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], desc: "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], desc: "Add a linter and code formatter, either RuboCop or Standard. Set a default with `bundle config set --global gem.linter (rubocop|standard)`" + 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`" def gem(name) require_relative "cli/gem" + + 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_args = args + [self] cmd_args.unshift(options) @@ -557,7 +605,7 @@ module Bundler File.expand_path("templates", __dir__) end - desc "clean [OPTIONS]", "Cleans up unused gems in your bundler directory", hide: true + 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 @@ -573,12 +621,8 @@ module Bundler 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_relative "cli/inject" - Inject.new(options.dup, name, version).run + def inject(*) + SharedHelpers.feature_removed! "The `inject` command has been replaced by the `add` command" end desc "lock", "Creates a lockfile without installing" @@ -610,17 +654,8 @@ 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_relative "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 @@ -641,7 +676,7 @@ module Bundler end end - if Bundler.feature_flag.plugins? + if Bundler.settings[:plugins] require_relative "cli/plugin" desc "plugin", "Manage the bundler plugins" subcommand "plugin", Plugin @@ -675,18 +710,15 @@ module Bundler end end - def self.check_deprecated_ext_option(arguments) - # when deprecated version of `--ext` is called - # print out deprecation warning and pretend `--ext=c` was provided - if deprecated_ext_value?(arguments) - message = "Extensions can now be generated using C or Rust, so `--ext` with no arguments has been deprecated. Please select a language, e.g. `--ext=rust` to generate a Rust extension. This gem will now be generated as if `--ext=c` was used." + 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." - SharedHelpers.major_deprecation 2, message, removed_message: removed_message - arguments[arguments.index("--ext")] = "--ext=c" + raise InvalidOption, removed_message end end - def self.deprecated_ext_value?(arguments) + def self.invalid_ext_value?(arguments) index = arguments.index("--ext") next_argument = arguments[index + 1] @@ -694,15 +726,15 @@ module Bundler # for example `bundle gem hello --ext c` return false if EXTENSIONS.include?(next_argument) - # deprecated call when --ext is called with no value in last position + # 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? - # deprecated call when --ext is followed by other parameter + # 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?("-") - # deprecated call when --ext is followed by gem name + # invalid call when --ext is followed by gem name # for example `bundle gem --ext hello_gem` return true if next_argument @@ -716,6 +748,19 @@ module Bundler 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 @@ -729,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 @@ -756,44 +801,30 @@ module Bundler nil end - def remembered_negative_flag_deprecation(name) - positive_name = name.gsub(/\Ano-/, "") - option = current_command.options[positive_name] - flag_name = "--no-" + option.switch_name.gsub(/\A--/, "") - - flag_deprecation(positive_name, flag_name, option) - end - - def remembered_flag_deprecation(name) + def remembered_flag_deprecation(name, negative: false, option_name: nil) option = current_command.options[name] flag_name = option.switch_name - - flag_deprecation(name, flag_name, option) - end - - def flag_deprecation(name, flag_name, option) - name_index = ARGV.find {|arg| flag_name == arg.split("=")[0] } - return unless name_index + 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, name.tr("-", "_"), value) + print_remembered_flag_deprecation(flag_name, option_name || name.tr("-", "_"), value) end def print_remembered_flag_deprecation(flag_name, option_name, option_value) - message = - "The `#{flag_name}` flag is deprecated because it relies on being " \ - "remembered across bundler invocations, which bundler will no longer " \ - "do in future versions. Instead please use `bundle config set #{option_name} " \ - "#{option_value}`, and stop using this flag" removed_message = "The `#{flag_name}` flag has been removed because it relied on being " \ - "remembered across bundler invocations, which bundler will no longer " \ - "do. Instead please use `bundle config set #{option_name} " \ - "#{option_value}`, and stop using this flag" - Bundler::SharedHelpers.major_deprecation 2, message, removed_message: removed_message + "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 12a681a816..20f76b59d1 100644 --- a/lib/bundler/cli/add.rb +++ b/lib/bundler/cli/add.rb @@ -14,6 +14,9 @@ module Bundler def run Bundler.ui.level = "warn" if options[:quiet] + 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"] @@ -31,12 +34,22 @@ module Bundler Injector.inject(dependencies, conservative_versioning: options[:version].nil?, # Perform conservative versioning only when version is not specified - optimistic: options[:optimistic], + pessimistic: options[:pessimistic], strict: options[:strict]) end def validate_options! - raise InvalidOption, "You cannot specify `--strict` and `--optimistic` at the same time." if options[:strict] && options[:optimistic] + 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? diff --git a/lib/bundler/cli/cache.rb b/lib/bundler/cli/cache.rb index 2e63a16ec3..59605df847 100644 --- a/lib/bundler/cli/cache.rb +++ b/lib/bundler/cli/cache.rb @@ -10,17 +10,12 @@ module Bundler def run Bundler.ui.level = "warn" if options[:quiet] - Bundler.settings.set_command_option_if_given :path, options[:path] 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.settings.temporary(cache_all_platforms: options["all-platforms"]) do - Bundler.load.cache(custom_path) + Bundler.load.cache end end @@ -33,11 +28,5 @@ module Bundler options["no-cache"] = true Bundler::CLI::Install.new(options).run end - - def setup_cache_all - all = options.fetch(:all, Bundler.feature_flag.cache_all? || nil) - - Bundler.settings.set_command_option_if_given :cache_all, all - end end end diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb index 7ef6deb2cf..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| @@ -94,11 +100,14 @@ module Bundler end def self.gem_not_found_message(missing_gem_name, alternatives) - require_relative "../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 @@ -130,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 77b502fe60..976cda7484 100644 --- a/lib/bundler/cli/config.rb +++ b/lib/bundler/cli/config.rb @@ -26,8 +26,7 @@ module Bundler 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." - removed_message = "Using the `config` command without a subcommand [list, get, set, unset] is has been removed. Use `bundle #{new_args.join(" ")}` instead." - SharedHelpers.major_deprecation 3, message, removed_message: removed_message + SharedHelpers.feature_deprecated! message Base.new(options, name, value, self).run end @@ -88,16 +87,21 @@ module Bundler if value.nil? warn_unused_scope "Ignoring --#{scope} since no value to set was given" + current_value = Bundler.settings[name] if options[:parseable] if value = Bundler.settings[name] Bundler.ui.info("#{name}=#{value}") end - return + else + confirm(name) end - confirm(name) - return + if current_value.nil? + exit 1 + else + return + end end Bundler.ui.info(message) if message diff --git a/lib/bundler/cli/console.rb b/lib/bundler/cli/console.rb index 2ba5e69bde..2d1a2ce458 100644 --- a/lib/bundler/cli/console.rb +++ b/lib/bundler/cli/console.rb @@ -20,9 +20,19 @@ 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) @@ -32,9 +42,6 @@ module Bundler "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 ce016e3ad2..5fd6a73d91 100644 --- a/lib/bundler/cli/doctor.rb +++ b/lib/bundler/cli/doctor.rb @@ -1,157 +1,33 @@ # frozen_string_literal: true -require "rbconfig" -require "shellwords" -require "fiddle" - 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.shellescape}`.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.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 check! - require_relative "check" - Bundler::CLI::Check.new({}).run - end - - def run - Bundler.ui.level = "warn" 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 do |f| - Fiddle.dlopen(f) - false - rescue Fiddle::DLError - true - 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_or_writable = [] - files_not_rw_and_owned_by_different_user = [] - files_not_owned_by_current_user_but_still_rw = [] - broken_symlinks = [] - Find.find(Bundler.bundle_path.to_s).each do |f| - if !File.exist?(f) - broken_symlinks << f - elsif !File.writable?(f) || !File.readable?(f) - if File.stat(f).uid != Process.uid - files_not_rw_and_owned_by_different_user << f - else - files_not_readable_or_writable << f - end - elsif File.stat(f).uid != Process.uid - files_not_owned_by_current_user_but_still_rw << 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_rw.any? - Bundler.ui.warn "Files exist in the Bundler home that are owned by another " \ - "user, but are still readable/writable. These files are:\n - #{files_not_owned_by_current_user_but_still_rw.join("\n - ")}" - - ok = false - end - - if files_not_rw_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/writable. These files are:\n - #{files_not_rw_and_owned_by_different_user.join("\n - ")}" - - ok = false - end - - if files_not_readable_or_writable.any? - Bundler.ui.warn "Files exist in the Bundler home that are not " \ - "readable/writable by the current user. These files are:\n - #{files_not_readable_or_writable.join("\n - ")}" - - ok = false - end - - ok + 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 9428e9db3b..2fdc416286 100644 --- a/lib/bundler/cli/exec.rb +++ b/lib/bundler/cli/exec.rb @@ -19,11 +19,13 @@ module Bundler 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) + 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 + bin_path = "./" + bin_path unless File.absolute_path?(bin_path) + kernel_exec(bin_path, *args) end - bin_path = "./" + bin_path unless File.absolute_path?(bin_path) - kernel_exec(bin_path, *args) else # exec using the given command kernel_exec(cmd, *args) @@ -69,6 +71,29 @@ module Bundler "#{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", diff --git a/lib/bundler/cli/gem.rb b/lib/bundler/cli/gem.rb index 22bcf0e47a..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,11 +7,7 @@ module Bundler end class CLI::Gem - TEST_FRAMEWORK_VERSIONS = { - "rspec" => "3.0", - "minitest" => "5.16", - "test-unit" => "3.0", - }.freeze + DEFAULT_GITHUB_USERNAME = "[USERNAME]" attr_reader :options, :gem_name, :thor, :name, :target, :extension @@ -26,12 +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 @extension - validate_rust_builder_rubygems_version if @extension == "rust" end def run @@ -48,13 +41,16 @@ module Bundler 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) - github_username = if options[:github_username].nil? - git_username - elsif options[:github_username] == false - "" + 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 - options[:github_username] + 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 = { @@ -69,12 +65,17 @@ module Bundler test: options[:test], ext: extension, exe: options[:exe], + bundle: options[:bundle], bundler_version: bundler_dependency_version, git: use_git, - github_username: github_username.empty? ? "[USERNAME]" : github_username, + 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) @@ -95,11 +96,21 @@ module Bundler bin/setup ] - templates.merge!("gitignore.tt" => ".gitignore") if use_git + 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] case test_framework when "rspec" @@ -109,6 +120,8 @@ module Bundler "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 @@ -123,12 +136,14 @@ module Bundler "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 @@ -136,19 +151,22 @@ module Bundler case config[:ci] when "github" templates.merge!("github/workflows/main.yml.tt" => ".github/workflows/main.yml") - config[:ci_config_path] = ".github " + 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[:ci_config_path] = ".gitlab-ci.yml " + config[:ignore_paths] << ".gitlab-ci.yml" when "circle" templates.merge!("circleci/config.yml.tt" => ".circleci/config.yml") - config[:ci_config_path] = ".circleci " + 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 https://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") @@ -156,12 +174,8 @@ 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 https://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") @@ -182,13 +196,13 @@ module Bundler config[:linter] = ask_and_set_linter case config[:linter] when "rubocop" - config[:linter_version] = rubocop_version Bundler.ui.info "RuboCop enabled in config" templates.merge!("rubocop.yml.tt" => ".rubocop.yml") + config[:ignore_paths] << ".rubocop.yml" when "standard" - config[:linter_version] = standard_version Bundler.ui.info "Standard enabled in config" templates.merge!("standard.yml.tt" => ".standard.yml") + config[:ignore_paths] << ".standard.yml" end if config[:exe] @@ -208,18 +222,31 @@ module Bundler 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 "Initializing git repo in #{target}" + Bundler.ui.info "\nInitializing git repo in #{target}" require "shellwords" `git init #{target.to_s.shellescape}` @@ -241,26 +268,33 @@ module Bundler IO.popen(%w[git add .], { chdir: target }, &:read) end + if config[:bundle] + Bundler.ui.info "Running bundle install in the new gem directory." + Dir.chdir(target) do + system("bundle install") + end + end + # Open gemspec in editor open_editor(options["edit"], target.join("#{name}.gemspec")) if options[:edit] - Bundler.ui.info "Gem '#{name}' was successfully created. " \ - "For more information on making a RubyGem visit https://bundler.io/guides/creating_gem.html" + 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 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 @@ -282,7 +316,7 @@ module Bundler test_framework = options[:test] || Bundler.settings["gem.test"] if test_framework.to_s.empty? - Bundler.ui.confirm "Do you want to generate tests with your gem?" + 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):" @@ -322,12 +356,11 @@ module Bundler ci_template = options[:ci] || Bundler.settings["gem.ci"] if ci_template.to_s.empty? - Bundler.ui.confirm "Do you want to set up continuous integration for your gem? " \ + 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" \ - "\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):" @@ -352,14 +385,12 @@ module Bundler def ask_and_set_linter return if skip?(:linter) linter_template = options[:linter] || Bundler.settings["gem.linter"] - linter_template = deprecated_rubocop_option if linter_template.nil? if linter_template.to_s.empty? - Bundler.ui.confirm "Do you want to add a code linter and formatter to your gem? " \ + 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" \ - "\n" + "* Standard: https://github.com/standardrb/standard\n" Bundler.ui.info hint_text("linter") result = Bundler.ui.ask "Enter a linter. rubocop/standard/(none):" @@ -386,27 +417,6 @@ module Bundler linter_template end - def deprecated_rubocop_option - if !options[:rubocop].nil? - if options[:rubocop] - Bundler::SharedHelpers.major_deprecation 2, - "--rubocop is deprecated, use --linter=rubocop", - removed_message: "--rubocop has been removed, use --linter=rubocop" - "rubocop" - else - Bundler::SharedHelpers.major_deprecation 2, - "--no-rubocop is deprecated, use --linter", - removed_message: "--no-rubocop has been removed, use --linter" - false - end - elsif !Bundler.settings["gem.rubocop"].nil? - Bundler::SharedHelpers.major_deprecation 2, - "config gem.rubocop is deprecated; we've updated your config to use gem.linter instead", - removed_message: "config gem.rubocop has been removed; we've updated your config to use gem.linter instead" - Bundler.settings["gem.rubocop"] ? "rubocop" : false - end - end - def bundler_dependency_version v = Gem::Version.new(Bundler::VERSION) req = v.segments[0..1] @@ -420,6 +430,10 @@ module Bundler 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| @@ -446,21 +460,16 @@ module Bundler end def required_ruby_version - "3.1.0" - end - - def rubocop_version - "1.21" - end - - def standard_version - "1.3" + "3.2.0" end - def validate_rust_builder_rubygems_version - if Gem::Version.new(rust_builder_required_rubygems_version) > Gem.rubygems_version - Bundler.ui.error "Your RubyGems version (#{Gem.rubygems_version}) is too old to build Rust extension. Please update your RubyGems using `gem update --system` or any other way and try again." - exit 1 + def github_username(git_username) + if options[:github_username].nil? + git_username + elsif options[:github_username] == false + "" + else + options[:github_username] end end end diff --git a/lib/bundler/cli/info.rb b/lib/bundler/cli/info.rb index d7a8530fba..cd01d4949b 100644 --- a/lib/bundler/cli/info.rb +++ b/lib/bundler/cli/info.rb @@ -39,8 +39,8 @@ module Bundler path = File.expand_path("../../..", __dir__) else path = spec.full_gem_path - if spec.deleted_gem? - return Bundler.ui.warn "The gem #{name} has been deleted. It was installed at: #{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 @@ -65,8 +65,8 @@ module Bundler 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.deleted_gem? - return Bundler.ui.warn "The gem #{name} has been deleted. Gemspec information is still available though:\n#{gem_info}" + 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 diff --git a/lib/bundler/cli/inject.rb b/lib/bundler/cli/inject.rb deleted file mode 100644 index 4838aba671..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.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 b0b354cf10..69affd1a10 100644 --- a/lib/bundler/cli/install.rb +++ b/lib/bundler/cli/install.rb @@ -20,54 +20,32 @@ module Bundler Bundler::SharedHelpers.set_env "RB_USER_INSTALL", "1" if Gem.freebsd_platform? - # Disable color in deployment mode - Bundler.ui.shell = Thor::Shell::Basic.new if options[:deployment] - if target_rbconfig_path = options[:"target-rbconfig"] Bundler.rubygems.set_target_rbconfig(target_rbconfig_path) end - check_for_options_conflicts - check_trust_policy - if options[:deployment] || options[:frozen] || Bundler.frozen_bundle? - unless Bundler.default_lockfile.exist? - flag = "--deployment flag" if options[:deployment] - flag ||= "--frozen flag" if options[:frozen] - 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 - - options[:local] = true if Bundler.app_cache.exist? - - Bundler.settings.set_command_option :deployment, true if options[:deployment] - Bundler.settings.set_command_option :frozen, true if options[:frozen] - 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 --all`", - removed_message: "The --binstubs option have been removed in favor of `bundle binstubs --all`" - 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) @@ -87,8 +65,6 @@ module Bundler Bundler::CLI::Common.output_post_install_messages installer.post_install_messages - warn_ambiguous_gems - if CLI::Common.clean_after_install? require_relative "clean" Bundler::CLI::Clean.new(options).run @@ -114,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) @@ -143,57 +103,25 @@ module Bundler Bundler.settings.set_command_option_if_given :"trust-policy", trust_policy end - def normalize_groups - check_for_group_conflicts_in_cli_options - - # 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] - Bundler.settings.set_command_option :with, options[:with] - end - def normalize_settings - Bundler.settings.set_command_option :path, nil if options[:system] - Bundler.settings.set_command_option_if_given :path, options[:path] - if options["standalone"] && Bundler.settings[:path].nil? && !options["local"] - Bundler.settings.temporary(path_relative_to_cwd: false) do - Bundler.settings.set_command_option :path, "bundle" - end + Bundler.settings.set_command_option :path, "bundle" end - bin_option = options["binstubs"] - bin_option = nil if bin_option&.empty? - Bundler.settings.set_command_option :bin, bin_option if options["binstubs"] - 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"] - normalize_groups if options[:without] || options[:with] - - options[:force] = options[:redownload] - end - - def warn_ambiguous_gems - # TODO: remove this when we drop Bundler 1.x support - Installer.ambiguous_gems.to_a.each do |name, installed_from_uri, *also_found_in_uris| - Bundler.ui.warn "Warning: the gem '#{name}' was found in multiple sources." - Bundler.ui.warn "Installed from: #{installed_from_uri}" - Bundler.ui.warn "Also found in:" - also_found_in_uris.each {|uri| Bundler.ui.warn " * #{uri}" } - Bundler.ui.warn "You should add a source requirement to restrict this gem to your preferred source." - Bundler.ui.warn "For example:" - Bundler.ui.warn " gem '#{name}', :source => '#{installed_from_uri}'" - Bundler.ui.warn "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 e16c9b0e39..cbfb7da2d8 100644 --- a/lib/bundler/cli/issue.rb +++ b/lib/bundler/cli/issue.rb @@ -10,7 +10,7 @@ module Bundler be sure to check out these resources: 1. Check out our troubleshooting guide for quick fixes to common issues: - https://github.com/rubygems/rubygems/blob/master/doc/bundler/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: https://bundler.io/ @@ -22,7 +22,7 @@ module Bundler 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, by filling in the new issue form located at - https://github.com/rubygems/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md, + https://github.com/ruby/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md, and copy and pasting the information below. EOS @@ -34,8 +34,8 @@ module Bundler end def doctor - require_relative "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 f56bf5b86a..6a467f45a9 100644 --- a/lib/bundler/cli/list.rb +++ b/lib/bundler/cli/list.rb @@ -1,11 +1,14 @@ # 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 @@ -25,6 +28,36 @@ module Bundler 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"] @@ -37,8 +70,6 @@ module Bundler Bundler.ui.info "Use `bundle info` to print more detailed information about a gem" end - private - def verify_group_exists(groups) (@without_group + @only_group).each do |group| raise InvalidOption, "`#{group}` group could not be found." unless groups.include?(group) diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb index ff08927363..2f78868936 100644 --- a/lib/bundler/cli/lock.rb +++ b/lib/bundler/cli/lock.rb @@ -35,16 +35,14 @@ module Bundler update = { bundler: bundler } end - file = options[:lockfile] - file = file ? Pathname.new(file).expand_path : Bundler.default_lockfile - Bundler.settings.temporary(frozen: false) do - definition = Bundler.definition(update, file) + definition = Bundler.definition(update, Bundler.default_lockfile) definition.add_checksums if options["add-checksums"] Bundler::CLI::Common.configure_gem_version_promoter(definition, options) if options[:update] - options["remove-platform"].each do |platform| + options["remove-platform"].each do |platform_string| + platform = Gem::Platform.new(platform_string) definition.remove_platform(platform) end @@ -70,8 +68,11 @@ module Bundler 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.lock + definition.write_lock(file, false) end end diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb index 1be44ff4b4..465e56ada2 100644 --- a/lib/bundler/cli/outdated.rb +++ b/lib/bundler/cli/outdated.rb @@ -26,6 +26,9 @@ module Bundler 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 } @@ -155,7 +158,7 @@ module Bundler return active_spec if strict - active_specs = active_spec.source.specs.search(current_spec.name).select {|spec| spec.match_platform(current_spec.platform) }.sort_by(&:version) + 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 @@ -199,7 +202,15 @@ module Bundler 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 @@ -215,13 +226,25 @@ module Bundler 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? @@ -283,11 +306,28 @@ module Bundler end def table_header - header = ["Gem", "Current", "Latest", "Requested", "Groups"] + 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]) diff --git a/lib/bundler/cli/plugin.rb b/lib/bundler/cli/plugin.rb index fd61ef0d95..32fa660fe0 100644 --- a/lib/bundler/cli/plugin.rb +++ b/lib/bundler/cli/plugin.rb @@ -10,11 +10,15 @@ module Bundler 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 (deprecated)" + 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 diff --git a/lib/bundler/cli/pristine.rb b/lib/bundler/cli/pristine.rb index cfd4a995a3..f463f0bce8 100644 --- a/lib/bundler/cli/pristine.rb +++ b/lib/bundler/cli/pristine.rb @@ -11,6 +11,7 @@ module Bundler definition = Bundler.definition definition.validate_runtime! installer = Bundler::Installer.new(Bundler.root, definition) + git_sources = [] ProcessLock.lock do installed_specs = definition.specs.reject do |spec| @@ -41,6 +42,9 @@ module Bundler 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 @@ -49,7 +53,7 @@ module Bundler true end.map(&:name) - jobs = installer.send(:installation_parallelization) + 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 diff --git a/lib/bundler/cli/show.rb b/lib/bundler/cli/show.rb index 1896a0b11c..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 @@ -24,7 +24,7 @@ module Bundler return unless spec path = spec.full_gem_path unless File.directory?(path) - return Bundler.ui.warn "The gem #{gem_name} has been deleted. It was installed at: #{path}" + 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) @@ -57,12 +57,8 @@ module Bundler 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.remotely! } - else - definition.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 985e8db051..d92ffd995f 100644 --- a/lib/bundler/cli/update.rb +++ b/lib/bundler/cli/update.rb @@ -15,7 +15,7 @@ module 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.feature_flag.plugins? + Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.settings[:plugins] sources = Array(options[:source]) groups = Array(options[:group]).map(&:to_sym) @@ -23,10 +23,10 @@ module 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 3, "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 @@ -63,9 +63,11 @@ module Bundler opts = options.dup opts["update"] = true opts["local"] = options[:local] - opts["force"] = options[:redownload] + 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! @@ -92,7 +94,7 @@ module Bundler locked_spec = locked_info[:spec] new_spec = Bundler.definition.specs[name].first unless new_spec - unless locked_spec.match_platform(Bundler.local_platform) + 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 diff --git a/lib/bundler/cli/viz.rb b/lib/bundler/cli/viz.rb deleted file mode 100644 index 5c09e00995..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.to_s.include?("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 a4f5bb1638..6865e30dbc 100644 --- a/lib/bundler/compact_index_client.rb +++ b/lib/bundler/compact_index_client.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "pathname" require "set" module Bundler @@ -28,11 +27,7 @@ module Bundler # 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 - # NOTE: MD5 is here not because we expect a server to respond with it, but - # because we use it to generate the etag on first request during the upgrade - # to the compact index client that uses opaque etags saved to files. - # Remove once 2.5.0 has been out for a while. - SUPPORTED_DIGESTS = { "sha-256" => :SHA256, "md5" => :MD5 }.freeze + 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: diff --git a/lib/bundler/compact_index_client/cache.rb b/lib/bundler/compact_index_client/cache.rb index bedd7f8028..3bae6c9efd 100644 --- a/lib/bundler/compact_index_client/cache.rb +++ b/lib/bundler/compact_index_client/cache.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "gem_parser" +require "rubygems/resolver/api_set/gem_parser" module Bundler class CompactIndexClient diff --git a/lib/bundler/compact_index_client/gem_parser.rb b/lib/bundler/compact_index_client/gem_parser.rb deleted file mode 100644 index 60a1817607..0000000000 --- a/lib/bundler/compact_index_client/gem_parser.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module Bundler - class CompactIndexClient - if defined?(Gem::Resolver::APISet::GemParser) - GemParser = Gem::Resolver::APISet::GemParser - else - class GemParser - EMPTY_ARRAY = [].freeze - private_constant :EMPTY_ARRAY - - def parse(line) - version_and_platform, rest = line.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) } : EMPTY_ARRAY - requirements = requirements ? requirements.map! {|d| parse_dependency(d) } : EMPTY_ARRAY - [version, platform, dependencies, requirements] - end - - private - - def parse_dependency(string) - dependency = string.split(":") - dependency[-1] = dependency[-1].split("&") if dependency.size > 1 - dependency[0] = -dependency[0] - dependency - end - end - end - end -end diff --git a/lib/bundler/compact_index_client/parser.rb b/lib/bundler/compact_index_client/parser.rb index 3276abdd68..ad0d17ed4a 100644 --- a/lib/bundler/compact_index_client/parser.rb +++ b/lib/bundler/compact_index_client/parser.rb @@ -64,14 +64,17 @@ module Bundler end def gem_parser - @gem_parser ||= GemParser.new + @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 - return unless (checksum_start = line.index(" ", name_end + 1) + 1) + 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 diff --git a/lib/bundler/compact_index_client/updater.rb b/lib/bundler/compact_index_client/updater.rb index 88c7146900..6066fdc7c4 100644 --- a/lib/bundler/compact_index_client/updater.rb +++ b/lib/bundler/compact_index_client/updater.rb @@ -37,7 +37,8 @@ module Bundler file.digests = parse_digests(response) # server may ignore Range and return the full response if response.is_a?(Gem::Net::HTTPPartialContent) - break false unless file.append(response.body.byteslice(1..-1)) + tail = response.body.byteslice(1..-1) + break false unless tail && file.append(tail) else file.write(response.body) end diff --git a/lib/bundler/current_ruby.rb b/lib/bundler/current_ruby.rb index 93e0c401c0..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,41 +11,28 @@ 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 - 2.6 - 2.7 - 3.0 - 3.1 - 3.2 - 3.3 - ].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 - truffleruby - windows - 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? - return true if Bundler::GemHelpers.generic_local_platform_is_ruby? + return true if Bundler::MatchPlatform.generic_local_platform_is_ruby? !windows? && (RUBY_ENGINE == "ruby" || RUBY_ENGINE == "rbx" || RUBY_ENGINE == "maglev" || RUBY_ENGINE == "truffleruby") end @@ -61,7 +50,10 @@ module Bundler end def maglev? - RUBY_ENGINE == "maglev" + 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 truffleruby? @@ -82,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 c3eae50f65..7a95671471 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -1,21 +1,23 @@ # frozen_string_literal: true 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, :locked_checksums, :locked_deps, :locked_gems, + :overrides, :platforms, :ruby_version, :lockfile, @@ -36,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 # @@ -57,23 +62,37 @@ 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 + @unlocking_all = false + strict = unlock.delete(:strict) @unlocking_bundler = unlock.delete(:bundler) @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 @prefer_local = false @specs = nil @ruby_version = ruby_version @gemfiles = gemfiles + @overrides = overrides @lockfile = lockfile @lockfile_contents = String.new @@ -83,72 +102,63 @@ module Bundler @locked_ruby_version = nil @new_platforms = [] - @removed_platform = nil + @removed_platforms = [] + @originally_invalid_platforms = [] 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 - @originally_locked_deps = @locked_gems.dependencies + @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 = @originally_locked_deps - @locked_specs = @originally_locked_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_gems = nil @locked_platforms = [] @most_specific_locked_platform = nil @platforms = [] @locked_deps = {} @locked_specs = SpecSet.new([]) - @originally_locked_deps = {} - @originally_locked_specs = @locked_specs @locked_sources = [] - @locked_checksums = Bundler.feature_flag.lockfile_checksums? - end - - locked_gem_sources = @locked_sources.select {|s| s.is_a?(Source::Rubygems) } - @multisource_allowed = locked_gem_sources.size == 1 && locked_gem_sources.first.multiple_remotes? && Bundler.frozen_bundle? - - if @multisource_allowed - unless sources.aggregate_global_source? - 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.major_deprecation 2, msg - end - - @sources.merged_gem_lockfile_sections!(locked_gem_sources.first) + @originally_locked_specs = @locked_specs + @originally_locked_sources = @locked_sources + @locked_checksums = Bundler.settings[:lockfile_checksums] end - @sources_to_unlock = @unlock.delete(: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) @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 - @explicit_unlocks = @unlock.delete(:gems) || [] - - if @unlock[:conservative] + if conservative @gems_to_unlock = @explicit_unlocks.any? ? @explicit_unlocks : @dependencies.map(&:name) else eager_unlock = @explicit_unlocks.map {|name| Dependency.new(name, ">= 0") } @@ -186,12 +196,14 @@ module Bundler def setup_domain!(options = {}) prefer_local! if options[:"prefer-local"] + sources.cached! + if options[:add_checksums] || (!options[:local] && install_needed?) - remotely! + sources.remote! true else Bundler.settings.set_command_option(:jobs, 1) unless install_needed? # to avoid the overhead of Bundler::Worker - with_cache! + sources.local! false end end @@ -243,6 +255,7 @@ module Bundler end def missing_specs + preload_git_sources resolve.missing_specs_for(requested_dependencies) end @@ -254,7 +267,7 @@ module Bundler rescue BundlerError => e @resolve = nil @resolver = nil - @resolution_packages = nil + @resolution_base = nil @source_requirements = nil @specs = nil @@ -279,12 +292,17 @@ module Bundler end def filter_relevant(dependencies) - platforms_array = [generic_local_platform].freeze dependencies.select do |d| - d.should_include? && !d.gem_platforms(platforms_array).empty? + 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 @@ -327,18 +345,14 @@ module Bundler SpecSet.new(filter_specs(@locked_specs, @dependencies - deleted_deps)) else Bundler.ui.debug "Found no changes, using resolution from the lockfile" - if @removed_platform || @locked_gems.may_include_redundant_platform_specific_gems? + 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 - if lockfile_exists? - Bundler.ui.debug "Found changes from the lockfile, re-resolving dependencies because #{change_reason}" - else - Bundler.ui.debug "Resolving dependencies because there's no lockfile" - end + Bundler.ui.debug resolve_needed_reason start_resolution end @@ -368,15 +382,53 @@ module Bundler msg = "`Definition#lock` was passed a target file argument. #{suggestion}" - Bundler::SharedHelpers.major_deprecation 2, msg + Bundler::SharedHelpers.feature_removed! msg end write_lock(target_lockfile, preserve_unknown_sections) end + def write_lock(file, preserve_unknown_sections) + return if Definition.no_lock || !lockfile || file.nil? + + 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") + + if @locked_bundler_version + locked_major = @locked_bundler_version.segments.first + current_major = bundler_version_to_lock.segments.first + + updating_major = locked_major < current_major + end + + preserve_unknown_sections ||= !updating_major && (Bundler.frozen_bundle? || !(unlocking? || @unlocking_bundler)) + + 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 + + if Bundler.frozen_bundle? + Bundler.ui.error "Cannot write a changed lockfile while frozen." + return + end + + 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 + 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 @@ -409,51 +461,18 @@ module Bundler raise ProductionError, "Frozen mode is set, but there's no lockfile" unless lockfile_exists? - 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}" } - - 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 - - reason = nothing_changed? ? "some dependencies were deleted from your gemfile" : change_reason - msg = String.new - msg << "#{reason.capitalize.strip}, but the lockfile can't be updated because frozen mode is set" - 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_gemfile_path} to version control.\n" + msg = lockfile_changes_summary("frozen mode is set") + return unless msg unless explicit_flag suggested_command = unless Bundler.settings.locations("frozen").keys.include?(:env) "bundle config set frozen false" end - msg << "If this is a development machine, remove the #{SharedHelpers.relative_lockfile_path} " \ + msg << "\n\nIf this is a development machine, remove the #{SharedHelpers.relative_lockfile_path} " \ "freeze by running `#{suggested_command}`." if suggested_command end - raise ProductionError, msg if added.any? || deleted.any? || changed.any? || !nothing_changed? + raise ProductionError, msg end def validate_runtime! @@ -474,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 @@ -487,15 +500,15 @@ module Bundler end def validate_platforms! - return if current_platform_locked? + return if current_platform_locked? || @platforms.include?(Gem::Platform::RUBY) raise ProductionError, "Your bundle only supports platforms #{@platforms.map(&:to_s)} " \ - "but your local platform is #{local_platform}. " \ - "Add the current platform to the lockfile with\n`bundle lock --add-platform #{local_platform}` and try again." + "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 - @platforms = resolve.normalize_platforms!(current_dependencies, platforms) + resolve.normalize_platforms!(current_dependencies, platforms) @resolve = SpecSet.new(resolve.for(current_dependencies, @platforms)) end @@ -508,10 +521,10 @@ module Bundler end def remove_platform(platform) - removed_platform = @platforms.delete(Gem::Platform.new(platform)) - @removed_platform ||= removed_platform - return if removed_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 nothing_changed? @@ -526,18 +539,68 @@ module Bundler @unlocking end - attr_writer :source_requirements - def add_checksums + require "rubygems/package" + @locked_checksums = true setup_domain!(add_checksums: true) - specs # force materialization to real specifications, so that checksums are fetched + # 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 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 + def install_needed? resolve_needed? || missing_specs? end @@ -553,6 +616,8 @@ module Bundler @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 @@ -562,53 +627,32 @@ module Bundler end def should_add_extra_platforms? - !lockfile_exists? && generic_local_platform_is_ruby? && !Bundler.settings[:force_ruby_platform] + !lockfile_exists? && Bundler::MatchPlatform.generic_local_platform_is_ruby? && !Bundler.settings[:force_ruby_platform] end def lockfile_exists? lockfile && File.exist?(lockfile) end - def write_lock(file, preserve_unknown_sections) - return if Definition.no_lock || file.nil? - - 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") - - if @locked_bundler_version - locked_major = @locked_bundler_version.segments.first - current_major = bundler_version_to_lock.segments.first - - updating_major = locked_major < current_major - end - - preserve_unknown_sections ||= !updating_major && (Bundler.frozen_bundle? || !(unlocking? || @unlocking_bundler)) - - 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 - - if Bundler.frozen_bundle? - Bundler.ui.error "Cannot write a changed lockfile while frozen." - return - end + def resolver + @resolver ||= new_resolver(resolution_base) + end - SharedHelpers.filesystem_access(file) do |p| - File.open(p, "wb") {|f| f.puts(contents) } - end + def expanded_dependencies + apply_overrides_to(dependencies_with_bundler) + metadata_dependencies end - def resolver - @resolver ||= Resolver.new(resolution_packages, gem_version_promoter, @most_specific_locked_platform) + def apply_overrides_to(deps) + return deps if @overrides.empty? + deps.map {|dep| apply_override_to(dep) } end - def expanded_dependencies - dependencies_with_bundler + metadata_dependencies + 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 @@ -618,14 +662,14 @@ module Bundler [Dependency.new("bundler", @unlocking_bundler)] + dependencies end - def resolution_packages - @resolution_packages ||= begin + def resolution_base + @resolution_base ||= begin last_resolve = converge_locked_specs remove_invalid_platforms! - packages = Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: @gems_to_unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local) - packages = additional_base_requirements_to_prevent_downgrades(packages, last_resolve) - packages = additional_base_requirements_to_force_updates(packages) - packages + 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 @@ -634,18 +678,12 @@ module Bundler end def materialize(dependencies) - # Tracks potential endless loops trying to re-resolve. - # TODO: Remove as dead code if not reports are received in a while - incorrect_spec = nil - specs = begin resolve.materialize(dependencies) rescue IncorrectLockfileDependencies => e - spec = e.spec - raise "Infinite loop while fixing lockfile dependencies" if incorrect_spec == spec + raise if Bundler.frozen_bundle? - incorrect_spec = spec - reresolve_without([spec]) + reresolve_without([e.spec]) retry end @@ -698,8 +736,7 @@ module Bundler still_incomplete_specs = resolve.incomplete_specs if still_incomplete_specs == incomplete_specs - package = resolution_packages.get_package(incomplete_specs.first.name) - resolver.raise_not_found! package + resolver.raise_incomplete! incomplete_specs end incomplete_specs = still_incomplete_specs @@ -721,84 +758,140 @@ module Bundler end def reresolve_without(incomplete_specs) - resolution_packages.delete(incomplete_specs) + resolution_base.delete(incomplete_specs) @resolve = start_resolution end def start_resolution - local_platform_needed_for_resolvability = @most_specific_non_local_locked_ruby_platform && !@platforms.include?(local_platform) - @platforms << local_platform if local_platform_needed_for_resolvability - add_platform(Gem::Platform::RUBY) if RUBY_ENGINE == "truffleruby" + 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 - if @most_specific_non_local_locked_ruby_platform - if spec_set_incomplete_for_platform?(result, @most_specific_non_local_locked_ruby_platform) - @platforms.delete(@most_specific_non_local_locked_ruby_platform) + @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(local_platform) + @platforms.delete(Bundler.local_platform) end end - @platforms = result.add_extra_platforms!(platforms) if should_add_extra_platforms? + 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? - sources.non_global_rubygems_sources.all?(&:dependency_api_available?) && !sources.aggregate_global_source? + 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| - MatchPlatform.platforms_match?(bundle_platform, local_platform) + Bundler.generic_local_platform == bundle_platform || Bundler.local_platform === bundle_platform end end def add_current_platform - return if @platforms.include?(local_platform) + return if @platforms.include?(Bundler.local_platform) - @most_specific_non_local_locked_ruby_platform = find_most_specific_locked_ruby_platform - return if @most_specific_non_local_locked_ruby_platform + @most_specific_non_local_locked_platform = find_most_specific_locked_platform + return if @most_specific_non_local_locked_platform - @platforms << local_platform + @platforms << Bundler.local_platform true end - def find_most_specific_locked_ruby_platform - return unless generic_local_platform_is_ruby? && current_platform_locked? + def find_most_specific_locked_platform + return unless current_platform_locked? @most_specific_locked_platform end - def change_reason - if unlocking? - unlock_targets = if @gems_to_unlock.any? - ["gems", @gems_to_unlock] - elsif @sources_to_unlock.any? - ["sources", @sources_to_unlock] + 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 - unlock_reason = if unlock_targets - "#{unlock_targets.first}: (#{unlock_targets.last.join(", ")})" + def change_reason + if resolve_needed? + if unlocking? + unlocking_reason else - @unlock[:ruby] ? "ruby" : "" + lockfile_changed_reason end + else + "some dependencies were deleted from your gemfile" + end + end - return "bundler is unlocking #{unlock_reason}" + 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"], - [@current_platform_missing, "your lockfile does not include the current platform"], - [@new_platforms.any?, "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 lock file is missing \"#{@missing_lockfile_dep}\""], + [@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_deps, "your lock file includes \"#{@locked_spec_with_missing_deps}\" but not some of its dependencies"], + [@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 @@ -815,8 +908,8 @@ 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 } + 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 } deps_for_source.uniq.sort != locked_deps_for_source.sort @@ -824,7 +917,7 @@ module Bundler 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) }) !locked_index.subset?(source.specs) rescue PathError, GitError => e @@ -854,29 +947,40 @@ module Bundler end def check_lockfile - @missing_lockfile_dep = nil - @locked_spec_with_invalid_deps = nil @locked_spec_with_missing_deps = nil + @locked_spec_with_missing_checksums = nil + @locked_spec_with_empty_checksums = nil - missing = [] + missing_deps = [] + missing_checksums = [] + empty_checksums = [] invalid = [] @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 << s if validation == :missing + missing_deps << s if validation == :missing invalid << s if validation == :invalid end - if missing.any? - @locked_specs.delete(missing) + @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? - @locked_spec_with_missing_deps = missing.first.name - elsif !@dependency_changes - @missing_lockfile_dep = current_dependencies.find do |d| - @locked_specs[d.name].empty? && d.name != "bundler" - end&.name + if missing_deps.any? + @locked_specs.delete(missing_deps) + + @locked_spec_with_missing_deps = missing_deps.first.name end if invalid.any? @@ -892,24 +996,6 @@ module Bundler end end - 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 - - 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) - end - end - def converge_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 @@ -919,7 +1005,7 @@ module Bundler 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 = @locked_gems.sources.find {|s| s == source && !s.equal?(source) } + 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 @@ -933,36 +1019,69 @@ module Bundler end end + sources.metadata_source.checksum_store.merge!(@locked_gems.metadata_source.checksum_store) if @locked_gems + changes end def converge_dependencies - changes = false + @missing_lockfile_dep = nil + @changed_dependencies = [] @dependencies.each do |dep| if dep.source dep.source = sources.get(dep.source) end + next unless relevant_deps?(dep) - unless locked_dep = @originally_locked_deps[dep.name] - changes = true - next + 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 - # 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, dep.type) - - # We already know the name matches from the hash lookup - # so we only need to check the requirement now - changes ||= dep.requirement != locked_dep.requirement + @changed_dependencies << name if dep_changed end - changes + converge_overrides_outside_dependencies + + @changed_dependencies.any? + 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) + + 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 @@ -994,26 +1113,43 @@ module Bundler specs.each do |s| name = s.name + next if @gems_to_unlock.include?(name) + dep = @dependencies.find {|d| s.satisfies?(d) } lockfile_source = s.source if dep - gemfile_source = dep.source || default_source - - deps << dep if !dep.source || lockfile_source.include?(dep.source) || new_deps.include?(dep) + replacement_source = dep.source - # Replace the locked dependency's source with the equivalent source from the Gemfile - s.source = gemfile_source + deps << dep if !replacement_source || lockfile_source.include?(replacement_source) || new_deps.include?(dep) else - # Replace the locked dependency's source with the default source, if the locked source is no longer in the Gemfile - s.source = default_source unless sources.get(lockfile_source) + parent_dep = @dependencies.find do |d| + next unless d.source && d.source != lockfile_source + next if d.source.is_a?(Source::Gemspec) + + parent_locked_specs = @originally_locked_specs[d.name] + + parent_locked_specs.any? do |parent_spec| + parent_spec.runtime_dependencies.any? {|rd| rd.name == s.name } + end + end + + 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 + # Replace the locked dependency's source with the equivalent source from the Gemfile + s.source = replacement_source || default_source + next if s.source_changed? + source = s.source next if @sources_to_unlock.include?(source.name) # Path sources have special logic - if source.instance_of?(Source::Path) || source.instance_of?(Source::Gemspec) || (source.instance_of?(Source::Git) && !@gems_to_unlock.include?(name) && deps.include?(dep)) + if source.is_a?(Source::Path) new_spec = source.specs[s].first if new_spec s.runtime_dependencies.replace(new_spec.runtime_dependencies) @@ -1024,11 +1160,6 @@ module Bundler end end - if dep.nil? && requested_dep = requested_dependencies.find {|d| name == d.name } - @gems_to_unlock << name - deps << requested_dep - end - converged << s end @@ -1046,17 +1177,56 @@ module Bundler @source_requirements ||= find_source_requirements 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 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 + + begin + needed_git_sources.each {|source| preload_git_source_worker.enq(source) } + ensure + preload_git_source_worker.stop + end + 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 + end + + # Git sources that should be excluded (only used by --without groups) + def excluded_git_sources + sources.git_sources - needed_git_sources + end + 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) source_requirements = if precompute_source_requirements_for_indirect_dependencies? - all_requirements = source_map.all_requirements + all_requirements = source_map.all_requirements(excluded) { default: default_source }.merge(all_requirements) else - { default: Source::RubygemsAggregate.new(sources, source_map) }.merge(source_map.direct_requirements) + { default: Source::RubygemsAggregate.new(sources, source_map, excluded) }.merge(source_map.direct_requirements) end - source_requirements.merge!(source_map.locked_requirements) if nothing_changed? + source_requirements.merge!(source_map.locked_requirements) if nothing_changed metadata_dependencies.each do |dep| source_requirements[dep.name] = sources.metadata_source end @@ -1097,57 +1267,63 @@ module Bundler current == proposed end - def additional_base_requirements_to_prevent_downgrades(resolution_packages, last_resolve) - return resolution_packages unless @locked_gems && !sources.expired_sources?(@locked_gems.sources) - converge_specs(@originally_locked_specs - last_resolve).each do |locked_spec| - next if locked_spec.source.is_a?(Source::Path) - resolution_packages.base_requirements[locked_spec.name] = Gem::Requirement.new(">= #{locked_spec.version}") + 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_packages + resolution_base end - def additional_base_requirements_to_force_updates(resolution_packages) - return resolution_packages if @explicit_unlocks.empty? - full_update = dup_for_full_unlock.resolve + 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_packages.base_requirements[name] = Gem::Requirement.new("= #{version}") if version - end - resolution_packages - end - - def dup_for_full_unlock - unlocked_definition = self.class.new(@lockfile, @dependencies, @sources, true, @ruby_version, @optional_groups, @gemfiles) - unlocked_definition.source_requirements = source_requirements - unlocked_definition.gem_version_promoter.tap do |gvp| - gvp.level = gem_version_promoter.level - gvp.strict = gem_version_promoter.strict - gvp.pre = gem_version_promoter.pre + resolution_base.base_requirements[name] = Gem::Requirement.new("= #{version}") if version end - unlocked_definition + resolution_base end def remove_invalid_platforms! return if Bundler.frozen_bundle? - platforms.reverse_each do |platform| - next if local_platform == platform || - @new_platforms.include?(platform) || - @path_changes || - @dependency_changes || - @locked_spec_with_invalid_deps || - !spec_set_incomplete_for_platform?(@originally_locked_specs, platform) + skips = (@new_platforms + [Bundler.local_platform]).uniq - remove_platform(platform) - end - end + # 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 - def spec_set_incomplete_for_platform?(spec_set, platform) - spec_set.incomplete_for_platform?(current_dependencies, platform) + @originally_invalid_platforms = @originally_locked_specs.remove_invalid_platforms!(current_dependencies, platforms, skips: skips) end 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 + + def new_resolver(base) + Resolver.new(base, gem_version_promoter, @most_specific_locked_platform) + end end end diff --git a/lib/bundler/dependency.rb b/lib/bundler/dependency.rb index 09a145b8c8..cb9c7a76ea 100644 --- a/lib/bundler/dependency.rb +++ b/lib/bundler/dependency.rb @@ -2,51 +2,92 @@ require "rubygems/dependency" require_relative "shared_helpers" -require_relative "rubygems_ext" module Bundler class Dependency < Gem::Dependency - attr_reader :autorequire - attr_reader :groups, :platforms, :gemfile, :path, :git, :github, :branch, :ref, :glob - - ALL_RUBY_VERSIONS = (18..27).to_a.concat((30..35).to_a).freeze - PLATFORM_MAP = { - ruby: [Gem::Platform::RUBY, ALL_RUBY_VERSIONS], - mri: [Gem::Platform::RUBY, ALL_RUBY_VERSIONS], - rbx: [Gem::Platform::RUBY], - truffleruby: [Gem::Platform::RUBY], - jruby: [Gem::Platform::JAVA, [18, 19]], - windows: [Gem::Platform::WINDOWS, ALL_RUBY_VERSIONS], - # deprecated - mswin: [Gem::Platform::MSWIN, ALL_RUBY_VERSIONS], - mswin64: [Gem::Platform::MSWIN64, ALL_RUBY_VERSIONS - [18]], - mingw: [Gem::Platform::MINGW, ALL_RUBY_VERSIONS], - x64_mingw: [Gem::Platform::X64_MINGW, 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 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"] - @path = options["path"] - @git = options["git"] - @github = options["github"] - @branch = options["branch"] - @ref = options["ref"] - @glob = options["glob"] - @platforms = Array(options["platforms"]) - @env = options["env"] - @should_include = options.fetch("should_include", true) - @gemfile = options["gemfile"] - @force_ruby_platform = options["force_ruby_platform"] if options.key?("force_ruby_platform") + @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) - @autorequire = Array(options["require"] || []) if options.key?("require") + @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) + + @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 @@ -56,37 +97,41 @@ module Bundler # passed in the `valid_platforms` parameter def gem_platforms(valid_platforms) return RUBY_PLATFORM_ARRAY if force_ruby_platform - return valid_platforms if @platforms.empty? + return valid_platforms if platforms.empty? - valid_platforms.select {|p| expanded_platforms.include?(GemHelpers.generic(p)) } + valid_platforms.select {|p| expanded_platforms.include?(Gem::Platform.generic(p)) } end def expanded_platforms - @expanded_platforms ||= @platforms.filter_map {|pl| PLATFORM_MAP[pl] }.flatten.uniq + @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? - type == :development + @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 diff --git a/lib/bundler/deployment.rb b/lib/bundler/deployment.rb index b432ae6ae1..3344449e82 100644 --- a/lib/bundler/deployment.rb +++ b/lib/bundler/deployment.rb @@ -1,69 +1,6 @@ # frozen_string_literal: true require_relative "shared_helpers" -Bundler::SharedHelpers.major_deprecation 2, "Bundler no longer integrates with " \ +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/digest.rb b/lib/bundler/digest.rb index 2c6d971f1b..158803033d 100644 --- a/lib/bundler/digest.rb +++ b/lib/bundler/digest.rb @@ -26,7 +26,7 @@ module Bundler 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) + 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 diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index 05c60f2f1a..6e2638a8be 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -9,19 +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 gemfile force_ruby_platform].freeze + platform platforms source install_if force_ruby_platform].freeze 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 + attr_reader :gemspecs, :gemfile, :overrides attr_accessor :dependencies def initialize @@ -38,6 +39,8 @@ module Bundler @gemspecs = [] @gemfile = nil @gemfiles = [] + @lockfile = nil + @overrides = [] add_git_sources end @@ -73,16 +76,16 @@ module Bundler case specs_by_name_and_version.size when 1 specs = specs_by_name_and_version.values.first - spec = specs.find {|s| s.match_platform(Bundler.local_platform) } || specs.first + spec = specs.find {|s| s.installable_on_platform?(Bundler.local_platform) } || specs.first @gemspecs << spec - gem spec.name, name: spec.name, path: path, glob: glob + 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}" @@ -94,85 +97,30 @@ module Bundler def gem(name, *args) options = args.last.is_a?(Hash) ? args.pop.dup : {} - options["gemfile"] = @gemfile version = args || [">= 0"] 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 } - if current.requirement != dep.requirement - current_requirement_open = current.requirements_list.include?(">= 0") - - gemspec_dep = [dep, current].find(&:gemspec_dev_dep?) - if gemspec_dep - gemfile_dep = [dep, current].find(&:runtime?) - - if gemfile_dep && !current_requirement_open - Bundler.ui.warn "A gemspec development dependency (#{gemspec_dep.name}, #{gemspec_dep.requirement}) is being overridden by a Gemfile dependency (#{gemfile_dep.name}, #{gemfile_dep.requirement}).\n" \ - "This behaviour may change in the future. Please remove either of them, or make sure they both have the same requirement\n" - elsif gemfile_dep.nil? - 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 - raise GemfileError, "Two gemspecs 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 #{current.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: #{current.name} (#{current.requirement}) and #{dep.name} (#{dep.requirement})" \ - "#{update_prompt}" - end - end + add_dependency(name, version, options) + 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 != dep.source - 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" - 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" \ - "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 + # 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 @@ -187,9 +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 - @sources.add_global_rubygems_remote(source) + @sources.add_global_rubygems_remote(source, cooldown: cooldown) end end @@ -209,8 +157,7 @@ module Bundler def path(path, options = {}, &blk) 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? @@ -242,9 +189,32 @@ module Bundler 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) check_primary_source_safety - Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles) + lockfile = @lockfile unless @lockfile.nil? + Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles, @overrides) end def group(*args, &blk) @@ -301,6 +271,113 @@ module Bundler 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 @@ -407,6 +484,13 @@ module Bundler 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"]) @@ -433,8 +517,6 @@ module Bundler opts["source"] = source end - opts["source"] ||= @source - opts["env"] ||= @env opts["platforms"] = platforms.dup opts["group"] = groups opts["should_include"] = install_if @@ -473,14 +555,10 @@ module Bundler def normalize_source(source) case source when :gemcutter, :rubygems, :rubyforge - message = - "The source :#{source} is deprecated because HTTP requests are insecure.\n" \ - "Please change your source to 'https://rubygems.org' if possible, or 'http://rubygems.org' if not." 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.major_deprecation 2, message, removed_message: removed_message - "http://rubygems.org" + Bundler::SharedHelpers.feature_removed! removed_message when String source else @@ -499,7 +577,7 @@ module Bundler " gem 'rails'\n" \ " end\n\n" - SharedHelpers.major_deprecation(2, msg.strip) + SharedHelpers.feature_removed! msg.strip end def check_rubygems_source_safety @@ -507,24 +585,10 @@ module Bundler end def multiple_global_source_warning - if Bundler.feature_flag.bundler_3_mode? - 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 - else - message = - "Your Gemfile contains multiple global 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." - removed_message = - "Your Gemfile contains multiple global sources. " \ - "Using `source` more than once without a block is a security risk, and " \ - "may result in installing unexpected gems. To resolve this error, use " \ - "a block to indicate which gems should come from the secondary source." - Bundler::SharedHelpers.major_deprecation 2, message, removed_message: removed_message - end + 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 diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb index e4780a1c2a..7c7ce107e2 100644 --- a/lib/bundler/endpoint_specification.rb +++ b/lib/bundler/endpoint_specification.rb @@ -5,8 +5,9 @@ module Bundler class EndpointSpecification < Gem::Specification include MatchRemoteMetadata - attr_reader :name, :version, :platform, :checksum - attr_accessor :remote, :dependencies, :locked_platform + attr_reader :name, :version, :platform, :checksum, :created_at + attr_writer :dependencies + attr_accessor :remote, :locked_platform def initialize(name, version, platform, spec_fetcher, dependencies, metadata = nil) super() @@ -14,7 +15,8 @@ module Bundler @version = Gem::Version.create version @platform = Gem::Platform.new(platform) @spec_fetcher = spec_fetcher - @dependencies = dependencies.map {|dep, reqs| build_dependency(dep, reqs) } + @dependencies = nil + @unbuilt_dependencies = dependencies @loaded_from = nil @remote_specification = nil @@ -31,6 +33,11 @@ module Bundler @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 @@ -138,6 +145,7 @@ module Bundler unless data @required_ruby_version = nil @required_rubygems_version = nil + @created_at = nil return end @@ -154,6 +162,15 @@ module Bundler @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 StandardError => e @@ -161,7 +178,7 @@ module Bundler end def build_dependency(name, requirements) - Gem::Dependency.new(name, requirements) + Dependency.new(name, requirements) end end end diff --git a/lib/bundler/env.rb b/lib/bundler/env.rb index 074bef6edc..2b29705060 100644 --- a/lib/bundler/env.rb +++ b/lib/bundler/env.rb @@ -78,17 +78,6 @@ module Bundler "not installed" end - def self.version_of(script) - return "not installed" unless Bundler.which(script) - `#{script} --version`.chomp - 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 = [] @@ -110,16 +99,8 @@ module Bundler 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 - out << ["Tools"] - out << [" Git", git_version] - out << [" RVM", ENV.fetch("rvm_version") { version_of("rvm") }] - out << [" rbenv", version_of("rbenv")] - out << [" chruby", chruby_version] - - %w[rubygems-bundler open_gem].each do |name| - specs = Bundler.rubygems.find_name(name) - out << [" #{name}", "(#{specs.map(&:version).join(",")})"] unless specs.empty? - end + 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*/, "") @@ -143,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 444ab6fd37..bf9478a299 100644 --- a/lib/bundler/environment_preserver.rb +++ b/lib/bundler/environment_preserver.rb @@ -6,6 +6,7 @@ module Bundler BUNDLER_KEYS = %w[ BUNDLE_BIN_PATH BUNDLE_GEMFILE + BUNDLE_LOCKFILE BUNDLER_VERSION BUNDLER_SETUP GEM_HOME diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb index 3fa90c5eb8..dff5d93128 100644 --- a/lib/bundler/errors.rb +++ b/lib/bundler/errors.rb @@ -25,6 +25,7 @@ module Bundler 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 @@ -76,11 +77,6 @@ module Bundler def mismatch_resolution_instructions removable, remote = [@existing, @checksum].partition(&:removable?) case removable.size - when 0 - msg = +"Mismatched checksums each have an authoritative source:\n" - msg << " 1. #{@existing.sources.reject(&:removable?).map(&:to_s).join(" and ")}\n" - msg << " 2. #{@checksum.sources.reject(&:removable?).map(&:to_s).join(" and ")}\n" - msg << "You may need to alter your Gemfile sources to resolve this issue.\n" when 1 msg = +"If you trust #{remote.first.sources.first}, to resolve this issue you can:\n" msg << removable.first.removal_instructions @@ -135,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" @@ -193,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 @@ -207,7 +222,9 @@ module Bundler class DirectoryRemovalError < BundlerError def initialize(orig_exception, msg) full_message = "#{msg}.\n" \ - "The underlying error was #{orig_exception.class}: #{orig_exception.message}, with backtrace:\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) @@ -248,10 +265,40 @@ module Bundler class InvalidArgumentError < BundlerError; status_code(40); end class IncorrectLockfileDependencies < BundlerError - attr_reader :spec + attr_reader :spec, :actual_dependencies, :lockfile_dependencies - def initialize(spec) + 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) diff --git a/lib/bundler/feature_flag.rb b/lib/bundler/feature_flag.rb index b19cf42cc3..dea8abedba 100644 --- a/lib/bundler/feature_flag.rb +++ b/lib/bundler/feature_flag.rb @@ -2,45 +2,15 @@ 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 - - settings_method("#{flag}?", flag, &default) - end - private_class_method :settings_flag + (1..10).each {|v| define_method("bundler_#{v}_mode?") { @major_version >= v } } - def self.settings_option(key, &default) - settings_method(key, key, &default) + def removed_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? - value - end + def deprecated_major?(target_major_version) + @major_version >= target_major_version end - private_class_method :settings_method - - (1..10).each {|v| define_method("bundler_#{v}_mode?") { @major_version >= v } } - - settings_flag(:allow_offline_install) { bundler_3_mode? } - settings_flag(:auto_clean_without_path) { bundler_3_mode? } - settings_flag(:cache_all) { bundler_3_mode? } - settings_flag(:default_install_uses_path) { bundler_3_mode? } - settings_flag(:forget_cli_options) { bundler_3_mode? } - settings_flag(:global_gem_cache) { bundler_3_mode? } - settings_flag(:lockfile_checksums) { bundler_3_mode? } - settings_flag(:path_relative_to_cwd) { bundler_3_mode? } - settings_flag(:plugins) { @bundler_version >= Gem::Version.new("1.14") } - settings_flag(:print_only_version_number) { bundler_3_mode? } - settings_flag(:setup_makes_kernel_gem_public) { !bundler_3_mode? } - settings_flag(:update_requires_all_flag) { bundler_4_mode? } - - settings_option(:default_cli_command) { bundler_3_mode? ? :cli_help : :install } def initialize(bundler_version) @bundler_version = Gem::Version.create(bundler_version) diff --git a/lib/bundler/fetcher.rb b/lib/bundler/fetcher.rb index 9d7e8e1aca..0b6ced6f39 100644 --- a/lib/bundler/fetcher.rb +++ b/lib/bundler/fetcher.rb @@ -2,7 +2,6 @@ require_relative "vendored_persistent" require_relative "vendored_timeout" -require "cgi" require_relative "vendored_securerandom" require "zlib" @@ -73,19 +72,57 @@ module Bundler 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, AuthenticationForbiddenError, FallbackError, SecurityError] - fail_errors << Gem::Requirement::BadRequirementError - fail_errors.concat(NET_ERRORS.map {|e| Gem::Net.const_get(e) }) - 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 @@ -294,13 +331,6 @@ module Bundler paths.find {|path| File.file? path } end - HTTP_ERRORS = [ - Gem::Timeout::Error, EOFError, SocketError, Errno::ENETDOWN, Errno::ENETUNREACH, - Errno::EINVAL, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EAGAIN, - Gem::Net::HTTPBadResponse, Gem::Net::HTTPHeaderSyntaxError, Gem::Net::ProtocolError, - Gem::Net::HTTP::Persistent::Error, Zlib::BufError, Errno::EHOSTUNREACH - ].freeze - def bundler_cert_store store = OpenSSL::X509::Store.new ssl_ca_cert = Bundler.settings[:ssl_ca_cert] || diff --git a/lib/bundler/fetcher/compact_index.rb b/lib/bundler/fetcher/compact_index.rb index 6c82d57011..52168111fe 100644 --- a/lib/bundler/fetcher/compact_index.rb +++ b/lib/bundler/fetcher/compact_index.rb @@ -110,7 +110,7 @@ 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}" Gem::Net::HTTPNotModified.new(nil, nil, nil) end diff --git a/lib/bundler/fetcher/dependency.rb b/lib/bundler/fetcher/dependency.rb index 0b807c9a4b..4f2414e33d 100644 --- a/lib/bundler/fetcher/dependency.rb +++ b/lib/bundler/fetcher/dependency.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true require_relative "base" -require "cgi" +require "cgi/escape" +require "cgi/util" unless defined?(CGI::EscapeExt) module Bundler class Fetcher @@ -49,7 +50,7 @@ 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.safe_load_marshal(marshalled_deps)) end @@ -73,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 868b39b959..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 @@ -32,7 +54,6 @@ module Bundler when Gem::Net::HTTPRequestedRangeNotSatisfiable new_headers = headers.dup new_headers.delete("Range") - new_headers["Accept-Encoding"] = "gzip" fetch(uri, new_headers) when Gem::Net::HTTPRequestEntityTooLarge raise FallbackError, response.body @@ -67,15 +88,19 @@ module Bundler connection.request(uri, req) rescue OpenSSL::SSL::SSLError raise CertificateFailureError.new(uri) - rescue *HTTP_ERRORS => e + rescue *HTTP_NON_RETRYABLE_ERRORS => e Bundler.ui.trace e - if e.is_a?(SocketError) || e.message.to_s.include?("host down:") - raise NetworkDownError, "Could not reach host #{uri.host}. Check your network " \ - "connection and try again." - else - raise HTTPError, "Network error while fetching #{filtered_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 diff --git a/lib/bundler/fetcher/gem_remote_fetcher.rb b/lib/bundler/fetcher/gem_remote_fetcher.rb index 3fc7b68263..3159e05688 100644 --- a/lib/bundler/fetcher/gem_remote_fetcher.rb +++ b/lib/bundler/fetcher/gem_remote_fetcher.rb @@ -5,6 +5,12 @@ 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"] diff --git a/lib/bundler/friendly_errors.rb b/lib/bundler/friendly_errors.rb index e61ed64450..5e8eaee6bb 100644 --- a/lib/bundler/friendly_errors.rb +++ b/lib/bundler/friendly_errors.rb @@ -80,7 +80,7 @@ module Bundler 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 fill in the new issue form located at #{new_issue_url}, and copy and paste the report template above in there. + 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 @@ -102,13 +102,14 @@ module Bundler def issues_url(exception) message = exception.message.lines.first.tr(":", " ").chomp message = message.split("-").first if exception.is_a?(Errno) - require "cgi" - "https://github.com/rubygems/rubygems/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/rubygems/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md" + "https://github.com/ruby/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md" end end diff --git a/lib/bundler/gem_helpers.rb b/lib/bundler/gem_helpers.rb deleted file mode 100644 index 75243873f2..0000000000 --- a/lib/bundler/gem_helpers.rb +++ /dev/null @@ -1,150 +0,0 @@ -# frozen_string_literal: true - -module Bundler - module GemHelpers - GENERIC_CACHE = { Gem::Platform::RUBY => Gem::Platform::RUBY } # rubocop:disable Style/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("x64-mingw-ucrt"), Gem::Platform.new("x64-mingw-ucrt")], - [Gem::Platform.new("mingw32"), Gem::Platform.new("x86-mingw32")], - ].freeze - - def generic(p) - 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(local_platform) - end - module_function :generic_local_platform - - def local_platform - Bundler.local_platform - end - module_function :local_platform - - def generic_local_platform_is_ruby? - generic_local_platform == Gem::Platform::RUBY - end - module_function :generic_local_platform_is_ruby? - - def platform_specificity_match(spec_platform, user_platform) - spec_platform = Gem::Platform.new(spec_platform) - - PlatformMatch.specificity_score(spec_platform, user_platform) - end - module_function :platform_specificity_match - - def select_all_platform_match(specs, platform, force_ruby: false, prefer_locked: false) - matching = if force_ruby - specs.select {|spec| spec.match_platform(Gem::Platform::RUBY) && spec.force_ruby_platform! } - else - specs.select {|spec| spec.match_platform(platform) } - end - - if prefer_locked - locked_originally = matching.select {|spec| spec.is_a?(LazySpecification) } - return locked_originally if locked_originally.any? - end - - matching - end - module_function :select_all_platform_match - - def 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) - - sort_and_filter_best_platform_match(matching, platform) - end - module_function :select_best_platform_match - - def select_best_local_platform_match(specs, force_ruby: false) - matching = select_all_platform_match(specs, local_platform, force_ruby: force_ruby).filter_map(&:materialized_for_installation) - - sort_best_platform_match(matching, local_platform) - end - module_function :select_best_local_platform_match - - 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 - module_function :sort_and_filter_best_platform_match - - def sort_best_platform_match(matching, platform) - matching.sort_by {|spec| platform_specificity_match(spec.platform, platform) } - end - module_function :sort_best_platform_match - - class PlatformMatch - def self.specificity_score(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 + - platform_version_match(spec_platform, user_platform) * 100 - end - - 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 - - def same_specificity(platform, spec, exemplary_spec) - platform_specificity_match(spec.platform, platform) == platform_specificity_match(exemplary_spec.platform, platform) - end - module_function :same_specificity - - def same_deps(spec, exemplary_spec) - same_runtime_deps = spec.dependencies.sort == exemplary_spec.dependencies.sort - same_metadata_deps = spec.required_ruby_version == exemplary_spec.required_ruby_version && spec.required_rubygems_version == exemplary_spec.required_rubygems_version - same_runtime_deps && same_metadata_deps - end - module_function :same_deps - end -end diff --git a/lib/bundler/gem_version_promoter.rb b/lib/bundler/gem_version_promoter.rb index ecc65b4956..d64dbacfdb 100644 --- a/lib/bundler/gem_version_promoter.rb +++ b/lib/bundler/gem_version_promoter.rb @@ -132,8 +132,6 @@ module Bundler # Specific version moves can't always reliably be done during sorting # as not all elements are compared against each other. def post_sort(result, unlock, locked_version) - # default :major behavior in Bundler does not do this - return result if major? if unlock || locked_version.nil? result else diff --git a/lib/bundler/graph.rb b/lib/bundler/graph.rb deleted file mode 100644 index b22b17a453..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 - warn "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 df46facc88..9aef2dfa12 100644 --- a/lib/bundler/index.rb +++ b/lib/bundler/index.rb @@ -46,13 +46,6 @@ module Bundler true end - def search_all(name, &blk) - return enum_for(:search_all, name) unless blk - specs_by_name(name).each(&blk) - @duplicates[name]&.each(&blk) - @sources.each {|source| source.search_all(name, &blk) } - end - # Search this index's specs, and any source indexes that this index knows # about, returning all of the results. def search(query) @@ -131,6 +124,11 @@ module Bundler 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 add spec @@ -153,8 +151,8 @@ 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 } + deps = spec.runtime_dependencies + other_deps = other_spec.runtime_dependencies deps.sort == other_deps.sort end diff --git a/lib/bundler/injector.rb b/lib/bundler/injector.rb index c0941e9909..6aa9179024 100644 --- a/lib/bundler/injector.rb +++ b/lib/bundler/injector.rb @@ -80,20 +80,19 @@ module Bundler 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.delete_prefix(version.release.to_s) if version.prerelease? - "#{version_prefix}#{segments[0..seg_end_index].join(".")}#{prerelease_suffix}" + "#{version_prefix}#{version.segments[0..seg_end_index].join(".")}#{prerelease_suffix}" end def version_prefix if @options[:strict] "= " - elsif @options[:optimistic] - ">= " - else + elsif @options[:pessimistic] "~> " + else + ">= " end end @@ -108,17 +107,17 @@ module Bundler end if d.groups != Array(:default) - group = d.groups.size == 1 ? ", :group => #{d.groups.first.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? - 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? + 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}#{path}#{git}#{github}#{branch}#{ref}#{glob}#{require_path}) end.join("\n") diff --git a/lib/bundler/inline.rb b/lib/bundler/inline.rb index f2f5b22cd3..a1b8e0475e 100644 --- a/lib/bundler/inline.rb +++ b/lib/bundler/inline.rb @@ -44,14 +44,16 @@ def gemfile(force_latest_compatible = false, options = {}, &gemfile) raise ArgumentError, "Unknown options: #{opts.keys.join(", ")}" unless opts.empty? old_gemfile = ENV["BUNDLE_GEMFILE"] + old_lockfile = ENV["BUNDLE_LOCKFILE"] Bundler.unbundle_env! 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" - Bundler::Plugin.gemfile_install(&gemfile) if Bundler.feature_flag.plugins? + Bundler::Plugin.gemfile_install(&gemfile) if Bundler.settings[:plugins] builder = Bundler::Dsl.new builder.instance_eval(&gemfile) @@ -60,14 +62,53 @@ def gemfile(force_latest_compatible = false, options = {}, &gemfile) definition.validate_runtime! if force_latest_compatible || definition.missing_specs? - 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}" + 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 + 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 @@ -82,6 +123,7 @@ def gemfile(force_latest_compatible = false, options = {}, &gemfile) 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 @@ -94,5 +136,11 @@ def gemfile(force_latest_compatible = false, options = {}, &gemfile) else ENV["BUNDLE_GEMFILE"] = "" end + + 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 c2da63c822..87d9a75627 100644 --- a/lib/bundler/installer.rb +++ b/lib/bundler/installer.rb @@ -7,12 +7,6 @@ require_relative "installer/gem_installer" module Bundler class Installer - class << self - attr_accessor :ambiguous_gems - - Installer.ambiguous_gems = [] - end - attr_reader :post_install_messages, :definition # Begins the installation process for Bundler. @@ -69,6 +63,11 @@ module Bundler Bundler.create_bundle_path ProcessLock.lock do + # 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? @@ -91,6 +90,11 @@ module Bundler 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| @@ -115,10 +119,6 @@ module Bundler ruby_command = Thor::Util.ruby_command ruby_command = ruby_command template_path = File.expand_path("templates/Executable", __dir__) - if spec.name == "bundler" - template_path += ".bundler" - spec.executables = %(bundle) - end template = File.read(template_path) exists = [] @@ -194,25 +194,17 @@ module Bundler standalone = options[:standalone] force = options[:force] local = options[:local] || options[:"prefer-local"] - jobs = installation_parallelization + 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 end - def installation_parallelization - if jobs = Bundler.settings[:jobs] - return jobs - end - - Bundler.settings.processor_count - end - def load_plugins Gem.load_plugins - requested_path_gems = @definition.requested_specs.select {|s| s.source.is_a?(Source::Path) } + 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 @@ -224,12 +216,13 @@ module Bundler end def ensure_specs_are_compatible! + overrides = @definition.overrides @definition.specs.each do |spec| - unless spec.matches_current_ruby? + 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 - unless spec.matches_current_rubygems? + 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 diff --git a/lib/bundler/installer/gem_installer.rb b/lib/bundler/installer/gem_installer.rb index 1da91857bd..f3b43c31ee 100644 --- a/lib/bundler/installer/gem_installer.rb +++ b/lib/bundler/installer/gem_installer.rb @@ -16,7 +16,6 @@ module Bundler def install_from_spec post_install_message = install Bundler.ui.debug "#{worker}: #{spec.name} (#{spec.version}) from #{spec.loaded_from}" - generate_executable_stubs [true, post_install_message] rescue Bundler::InstallHookError, Bundler::SecurityError, Bundler::APIResponseMismatchError, Bundler::InsecureInstallPathError raise @@ -26,6 +25,20 @@ module Bundler [false, specific_failure_message(e)] end + 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) @@ -71,15 +84,5 @@ module Bundler 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 d10e5ec924..fef326ed0a 100644 --- a/lib/bundler/installer/parallel_installer.rb +++ b/lib/bundler/installer/parallel_installer.rb @@ -6,7 +6,7 @@ require_relative "gem_installer" module Bundler class ParallelInstaller class SpecInstallation - attr_accessor :spec, :name, :full_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 @@ -24,6 +24,10 @@ module Bundler state == :enqueued end + def enqueue_with_priority? + state == :installable && spec.extensions.any? + end + def failed? state == :failed end @@ -32,29 +36,21 @@ module Bundler state == :none end - def has_post_install_message? - !post_install_message.empty? - end - - def ignorable_dependency?(dep) - dep.type == :development || dep.name == @name - end + def ready_to_install?(installed_specs) + return false unless state == :downloaded - # Checks installed dependencies against spec's dependencies to make - # sure needed dependencies have been installed. - def dependencies_installed?(installed_specs) - dependencies.all? {|d| installed_specs.include? d.name } + spec.extensions.none? || dependencies_installed?(installed_specs) end - # Represents only the non-development dependencies, the ones that are - # itself and are in the total list. - def dependencies - @dependencies ||= all_dependencies.reject {|dep| ignorable_dependency? dep } + def has_post_install_message? + !post_install_message.empty? end - # Represents all dependencies - def all_dependencies - @spec.dependencies + # 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 to_s @@ -75,6 +71,12 @@ module Bundler @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 @@ -84,6 +86,7 @@ module Bundler def call if @rake + do_download(@rake, 0) do_install(@rake, 0) Gem::Specification.reset end @@ -107,26 +110,54 @@ module Bundler end 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) + 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, @local + ) + + 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 ) @@ -147,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? @@ -185,18 +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 - installed_specs = {} + def enqueue_specs(installed_specs) @specs.each do |spec| - next unless spec.installed? - installed_specs[spec.name] = true - end - - @specs.each do |spec| - if spec.ready_to_enqueue? && spec.dependencies_installed?(installed_specs) - spec.state = :enqueued - worker_pool.enq 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/lazy_specification.rb b/lib/bundler/lazy_specification.rb index e3089f230f..0da621d21f 100644 --- a/lib/bundler/lazy_specification.rb +++ b/lib/bundler/lazy_specification.rb @@ -9,7 +9,7 @@ module Bundler include ForcePlatform attr_reader :name, :version, :platform, :materialization - attr_accessor :source, :remote, :force_ruby_platform, :dependencies, :required_ruby_version, :required_rubygems_version + attr_accessor :source, :remote, :force_ruby_platform, :dependencies, :required_ruby_version, :required_rubygems_version, :overrides # # For backwards compatibility with existing lockfiles, if the most specific @@ -30,10 +30,11 @@ module Bundler 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 = [] @@ -43,6 +44,7 @@ module Bundler @original_source = source @source = source + @materialization_options = materialization_options @force_ruby_platform = default_force_ruby_platform @most_specific_locked_platform = nil @@ -121,13 +123,10 @@ module Bundler out end - def materialize_strictly - source.local! - - matching_specs = source.specs.search(self) - return self if matching_specs.empty? + def materialize_for_cache + source.remote! - __materialize__(matching_specs) + materialize(self, &:first) end def materialized_for_installation @@ -140,53 +139,17 @@ module Bundler source.local! if use_exact_resolved_specifications? - materialize_strictly - else - matching_specs = source.specs.search([name, version]) - return self if matching_specs.empty? + spec = materialize(self) {|specs| choose_compatible(specs, fallback_to_non_installable: false) } + return spec if spec - target_platform = source.is_a?(Source::Path) ? platform : local_platform - - installable_candidates = GemHelpers.select_best_platform_match(matching_specs, target_platform) - - specification = __materialize__(installable_candidates, fallback_to_non_installable: false) - return specification unless specification.nil? - - if target_platform != platform - installable_candidates = GemHelpers.select_best_platform_match(matching_specs, platform) - end - - __materialize__(installable_candidates) - end - 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 - # lock file, 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 __materialize__(candidates, fallback_to_non_installable: Bundler.frozen_bundle?) - search = candidates.reverse.find do |spec| - spec.is_a?(StubSpecification) || spec.matches_current_metadata? - end - if search.nil? && fallback_to_non_installable - search = candidates.last - elsif search && search.full_name == full_name - # We don't validate locally installed dependencies but accept what's in - # the lockfile instead for performance, since loading locally installed - # dependencies would mean evaluating all gemspecs, which would affect - # `bundler/setup` performance - if search.is_a?(StubSpecification) - search.dependencies = dependencies - else - if !source.is_a?(Source::Path) && search.runtime_dependencies.sort != dependencies.sort - raise IncorrectLockfileDependencies.new(self) - end - - search.locked_platform = platform if search.instance_of?(RemoteSpecification) || search.instance_of?(EndpointSpecification) + # 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 + materialize([name, version]) {|specs| resolve_best_platform(specs) } end - search end def inspect @@ -206,16 +169,104 @@ module Bundler @force_ruby_platform = true end + def replace_source_with!(gemfile_source) + return unless gemfile_source.can_lock?(self) + + @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 + + # 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 = generic_local_platform == Gem::Platform::JAVA ? Gem::Platform::JAVA : Gem::Platform::RUBY + 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 + + def materialize(query) + matching_specs = source.specs.search(query) + return self if matching_specs.empty? + + 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 904552fa0c..2a3ad22480 100644 --- a/lib/bundler/lockfile_generator.rb +++ b/lib/bundler/lockfile_generator.rb @@ -71,7 +71,8 @@ module Bundler checksums = definition.resolve.map do |spec| spec.source.checksum_store.to_lock(spec) end - add_section("CHECKSUMS", checksums) + + add_section("CHECKSUMS", checksums + bundler_checksum) end def add_locked_ruby_version @@ -95,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 a0f75bae1f..852fc631f3 100644 --- a/lib/bundler/lockfile_parser.rb +++ b/lib/bundler/lockfile_parser.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true +require_relative "shared_helpers" + module Bundler class LockfileParser - include GemHelpers - class Position attr_reader :line, :column def initialize(line, column) @@ -28,6 +28,7 @@ module Bundler attr_reader( :sources, + :metadata_source, :dependencies, :specs, :platforms, @@ -94,24 +95,37 @@ module Bundler lockfile_contents.split(BUNDLED).last.strip end - def initialize(lockfile) + def initialize(lockfile, strict: false, lockfile_path: nil) @platforms = [] @sources = [] + @metadata_source = Source::Metadata.new @dependencies = {} @parse_method = nil @specs = {} - @lockfile_path = begin + @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 + @valid = lockfile.strip.empty? || + lockfile.split(/(?:\r?\n)+/).any? {|l| KNOWN_SECTIONS.include?(l) } + + 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)+)/) do |line| # split alternates between the line and the following whitespace next @pos.advance!(line) if line.match?(/^\s*$/) @@ -139,8 +153,13 @@ module Bundler 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| - platform_specificity_match(bundle_platform, local_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 @@ -156,6 +175,10 @@ module Bundler bundler_version.nil? || bundler_version < Gem::Version.new("1.16.2") end + def valid? + @valid + end + private TYPES = { @@ -239,7 +262,6 @@ module Bundler spaces = $1 return unless spaces.size == 2 checksums = $6 - return unless checksums name = $2 version = $3 platform = $4 @@ -247,12 +269,21 @@ module Bundler version = Gem::Version.new(version) platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY full_name = Gem::NameTuple.new(name, version, platform).full_name - return unless spec = @specs[full_name] + spec = @specs[full_name] - 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) + 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 @@ -268,7 +299,7 @@ module Bundler version = Gem::Version.new(version) platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY - @current_spec = LazySpecification.new(name, version, platform, @current_source) + @current_spec = LazySpecification.new(name, version, platform, @current_source, strict: @strict) @current_source.add_dependency_names(name) @specs[@current_spec.full_name] = @current_spec diff --git a/lib/bundler/man/bundle-add.1 b/lib/bundler/man/bundle-add.1 index 47e4e3482a..0956aa83f1 100644 --- a/lib/bundler/man/bundle-add.1 +++ b/lib/bundler/man/bundle-add.1 @@ -1,10 +1,10 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ADD" "1" "January 2025" "" +.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] [\-\-quiet] [\-\-skip\-install] [\-\-strict|\-\-optimistic] +\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" @@ -46,10 +46,16 @@ Do not print progress information to the standard output\. Adds the gem to the Gemfile but does not install it\. .TP \fB\-\-optimistic\fR -Adds optimistic declaration of version\. +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\. @@ -62,7 +68,7 @@ You can add the \fBrails\fR gem with version greater than 1\.1 (not including 1\ .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 +\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 diff --git a/lib/bundler/man/bundle-add.1.ronn b/lib/bundler/man/bundle-add.1.ronn index 48c0c66b09..8c65af0cc0 100644 --- a/lib/bundler/man/bundle-add.1.ronn +++ b/lib/bundler/man/bundle-add.1.ronn @@ -5,7 +5,7 @@ bundle-add(1) -- Add gem to the Gemfile and run bundle install `bundle add` <GEM_NAME> [--group=GROUP] [--version=VERSION] [--source=SOURCE] [--path=PATH] [--git=GIT|--github=GITHUB] [--branch=BRANCH] [--ref=REF] - [--quiet] [--skip-install] [--strict|--optimistic] + [--cooldown=NUMBER] [--quiet] [--skip-install] [--strict|--optimistic] ## DESCRIPTION @@ -51,11 +51,19 @@ Adds the named gem to the [`Gemfile(5)`][Gemfile(5)] and run `bundle install`. Adds the gem to the Gemfile but does not install it. * `--optimistic`: - Adds optimistic declaration of version. + 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. @@ -70,7 +78,7 @@ Adds the named gem to the [`Gemfile(5)`][Gemfile(5)] and run `bundle install`. 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"` + `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`. diff --git a/lib/bundler/man/bundle-binstubs.1 b/lib/bundler/man/bundle-binstubs.1 index f175eaa917..246daeae53 100644 --- a/lib/bundler/man/bundle-binstubs.1 +++ b/lib/bundler/man/bundle-binstubs.1 @@ -1,24 +1,21 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-BINSTUBS" "1" "January 2025" "" +.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] [\-\-path PATH] [\-\-standalone] [\-\-all\-platforms] +\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 \fB\-\-path\fR directory if one has been set\. Calling binstubs with [GEM [GEM]] will create binstubs for all given gems\. +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\-\-path[=PATH]\fR -The location to install the specified binstubs to\. This defaults to \fBbin\fR\. -.TP \fB\-\-standalone\fR Makes binstubs that can work without depending on Rubygems or Bundler at runtime\. .TP diff --git a/lib/bundler/man/bundle-binstubs.1.ronn b/lib/bundler/man/bundle-binstubs.1.ronn index ec2cfd80db..cbe5983f4d 100644 --- a/lib/bundler/man/bundle-binstubs.1.ronn +++ b/lib/bundler/man/bundle-binstubs.1.ronn @@ -3,7 +3,7 @@ bundle-binstubs(1) -- Install the binstubs of the listed gems ## SYNOPSIS -`bundle binstubs` <GEM_NAME> [--force] [--path PATH] [--standalone] [--all-platforms] +`bundle binstubs` <GEM_NAME> [--force] [--standalone] [--all-platforms] ## DESCRIPTION @@ -19,17 +19,15 @@ 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 `--path` directory if one has been set. -Calling binstubs with [GEM [GEM]] will create binstubs for all given gems. +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. -* `--path[=PATH]`: - The location to install the specified binstubs to. This defaults to `bin`. - * `--standalone`: Makes binstubs that can work without depending on Rubygems or Bundler at runtime. diff --git a/lib/bundler/man/bundle-cache.1 b/lib/bundler/man/bundle-cache.1 index c5ded3ad24..38ea047961 100644 --- a/lib/bundler/man/bundle-cache.1 +++ b/lib/bundler/man/bundle-cache.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CACHE" "1" "January 2025" "" +.TH "BUNDLE\-CACHE" "1" "May 2026" "" .SH "NAME" \fBbundle\-cache\fR \- Package your needed \fB\.gem\fR files into your application .SH "SYNOPSIS" @@ -11,9 +11,6 @@ alias: \fBpackage\fR, \fBpack\fR 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\fR -Include all sources (including path and git)\. -.TP \fB\-\-all\-platforms\fR Include gems for all platforms present in the lockfile, not only the current one\. .TP @@ -26,19 +23,10 @@ Use the specified gemfile instead of Gemfile\. \fB\-\-no\-install\fR Don't install the gems, only update the cache\. .TP -\fB\-\-no\-prune\fR -Don't remove stale gems from the cache\. -.TP -\fB\-\-path=PATH\fR -Specify a different path than the system default ($BUNDLE_PATH or $GEM_HOME)\. -.TP \fB\-\-quiet\fR Only output warnings and errors\. -.TP -\fB\-\-frozen\fR -Do not allow the Gemfile\.lock to be updated after this bundle cache operation's install\. .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 needs to be explicitly enabled via the \fB\-\-all\fR option\. Once used, the \fB\-\-all\fR option will be remembered\. +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" diff --git a/lib/bundler/man/bundle-cache.1.ronn b/lib/bundler/man/bundle-cache.1.ronn index ffcc07c7b1..51846c96b4 100644 --- a/lib/bundler/man/bundle-cache.1.ronn +++ b/lib/bundler/man/bundle-cache.1.ronn @@ -15,9 +15,6 @@ use the gems in the cache in preference to the ones on `rubygems.org`. ## OPTIONS -* `--all`: - Include all sources (including path and git). - * `--all-platforms`: Include gems for all platforms present in the lockfile, not only the current one. @@ -30,23 +27,13 @@ use the gems in the cache in preference to the ones on `rubygems.org`. * `--no-install`: Don't install the gems, only update the cache. -* `--no-prune`: - Don't remove stale gems from the cache. - -* `--path=PATH`: - Specify a different path than the system default ($BUNDLE_PATH or $GEM_HOME). - * `--quiet`: Only output warnings and errors. -* `--frozen`: - Do not allow the Gemfile.lock to be updated after this bundle cache operation's install. - ## GIT AND PATH GEMS The `bundle cache` command can also package `:git` and `:path` dependencies -besides .gem files. This needs to be explicitly enabled via the `--all` option. -Once used, the `--all` option will be remembered. +besides .gem files. This can be disabled setting `cache_all` to false. ## SUPPORT FOR MULTIPLE PLATFORMS diff --git a/lib/bundler/man/bundle-check.1 b/lib/bundler/man/bundle-check.1 index f30afeda46..6cd474d90a 100644 --- a/lib/bundler/man/bundle-check.1 +++ b/lib/bundler/man/bundle-check.1 @@ -1,10 +1,10 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CHECK" "1" "January 2025" "" +.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] [\-\-path=PATH] +\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 @@ -18,7 +18,4 @@ 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)]\. -.TP -\fB\-\-path=PATH\fR -Specify a different path than the system default (\fB$BUNDLE_PATH\fR or \fB$GEM_HOME\fR)\. Bundler will remember this value for future installs on this machine\. diff --git a/lib/bundler/man/bundle-check.1.ronn b/lib/bundler/man/bundle-check.1.ronn index 23bb6f3eb8..92589159c9 100644 --- a/lib/bundler/man/bundle-check.1.ronn +++ b/lib/bundler/man/bundle-check.1.ronn @@ -5,7 +5,6 @@ bundle-check(1) -- Verifies if dependencies are satisfied by installed gems `bundle check` [--dry-run] [--gemfile=FILE] - [--path=PATH] ## DESCRIPTION @@ -25,7 +24,3 @@ installed on the local machine, if they satisfy the requirements. * `--gemfile=GEMFILE`: Use the specified gemfile instead of the [`Gemfile(5)`][Gemfile(5)]. - -* `--path=PATH`: - Specify a different path than the system default (`$BUNDLE_PATH` or `$GEM_HOME`). - Bundler will remember this value for future installs on this machine. diff --git a/lib/bundler/man/bundle-clean.1 b/lib/bundler/man/bundle-clean.1 index e7216d3e15..eb90636c17 100644 --- a/lib/bundler/man/bundle-clean.1 +++ b/lib/bundler/man/bundle-clean.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CLEAN" "1" "January 2025" "" +.TH "BUNDLE\-CLEAN" "1" "May 2026" "" .SH "NAME" \fBbundle\-clean\fR \- Cleans up unused gems in your bundler directory .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index fb45e631c5..c055c8a415 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -1,16 +1,16 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CONFIG" "1" "January 2025" "" +.TH "BUNDLE\-CONFIG" "1" "May 2026" "" .SH "NAME" \fBbundle\-config\fR \- Set bundler configuration options .SH "SYNOPSIS" -\fBbundle config\fR list +\fBbundle config\fR [list] .br -\fBbundle config\fR [get] NAME +\fBbundle config\fR [get [\-\-local|\-\-global]] NAME .br -\fBbundle config\fR [set] NAME VALUE +\fBbundle config\fR [set [\-\-local|\-\-global]] NAME VALUE .br -\fBbundle config\fR unset NAME +\fBbundle config\fR unset [\-\-local|\-\-global] NAME .SH "DESCRIPTION" This command allows you to interact with Bundler's configuration system\. .P @@ -25,65 +25,40 @@ Global config (\fB~/\.bundle/config\fR) 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 -Executing \fBbundle config get <name>\fR will print the value of that configuration setting, and where it was set\. -.P -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\. See \fB\-\-local\fR and \fB\-\-global\fR options below\. +\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\. -.P +.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\. -.P +.SS "unset" Executing \fBbundle config unset <name>\fR will delete the configuration in both local and global sources\. .P -Executing \fBbundle config unset \-\-global <name>\fR will delete the configuration only from the user configuration\. -.P -Executing \fBbundle config unset \-\-local <name>\fR will delete the configuration only from the local application\. -.P -Executing bundle with the \fBBUNDLE_IGNORE_CONFIG\fR environment variable set will cause it to ignore all configuration\. -.SH "REMEMBERING OPTIONS" -Flags passed to \fBbundle install\fR or the Bundler runtime, such as \fB\-\-path foo\fR or \fB\-\-without production\fR, are remembered between commands and saved to your local application's configuration (normally, \fB\./\.bundle/config\fR)\. -.P -However, this will be changed in bundler 3, so it's better not to rely on this behavior\. If these options must be remembered, it's better to set them using \fBbundle config\fR (e\.g\., \fBbundle config set \-\-local path foo\fR)\. -.P -The options that can be configured are: -.TP -\fBbin\fR -Creates a directory (defaults to \fB~/bin\fR) and place any executables from the gem there\. 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, this flag will create a \fBbin/rails\fR executable that ensures that all referred dependencies will be resolved using the bundled gems\. -.TP -\fBdeployment\fR -In deployment mode, Bundler will 'roll\-out' the bundle for \fBproduction\fR use\. Please check carefully if you want to have this option enabled in \fBdevelopment\fR or \fBtest\fR environments\. +\fBOPTIONS\fR .TP -\fBonly\fR -A space\-separated list of groups to install only gems of the specified groups\. -.TP -\fBpath\fR -The location to install the specified gems to\. This defaults to Rubygems' setting\. Bundler shares this location with Rubygems, \fBgem install \|\.\|\.\|\.\fR will have gem installed there, too\. Therefore, gems installed without a \fB\-\-path \|\.\|\.\|\.\fR setting will show up by calling \fBgem list\fR\. Accordingly, gems installed to other locations will not get listed\. -.TP -\fBwithout\fR -A space\-separated list of groups referencing gems to skip during installation\. +\fB\-\-local\fR +Executing \fBbundle config unset \-\-local <name>\fR will delete the configuration only from the local application\. .TP -\fBwith\fR -A space\-separated list of \fBoptional\fR groups referencing gems to include during installation\. -.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\. +\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 @@ -95,27 +70,40 @@ Any periods in the configuration keys must be replaced with two underscores when .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 -\fBallow_offline_install\fR (\fBBUNDLE_ALLOW_OFFLINE_INSTALL\fR): Allow Bundler to use cached data when installing without network access\. -.IP "\(bu" 4 -\fBauto_clean_without_path\fR (\fBBUNDLE_AUTO_CLEAN_WITHOUT_PATH\fR): Automatically run \fBbundle clean\fR after installing when an explicit \fBpath\fR has not been set and Bundler is not installing into the system gems\. +\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): Install executables from gems in the bundle to the specified directory\. Defaults to \fBfalse\fR\. +\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 configured on bundler 1 and bundler 2, but will be the default on bundler 3\. +\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\. +\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 -\fBdefault_install_uses_path\fR (\fBBUNDLE_DEFAULT_INSTALL_USES_PATH\fR): Whether a \fBbundle install\fR without an explicit \fB\-\-path\fR argument defaults to installing gems in \fB\.bundle\fR\. +\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): Disallow changes to the \fBGemfile\fR\. When the \fBGemfile\fR is changed and the lockfile has not been updated, running Bundler commands will be blocked\. +\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 @@ -131,15 +119,15 @@ The following is a list of all configuration keys and their purpose\. You can le .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 changes to the \fBGemfile\fR\. When the \fBGemfile\fR is changed and the lockfile has not been updated, running Bundler commands will be blocked\. Defaults to \fBtrue\fR when \fB\-\-deployment\fR is used\. +\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 \fBREADME\fR file 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\. +\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 globally, rather than locally to the installing Ruby installation\. +\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 @@ -147,40 +135,42 @@ The following is a list of all configuration keys and their purpose\. You can le .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 install in parallel\. Defaults to the number of available processors\. +\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 -\fBlockfile_checksums\fR (\fBBUNDLE_LOCKFILE_CHECKSUMS\fR): Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources\. +\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\. +\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\. Defaults to \fBGem\.dir\fR\. When \-\-deployment is used, defaults to vendor/bundle\. +\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 -\fBpath_relative_to_cwd\fR (\fBBUNDLE_PATH_RELATIVE_TO_CWD\fR) Makes \fB\-\-path\fR relative to the CWD instead of the \fBGemfile\fR\. -.IP "\(bu" 4 \fBplugins\fR (\fBBUNDLE_PLUGINS\fR): Enable Bundler's experimental plugin system\. .IP "\(bu" 4 -\fBprefer_patch\fR (BUNDLE_PREFER_PATCH): Prefer updating only to next patch version during updates\. Makes \fBbundle update\fR calls equivalent to \fBbundler update \-\-patch\fR\. -.IP "\(bu" 4 -\fBprint_only_version_number\fR (\fBBUNDLE_PRINT_ONLY_VERSION_NUMBER\fR): Print only version number from \fBbundler \-\-version\fR\. +\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 -\fBsetup_makes_kernel_gem_public\fR (\fBBUNDLE_SETUP_MAKES_KERNEL_GEM_PUBLIC\fR): Have \fBBundler\.setup\fR make the \fBKernel#gem\fR method public, even though RubyGems declares it as private\. -.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\. @@ -195,12 +185,32 @@ The following is a list of all configuration keys and their purpose\. You can le .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 \fB:\fR\-separated list of groups whose gems bundler should install\. +\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 \fB:\fR\-separated list of groups whose gems bundler should not install\. +\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 @@ -209,7 +219,16 @@ bundle config set \-\-local local\.GEM_NAME /path/to/local/git/repository .fi .IP "" 0 .P -For example, in order to use a local Rack repository, a developer could call: +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 @@ -221,6 +240,13 @@ Now instead of checking out the remote git repository, the local override will b 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 @@ -276,7 +302,7 @@ export BUNDLE_GEMS__LONGEROUS__COM="claudette:s00pers3krit" 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/rubygems/rubygems\.git username:password +bundle config set \-\-global https://github\.com/ruby/rubygems\.git username:password .fi .IP "" 0 .P diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn index 00e2081959..72f891b428 100644 --- a/lib/bundler/man/bundle-config.1.ronn +++ b/lib/bundler/man/bundle-config.1.ronn @@ -3,10 +3,10 @@ bundle-config(1) -- Set bundler configuration options ## SYNOPSIS -`bundle config` list<br> -`bundle config` [get] NAME<br> -`bundle config` [set] NAME VALUE<br> -`bundle config` unset NAME +`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 @@ -19,98 +19,67 @@ Bundler loads configuration settings in this order: 3. Global config (`~/.bundle/config`) 4. Bundler default config -Executing `bundle config list` will print a list of all bundler -configuration for the current bundle, and where that configuration -was set. - -Executing `bundle config get <name>` will print the value of that configuration -setting, and where it was 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. See `--local` and `--global` options below. - -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`. - -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. - -Executing `bundle config unset <name>` will delete the configuration in both -local and global sources. - -Executing `bundle config unset --global <name>` will delete the configuration -only from the user configuration. - -Executing `bundle config unset --local <name>` will delete the configuration -only from the local application. - -Executing bundle with the `BUNDLE_IGNORE_CONFIG` environment variable set will +Executing `bundle` with the `BUNDLE_IGNORE_CONFIG` environment variable set will cause it to ignore all configuration. -## REMEMBERING OPTIONS +## SUB-COMMANDS -Flags passed to `bundle install` or the Bundler runtime, such as `--path foo` or -`--without production`, are remembered between commands and saved to your local -application's configuration (normally, `./.bundle/config`). +### list (default command) -However, this will be changed in bundler 3, so it's better not to rely on this -behavior. If these options must be remembered, it's better to set them using -`bundle config` (e.g., `bundle config set --local path foo`). +Executing `bundle config list` will print a list of all bundler +configuration for the current bundle, and where that configuration +was set. -The options that can be configured are: +### get -* `bin`: - Creates a directory (defaults to `~/bin`) and place any executables from the - gem there. 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, this flag will create a - `bin/rails` executable that ensures that all referred dependencies will be - resolved using the bundled gems. +Executing `bundle config get <name>` will print the value of that configuration +setting, and all locations where it was set. -* `deployment`: - In deployment mode, Bundler will 'roll-out' the bundle for - `production` use. Please check carefully if you want to have this option - enabled in `development` or `test` environments. +**OPTIONS** -* `only`: - A space-separated list of groups to install only gems of the specified groups. +* `--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. -* `path`: - The location to install the specified gems to. This defaults to Rubygems' - setting. Bundler shares this location with Rubygems, `gem install ...` will - have gem installed there, too. Therefore, gems installed without a - `--path ...` setting will show up by calling `gem list`. Accordingly, gems - installed to other locations will not get listed. +* `--global`: + Get configuration from configuration file global to all bundles executed as + the current user, namely, from `~/.bundle/config`. -* `without`: - A space-separated list of groups referencing gems to skip during installation. +### set -* `with`: - A space-separated list of **optional** groups referencing gems to include during installation. +Executing `bundle config set <name> <value>` defaults to setting `local` +configuration if executing from within a local application, otherwise it will +set `global` configuration. -## BUILD OPTIONS +**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. +* `--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`. -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. +* `--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. - gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config +### unset -Since the specific location of that executable can change from machine -to machine, you can specify these flags on a per-machine basis. +Executing `bundle config unset <name>` will delete the configuration in both +local and global sources. - bundle config set --global build.mysql --with-mysql-config=/usr/local/mysql/bin/mysql_config +**OPTIONS** -After running this command, every time bundler needs to install the -`mysql` gem, it will pass along the flags you specified. +* `--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 @@ -137,19 +106,25 @@ the environment variable `BUNDLE_LOCAL__RACK`. 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). -* `allow_offline_install` (`BUNDLE_ALLOW_OFFLINE_INSTALL`): - Allow Bundler to use cached data when installing without network access. -* `auto_clean_without_path` (`BUNDLE_AUTO_CLEAN_WITHOUT_PATH`): - Automatically run `bundle clean` after installing when an explicit `path` - has not been set and Bundler is not installing into the system gems. +* `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`): - Install executables from gems in the bundle to the specified directory. - Defaults to `false`. + 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 - configured on bundler 1 and bundler 2, but will be the default on bundler 3. + 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`): @@ -158,15 +133,46 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). Defaults to `vendor/cache`. * `clean` (`BUNDLE_CLEAN`): Whether Bundler should run `bundle clean` automatically after - `bundle install`. + `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`. -* `default_install_uses_path` (`BUNDLE_DEFAULT_INSTALL_USES_PATH`): - Whether a `bundle install` without an explicit `--path` argument defaults - to installing gems in `.bundle`. +* `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`): - Disallow changes to the `Gemfile`. When the `Gemfile` is changed and the - lockfile has not been updated, running Bundler commands will be blocked. + 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. @@ -188,12 +194,13 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). 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 changes to the `Gemfile`. When the `Gemfile` is changed and the - lockfile has not been updated, running Bundler commands will be blocked. - Defaults to `true` when `--deployment` is used. + 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 `README` file when you - create a new gem via `bundle gem` command. It can be overridden by passing an + 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` @@ -205,8 +212,8 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). 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 globally, rather than locally to the - installing Ruby installation. + 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`): @@ -215,38 +222,51 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). * `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 install in parallel. Defaults to the number of - available processors. + 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. + 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`. Defaults to `Gem.dir`. When --deployment - is used, defaults to vendor/bundle. + 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`). -* `path_relative_to_cwd` (`BUNDLE_PATH_RELATIVE_TO_CWD`) - Makes `--path` relative to the CWD instead of the `Gemfile`. * `plugins` (`BUNDLE_PLUGINS`): Enable Bundler's experimental plugin system. -* `prefer_patch` (BUNDLE_PREFER_PATCH): +* `prefer_patch` (`BUNDLE_PREFER_PATCH`): Prefer updating only to next patch version during updates. Makes `bundle update` calls equivalent to `bundler update --patch`. -* `print_only_version_number` (`BUNDLE_PRINT_ONLY_VERSION_NUMBER`): - Print only version number from `bundler --version`. * `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`. -* `setup_makes_kernel_gem_public` (`BUNDLE_SETUP_MAKES_KERNEL_GEM_PUBLIC`): - Have `Bundler.setup` make the `Kernel#gem` method public, even though - RubyGems declares it as private. * `shebang` (`BUNDLE_SHEBANG`): The program name that should be invoked for generated binstubs. Defaults to the ruby install name used to generate the binstub. @@ -255,6 +275,10 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). 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. @@ -273,6 +297,9 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). 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`. @@ -280,9 +307,28 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). `system` will use the system version of Bundler, and `x.y.z` will use the specified version of Bundler. * `with` (`BUNDLE_WITH`): - A `:`-separated list of groups whose gems bundler should install. + A space-separated or `:`-separated list of groups whose gems bundler should install. * `without` (`BUNDLE_WITHOUT`): - A `:`-separated list of groups whose gems bundler should not install. + 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 @@ -292,7 +338,16 @@ up a local override: bundle config set --local local.GEM_NAME /path/to/local/git/repository -For example, in order to use a local Rack repository, a developer could call: +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 @@ -318,6 +373,11 @@ 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 @@ -361,7 +421,7 @@ Or you can set the credentials as an environment variable like this: For gems with a git source with HTTP(S) URL you can specify credentials like so: - bundle config set --global https://github.com/rubygems/rubygems.git username:password + bundle config set --global https://github.com/ruby/rubygems.git username:password Or you can set the credentials as an environment variable like so: diff --git a/lib/bundler/man/bundle-console.1 b/lib/bundler/man/bundle-console.1 index 287c976c2b..5d3f65365f 100644 --- a/lib/bundler/man/bundle-console.1 +++ b/lib/bundler/man/bundle-console.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CONSOLE" "1" "January 2025" "" +.TH "BUNDLE\-CONSOLE" "1" "May 2026" "" .SH "NAME" \fBbundle\-console\fR \- Open an IRB session with the bundle pre\-loaded .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-doctor.1 b/lib/bundler/man/bundle-doctor.1 index ca385c3b86..4c59871b66 100644 --- a/lib/bundler/man/bundle-doctor.1 +++ b/lib/bundler/man/bundle-doctor.1 @@ -1,14 +1,21 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-DOCTOR" "1" "January 2025" "" +.TH "BUNDLE\-DOCTOR" "1" "May 2026" "" .SH "NAME" \fBbundle\-doctor\fR \- Checks the bundle for common problems .SH "SYNOPSIS" -\fBbundle doctor\fR [\-\-quiet] [\-\-gemfile=GEMFILE] +\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 by bundle\-doctor include: +Examples of common problems caught include: .IP "\(bu" 4 Invalid Bundler settings .IP "\(bu" 4 @@ -20,11 +27,43 @@ Uninstalled gems .IP "\(bu" 4 Missing dependencies .IP "" 0 -.SH "OPTIONS" +.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 index 5970f6188b..7495099ff5 100644 --- a/lib/bundler/man/bundle-doctor.1.ronn +++ b/lib/bundler/man/bundle-doctor.1.ronn @@ -3,16 +3,27 @@ bundle-doctor(1) -- Checks the bundle for common problems ## SYNOPSIS -`bundle doctor` [--quiet] - [--gemfile=GEMFILE] +`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 by bundle-doctor include: +Examples of common problems caught include: * Invalid Bundler settings * Mismatched Ruby versions @@ -20,7 +31,7 @@ Examples of common problems caught by bundle-doctor include: * Uninstalled gems * Missing dependencies -## OPTIONS +**OPTIONS** * `--quiet`: Only output warnings and errors. @@ -31,3 +42,36 @@ Examples of common problems caught by bundle-doctor include: 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 index 45f97c9283..25fcb64891 100644 --- a/lib/bundler/man/bundle-env.1 +++ b/lib/bundler/man/bundle-env.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ENV" "1" "January 2025" "" +.TH "BUNDLE\-ENV" "1" "May 2026" "" .SH "NAME" \fBbundle\-env\fR \- Print information about the environment Bundler is running under .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-exec.1 b/lib/bundler/man/bundle-exec.1 index fa02c4234c..c3a6a09d57 100644 --- a/lib/bundler/man/bundle-exec.1 +++ b/lib/bundler/man/bundle-exec.1 @@ -1,10 +1,10 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-EXEC" "1" "January 2025" "" +.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 [\-\-keep\-file\-descriptors] [\-\-gemfile=GEMFILE] \fIcommand\fR +\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 @@ -13,9 +13,6 @@ Essentially, if you would normally have run something like \fBrspec spec/my_spec Note that \fBbundle exec\fR does not require that an executable is available on your shell's \fB$PATH\fR\. .SH "OPTIONS" .TP -\fB\-\-keep\-file\-descriptors\fR -Passes all file descriptors to the new processes\. Default is true from bundler version 2\.2\.26\. Setting it to false is now deprecated\. -.TP \fB\-\-gemfile=GEMFILE\fR Use the specified gemfile instead of [\fBGemfile(5)\fR][Gemfile(5)]\. .SH "BUNDLE INSTALL \-\-BINSTUBS" @@ -74,8 +71,8 @@ end Bundler provides convenience helpers that wrap \fBsystem\fR and \fBexec\fR, and they can be used like this: .IP "" 4 .nf -Bundler\.clean_system('brew install wget') -Bundler\.clean_exec('brew install wget') +Bundler\.unbundled_system('brew install wget') +Bundler\.unbundled_exec('brew install wget') .fi .IP "" 0 .SH "RUBYGEMS PLUGINS" diff --git a/lib/bundler/man/bundle-exec.1.ronn b/lib/bundler/man/bundle-exec.1.ronn index 8da1e2348b..e51a66a084 100644 --- a/lib/bundler/man/bundle-exec.1.ronn +++ b/lib/bundler/man/bundle-exec.1.ronn @@ -3,7 +3,7 @@ bundle-exec(1) -- Execute a command in the context of the bundle ## SYNOPSIS -`bundle exec` [--keep-file-descriptors] [--gemfile=GEMFILE] <command> +`bundle exec` [--gemfile=GEMFILE] <command> ## DESCRIPTION @@ -20,10 +20,6 @@ available on your shell's `$PATH`. ## OPTIONS -* `--keep-file-descriptors`: - Passes all file descriptors to the new processes. Default is true from - bundler version 2.2.26. Setting it to false is now deprecated. - * `--gemfile=GEMFILE`: Use the specified gemfile instead of [`Gemfile(5)`][Gemfile(5)]. @@ -108,8 +104,8 @@ need to use `with_unbundled_env`. Bundler provides convenience helpers that wrap `system` and `exec`, and they can be used like this: - Bundler.clean_system('brew install wget') - Bundler.clean_exec('brew install wget') + Bundler.unbundled_system('brew install wget') + Bundler.unbundled_exec('brew install wget') ## RUBYGEMS PLUGINS diff --git a/lib/bundler/man/bundle-fund.1 b/lib/bundler/man/bundle-fund.1 index fd6f8f4ac1..caee1f81dd 100644 --- a/lib/bundler/man/bundle-fund.1 +++ b/lib/bundler/man/bundle-fund.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-FUND" "1" "January 2025" "" +.TH "BUNDLE\-FUND" "1" "May 2026" "" .SH "NAME" \fBbundle\-fund\fR \- Lists information about gems seeking funding assistance .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-gem.1 b/lib/bundler/man/bundle-gem.1 index 7840fdda23..87d7568246 100644 --- a/lib/bundler/man/bundle-gem.1 +++ b/lib/bundler/man/bundle-gem.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-GEM" "1" "January 2025" "" +.TH "BUNDLE\-GEM" "1" "May 2026" "" .SH "NAME" \fBbundle\-gem\fR \- Generate a project skeleton for creating a rubygem .SH "SYNOPSIS" @@ -19,67 +19,87 @@ The generated project skeleton can be customized with OPTIONS, as explained belo \fBgem\.test\fR .IP "" 0 .SH "OPTIONS" -.IP "\(bu" 4 -\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\. -.IP "\(bu" 4 -\fB\-\-no\-exe\fR: Do not create a binary (overrides \fB\-\-exe\fR specified in the global config)\. -.IP "\(bu" 4 -\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\. -.IP "\(bu" 4 -\fB\-\-no\-coc\fR: Do not create a \fBCODE_OF_CONDUCT\.md\fR (overrides \fB\-\-coc\fR specified in the global config)\. -.IP "\(bu" 4 -\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\. -.IP "\(bu" 4 -\fB\-\-no\-changelog\fR: Do not create a \fBCHANGELOG\.md\fR (overrides \fB\-\-changelog\fR specified in the global config)\. -.IP "\(bu" 4 -\fB\-\-ext=c\fR, \fB\-\-ext=rust\fR: Add boilerplate for C or Rust (currently magnus \fIhttps://docs\.rs/magnus\fR based) extension code to the generated project\. This behavior is disabled by default\. -.IP "\(bu" 4 -\fB\-\-no\-ext\fR: Do not add extension code (overrides \fB\-\-ext\fR specified in the global config)\. -.IP "\(bu" 4 -\fB\-\-git\fR: Initialize a git repo inside your library\. -.IP "\(bu" 4 -\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\. -.IP "\(bu" 4 -\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\. -.IP "\(bu" 4 -\fB\-\-no\-mit\fR: Do not create a \fBLICENSE\.txt\fR (overrides \fB\-\-mit\fR specified in the global config)\. -.IP "\(bu" 4 -\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: +.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\. -.IP "\(bu" 4 -\fB\-\-no\-test\fR: Do not use a test framework (overrides \fB\-\-test\fR specified in the global config)\. -.IP "\(bu" 4 -\fB\-\-changelog\fR: Generate changelog file\. Set a default with \fBbundle config set \-\-global gem\.changelog true\fR\. -.IP "\(bu" 4 -\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: +.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\. -.IP "\(bu" 4 -\fB\-\-no\-ci\fR: Do not use a continuous integration service (overrides \fB\-\-ci\fR specified in the global config)\. -.IP "\(bu" 4 -\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: +.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\. -.IP "\(bu" 4 -\fB\-\-no\-linter\fR: Do not add a linter (overrides \fB\-\-linter\fR specified in the global config)\. -.IP "\(bu" 4 -\fB\-\-rubocop\fR: Add rubocop to the generated Rakefile and gemspec\. Set a default with \fBbundle config set \-\-global gem\.rubocop true\fR\. -.IP "\(bu" 4 -\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\. -.IP "" 0 +.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 diff --git a/lib/bundler/man/bundle-gem.1.ronn b/lib/bundler/man/bundle-gem.1.ronn index 13dc55c310..488c8113e4 100644 --- a/lib/bundler/man/bundle-gem.1.ronn +++ b/lib/bundler/man/bundle-gem.1.ronn @@ -41,17 +41,18 @@ configuration file using the following names: Do not create a `CODE_OF_CONDUCT.md` (overrides `--coc` specified in the global config). -* `--changelog` +* `--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=rust`: - Add boilerplate for C or Rust (currently [magnus](https://docs.rs/magnus) based) extension code to the generated project. This behavior +* `--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`: @@ -95,9 +96,6 @@ configuration file using the following names: Do not use a test framework (overrides `--test` specified in the global config). -* `--changelog`: - Generate changelog file. Set a default with `bundle config set --global gem.changelog true`. - * `--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` @@ -137,13 +135,16 @@ configuration file using the following names: * `--no-linter`: Do not add a linter (overrides `--linter` specified in the global config). -* `--rubocop`: - Add rubocop to the generated Rakefile and gemspec. Set a default with `bundle config set --global gem.rubocop true`. - * `--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 index 2ac1c26997..3bcfd047e5 100644 --- a/lib/bundler/man/bundle-help.1 +++ b/lib/bundler/man/bundle-help.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-HELP" "1" "January 2025" "" +.TH "BUNDLE\-HELP" "1" "May 2026" "" .SH "NAME" \fBbundle\-help\fR \- Displays detailed help for each subcommand .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-info.1 b/lib/bundler/man/bundle-info.1 index 22a2bae2bd..49c2295f8c 100644 --- a/lib/bundler/man/bundle-info.1 +++ b/lib/bundler/man/bundle-info.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INFO" "1" "January 2025" "" +.TH "BUNDLE\-INFO" "1" "May 2026" "" .SH "NAME" \fBbundle\-info\fR \- Show information for the given gem in your bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-init.1 b/lib/bundler/man/bundle-init.1 index 4d59572d05..63e2376c3f 100644 --- a/lib/bundler/man/bundle-init.1 +++ b/lib/bundler/man/bundle-init.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INIT" "1" "January 2025" "" +.TH "BUNDLE\-INIT" "1" "May 2026" "" .SH "NAME" \fBbundle\-init\fR \- Generates a Gemfile into the current working directory .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-inject.1 b/lib/bundler/man/bundle-inject.1 deleted file mode 100644 index ab9d5e83f6..0000000000 --- a/lib/bundler/man/bundle-inject.1 +++ /dev/null @@ -1,31 +0,0 @@ -.\" generated with Ronn-NG/v0.10.1 -.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INJECT" "1" "January 2025" "" -.SH "NAME" -\fBbundle\-inject\fR \- Add named gem(s) with version requirements to Gemfile -.SH "SYNOPSIS" -\fBbundle inject\fR [GEM] [VERSION] [\-\-source=SOURCE] [\-\-group=GROUP] -.SH "DESCRIPTION" -Adds the named gem(s) with their version requirements to the resolved [\fBGemfile(5)\fR][Gemfile(5)]\. -.P -This command will add the gem to both your [\fBGemfile(5)\fR][Gemfile(5)] and Gemfile\.lock if it isn't listed yet\. -.P -Example: -.IP "" 4 -.nf -bundle install -bundle inject 'rack' '> 0' -.fi -.IP "" 0 -.P -This will inject the 'rack' gem with a version greater than 0 in your [\fBGemfile(5)\fR][Gemfile(5)] and Gemfile\.lock\. -.P -The \fBbundle inject\fR command was deprecated in Bundler 2\.1 and will be removed in Bundler 3\.0\. -.SH "OPTIONS" -.TP -\fB\-\-source=SOURCE\fR -Install gem from the given source\. -.TP -\fB\-\-group=GROUP\fR -Install gem into a bundler group\. - diff --git a/lib/bundler/man/bundle-inject.1.ronn b/lib/bundler/man/bundle-inject.1.ronn deleted file mode 100644 index e2a911b6d8..0000000000 --- a/lib/bundler/man/bundle-inject.1.ronn +++ /dev/null @@ -1,32 +0,0 @@ -bundle-inject(1) -- Add named gem(s) with version requirements to Gemfile -========================================================================= - -## SYNOPSIS - -`bundle inject` [GEM] [VERSION] [--source=SOURCE] [--group=GROUP] - -## DESCRIPTION - -Adds the named gem(s) with their version requirements to the resolved -[`Gemfile(5)`][Gemfile(5)]. - -This command will add the gem to both your [`Gemfile(5)`][Gemfile(5)] and Gemfile.lock if it -isn't listed yet. - -Example: - - bundle install - bundle inject 'rack' '> 0' - -This will inject the 'rack' gem with a version greater than 0 in your -[`Gemfile(5)`][Gemfile(5)] and Gemfile.lock. - -The `bundle inject` command was deprecated in Bundler 2.1 and will be removed in Bundler 3.0. - -## OPTIONS - -* `--source=SOURCE`: - Install gem from the given source. - -* `--group=GROUP`: - Install gem into a bundler group. diff --git a/lib/bundler/man/bundle-install.1 b/lib/bundler/man/bundle-install.1 index efec8b8d4f..801768c7ec 100644 --- a/lib/bundler/man/bundle-install.1 +++ b/lib/bundler/man/bundle-install.1 @@ -1,10 +1,10 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INSTALL" "1" "January 2025" "" +.TH "BUNDLE\-INSTALL" "1" "May 2026" "" .SH "NAME" \fBbundle\-install\fR \- Install the dependencies specified in your Gemfile .SH "SYNOPSIS" -\fBbundle install\fR [\-\-binstubs[=DIRECTORY]] [\-\-clean] [\-\-deployment] [\-\-frozen] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-local] [\-\-no\-cache] [\-\-no\-prune] [\-\-path PATH] [\-\-prefer\-local] [\-\-quiet] [\-\-redownload] [\-\-retry=NUMBER] [\-\-shebang=SHEBANG] [\-\-standalone[=GROUP[ GROUP\|\.\|\.\|\.]]] [\-\-system] [\-\-trust\-policy=TRUST\-POLICY] [\-\-target\-rbconfig=TARGET\-RBCONFIG] [\-\-with=GROUP[ GROUP\|\.\|\.\|\.]] [\-\-without=GROUP[ GROUP\|\.\|\.\|\.]] +\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 @@ -12,30 +12,12 @@ If a \fBGemfile\.lock\fR does exist, and you have not updated your Gemfile(5), B .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" -The \fB\-\-clean\fR, \fB\-\-deployment\fR, \fB\-\-frozen\fR, \fB\-\-no\-prune\fR, \fB\-\-path\fR, \fB\-\-shebang\fR, \fB\-\-system\fR, \fB\-\-without\fR and \fB\-\-with\fR options are deprecated because they only make sense if they are applied to every subsequent \fBbundle install\fR run automatically and that requires \fBbundler\fR to silently remember them\. Since \fBbundler\fR will no longer remember CLI flags in future versions, \fBbundle config\fR (see bundle\-config(1)) should be used to apply them permanently\. .TP -\fB\-\-binstubs[=BINSTUBS]\fR -Binstubs are scripts that wrap around executables\. Bundler creates a small Ruby file (a binstub) that loads Bundler, runs the command, and puts it in \fBbin/\fR\. This lets you link the binstub inside of an application to the exact gem version the application needs\. -.IP -Creates a directory (defaults to \fB~/bin\fR when the option is used without a value, or to the given \fB<BINSTUBS>\fR directory otherwise) and places any executables from the gem there\. 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, this flag will create a \fBbin/rails\fR executable that ensures that all referred dependencies will be resolved using the bundled gems\. -.TP -\fB\-\-clean\fR -On finishing the installation Bundler is going to remove any gems not present in the current Gemfile(5)\. Don't worry, gems currently in use will not be removed\. -.IP -This option is deprecated in favor of the \fBclean\fR setting\. -.TP -\fB\-\-deployment\fR -In \fIdeployment mode\fR, Bundler will 'roll\-out' the bundle for production or CI use\. Please check carefully if you want to have this option enabled in your development environment\. -.IP -This option is deprecated in favor of the \fBdeployment\fR setting\. +\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\-\-redownload\fR, \fB\-\-force\fR -Force download every gem, even if the required versions are already available locally\. -.TP -\fB\-\-frozen\fR -Do not allow the Gemfile\.lock to be updated after this install\. Exits non\-zero if there are going to be changes to the Gemfile\.lock\. -.IP -This option is deprecated in favor of the \fBfrozen\fR setting\. +\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\. @@ -49,21 +31,19 @@ The maximum number of parallel download and install jobs\. The default is the nu \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\-prune\fR -Don't remove stale gems from the cache when the installation finishes\. -.IP -This option is deprecated in favor of the \fBno_prune\fR setting\. -.TP -\fB\-\-path=PATH\fR -The location to install the specified gems to\. This defaults to Rubygems' setting\. Bundler shares this location with Rubygems, \fBgem install \|\.\|\.\|\.\fR will have gem installed there, too\. Therefore, gems installed without a \fB\-\-path \|\.\|\.\|\.\fR setting will show up by calling \fBgem list\fR\. Accordingly, gems installed to other locations will not get listed\. +\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 option is deprecated in favor of the \fBpath\fR setting\. +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\. @@ -71,36 +51,16 @@ Do not print progress information to the standard output\. \fB\-\-retry=[<number>]\fR Retry failed network or git requests for \fInumber\fR times\. .TP -\fB\-\-shebang=SHEBANG\fR -Uses the specified ruby executable (usually \fBruby\fR) to execute the scripts created with \fB\-\-binstubs\fR\. In addition, if you use \fB\-\-binstubs\fR together with \fB\-\-shebang jruby\fR these executables will be changed to execute \fBjruby\fR instead\. -.IP -This option is deprecated in favor of the \fBshebang\fR setting\. -.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\. Using this option implicitly sets \fBpath\fR, which is a [remembered option][REMEMBERED OPTIONS]\. -.TP -\fB\-\-system\fR -Installs the gems specified in the bundle to the system's Rubygems location\. This overrides any previous configuration of \fB\-\-path\fR\. -.IP -This option is deprecated in favor of the \fBsystem\fR setting\. +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\. -.TP -\fB\-\-with=<list>\fR -A space\-separated list of groups referencing gems to install\. If an optional group is given it is installed\. If a group is given that is in the remembered list of groups given to \-\-without, it is removed from that list\. -.IP -This option is deprecated in favor of the \fBwith\fR setting\. -.TP -\fB\-\-without=<list>\fR -A space\-separated list of groups referencing gems to skip during installation\. If a group is given that is in the remembered list of groups given to \-\-with, it is removed from that list\. -.IP -This option is deprecated in favor of the \fBwithout\fR setting\. .SH "DEPLOYMENT MODE" -Bundler's defaults are optimized for development\. To switch to defaults optimized for deployment and for CI, use the \fB\-\-deployment\fR flag\. Do not activate deployment mode on development machines, as it will cause an error when the Gemfile(5) is modified\. +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 @@ -120,14 +80,14 @@ In development, it's convenient to share the gems used in your application with .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, \fBbundle install \-\-deployment\fR installs gems to the \fBvendor/bundle\fR directory in the application\. This may be overridden using the \fB\-\-path\fR option\. +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 \fB\-\-without\fR option\. This option takes a space\-separated list of groups\. +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 \fB\-\-without\fR option will skip \fIinstalling\fR the gems in the specified groups, it will still \fIdownload\fR those gems and use them to resolve the dependencies of every gem in your Gemfile(5)\. +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 @@ -148,7 +108,7 @@ end .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 run \fBbundle install \-\-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\. +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 diff --git a/lib/bundler/man/bundle-install.1.ronn b/lib/bundler/man/bundle-install.1.ronn index a3606826df..56fd8bdf42 100644 --- a/lib/bundler/man/bundle-install.1.ronn +++ b/lib/bundler/man/bundle-install.1.ronn @@ -3,28 +3,21 @@ bundle-install(1) -- Install the dependencies specified in your Gemfile ## SYNOPSIS -`bundle install` [--binstubs[=DIRECTORY]] - [--clean] - [--deployment] - [--frozen] +`bundle install` [--cooldown=NUMBER] + [--force] [--full-index] [--gemfile=GEMFILE] [--jobs=NUMBER] [--local] + [--lockfile=LOCKFILE] [--no-cache] - [--no-prune] - [--path PATH] + [--no-lock] [--prefer-local] [--quiet] - [--redownload] [--retry=NUMBER] - [--shebang=SHEBANG] [--standalone[=GROUP[ GROUP...]]] - [--system] [--trust-policy=TRUST-POLICY] [--target-rbconfig=TARGET-RBCONFIG] - [--with=GROUP[ GROUP...]] - [--without=GROUP[ GROUP...]] ## DESCRIPTION @@ -45,50 +38,15 @@ update process below under [CONSERVATIVE UPDATING][]. ## OPTIONS -The `--clean`, `--deployment`, `--frozen`, `--no-prune`, `--path`, `--shebang`, -`--system`, `--without` and `--with` options are deprecated because they only -make sense if they are applied to every subsequent `bundle install` run -automatically and that requires `bundler` to silently remember them. Since -`bundler` will no longer remember CLI flags in future versions, `bundle config` -(see bundle-config(1)) should be used to apply them permanently. +* `--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. -* `--binstubs[=BINSTUBS]`: - Binstubs are scripts that wrap around executables. Bundler creates a small Ruby - file (a binstub) that loads Bundler, runs the command, and puts it in `bin/`. - This lets you link the binstub inside of an application to the exact gem - version the application needs. - - Creates a directory (defaults to `~/bin` when the option is used without a - value, or to the given `<BINSTUBS>` directory otherwise) and places any - executables from the gem there. 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, this flag will - create a `bin/rails` executable that ensures that all referred dependencies - will be resolved using the bundled gems. - -* `--clean`: - On finishing the installation Bundler is going to remove any gems not present - in the current Gemfile(5). Don't worry, gems currently in use will not be - removed. - - This option is deprecated in favor of the `clean` setting. - -* `--deployment`: - In [deployment mode][DEPLOYMENT MODE], Bundler will 'roll-out' the bundle for - production or CI use. Please check carefully if you want to have this option - enabled in your development environment. - - This option is deprecated in favor of the `deployment` setting. - -* `--redownload`, `--force`: - Force download every gem, even if the required versions are already available - locally. - -* `--frozen`: - Do not allow the Gemfile.lock to be updated after this install. Exits - non-zero if there are going to be changes to the Gemfile.lock. - - This option is deprecated in favor of the `frozen` setting. +* `--force`, `--redownload`: + Force reinstalling every gem, even if already installed. * `--full-index`: Bundler will not call Rubygems' API endpoint (default) but download and cache @@ -112,6 +70,10 @@ automatically and that requires `bundler` to silently remember them. Since 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 @@ -123,19 +85,14 @@ automatically and that requires `bundler` to silently remember them. Since does not remove any gems in the cache but keeps the newly bundled gems from being cached during the install. -* `--no-prune`: - Don't remove stale gems from the cache when the installation finishes. - - This option is deprecated in favor of the `no_prune` setting. +* `--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. -* `--path=PATH`: - The location to install the specified gems to. This defaults to Rubygems' - setting. Bundler shares this location with Rubygems, `gem install ...` will - have gem installed there, too. Therefore, gems installed without a - `--path ...` setting will show up by calling `gem list`. Accordingly, gems - installed to other locations will not get listed. - - This option is deprecated in favor of the `path` setting. + 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. @@ -143,27 +100,12 @@ automatically and that requires `bundler` to silently remember them. Since * `--retry=[<number>]`: Retry failed network or git requests for <number> times. -* `--shebang=SHEBANG`: - Uses the specified ruby executable (usually `ruby`) to execute the scripts - created with `--binstubs`. In addition, if you use `--binstubs` together with - `--shebang jruby` these executables will be changed to execute `jruby` - instead. - - This option is deprecated in favor of the `shebang` setting. - * `--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. Using this option implicitly sets `path`, which is a - [remembered option][REMEMBERED OPTIONS]. - -* `--system`: - Installs the gems specified in the bundle to the system's Rubygems location. - This overrides any previous configuration of `--path`. - - This option is deprecated in favor of the `system` setting. + in the manner required. * `--trust-policy=TRUST-POLICY`: Apply the Rubygems security policy <policy>, where policy is one of @@ -174,26 +116,11 @@ automatically and that requires `bundler` to silently remember them. Since * `--target-rbconfig=TARGET-RBCONFIG`: Path to rbconfig.rb for the deployment target platform. -* `--with=<list>`: - A space-separated list of groups referencing gems to install. If an - optional group is given it is installed. If a group is given that is - in the remembered list of groups given to --without, it is removed - from that list. - - This option is deprecated in favor of the `with` setting. - -* `--without=<list>`: - A space-separated list of groups referencing gems to skip during installation. - If a group is given that is in the remembered list of groups given - to --with, it is removed from that list. - - This option is deprecated in favor of the `without` setting. - ## DEPLOYMENT MODE Bundler's defaults are optimized for development. To switch to -defaults optimized for deployment and for CI, use the `--deployment` -flag. Do not activate deployment mode on development machines, as it +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. @@ -225,9 +152,9 @@ will cause an error when the Gemfile(5) is modified. gems to the system, or the web server may not have permission to read them. - As a result, `bundle install --deployment` installs gems to - the `vendor/bundle` directory in the application. This may be - overridden using the `--path` option. + 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 @@ -235,12 +162,12 @@ 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` option. This option takes +certain groups with the `without` setting. This setting takes a space-separated list of groups. -While the `--without` option will skip _installing_ the gems in the -specified groups, it will still _download_ those gems and use them to -resolve the dependencies of every gem in your Gemfile(5). +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 @@ -266,7 +193,7 @@ For a simple illustration, consider the following Gemfile(5): In this case, `sinatra` depends on any version of Rack (`>= 1.0`), while `rack-perftools-profiler` depends on 1.x (`~> 1.0`). -When you run `bundle install --without production` in development, we +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 diff --git a/lib/bundler/man/bundle-issue.1 b/lib/bundler/man/bundle-issue.1 index 6fcfc70a80..3af277ef86 100644 --- a/lib/bundler/man/bundle-issue.1 +++ b/lib/bundler/man/bundle-issue.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ISSUE" "1" "January 2025" "" +.TH "BUNDLE\-ISSUE" "1" "May 2026" "" .SH "NAME" \fBbundle\-issue\fR \- Get help reporting Bundler issues .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-licenses.1 b/lib/bundler/man/bundle-licenses.1 index 8ac8358906..ab5996d2be 100644 --- a/lib/bundler/man/bundle-licenses.1 +++ b/lib/bundler/man/bundle-licenses.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LICENSES" "1" "January 2025" "" +.TH "BUNDLE\-LICENSES" "1" "May 2026" "" .SH "NAME" \fBbundle\-licenses\fR \- Print the license of all gems in the bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-list.1 b/lib/bundler/man/bundle-list.1 index 83c246fd99..e759e0d449 100644 --- a/lib/bundler/man/bundle-list.1 +++ b/lib/bundler/man/bundle-list.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LIST" "1" "January 2025" "" +.TH "BUNDLE\-LIST" "1" "May 2026" "" .SH "NAME" \fBbundle\-list\fR \- List all the gems in the bundle .SH "SYNOPSIS" @@ -19,6 +19,8 @@ bundle list \-\-without\-group test 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 @@ -32,4 +34,7 @@ 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 index 81bee0ac33..9ec2b13282 100644 --- a/lib/bundler/man/bundle-list.1.ronn +++ b/lib/bundler/man/bundle-list.1.ronn @@ -21,6 +21,8 @@ bundle list --only-group dev bundle list --only-group dev test --paths +bundle list --format json + ## OPTIONS * `--name-only`: @@ -34,3 +36,6 @@ bundle list --only-group dev test --paths * `--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 index 60c93dd147..396c8ff6ca 100644 --- a/lib/bundler/man/bundle-lock.1 +++ b/lib/bundler/man/bundle-lock.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LOCK" "1" "January 2025" "" +.TH "BUNDLE\-LOCK" "1" "May 2026" "" .SH "NAME" \fBbundle\-lock\fR \- Creates / Updates a lockfile without installing .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-open.1 b/lib/bundler/man/bundle-open.1 index d97a332b02..2aab59f14b 100644 --- a/lib/bundler/man/bundle-open.1 +++ b/lib/bundler/man/bundle-open.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-OPEN" "1" "January 2025" "" +.TH "BUNDLE\-OPEN" "1" "May 2026" "" .SH "NAME" \fBbundle\-open\fR \- Opens the source directory for a gem in your bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-outdated.1 b/lib/bundler/man/bundle-outdated.1 index 0fdc047e6b..c2f8086e24 100644 --- a/lib/bundler/man/bundle-outdated.1 +++ b/lib/bundler/man/bundle-outdated.1 @@ -1,10 +1,10 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-OUTDATED" "1" "January 2025" "" +.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] +\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" @@ -53,6 +53,9 @@ 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" @@ -61,42 +64,42 @@ The 3 filtering options do not affect the resolution of versions, merely what ve If the regular output shows the following: .IP "" 4 .nf -* Gem Current Latest Requested Groups -* faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test -* hashie 1\.2\.0 3\.4\.6 = 1\.2\.0 default -* headless 2\.2\.3 2\.3\.1 = 2\.2\.3 test +* 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 -* hashie 1\.2\.0 3\.4\.6 = 1\.2\.0 default +* 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 -* headless 2\.2\.3 2\.3\.1 = 2\.2\.3 test +* 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 -* faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test +* 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 -* faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test +* 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 diff --git a/lib/bundler/man/bundle-outdated.1.ronn b/lib/bundler/man/bundle-outdated.1.ronn index 6f67a31977..e5badac2e9 100644 --- a/lib/bundler/man/bundle-outdated.1.ronn +++ b/lib/bundler/man/bundle-outdated.1.ronn @@ -16,6 +16,7 @@ bundle-outdated(1) -- List installed gems with newer versions available [--filter-minor] [--filter-patch] [--only-explicit] + [--cooldown=NUMBER] ## DESCRIPTION @@ -71,6 +72,12 @@ are up to date, Bundler will exit with a status of 0. Otherwise, it will exit 1. * `--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. @@ -82,29 +89,29 @@ in the output. If the regular output shows the following: - * Gem Current Latest Requested Groups - * faker 1.6.5 1.6.6 ~> 1.4 development, test - * hashie 1.2.0 3.4.6 = 1.2.0 default - * headless 2.2.3 2.3.1 = 2.2.3 test + * 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 - * hashie 1.2.0 3.4.6 = 1.2.0 default + * 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 - * headless 2.2.3 2.3.1 = 2.2.3 test + * 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 - * faker 1.6.5 1.6.6 ~> 1.4 development, test + * 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 - * faker 1.6.5 1.6.6 ~> 1.4 development, test + * 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 index 8f9e00cf94..39b7111263 100644 --- a/lib/bundler/man/bundle-platform.1 +++ b/lib/bundler/man/bundle-platform.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PLATFORM" "1" "January 2025" "" +.TH "BUNDLE\-PLATFORM" "1" "May 2026" "" .SH "NAME" \fBbundle\-platform\fR \- Displays platform compatibility information .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-plugin.1 b/lib/bundler/man/bundle-plugin.1 index f70557ca4b..d182c7789b 100644 --- a/lib/bundler/man/bundle-plugin.1 +++ b/lib/bundler/man/bundle-plugin.1 @@ -1,12 +1,12 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PLUGIN" "1" "January 2025" "" +.TH "BUNDLE\-PLUGIN" "1" "May 2026" "" .SH "NAME" \fBbundle\-plugin\fR \- Manage Bundler plugins .SH "SYNOPSIS" -\fBbundle plugin\fR install PLUGINS [\-\-source=\fISOURCE\fR] [\-\-version=\fIversion\fR] [\-\-git=\fIgit\-url\fR] [\-\-branch=\fIbranch\fR|\-\-ref=\fIrev\fR] [\-\-path=\fIpath\fR] +\fBbundle plugin\fR install PLUGINS [\-\-source=SOURCE] [\-\-version=VERSION] [\-\-git=GIT] [\-\-branch=BRANCH|\-\-ref=REF] [\-\-path=PATH] .br -\fBbundle plugin\fR uninstall PLUGINS +\fBbundle plugin\fR uninstall PLUGINS [\-\-all] .br \fBbundle plugin\fR list .br @@ -16,18 +16,23 @@ You can install, uninstall, and list plugin(s) with this command to extend funct .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 -\fBbundle plugin install bundler\-graph\fR -Install bundler\-graph gem from globally configured sources (defaults to RubyGems\.org)\. The global source, specified in source in Gemfile is ignored\. -.TP -\fBbundle plugin install bundler\-graph \-\-source https://example\.com\fR -Install bundler\-graph gem from example\.com\. The global source, specified in source in Gemfile is not considered\. +\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 -\fBbundle plugin install bundler\-graph \-\-version 0\.2\.1\fR -You can specify the version of the gem via \fB\-\-version\fR\. +\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 -\fBbundle plugin install bundler\-graph \-\-git https://github\.com/rubygems/bundler\-graph\fR -Install bundler\-graph gem from Git repository\. You can use standard Git URLs like: +\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 @@ -37,12 +42,25 @@ Install bundler\-graph gem from Git repository\. You can use standard Git URLs l .br \fBfile:///path/to/repo\fR .IP -When you specify \fB\-\-git\fR, you can use \fB\-\-branch\fR or \fB\-\-ref\fR to specify any branch, tag, or commit hash (revision) to use\. +Example: \fBbundle plugin install bundler\-graph \-\-git https://github\.com/rubygems/bundler\-graph\fR .TP -\fBbundle plugin install bundler\-graph \-\-path \.\./bundler\-graph\fR -Install bundler\-graph gem from a local path\. +\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 diff --git a/lib/bundler/man/bundle-plugin.1.ronn b/lib/bundler/man/bundle-plugin.1.ronn index b0a34660ea..b54e0c08b4 100644 --- a/lib/bundler/man/bundle-plugin.1.ronn +++ b/lib/bundler/man/bundle-plugin.1.ronn @@ -3,10 +3,10 @@ bundle-plugin(1) -- Manage Bundler plugins ## SYNOPSIS -`bundle plugin` install PLUGINS [--source=<SOURCE>] [--version=<version>] - [--git=<git-url>] [--branch=<branch>|--ref=<rev>] - [--path=<path>]<br> -`bundle plugin` uninstall PLUGINS<br> +`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] @@ -20,32 +20,53 @@ You can install, uninstall, and list plugin(s) with this command to extend funct Install the given plugin(s). -* `bundle plugin install bundler-graph`: - Install bundler-graph gem from globally configured sources (defaults to RubyGems.org). The global source, specified in source in Gemfile is ignored. +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. -* `bundle plugin install bundler-graph --source https://example.com`: - Install bundler-graph gem from example.com. The global source, specified in source in Gemfile is not considered. +**OPTIONS** -* `bundle plugin install bundler-graph --version 0.2.1`: - You can specify the version of the gem via `--version`. +* `--source=SOURCE`: + Install the plugin gem from a specific source, rather than from globally configured sources. -* `bundle plugin install bundler-graph --git https://github.com/rubygems/bundler-graph`: - Install bundler-graph gem from Git repository. You can use standard Git URLs like: + 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` - When you specify `--git`, you can use `--branch` or `--ref` to specify any branch, tag, or commit hash (revision) to use. + 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. -* `bundle plugin install bundler-graph --path ../bundler-graph`: - Install bundler-graph gem from a local path. +* `--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. diff --git a/lib/bundler/man/bundle-pristine.1 b/lib/bundler/man/bundle-pristine.1 index 4ef23c6d26..f6cc066571 100644 --- a/lib/bundler/man/bundle-pristine.1 +++ b/lib/bundler/man/bundle-pristine.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PRISTINE" "1" "January 2025" "" +.TH "BUNDLE\-PRISTINE" "1" "May 2026" "" .SH "NAME" \fBbundle\-pristine\fR \- Restores installed gems to their pristine condition .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-remove.1 b/lib/bundler/man/bundle-remove.1 index 959906a02e..2ca40e74db 100644 --- a/lib/bundler/man/bundle-remove.1 +++ b/lib/bundler/man/bundle-remove.1 @@ -1,21 +1,15 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-REMOVE" "1" "January 2025" "" +.TH "BUNDLE\-REMOVE" "1" "May 2026" "" .SH "NAME" \fBbundle\-remove\fR \- Removes gems from the Gemfile .SH "SYNOPSIS" -\fBbundle remove [GEM [GEM \|\.\|\.\|\.]] [\-\-install]\fR +`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\. -.SH "OPTIONS" -.TP -\fB\-\-install\fR -Runs \fBbundle install\fR after the given gems have been removed from the Gemfile, which ensures that both the lockfile and the installed gems on disk are also updated to remove the given gem(s)\. .P Example: .P bundle remove rails .P bundle remove rails rack -.P -bundle remove rails rack \-\-install diff --git a/lib/bundler/man/bundle-remove.1.ronn b/lib/bundler/man/bundle-remove.1.ronn index ceb1a980be..49cb4dc1fd 100644 --- a/lib/bundler/man/bundle-remove.1.ronn +++ b/lib/bundler/man/bundle-remove.1.ronn @@ -3,21 +3,14 @@ bundle-remove(1) -- Removes gems from the Gemfile ## SYNOPSIS -`bundle remove [GEM [GEM ...]] [--install]` +`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. -## OPTIONS - -* `--install`: - Runs `bundle install` after the given gems have been removed from the Gemfile, which ensures that both the lockfile and the installed gems on disk are also updated to remove the given gem(s). - Example: bundle remove rails bundle remove rails rack - -bundle remove rails rack --install diff --git a/lib/bundler/man/bundle-show.1 b/lib/bundler/man/bundle-show.1 index bf228e8498..a2142694b8 100644 --- a/lib/bundler/man/bundle-show.1 +++ b/lib/bundler/man/bundle-show.1 @@ -1,10 +1,10 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-SHOW" "1" "January 2025" "" +.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] [\-\-outdated] +\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 @@ -13,7 +13,4 @@ Calling show with [GEM] will list the exact location of that gem on your machine .TP \fB\-\-paths\fR List the paths of all gems that are required by your [\fBGemfile(5)\fR][Gemfile(5)], sorted by gem name\. -.TP -\fB\-\-outdated\fR -Show verbose output including whether gems are outdated\. diff --git a/lib/bundler/man/bundle-show.1.ronn b/lib/bundler/man/bundle-show.1.ronn index 6e80b04696..a6a59a1445 100644 --- a/lib/bundler/man/bundle-show.1.ronn +++ b/lib/bundler/man/bundle-show.1.ronn @@ -5,7 +5,6 @@ bundle-show(1) -- Shows all the gems in your bundle, or the path to a gem `bundle show` [GEM] [--paths] - [--outdated] ## DESCRIPTION @@ -20,6 +19,3 @@ machine. * `--paths`: List the paths of all gems that are required by your [`Gemfile(5)`][Gemfile(5)], sorted by gem name. - -* `--outdated`: - Show verbose output including whether gems are outdated. diff --git a/lib/bundler/man/bundle-update.1 b/lib/bundler/man/bundle-update.1 index a067615150..94161083fc 100644 --- a/lib/bundler/man/bundle-update.1 +++ b/lib/bundler/man/bundle-update.1 @@ -1,10 +1,10 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-UPDATE" "1" "January 2025" "" +.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]] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-quiet] [\-\-patch|\-\-minor|\-\-major] [\-\-pre] [\-\-redownload] [\-\-strict] [\-\-conservative] +\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 @@ -29,6 +29,9 @@ Update the locked version of Ruby to the current version of Ruby\. \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 @@ -44,9 +47,6 @@ Retry failed network or git requests for \fInumber\fR times\. \fB\-\-quiet\fR Only output warnings and errors\. .TP -\fB\-\-redownload\fR, \fB\-\-force\fR -Force downloading every gem\. -.TP \fB\-\-patch\fR Prefer updating only to next patch version\. .TP @@ -64,6 +64,9 @@ Do not allow any gem to be updated past latest \fB\-\-patch\fR | \fB\-\-minor\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 diff --git a/lib/bundler/man/bundle-update.1.ronn b/lib/bundler/man/bundle-update.1.ronn index 1b8b31951d..72fbf054d1 100644 --- a/lib/bundler/man/bundle-update.1.ronn +++ b/lib/bundler/man/bundle-update.1.ronn @@ -9,13 +9,14 @@ bundle-update(1) -- Update your gems to the latest available versions [--local] [--ruby] [--bundler[=VERSION]] + [--cooldown=NUMBER] + [--force] [--full-index] [--gemfile=GEMFILE] [--jobs=NUMBER] [--quiet] [--patch|--minor|--major] [--pre] - [--redownload] [--strict] [--conservative] @@ -54,6 +55,9 @@ gem. * `--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. @@ -70,9 +74,6 @@ gem. * `--quiet`: Only output warnings and errors. -* `--redownload`, `--force`: - Force downloading every gem. - * `--patch`: Prefer updating only to next patch version. @@ -91,6 +92,13 @@ gem. * `--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 diff --git a/lib/bundler/man/bundle-version.1 b/lib/bundler/man/bundle-version.1 index 9f41cdc4a3..751a408312 100644 --- a/lib/bundler/man/bundle-version.1 +++ b/lib/bundler/man/bundle-version.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-VERSION" "1" "January 2025" "" +.TH "BUNDLE\-VERSION" "1" "May 2026" "" .SH "NAME" \fBbundle\-version\fR \- Prints Bundler version information .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-viz.1 b/lib/bundler/man/bundle-viz.1 deleted file mode 100644 index 0eb7733d8b..0000000000 --- a/lib/bundler/man/bundle-viz.1 +++ /dev/null @@ -1,30 +0,0 @@ -.\" generated with Ronn-NG/v0.10.1 -.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-VIZ" "1" "January 2025" "" -.SH "NAME" -\fBbundle\-viz\fR \- Generates a visual dependency graph for your Gemfile -.SH "SYNOPSIS" -\fBbundle viz\fR [\-\-file=FILE] [\-\-format=FORMAT] [\-\-requirements] [\-\-version] [\-\-without=GROUP GROUP] -.SH "DESCRIPTION" -\fBviz\fR generates a PNG file of the current \fBGemfile(5)\fR as a dependency graph\. \fBviz\fR requires the ruby\-graphviz gem (and its dependencies)\. -.P -The associated gems must also be installed via \fBbundle install(1)\fR \fIbundle\-install\.1\.html\fR\. -.P -\fBviz\fR command was deprecated in Bundler 2\.2\. Use bundler\-graph plugin \fIhttps://github\.com/rubygems/bundler\-graph\fR instead\. -.SH "OPTIONS" -.TP -\fB\-\-file=FILE\fR, \fB\-f=FILE\fR -The name to use for the generated file\. See \fB\-\-format\fR option -.TP -\fB\-\-format=FORMAT\fR, \fB\-F=FORMAT\fR -This is output format option\. Supported format is png, jpg, svg, dot \|\.\|\.\|\. -.TP -\fB\-\-requirements\fR, \fB\-R\fR -Set to show the version of each required dependency\. -.TP -\fB\-\-version\fR, \fB\-v\fR -Set to show each gem version\. -.TP -\fB\-\-without=<list>\fR, \fB\-W=<list>\fR -Exclude gems that are part of the specified named group\. - diff --git a/lib/bundler/man/bundle-viz.1.ronn b/lib/bundler/man/bundle-viz.1.ronn deleted file mode 100644 index 730b0eed22..0000000000 --- a/lib/bundler/man/bundle-viz.1.ronn +++ /dev/null @@ -1,36 +0,0 @@ -bundle-viz(1) -- Generates a visual dependency graph for your Gemfile -===================================================================== - -## SYNOPSIS - -`bundle viz` [--file=FILE] - [--format=FORMAT] - [--requirements] - [--version] - [--without=GROUP GROUP] - -## DESCRIPTION - -`viz` generates a PNG file of the current `Gemfile(5)` as a dependency graph. -`viz` requires the ruby-graphviz gem (and its dependencies). - -The associated gems must also be installed via [`bundle install(1)`](bundle-install.1.html). - -`viz` command was deprecated in Bundler 2.2. Use [bundler-graph plugin](https://github.com/rubygems/bundler-graph) instead. - -## OPTIONS - -* `--file=FILE`, `-f=FILE`: - The name to use for the generated file. See `--format` option - -* `--format=FORMAT`, `-F=FORMAT`: - This is output format option. Supported format is png, jpg, svg, dot ... - -* `--requirements`, `-R`: - Set to show the version of each required dependency. - -* `--version`, `-v`: - Set to show each gem version. - -* `--without=<list>`, `-W=<list>`: - Exclude gems that are part of the specified named group. diff --git a/lib/bundler/man/bundle.1 b/lib/bundler/man/bundle.1 index ed7ad3afa3..167815631a 100644 --- a/lib/bundler/man/bundle.1 +++ b/lib/bundler/man/bundle.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE" "1" "January 2025" "" +.TH "BUNDLE" "1" "May 2026" "" .SH "NAME" \fBbundle\fR \- Ruby Dependency Management .SH "SYNOPSIS" @@ -66,9 +66,6 @@ Open an installed gem in the editor \fBbundle lock(1)\fR \fIbundle\-lock\.1\.html\fR Generate a lockfile for your dependencies .TP -\fBbundle viz(1)\fR \fIbundle\-viz\.1\.html\fR (deprecated) -Generate a visual representation of your dependencies -.TP \fBbundle init(1)\fR \fIbundle\-init\.1\.html\fR Generate a simple \fBGemfile\fR, placed in the current directory .TP @@ -94,9 +91,3 @@ Manage Bundler plugins 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\. -.SH "OBSOLETE" -These commands are obsolete and should no longer be used: -.IP "\(bu" 4 -\fBbundle inject(1)\fR -.IP "" 0 - diff --git a/lib/bundler/man/bundle.1.ronn b/lib/bundler/man/bundle.1.ronn index 8245effabd..1c2b3df7af 100644 --- a/lib/bundler/man/bundle.1.ronn +++ b/lib/bundler/man/bundle.1.ronn @@ -76,9 +76,6 @@ We divide `bundle` subcommands into primary commands and utilities: * [`bundle lock(1)`](bundle-lock.1.html): Generate a lockfile for your dependencies -* [`bundle viz(1)`](bundle-viz.1.html) (deprecated): - Generate a visual representation of your dependencies - * [`bundle init(1)`](bundle-init.1.html): Generate a simple `Gemfile`, placed in the current directory @@ -108,9 +105,3 @@ We divide `bundle` subcommands into primary commands and utilities: 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. - -## OBSOLETE - -These commands are obsolete and should no longer be used: - -* `bundle inject(1)` diff --git a/lib/bundler/man/gemfile.5 b/lib/bundler/man/gemfile.5 index 579c82498a..64e93c4b15 100644 --- a/lib/bundler/man/gemfile.5 +++ b/lib/bundler/man/gemfile.5 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "GEMFILE" "5" "January 2025" "" +.TH "GEMFILE" "5" "May 2026" "" .SH "NAME" \fBGemfile\fR \- A format for describing gem dependencies for Ruby programs .SH "SYNOPSIS" @@ -460,6 +460,50 @@ The \fBgemspec\fR method adds any runtime dependencies as gem requirements in th 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 @@ -469,4 +513,35 @@ For implicit gems (dependencies of explicit gems), any source, git, or path repo .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 index 802549737e..69fef90654 100644 --- a/lib/bundler/man/gemfile.5.ronn +++ b/lib/bundler/man/gemfile.5.ronn @@ -541,6 +541,59 @@ 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, @@ -556,3 +609,31 @@ bundler uses the following priority order: 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 index 3ea3495f1b..f610ba852a 100644 --- a/lib/bundler/man/index.txt +++ b/lib/bundler/man/index.txt @@ -15,7 +15,6 @@ 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-inject(1) bundle-inject.1 bundle-install(1) bundle-install.1 bundle-issue(1) bundle-issue.1 bundle-licenses(1) bundle-licenses.1 @@ -30,4 +29,3 @@ bundle-remove(1) bundle-remove.1 bundle-show(1) bundle-show.1 bundle-update(1) bundle-update.1 bundle-version(1) bundle-version.1 -bundle-viz(1) bundle-viz.1 diff --git a/lib/bundler/match_metadata.rb b/lib/bundler/match_metadata.rb index f6cc27df32..92aeb2e893 100644 --- a/lib/bundler/match_metadata.rb +++ b/lib/bundler/match_metadata.rb @@ -13,5 +13,38 @@ module Bundler 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 ece9fb8679..479818e5ec 100644 --- a/lib/bundler/match_platform.rb +++ b/lib/bundler/match_platform.rb @@ -1,23 +1,42 @@ # frozen_string_literal: true -require_relative "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 gemspec_platform == Gem::Platform::RUBY - return true if local_platform == gemspec_platform - gemspec_platform = Gem::Platform.new(gemspec_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 index 5e46d52441..601af7e55d 100644 --- a/lib/bundler/match_remote_metadata.rb +++ b/lib/bundler/match_remote_metadata.rb @@ -6,19 +6,34 @@ module Bundler # API didn't include that field, so some marshalled specs in the index have it # set to +nil+. def matches_current_ruby? - @required_ruby_version ||= _remote_specification.required_ruby_version || Gem::Requirement.default - + ensure_required_ruby_version_loaded super end def matches_current_rubygems? - # 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+. - @required_rubygems_version ||= _remote_specification.required_rubygems_version || Gem::Requirement.default + 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 diff --git a/lib/bundler/materialization.rb b/lib/bundler/materialization.rb index 6542c07649..82e48464a7 100644 --- a/lib/bundler/materialization.rb +++ b/lib/bundler/materialization.rb @@ -22,14 +22,14 @@ module Bundler @specs ||= if @candidates.nil? [] elsif platform - GemHelpers.select_best_platform_match(@candidates, platform, force_ruby: dep.force_ruby_platform) + MatchPlatform.select_best_platform_match(@candidates, platform, force_ruby: dep.force_ruby_platform) else - GemHelpers.select_best_local_platform_match(@candidates, force_ruby: dep.force_ruby_platform || dep.default_force_ruby_platform) + MatchPlatform.select_best_local_platform_match(@candidates, force_ruby: dep.force_ruby_platform || dep.default_force_ruby_platform) end end def dependencies - specs.first.runtime_dependencies.map {|d| [d, platform] } + (materialized_spec || specs.first).runtime_dependencies.map {|d| [d, platform] } end def materialized_spec 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 7790a3f6f8..faca6bea53 100644 --- a/lib/bundler/plugin.rb +++ b/lib/bundler/plugin.rb @@ -113,7 +113,7 @@ module Bundler return if definition.dependencies.empty? - plugins = definition.dependencies.map(&:name).reject {|p| index.installed? p } + plugins = definition.dependencies.map(&:name) installed_specs = Installer.new.install_definition(definition) save_plugins plugins, installed_specs, builder.inferred_plugins @@ -195,7 +195,7 @@ 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 from_lock(locked_opts) @@ -220,7 +220,7 @@ 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 @@ -253,10 +253,13 @@ module Bundler # @param [Array<String>] names of inferred source plugins that can be ignored def save_plugins(plugins, specs, optional_plugins = []) plugins.each do |name| - next if index.installed?(name) - spec = specs[name] + # 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 diff --git a/lib/bundler/plugin/api/source.rb b/lib/bundler/plugin/api/source.rb index 690f379389..798326673a 100644 --- a/lib/bundler/plugin/api/source.rb +++ b/lib/bundler/plugin/api/source.rb @@ -67,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. diff --git a/lib/bundler/plugin/events.rb b/lib/bundler/plugin/events.rb index 29c05098ae..3fbf60307e 100644 --- a/lib/bundler/plugin/events.rb +++ b/lib/bundler/plugin/events.rb @@ -31,6 +31,54 @@ module Bundler 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 @@ -46,18 +94,18 @@ module Bundler define :GEM_AFTER_INSTALL, "after-install" # @!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 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" @@ -70,16 +118,10 @@ module Bundler define :GEM_AFTER_REQUIRE, "after-require" # @!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 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" + 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 0b1234bbf3..1dfb061304 100644 --- a/lib/bundler/plugin/index.rb +++ b/lib/bundler/plugin/index.rb @@ -31,7 +31,7 @@ module Bundler begin load_index(global_index_file, true) - rescue GenericSystemCallError + rescue PermissionError # no need to fail when on a read-only FS, for example nil rescue ArgumentError => e @@ -119,6 +119,12 @@ 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 @@ -157,6 +163,8 @@ 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&.exist? && !index_f.size.zero? break unless valid_file @@ -168,8 +176,8 @@ module Bundler @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 @@ -178,11 +186,13 @@ 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, + "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, } @@ -192,6 +202,40 @@ module Bundler 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 ac3c3ea7f3..9be8b36843 100644 --- a/lib/bundler/plugin/installer.rb +++ b/lib/bundler/plugin/installer.rb @@ -43,16 +43,6 @@ module Bundler private def check_sources_consistency!(options) - if options.key?(:git) && options.key?(:local_git) - raise InvalidOption, "Remote and local plugin git sources can't be both specified" - end - - # back-compat; local_git is an alias for git - if options.key?(:local_git) - Bundler::SharedHelpers.major_deprecation(2, "--local_git is deprecated, use --git") - options[:git] = options.delete(:local_git) - end - if (options.keys & [:source, :git, :path]).length > 1 raise InvalidOption, "Only one of --source, --git, or --path may be specified" end @@ -120,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/path.rb b/lib/bundler/plugin/installer/path.rb index 58a8fa7426..58c4924eb0 100644 --- a/lib/bundler/plugin/installer/path.rb +++ b/lib/bundler/plugin/installer/path.rb @@ -8,6 +8,14 @@ module Bundler 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 diff --git a/lib/bundler/plugin/source_list.rb b/lib/bundler/plugin/source_list.rb index 746996de55..d929ade29e 100644 --- a/lib/bundler/plugin/source_list.rb +++ b/lib/bundler/plugin/source_list.rb @@ -23,7 +23,7 @@ module Bundler private - def rubygems_aggregate_class + def source_class Plugin::Installer::Rubygems end end diff --git a/lib/bundler/remote_specification.rb b/lib/bundler/remote_specification.rb index ab163e2b04..dcaaf6af2e 100644 --- a/lib/bundler/remote_specification.rb +++ b/lib/bundler/remote_specification.rb @@ -12,7 +12,7 @@ module Bundler attr_reader :name, :version, :platform attr_writer :dependencies - attr_accessor :source, :remote, :locked_platform + attr_accessor :source, :remote, :locked_platform, :created_at def initialize(name, version, platform, spec_fetcher) @name = name diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 56a6c7ac67..422b726980 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -12,8 +12,7 @@ module Bundler require_relative "resolver/candidate" require_relative "resolver/incompatibility" require_relative "resolver/root" - - include GemHelpers + require_relative "resolver/strategy" def initialize(base, gem_version_promoter, most_specific_locked_platform = nil) @source_requirements = base.source_requirements @@ -65,7 +64,9 @@ module Bundler @cached_dependencies = Hash.new do |dependencies, package| dependencies[package] = Hash.new do |versions, version| - versions[version] = to_dependency_hash(version.dependencies.reject {|d| d.name == package.name }, @packages) + 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 @@ -78,9 +79,10 @@ module Bundler end def solve_versions(root:, logger:) - solver = PubGrub::VersionSolver.new(source: self, root: root, logger: 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 @@ -121,9 +123,25 @@ module Bundler explanation << extended_explanation end + override_summary = override_diagnostic_summary + explanation << override_summary if override_summary + raise SolveFailure.new(explanation) end + 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 find_names_to_relax(incompatibility) names_to_unlock = [] names_to_allow_prereleases_for = [] @@ -166,16 +184,8 @@ module Bundler PubGrub::VersionConstraint.new(package, range: range) end - def versions_for(package, range=VersionRange.any) - versions = select_sorted_versions(package, range) - - # Conditional avoids (among other things) calling - # sort_versions_by_preferred with the root package - if versions.size > 1 - sort_versions_by_preferred(package, versions) - else - versions - end + def versions_for(package, range = VersionRange.any) + range.select_versions(@sorted_versions[package]) end def no_versions_incompatibility_for(package, unsatisfied_term) @@ -193,6 +203,9 @@ module Bundler 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 label = "#{name} (#{constraint_string})" extended_explanation = other_specs_matching_message(specs_matching_other_platforms, label) if specs_matching_other_platforms.any? @@ -237,7 +250,7 @@ module Bundler sorted_versions[high] end - range = PubGrub::VersionRange.new(min: low, max: high, include_min: true) + range = PubGrub::VersionRange.new(min: low, max: high, include_min: !low.nil?) self_constraint = PubGrub::VersionConstraint.new(package, range: range) @@ -280,12 +293,12 @@ module Bundler next groups if platform_specs.all?(&:empty?) end - ruby_specs = select_best_platform_match(specs, Gem::Platform::RUBY) + 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 |specs| - ruby_group.merge(Resolver::SpecGroup.new(specs)) + platform_specs.each do |s| + ruby_group.merge(Resolver::SpecGroup.new(s)) end groups << Resolver::Candidate.new(version, group: ruby_group, priority: -1) @@ -319,6 +332,16 @@ module Bundler "Gemfile" end + def raise_incomplete!(incomplete_specs) + raise_not_found!(@base.get_package(incomplete_specs.first.name)) + end + + def sort_versions_by_preferred(package, versions) + @gem_version_promoter.sort_versions(package, versions) + end + + private + def raise_not_found!(package) name = package.name source = source_for(name) @@ -352,10 +375,30 @@ module Bundler 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 - private + 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]) @@ -379,7 +422,7 @@ module Bundler end def filter_specs(specs, package) - filter_remote_specs(filter_prereleases(specs, package), package) + filter_remote_specs(filter_cooldown(filter_prereleases(specs, package)), package) end def filter_prereleases(specs, package) @@ -388,10 +431,53 @@ module Bundler 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 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 + + def cooldown_now + @cooldown_now ||= Time.now + end + def filter_remote_specs(specs, package) - return specs unless package.prefer_local? + if package.prefer_local? + local_specs = specs.select {|s| s.is_a?(StubSpecification) } - specs.select {|s| s.is_a?(StubSpecification) } + if local_specs.empty? + package.consider_remote_versions! + specs + else + local_specs + end + else + specs + end end # Ignore versions that depend on themselves incorrectly @@ -405,10 +491,6 @@ module Bundler requirement.satisfied_by?(spec.version) || spec.source.is_a?(Source::Gemspec) end - def sort_versions_by_preferred(package, versions) - @gem_version_promoter.sort_versions(package, versions) - end - def repository_for(package) source_for(package.name) end @@ -424,7 +506,7 @@ module Bundler next [dep_package, dep_constraint] if name == "bundler" dep_range = dep_constraint.range - versions = select_sorted_versions(dep_package, dep_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) @@ -432,7 +514,7 @@ module Bundler end dep_package.consider_prereleases! if dep_package.ignores_prereleases? dep_package.consider_remote_versions! if dep_package.prefer_local? - versions = select_sorted_versions(dep_package, dep_range) + versions = versions_for(dep_package, dep_range) end if versions.empty? && select_all_versions(dep_package, dep_range).any? @@ -447,10 +529,6 @@ module Bundler end.to_h end - def select_sorted_versions(package, range) - range.select_versions(@sorted_versions[package]) - end - def select_all_versions(package, range) range.select_versions(@all_versions[package]) end @@ -492,7 +570,7 @@ module Bundler end def to_dependency_hash(dependencies, packages) - dependencies.inject({}) do |deps, dep| + apply_overrides(dependencies).inject({}) do |deps, dep| package = packages[dep.name] current_req = deps[package] @@ -508,6 +586,33 @@ module Bundler end end + 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 + end + + 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) diff --git a/lib/bundler/resolver/base.rb b/lib/bundler/resolver/base.rb index 932a92ff41..00bdd08303 100644 --- a/lib/bundler/resolver/base.rb +++ b/lib/bundler/resolver/base.rb @@ -5,9 +5,10 @@ require_relative "package" module Bundler class Resolver class Base - attr_reader :packages, :requirements, :source_requirements, :locked_specs + 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] diff --git a/lib/bundler/resolver/candidate.rb b/lib/bundler/resolver/candidate.rb index 30fd6fe2fd..5298b2530f 100644 --- a/lib/bundler/resolver/candidate.rb +++ b/lib/bundler/resolver/candidate.rb @@ -17,7 +17,7 @@ module Bundler # 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 lock files, and so on. + # specifications that can be installed, written to lockfiles, and so on. # class Candidate include Comparable @@ -48,35 +48,38 @@ module Bundler @version.segments end - def sort_obj - [@version, @priority] - end - def <=>(other) return unless other.is_a?(self.class) - sort_obj <=> other.sort_obj + 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) - sort_obj == other.sort_obj + version == other.version && priority == other.priority end def eql?(other) return unless other.is_a?(self.class) - sort_obj.eql?(other.sort_obj) + version.eql?(other.version) && priority.eql?(other.priority) end def hash - sort_obj.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/package.rb b/lib/bundler/resolver/package.rb index 7bdcd7e9eb..3906be3f57 100644 --- a/lib/bundler/resolver/package.rb +++ b/lib/bundler/resolver/package.rb @@ -15,19 +15,24 @@ module Bundler class Package attr_reader :name, :platforms, :dependency, :locked_version - def initialize(name, platforms, locked_specs:, unlock:, prerelease: false, prefer_local: false, dependency: nil) + 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 {|platform| GemHelpers.select_best_platform_match(specs, platform, prefer_locked: !unlock?) } + 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 @@ -55,7 +60,7 @@ module Bundler end def unlock? - @unlock.empty? || @unlock.include?(name) + @unlock == true || @unlock.include?(name) end def ignores_prereleases? diff --git a/lib/bundler/resolver/spec_group.rb b/lib/bundler/resolver/spec_group.rb index c3b72e2798..ac6ba86c4c 100644 --- a/lib/bundler/resolver/spec_group.rb +++ b/lib/bundler/resolver/spec_group.rb @@ -39,9 +39,7 @@ module Bundler end def dependencies - @dependencies ||= @specs.flat_map do |spec| - __dependencies(spec) + metadata_dependencies(spec) - end.uniq.sort + @dependencies ||= @specs.flat_map(&:expanded_dependencies).uniq.sort end def ==(other) @@ -71,28 +69,6 @@ module Bundler def exemplary_spec @specs.first end - - def __dependencies(spec) - dependencies = [] - spec.dependencies.each do |dep| - next if dep.type == :development - dependencies << Dependency.new(dep.name, dep.requirement) - end - dependencies - end - - def metadata_dependencies(spec) - [ - metadata_dependency("Ruby", spec.required_ruby_version), - metadata_dependency("RubyGems", spec.required_rubygems_version), - ].compact - end - - def metadata_dependency(name, requirement) - return if requirement.nil? || requirement.none? - - Dependency.new("#{name}\0", requirement) - end 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 090cb7e2ca..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) @@ -48,9 +56,27 @@ 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 in case dots preceded this - Bundler.ui.warn "Retrying #{name} due to error (#{current_run.next}/#{total_runs}): #{e.class} #{e.message}", true + 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? diff --git a/lib/bundler/ruby_dsl.rb b/lib/bundler/ruby_dsl.rb index cd88253f46..5e52f38c8f 100644 --- a/lib/bundler/ruby_dsl.rb +++ b/lib/bundler/ruby_dsl.rb @@ -42,21 +42,26 @@ module Bundler # 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" or "ruby 3.2.2" capturing version string up to the first space or comment - if /^ # 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) - "? # Optional opening quote - ( # Start capturing group - [^\s#"]+ # One or more chars that aren't spaces, #, or quotes - ) # End capturing group - "? # Optional closing quote - /x.match(file_content) - $1 + # 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 + 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 0ed5cbc6ca..aeff07582e 100644 --- a/lib/bundler/ruby_version.rb +++ b/lib/bundler/ruby_version.rb @@ -43,7 +43,6 @@ module Bundler def to_s(versions = self.versions) output = String.new("ruby #{versions_string(versions)}") - output << "p#{patchlevel}" if patchlevel && patchlevel != "-1" output << " (#{engine} #{versions_string(engine_versions)})" unless engine == "ruby" output @@ -72,8 +71,7 @@ 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 @@ -98,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 diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb index bc7f65c7f7..4ad2bdf46f 100644 --- a/lib/bundler/rubygems_ext.rb +++ b/lib/bundler/rubygems_ext.rb @@ -13,15 +13,6 @@ require "rubygems" unless defined?(Gem) # `Gem::Source` from the redefined `Gem::Specification#source`. require "rubygems/source" -# Cherry-pick fixes to `Gem.ruby_version` to be useful for modern Bundler -# versions and ignore patchlevels -# (https://github.com/rubygems/rubygems/pull/5472, -# https://github.com/rubygems/rubygems/pull/5486). May be removed once RubyGems -# 3.3.12 support is dropped. -unless Gem.ruby_version.to_s == RUBY_VERSION || RUBY_PATCHLEVEL == -1 - Gem.instance_variable_set(:@ruby_version, Gem::Version.new(RUBY_VERSION)) -end - module Gem # Can be removed once RubyGems 3.5.11 support is dropped unless Gem.respond_to?(:freebsd_platform?) @@ -58,14 +49,133 @@ module Gem end end + require "rubygems/platform" + + 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 - # Can be removed once RubyGems 3.3.15 support is dropped - FLATTENS_REQUIRED_PATHS = Specification.new.respond_to?(:flatten_require_paths).freeze - class Specification # Can be removed once RubyGems 3.5.15 support is dropped correct_array_attributes = @@default_value.select {|_k,v| v.is_a?(Array) }.keys @@ -77,7 +187,6 @@ module Gem require_relative "match_platform" include ::Bundler::MatchMetadata - include ::Bundler::MatchPlatform attr_accessor :remote, :relative_loaded_from @@ -133,23 +242,6 @@ module Gem full_gem_path end - unless const_defined?(:LATEST_RUBY_WITHOUT_PATCH_VERSIONS) - LATEST_RUBY_WITHOUT_PATCH_VERSIONS = Gem::Version.new("2.1") - - alias_method :rg_required_ruby_version=, :required_ruby_version= - def required_ruby_version=(req) - self.rg_required_ruby_version = req - - @required_ruby_version.requirements.map! do |op, v| - if v >= LATEST_RUBY_WITHOUT_PATCH_VERSIONS && v.release.segments.size == 4 - [op == "~>" ? "=" : op, Gem::Version.new(v.segments.tap {|s| s.delete_at(3) }.join("."))] - else - [op, v] - end - end - end - end - def insecurely_materialized? false end @@ -177,35 +269,30 @@ module Gem dependencies - development_dependencies end - def deleted_gem? + 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 - unless FLATTENS_REQUIRED_PATHS - def flatten_require_paths - return unless raw_require_paths.first.is_a?(Array) - - warn "#{name} #{version} includes a gemspec with `require_paths` set to an array of arrays. Newer versions of this gem might've already fixed this" - raw_require_paths.flatten! - 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) - class << self - module RequirePathFlattener - def from_yaml(input) - spec = super(input) - spec.flatten_require_paths - spec - end + super end - - prepend RequirePathFlattener end + + prepend FixPathSourceMissingExtensions end private @@ -288,86 +375,6 @@ module Gem end end - require "rubygems/platform" - - class Platform - JAVA = Gem::Platform.new("java") - MSWIN = Gem::Platform.new("mswin32") - MSWIN64 = Gem::Platform.new("mswin64") - MINGW = Gem::Platform.new("x86-mingw32") - X64_MINGW = [Gem::Platform.new("x64-mingw32"), - Gem::Platform.new("x64-mingw-ucrt")].freeze - WINDOWS = [MSWIN, MSWIN64, MINGW, X64_MINGW].flatten.freeze - X64_LINUX = Gem::Platform.new("x86_64-linux") - X64_LINUX_MUSL = Gem::Platform.new("x86_64-linux-musl") - - if X64_LINUX === X64_LINUX_MUSL - remove_method :=== - - def ===(other) - return nil unless Gem::Platform === other - - # universal-mingw32 matches x64-mingw-ucrt - return true if (@cpu == "universal" || other.cpu == "universal") && - @os.start_with?("mingw") && other.os.start_with?("mingw") - - # cpu - ([nil,"universal"].include?(@cpu) || [nil, "universal"].include?(other.cpu) || @cpu == other.cpu || - (@cpu == "arm" && other.cpu.start_with?("armv"))) && - - # os - @os == other.os && - - # version - ( - (@os != "linux" && (@version.nil? || other.version.nil?)) || - (@os == "linux" && (normalized_linux_version_ext == other.normalized_linux_version_ext || ["musl#{@version}", "musleabi#{@version}", "musleabihf#{@version}"].include?(other.version))) || - @version == other.version - ) - end - - # This is a copy of RubyGems 3.3.23 or higher `normalized_linux_method`. - # Once only 3.3.23 is supported, we can use the method in RubyGems. - def normalized_linux_version_ext - return nil unless @version - - without_gnu_nor_abi_modifiers = @version.sub(/\Agnu/, "").sub(/eabi(hf)?\Z/, "") - return nil if without_gnu_nor_abi_modifiers.empty? - - without_gnu_nor_abi_modifiers - end - end - end - - Platform.singleton_class.module_eval do - unless Platform.singleton_methods.include?(:match_spec?) - def match_spec?(spec) - match_gem?(spec.platform, spec.name) - end - - def match_gem?(platform, gem_name) - match_platforms?(platform, Gem.platforms) - end - end - - match_platforms_defined = Gem::Platform.respond_to?(:match_platforms?, true) - - if !match_platforms_defined || Gem::Platform.send(:match_platforms?, Gem::Platform::X64_LINUX_MUSL, [Gem::Platform::X64_LINUX]) - - private - - remove_method :match_platforms? if match_platforms_defined - - def match_platforms?(platform, platforms) - platforms.any? do |local_platform| - platform.nil? || - local_platform == platform || - (local_platform != Gem::Platform::RUBY && platform =~ local_platform) - end - end - end - end - # 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) @@ -396,6 +403,11 @@ module Gem @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 require "rubygems/name_tuple" @@ -405,7 +417,7 @@ module Gem 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 - def initialize(name, version, platform=Gem::Platform::RUBY) + def initialize(name, version, platform = Gem::Platform::RUBY) if Gem::Platform === platform initialize_with_platform(name, version, platform.to_s) else @@ -442,6 +454,39 @@ module Gem 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" @@ -455,15 +500,4 @@ module Gem Package::TarReader::Entry.prepend(FixFullNameEncoding) end - - require "rubygems/uri" - - # Can be removed once RubyGems 3.3.15 support is dropped - unless Gem::Uri.respond_to?(:redact) - class Uri - def self.redact(uri) - new(uri).redacted - end - end - end end diff --git a/lib/bundler/rubygems_gem_installer.rb b/lib/bundler/rubygems_gem_installer.rb index 1af1b85ff0..fc019f54d2 100644 --- a/lib/bundler/rubygems_gem_installer.rb +++ b/lib/bundler/rubygems_gem_installer.rb @@ -20,14 +20,18 @@ module Bundler strict_rm_rf spec.extension_dir SharedHelpers.filesystem_access(gem_dir, :create) do - FileUtils.mkdir_p gem_dir, mode: 0o755 + FileUtils.mkdir_p gem_dir end SharedHelpers.filesystem_access(gem_dir, :write) do extract_files end - build_extensions if spec.extensions.any? + if options[:build_extension] == false + warn_skipped_extensions + elsif spec.extensions.any? + build_extensions + end write_build_info_file run_post_build_hooks @@ -35,7 +39,12 @@ module Bundler generate_bin end - generate_plugins + if options[:install_plugin] == false + remove_stale_plugins + warn_skipped_plugins + else + generate_plugins + end write_spec @@ -69,10 +78,7 @@ module Bundler end def generate_plugins - return unless Gem::Installer.instance_methods(false).include?(:generate_plugins) - - latest = Gem::Specification.stubs_for(spec.name).first - return if latest && latest.version > spec.version + return unless Gem::Installer.method_defined?(:generate_plugins, false) ensure_writable_dir @plugins_dir @@ -83,6 +89,20 @@ module Bundler 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) @@ -103,6 +123,10 @@ module Bundler end end + def build_jobs + Bundler.settings[:jobs] || super + end + def build_extensions extension_cache_path = options[:bundler_extension_cache_path] extension_dir = spec.extension_dir diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb index a8571f5115..e04ef23259 100644 --- a/lib/bundler/rubygems_integration.rb +++ b/lib/bundler/rubygems_integration.rb @@ -177,7 +177,7 @@ module Bundler end end - def replace_gem(specs, specs_by_name) + def replace_gem(specs_by_name) executables = nil [::Kernel.singleton_class, ::Kernel].each do |kernel_class| @@ -214,16 +214,11 @@ module Bundler e.requirement = dep.requirement raise e end - - # backwards compatibility shim, see https://github.com/rubygems/bundler/issues/5102 - kernel_class.send(:public, :gem) if Bundler.feature_flag.setup_makes_kernel_gem_public? end end # Used to give better error messages when activating specs outside of the current bundle def replace_bin_path(specs_by_name) - gem_class = (class << Gem; self; end) - 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 @@ -274,7 +269,7 @@ module Bundler else Gem::BUNDLED_GEMS.replace_require(specs) if Gem::BUNDLED_GEMS.respond_to?(:replace_require) end - replace_gem(specs, specs_by_name) + replace_gem(specs_by_name) stub_rubygems(specs_by_name.values) replace_bin_path(specs_by_name) @@ -293,7 +288,6 @@ module Bundler default_spec_name = default_spec.name next if specs_by_name.key?(default_spec_name) - specs << default_spec specs_by_name[default_spec_name] = default_spec end @@ -346,9 +340,13 @@ module Bundler Gem::Specification.all = specs end - redefine_method((class << Gem; self; end), :finish_resolve) do |*| + redefine_method(gem_class, :finish_resolve) do |*| [] end + + redefine_method(gem_class, :load_plugins) do |*| + load_plugin_files specs.flat_map(&:plugins) + end end def plain_specs @@ -418,11 +416,7 @@ module Bundler end def all_specs - SharedHelpers.major_deprecation 2, "Bundler.rubygems.all_specs has been removed in favor of Bundler.rubygems.installed_specs" - - Gem::Specification.stubs.map do |stub| - StubSpecification.from_stub(stub) - end + SharedHelpers.feature_removed! "Bundler.rubygems.all_specs has been removed in favor of Bundler.rubygems.installed_specs" end def installed_specs @@ -438,7 +432,7 @@ module Bundler end def find_bundler(version) - find_name("bundler").find {|s| s.version.to_s == version } + find_name("bundler").find {|s| s.version.to_s == version.to_s } end def find_name(name) @@ -448,6 +442,12 @@ module Bundler def default_stubs Gem::Specification.default_stubs("*.gemspec") end + + private + + def gem_class + class << Gem; self; end + end end def self.rubygems diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb index b72d3786bd..5280e72aa2 100644 --- a/lib/bundler/runtime.rb +++ b/lib/bundler/runtime.rb @@ -50,35 +50,30 @@ module Bundler Plugin.hook(Plugin::Events::GEM_BEFORE_REQUIRE_ALL, dependencies) dependencies.each do |dep| - required_file = nil Plugin.hook(Plugin::Events::GEM_BEFORE_REQUIRE, dep) - 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 RuntimeError => e - raise e if e.is_a?(LoadError) # we handle this a little later + # 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 - raise if dep.autorequire || e.path != required_file - - if dep.autorequire.nil? && dep.name.include?("-") - begin - namespaced_file = dep.name.tr("-", "/") - Kernel.require namespaced_file - rescue LoadError => e - raise if e.path != namespaced_file - end + rescue StandardError => e + raise Bundler::GemRequireError.new e, + "There was an error while trying to load the gem '#{file}'." end end @@ -135,11 +130,14 @@ module Bundler specs_to_cache.each do |spec| next if spec.name == "bundler" - next if spec.source.is_a?(Source::Gemspec) - if spec.source.respond_to?(:migrate_cache) - spec.source.migrate_cache(custom_path, local: local) - elsif spec.source.respond_to?(:cache) - spec.source.cache(spec, custom_path) + + 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 @@ -176,7 +174,14 @@ module Bundler spec_cache_paths = [] spec_gemspec_paths = [] spec_extension_paths = [] - Bundler.rubygems.add_default_gems_to(specs).values.each do |spec| + 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) @@ -242,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 diff --git a/lib/bundler/self_manager.rb b/lib/bundler/self_manager.rb index 2aeac6be52..82efbf56a4 100644 --- a/lib/bundler/self_manager.rb +++ b/lib/bundler/self_manager.rb @@ -7,13 +7,15 @@ module Bundler # class SelfManager def restart_with_locked_bundler_if_needed - return unless needs_switching? && installed? + 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 - return unless needs_switching? + restart_version = find_restart_version + return unless restart_version if restart_version == lockfile_version Bundler.ui.info \ @@ -29,8 +31,6 @@ module Bundler end def update_bundler_and_restart_with_it_if_needed(target) - return unless autoswitching_applies? - spec = resolve_update_version_from(target) return unless spec @@ -38,7 +38,7 @@ module Bundler Bundler.ui.info "Updating bundler to #{version}." - install(spec) + install(spec) unless installed?(version) restart_with(version) end @@ -63,52 +63,43 @@ module Bundler 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"] - # Bundler specs need some stuff to be required before Bundler starts - # running, for example, for faking the compact index API. However, these - # flags are lost when we reexec to a different version of Bundler. In the - # future, we may be able to properly reconstruct the original Ruby - # invocation (see https://bugs.ruby-lang.org/issues/6648), but for now - # there's no way to do it, so we need to be explicit about how to re-exec. - # This may be a feature end users request at some point, but maybe by that - # time, we have builtin tools to do. So for now, we use an undocumented - # ENV variable only for our specs. - bundler_spec_original_cmd = ENV["BUNDLER_SPEC_ORIGINAL_CMD"] - if bundler_spec_original_cmd - require "shellwords" - cmd = [*Shellwords.shellsplit(bundler_spec_original_cmd), *ARGV] - else - argv0 = File.exist?($PROGRAM_NAME) ? $PROGRAM_NAME : Process.argv0 - cmd = [argv0, *ARGV] - cmd.unshift(Gem.ruby) unless File.executable?(argv0) - end + 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, "GEM_PATH" => configured_gem_path, "BUNDLER_VERSION" => version.to_s }, + { + "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? + def needs_switching?(restart_version) autoswitching_applies? && - Bundler.settings[:version] != "system" && released?(restart_version) && - !running?(restart_version) && - !updating? + !running?(restart_version) end def autoswitching_applies? - ENV["BUNDLER_VERSION"].nil? && + (ENV["BUNDLER_VERSION"].nil? || ENV["BUNDLER_VERSION"].empty?) && ruby_can_restart_with_same_arguments? && - SharedHelpers.in_bundle? && lockfile_version end @@ -142,6 +133,7 @@ module Bundler 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? @@ -171,18 +163,14 @@ module Bundler $PROGRAM_NAME != "-e" end - def updating? - "update".start_with?(ARGV.first || " ") && ARGV[1..-1].any? {|a| a.start_with?("--bundler") } - end - - def installed? + def installed?(restart_version) Bundler.configure Bundler.rubygems.find_bundler(restart_version.to_s) end def current_version - @current_version ||= Gem::Version.new(Bundler::VERSION) + @current_version ||= Bundler.gem_version end def lockfile_version @@ -194,13 +182,16 @@ module Bundler @lockfile_version = nil end - def restart_version - return @restart_version if defined?(@restart_version) - # BUNDLE_VERSION=x.y.z - @restart_version = Gem::Version.new(Bundler.settings[:version]) - rescue ArgumentError - # BUNDLE_VERSION=lockfile - @restart_version = lockfile_version + 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 cde01e0181..fd77c2f7fc 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -7,13 +7,10 @@ module Bundler autoload :Validator, File.expand_path("settings/validator", __dir__) BOOL_KEYS = %w[ - allow_offline_install - auto_clean_without_path auto_install cache_all cache_all_platforms clean - default_install_uses_path deployment disable_checksum_validation disable_exec_load @@ -22,45 +19,32 @@ module Bundler disable_shared_gems disable_version_check 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 inline lockfile_checksums + no_build_extension no_install + no_install_plugin no_prune - path_relative_to_cwd path.system plugins prefer_patch - print_only_version_number - setup_makes_kernel_gem_public silence_deprecations silence_root_warning update_requires_all_flag - ].freeze - - REMEMBERED_KEYS = %w[ - bin - cache_all - clean - deployment - frozen - no_prune - path - shebang - path.system - without - with + verbose ].freeze NUMBER_KEYS = %w[ + cooldown jobs redirect retry @@ -78,14 +62,17 @@ module Bundler 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 @@ -99,6 +86,11 @@ module Bundler "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) @@ -130,12 +122,8 @@ module Bundler end def set_command_option(key, value) - if !is_remembered(key) || Bundler.feature_flag.forget_cli_options? - temporary(key => value) - value - else - set_local(key, value) - end + temporary(key => value) + value end def set_command_option_if_given(key, value) @@ -274,7 +262,7 @@ module Bundler def use_system_gems? return true if system_path return false if explicit_path - !Bundler.feature_flag.default_install_uses_path? + !Bundler.feature_flag.bundler_5_mode? end def base_path @@ -319,6 +307,10 @@ 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_config, @global_config].each do |settings| @@ -389,10 +381,6 @@ module Bundler ARRAY_KEYS.include?(self.class.key_to_s(key)) end - def is_remembered(key) - REMEMBERED_KEYS.include?(self.class.key_to_s(key)) - end - def is_credential(key) key == "gem.push_key" end @@ -496,7 +484,7 @@ module Bundler SharedHelpers.filesystem_access(config_file, :read) do |file| valid_file = file.exist? && !file.size.zero? return {} unless valid_file - serializer_class.load(file.read).inject({}) do |config, (k, v)| + (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!(".", "__") diff --git a/lib/bundler/settings/validator.rb b/lib/bundler/settings/validator.rb index 0a57ea7f03..70a0ca36d4 100644 --- a/lib/bundler/settings/validator.rb +++ b/lib/bundler/settings/validator.rb @@ -75,27 +75,11 @@ module Bundler end end - rule %w[path], "relative paths are expanded relative to the current working directory" do |key, value, settings| - next if value.nil? - - path = Pathname.new(value) - next if !path.relative? || !Bundler.feature_flag.path_relative_to_cwd? - - path = path.expand_path - - root = begin - Bundler.root - rescue GemfileNotFound - Pathname.pwd.expand_path - end - - path = begin - path.relative_path_from(root) - rescue ArgumentError - path - end - - set(settings, key, path.to_s) + 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 diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb index 9b398830f7..2aa8abe0a0 100644 --- a/lib/bundler/shared_helpers.rb +++ b/lib/bundler/shared_helpers.rb @@ -4,8 +4,6 @@ require_relative "version" require_relative "rubygems_integration" require_relative "current_ruby" -autoload :Pathname, "pathname" - module Bundler autoload :WINDOWS, File.expand_path("constants", __dir__) autoload :FREEBSD, File.expand_path("constants", __dir__) @@ -25,6 +23,9 @@ module Bundler end def default_lockfile + given = ENV["BUNDLE_LOCKFILE"] + return Pathname.new(given) if given && !given.empty? + gemfile = default_gemfile case gemfile.basename.to_s @@ -57,7 +58,7 @@ module Bundler def pwd Bundler.rubygems.ext_lock.synchronize do - Pathname.pwd + Dir.pwd end end @@ -104,7 +105,8 @@ module Bundler def filesystem_access(path, action = :write, &block) yield(path.dup) rescue Errno::EACCES => e - raise unless e.message.include?(path.to_s) || action == :create + path_basename = File.basename(path.to_s) + raise unless e.message.include?(path_basename) || action == :create raise PermissionError.new(path, action) rescue Errno::EAGAIN @@ -115,30 +117,27 @@ module Bundler raise NoSpaceOnDeviceError.new(path, action) 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 #{[:create, :write].include?(action) ? "creating" : "accessing"} `#{path}`.") end - def major_deprecation(major_version, message, removed_message: nil, print_caller_location: false) - if print_caller_location - caller_location = caller_locations(2, 2).first - suffix = " (called at #{caller_location.path}:#{caller_location.lineno})" - message += suffix - removed_message += suffix if removed_message - end + def feature_deprecated!(message) + return unless prints_major_deprecations? - bundler_major_version = Bundler.bundler_major_version - if bundler_major_version > major_version - require_relative "errors" - raise DeprecatedError, "[REMOVED] #{removed_message || message}" - end - - return unless bundler_major_version >= major_version && prints_major_deprecations? Bundler.ui.warn("[DEPRECATED] #{message}") end + 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) } @@ -302,6 +301,7 @@ module Bundler def set_bundle_variables 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 @@ -382,7 +382,6 @@ module Bundler end def prints_major_deprecations? - require_relative "../bundler" return false if Bundler.settings[:silence_deprecations] require_relative "deprecate" return false if Bundler::Deprecate.skip diff --git a/lib/bundler/similarity_detector.rb b/lib/bundler/similarity_detector.rb deleted file mode 100644 index 50e66b9cab..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 - - # https://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 232873503b..cf71be8801 100644 --- a/lib/bundler/source.rb +++ b/lib/bundler/source.rb @@ -31,6 +31,8 @@ module Bundler message end + def download(*); end + def can_lock?(spec) spec.source == self end @@ -79,7 +81,7 @@ module Bundler end def extension_cache_path(spec) - return unless Bundler.feature_flag.global_gem_cache? + 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, 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 fde05e472b..a002a2570a 100644 --- a/lib/bundler/source/git.rb +++ b/lib/bundler/source/git.rb @@ -191,8 +191,13 @@ module Bundler set_cache_path!(app_cache_path) if use_app_cache? if requires_checkout? && !@copied - fetch unless use_app_cache? - checkout + 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 @@ -238,7 +243,7 @@ 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 ||= if Bundler.feature_flag.global_gem_cache? + @cache_path ||= if Bundler.settings[:global_gem_cache] Bundler.user_cache else Bundler.bundle_path.join("cache", "bundler") @@ -268,7 +273,7 @@ module Bundler private def cache_to(custom_path, try_migrate: false) - return unless Bundler.feature_flag.cache_all? + return unless Bundler.settings[:cache_all] app_cache_path = app_cache_path(custom_path) @@ -360,7 +365,11 @@ module Bundler end def locked_revision_checked_out? - locked_revision && locked_revision == revision && install_path.exist? + locked_revision && locked_revision == revision && installed? + end + + def installed? + git_proxy.installed_to?(install_path) end def base_name @@ -412,7 +421,6 @@ module Bundler 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 diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb index a73e893829..8094dcaa9d 100644 --- a/lib/bundler/source/git/git_proxy.rb +++ b/lib/bundler/source/git/git_proxy.rb @@ -16,7 +16,7 @@ module Bundler def initialize(command) msg = String.new 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/rubygems/rubygems/issues/new?labels=Bundler&template=bundler-related-issue.md " + 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 @@ -57,6 +57,29 @@ module Bundler attr_accessor :path, :uri, :branch, :tag, :ref, :explicit_ref attr_writer :revision + 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 @@ -92,11 +115,11 @@ module Bundler end def version - @version ||= full_version.match(/((\.?\d+)+).*/)[1] + self.class.version end def full_version - @full_version ||= git_local("--version").sub(/git version\s*/, "").strip + self.class.full_version end def checkout @@ -121,7 +144,7 @@ module Bundler FileUtils.rm_rf(p) end git "clone", "--no-checkout", "--quiet", path.to_s, destination.to_s - File.chmod(((File.stat(destination).mode | 0o777) & ~File.umask), destination) + File.chmod((File.stat(destination).mode | 0o777) & ~File.umask, destination) rescue Errno::EEXIST => e file_path = e.message[%r{.*?((?:[a-zA-Z]:)?/.*)}, 1] raise GitError, "Bundler could not install a gem because it needs to " \ @@ -137,7 +160,7 @@ module Bundler git "fetch", "--force", "--quiet", *extra_fetch_args(ref), dir: destination end - git "reset", "--hard", @revision, dir: destination + git "reset", "--hard", revision, dir: destination if submodules git_retry "submodule", "update", "--init", "--recursive", dir: destination @@ -147,10 +170,16 @@ module Bundler 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", "--force", "--quiet", "--no-tags", *args, "--", configured_uri, refspec].compact + command = fetch_command(args) command_with_no_credentials = check_allowed(command) Bundler::Retry.new("`#{command_with_no_credentials}` at #{path}", [MissingGitRevisionError]).attempts do @@ -160,6 +189,11 @@ module Bundler 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 @@ -172,23 +206,22 @@ module Bundler FileUtils.mkdir_p(p) end - command = ["clone", "--bare", "--no-hardlinks", "--quiet", *extra_clone_args, "--", configured_uri, path.to_s] + 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") + 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 - idx = command.index("--depth") - if idx - command.delete_at(idx) - command.delete_at(idx) + if shallow? + clone_args -= depth_args + command = clone_command(clone_args) command_with_no_credentials = check_allowed(command) - - err += "Retrying without --depth argument." end raise GitCommandError.new(command_with_no_credentials, path, err) end @@ -197,14 +230,14 @@ module Bundler def clone_needs_unshallow? return false unless path.join("shallow").exist? - return true if full_clone? + return true unless shallow? @revision && @revision != head_revision end def extra_ref return false if not_pinned? - return true unless full_clone? + return true if shallow? ref.start_with?("refs/") end @@ -256,7 +289,7 @@ module Bundler end def not_pinned? - branch || tag || ref.nil? + branch_option || ref.nil? end def pinned_to_full_sha? @@ -298,8 +331,8 @@ module Bundler end def has_revision_cached? - return unless @revision && path.exist? - git("cat-file", "-e", @revision, dir: path) + return unless commit && path.exist? + git("cat-file", "-e", commit, dir: path) true rescue GitError false @@ -399,13 +432,14 @@ module Bundler end def capture3_args_for(cmd, dir) - return ["git", *cmd] unless 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"] - if Bundler.feature_flag.bundler_3_mode? || supports_minus_c? - ["git", "-C", dir.to_s, *cmd] - else - ["git", *cmd, { chdir: dir.to_s }] - end + return ["git", *opts, *cmd] unless dir + + ["git", "-C", dir.to_s, *opts, *cmd] end def extra_clone_args @@ -420,12 +454,20 @@ module Bundler # anyways. return args if @revision - args += ["--branch", branch || tag] if branch || tag + 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 [] if full_clone? + return [] unless shallow? ["--depth", depth.to_s] end @@ -436,12 +478,12 @@ module Bundler extra_args end - def full_clone? - depth.nil? + def branch_option + branch || tag end - def supports_minus_c? - @supports_minus_c ||= Gem::Version.new(version) >= Gem::Version.new("1.8.5") + def shallow? + !depth.nil? end def needs_allow_any_sha1_in_want? diff --git a/lib/bundler/source/metadata.rb b/lib/bundler/source/metadata.rb index fd959cd64e..ecf8895187 100644 --- a/lib/bundler/source/metadata.rb +++ b/lib/bundler/source/metadata.rb @@ -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 a24cd8159e..366a23aea7 100644 --- a/lib/bundler/source/path.rb +++ b/lib/bundler/source/path.rb @@ -24,7 +24,7 @@ module Bundler @path = Pathname.new(options["path"]) expanded_path = expand(@path) @path = if @path.relative? - expanded_path.relative_path_from(root_path.expand_path) + expanded_path.relative_path_from(File.expand_path(root_path)) else expanded_path end @@ -53,6 +53,8 @@ module Bundler "source at `#{@path}`" end + alias_method :identifier, :to_s + alias_method :to_gemfile, :path def hash @@ -60,8 +62,8 @@ module Bundler 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 @@ -81,7 +83,7 @@ module Bundler 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? @@ -124,11 +126,7 @@ module Bundler end def expand(somepath) - if Bundler.current_ruby.jruby? # TODO: Unify when https://github.com/rubygems/bundler/issues/7598 fixed upstream and all supported jrubies include the fix - somepath.expand_path(root_path).expand_path - else - somepath.expand_path(root_path) - end + somepath.expand_path(root_path) rescue ArgumentError => e Bundler.ui.debug(e) raise PathError, "There was an error while trying to use the path " \ @@ -167,6 +165,13 @@ module Bundler 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) @@ -215,10 +220,11 @@ module Bundler # Some gem authors put absolute paths in their gemspec # and we have to save them from themselves spec.files = spec.files.filter_map do |path| - next path unless /\A#{Pathname::SEPARATOR_PAT}/o.match?(path) + pathname = Pathname.new(path) + next path unless pathname.absolute? next if File.directory?(path) begin - Pathname.new(path).relative_path_from(gem_dir).to_s + pathname.relative_path_from(gem_dir).to_s rescue ArgumentError path end diff --git a/lib/bundler/source/path/installer.rb b/lib/bundler/source/path/installer.rb index 0af28fe770..39765e5da2 100644 --- a/lib/bundler/source/path/installer.rb +++ b/lib/bundler/source/path/installer.rb @@ -24,7 +24,7 @@ module Bundler def post_install run_hooks(:pre_install) - unless @disable_extensions + unless @disable_extensions || Bundler.settings[:no_build_extension] build_extensions run_hooks(:post_build) end diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index f1d6dcb3b9..ed864604fe 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -8,21 +8,26 @@ module Bundler autoload :Remote, File.expand_path("rubygems/remote", __dir__) # Ask for X gems per API request - API_REQUEST_SIZE = 50 + API_REQUEST_SIZE = 100 + REQUIRE_MUTEX = Mutex.new attr_accessor :remotes def initialize(options = {}) @options = options @remotes = [] + @remote_cooldowns = {} @dependency_names = [] @allow_remote = false @allow_cached = false @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 @@ -145,6 +150,13 @@ module Bundler # 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 @@ -158,66 +170,57 @@ module Bundler end end + backfill_created_at(index, remote_created_at) unless remote_created_at.empty? + index end end - def install(spec, options = {}) + def download(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 + return true end - if spec.remote - # Check for this spec from other sources - uris = [spec.remote, *remotes_for_spec(spec)].map(&:anonymized_uri).uniq - Installer.ambiguous_gems << [spec.name, *uris] if uris.length > 1 - end - - path = fetch_gem_if_possible(spec, options[:previous_spec]) - raise GemNotFound, "Could not find #{spec.file_name} for installation" unless path - - return if Bundler.settings[:no_install] - - install_path = rubygems_dir - bin_path = Bundler.system_bindir - - require_relative "../rubygems_gem_installer" - - installer = Bundler::RubyGemsGemInstaller.at( - path, - security_policy: Bundler.rubygems.security_policies[Bundler.settings["trust-policy"]], - install_dir: install_path.to_s, - bin_dir: bin_path.to_s, - ignore_dependencies: true, - wrappers: true, - env_shebang: true, - build_args: options[:build_args], - bundler_extension_cache_path: extension_cache_path(spec) - ) + installer = rubygems_gem_installer(spec, options) if spec.remote s = begin installer.spec rescue Gem::Package::FormatError - Bundler.rm_rf(path) + Bundler.rm_rf(installer.gem) raise rescue Gem::Security::Exception => e raise SecurityError, - "The gem #{File.basename(path, ".gem")} can't be installed because " \ + "The gem #{installer.gem} can't be installed because " \ "the security policy didn't allow it, with the message: #{e.message}" end spec.__swap__(s) end + spec + 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 + + return if Bundler.settings[:no_install] + + 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 = installer.install + installed_spec = nil + + Gem.time("Installed #{spec.name} in", 0, true) do + installed_spec = installer.install + end spec.full_gem_path = installed_spec.full_gem_path spec.loaded_from = installed_spec.loaded_from @@ -251,9 +254,14 @@ 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 cooldown_for(uri) + @remote_cooldowns[uri] end def spec_names @@ -274,7 +282,7 @@ module Bundler def remote_fetchers @remote_fetchers ||= remotes.to_h do |uri| - remote = Source::Rubygems::Remote.new(uri) + remote = Source::Rubygems::Remote.new(uri, cooldown: cooldown_for(uri)) [remote, Bundler::Fetcher.new(remote)] end.freeze end @@ -322,6 +330,13 @@ module Bundler @allow_remote && api_fetchers.any? end + def clear_cache + @specs = nil + @installed_specs = nil + @default_specs = nil + @cached_specs = nil + end + protected def remote_names @@ -332,13 +347,6 @@ module Bundler remotes.map(&method(:remove_auth)) end - def remotes_for_spec(spec) - specs.search_all(spec.name).inject([]) do |uris, s| - uris << s.remote if s.remote - uris - end - end - def cached_gem(spec) global_cache_path = download_cache_path(spec) caches << global_cache_path if global_cache_path @@ -454,7 +462,7 @@ module Bundler end def installed?(spec) - installed_specs[spec].any? && !spec.deleted_gem? + installed_specs[spec].any? && !spec.installation_missing? end def rubygems_dir @@ -471,6 +479,31 @@ module Bundler 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 @@ -491,7 +524,15 @@ module Bundler uri = spec.remote.uri Bundler.ui.confirm("Fetching #{version_message(spec, previous_spec)}") gem_remote_fetcher = remote_fetchers.fetch(spec.remote).gem_remote_fetcher - Bundler.rubygems.download_gem(spec, uri, download_cache_path, 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 # Returns the global cache path of the calling Rubygems::Source object. @@ -506,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) + 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 9c5c06de24..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. # @@ -28,10 +41,15 @@ module Bundler 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_digest = SharedHelpers.digest(:MD5).hexdigest(uri_parts.compact.join(".")) + 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 diff --git a/lib/bundler/source/rubygems_aggregate.rb b/lib/bundler/source/rubygems_aggregate.rb index 99ef81ad54..8aeaa375fa 100644 --- a/lib/bundler/source/rubygems_aggregate.rb +++ b/lib/bundler/source/rubygems_aggregate.rb @@ -5,9 +5,10 @@ module Bundler class RubygemsAggregate attr_reader :source_map, :sources - def initialize(sources, source_map) + def initialize(sources, source_map, excluded_sources = []) @sources = sources @source_map = source_map + @excluded_sources = excluded_sources @index = build_index end @@ -31,6 +32,8 @@ module Bundler 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! diff --git a/lib/bundler/source_list.rb b/lib/bundler/source_list.rb index b7b2c4209a..ab7002d6e5 100644 --- a/lib/bundler/source_list.rb +++ b/lib/bundler/source_list.rb @@ -9,7 +9,7 @@ module Bundler :metadata_source def global_rubygems_source - @global_rubygems_source ||= rubygems_aggregate_class.new("allow_local" => true) + @global_rubygems_source ||= source_class.new("allow_local" => true) end def initialize @@ -21,19 +21,9 @@ module Bundler @rubygems_sources = [] @metadata_source = Source::Metadata.new - @merged_gem_lockfile_sections = false @local_mode = true end - def merged_gem_lockfile_sections? - @merged_gem_lockfile_sections - end - - def merged_gem_lockfile_sections!(replacement_source) - @merged_gem_lockfile_sections = true - @global_rubygems_source = replacement_source - end - def aggregate_global_source? global_rubygems_source.multiple_remotes? end @@ -69,8 +59,8 @@ module Bundler add_source_to_list Plugin.source(source).new(options), @plugin_sources end - def add_global_rubygems_remote(uri) - global_rubygems_source.add_remote(uri) + def add_global_rubygems_remote(uri, cooldown: nil) + global_rubygems_source.add_remote(uri, cooldown: cooldown) global_rubygems_source end @@ -90,10 +80,6 @@ module Bundler @rubygems_sources end - def rubygems_remotes - rubygems_sources.flat_map(&:remotes).uniq - end - def all_sources path_sources + git_sources + plugin_sources + rubygems_sources + [metadata_source] end @@ -103,7 +89,7 @@ module Bundler end def get(source) - source_list_for(source).find {|s| equivalent_source?(source, s) } + source_list_for(source).find {|s| s.include?(source) } end def lock_sources @@ -115,11 +101,7 @@ module Bundler end def lock_rubygems_sources - if merged_gem_lockfile_sections? - [combine_rubygems_sources] - else - rubygems_sources.sort_by(&:identifier) - end + rubygems_sources.sort_by(&:identifier) end # Returns true if there are changes @@ -129,16 +111,7 @@ module Bundler @rubygems_sources, @path_sources, @git_sources, @plugin_sources = map_sources(replacement_sources) @global_rubygems_source = global_replacement_source(replacement_sources) - different_sources?(lock_sources, replacement_sources) - end - - # Returns true if there are changes - def expired_sources?(replacement_sources) - return false if replacement_sources.empty? - - lock_sources = dup_with_replaced_sources(replacement_sources).lock_sources - - different_sources?(lock_sources, replacement_sources) + !equivalent_sources?(lock_sources, replacement_sources) end def prefer_local! @@ -163,54 +136,66 @@ module Bundler all_sources.each(&:remote!) end - private - - def dup_with_replaced_sources(replacement_sources) - new_source_list = dup - new_source_list.replace_sources!(replacement_sources) - new_source_list + 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) || source + replace_rubygems_source(replacement_sources, source) end git, plugin = [@git_sources, @plugin_sources].map do |sources| sources.map do |source| - replacement_sources.find {|s| s == source } || source + replace_source(replacement_sources, source) end end path = @path_sources.map do |source| - replacement_sources.find {|s| s == (source.is_a?(Source::Gemspec) ? source.as_path_source : source) } || source + replace_path_source(replacement_sources, source) end [rubygems, path, git, plugin] end def global_replacement_source(replacement_sources) - replacement_source = replace_rubygems_source(replacement_sources, global_rubygems_source) - return global_rubygems_source unless replacement_source - - replacement_source.local! - replacement_source + 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 + + def replace_source(replacement_sources, gemfile_source) replacement_source = replacement_sources.find {|s| s == gemfile_source } - return unless replacement_source + return gemfile_source unless replacement_source + + replacement_source = yield(replacement_source) if block_given? - # locked sources never include credentials so always prefer remotes from the gemfile - replacement_source.remotes = gemfile_source.remotes replacement_source end - def different_sources?(lock_sources, replacement_sources) - !equivalent_sources?(lock_sources, replacement_sources) + 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 rubygems_aggregate_class + def source_class Source::Rubygems end @@ -229,10 +214,6 @@ 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"] @@ -247,9 +228,5 @@ module Bundler def equivalent_sources?(lock_sources, replacement_sources) lock_sources.sort_by(&:identifier) == replacement_sources.sort_by(&:identifier) end - - def equivalent_source?(source, other_source) - source == other_source - end end end diff --git a/lib/bundler/source_map.rb b/lib/bundler/source_map.rb index ca73e01f9d..513eb37f8b 100644 --- a/lib/bundler/source_map.rb +++ b/lib/bundler/source_map.rb @@ -14,24 +14,25 @@ module Bundler direct_requirements.reject {|_, source| source == skip }.keys end - def all_requirements + def all_requirements(excluded_sources = []) requirements = direct_requirements.dup - unmet_deps = sources.non_default_explicit_sources.map do |source| + 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 - no_ambiguous_sources = Bundler.feature_flag.bundler_3_mode? - msg = ["The gem '#{indirect_dependency_name}' was found in multiple relevant sources."] msg.concat [previous_source, source].map {|s| " * #{s}" }.sort - msg << "You #{no_ambiguous_sources ? :must : :should} add this gem to the source block for the source you wish it to be installed from." + 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 if no_ambiguous_sources - Bundler.ui.warn "Warning: #{msg}" + raise SecurityError, msg end end diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb index f0082d46d5..ae5e5cbaa9 100644 --- a/lib/bundler/spec_set.rb +++ b/lib/bundler/spec_set.rb @@ -11,16 +11,11 @@ module Bundler @specs = specs end - def for(dependencies, platforms_or_legacy_check = [nil], legacy_platforms = [nil], skips: []) - platforms = if [true, false].include?(platforms_or_legacy_check) - Bundler::SharedHelpers.major_deprecation 2, + 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", - print_caller_location: true - - legacy_platforms - else - platforms_or_legacy_check + "SpecSet#for always implicitly performs validation. Please remove this parameter" end materialize_dependencies(dependencies, platforms, skips: skips) @@ -29,9 +24,10 @@ module Bundler end def normalize_platforms!(deps, platforms) - complete_platforms = add_extra_platforms!(platforms) + remove_invalid_platforms!(deps, platforms) + add_extra_platforms!(platforms) - complete_platforms.map do |platform| + platforms.map! do |platform| next platform if platform == Gem::Platform::RUBY begin @@ -44,26 +40,48 @@ module Bundler next platform if incomplete_for_platform?(deps, less_specific_platform) less_specific_platform - end.uniq + 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 + + invalid_platforms end def add_extra_platforms!(platforms) - return platforms.concat([Gem::Platform::RUBY]).uniq if @specs.empty? + 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 GemHelpers.generic(platform) == Gem::Platform::RUBY + next unless Gem::Platform.generic(platform) == Gem::Platform::RUBY complete_platform(platform) end - return platforms if new_platforms.empty? + return if new_platforms.empty? 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 - - platforms end def validate_deps(s) @@ -83,15 +101,13 @@ module Bundler end def []=(key, value) - @specs << value + delete_by_name(key) - reset! + add_spec(value) end def delete(specs) - Array(specs).each {|spec| @specs.delete(spec) } - - reset! + Array(specs).each {|spec| remove_spec(spec) } end def sort! @@ -117,20 +133,22 @@ module Bundler def materialized_for_all_platforms @specs.map do |s| next s unless s.is_a?(LazySpecification) - s.source.remote! - spec = s.materialize_strictly + spec = s.materialize_for_cache raise GemNotFound, "Could not find #{s.full_name} in any of the sources" unless spec spec end end def incomplete_for_platform?(deps, platform) - return false if @specs.empty? + 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.any? + validation_set.incomplete_specs end def missing_specs_for(deps) @@ -156,11 +174,11 @@ module Bundler end def -(other) - SpecSet.new(to_a - other.to_a) + 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) @@ -169,12 +187,14 @@ module Bundler def delete_by_name(name) @specs.reject! {|spec| spec.name == name } + @sorted&.reject! {|spec| spec.name == name } + return if @lookup.nil? - reset! + @lookup[name] = nil end def version_for(name) - self[name].first&.version + exemplary_spec(name)&.version end def what_required(spec) @@ -185,7 +205,7 @@ module Bundler end def <<(spec) - @specs << spec + SharedHelpers.feature_removed! "SpecSet#<< has been removed with no replacement" end def length @@ -212,6 +232,10 @@ module Bundler 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: []) @@ -245,23 +269,30 @@ module Bundler @materializations.filter_map(&:materialized_spec) end - def reset! - @sorted = nil - @lookup = nil - 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 = GemHelpers.select_best_platform_match(matching_specs, platform).find do |s| - valid?(s) + 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 - new_specs << LazySpecification.from_spec(platform_spec) unless specs.include?(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 @@ -269,9 +300,7 @@ module Bundler end if valid_platform && new_specs.any? - @specs.concat(new_specs) - - reset! + new_specs.each {|spec| add_spec(spec) } end valid_platform @@ -282,8 +311,13 @@ module Bundler end def additional_variants_from(other) - other.select do |spec| - version_for(spec.name) == spec.version && valid_dependencies?(spec) + 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 @@ -292,15 +326,12 @@ module Bundler 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[0]}' or gem '#{cgems[1]}' and try again." - end + @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) @@ -311,8 +342,7 @@ module Bundler @lookup ||= begin lookup = {} @specs.each do |s| - lookup[s.name] ||= [] - lookup[s.name] << s + index_spec(lookup, s.name, s) end lookup end @@ -333,5 +363,40 @@ module Bundler 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/stub_specification.rb b/lib/bundler/stub_specification.rb index 026f753d41..b353642b40 100644 --- a/lib/bundler/stub_specification.rb +++ b/lib/bundler/stub_specification.rb @@ -52,6 +52,7 @@ module Bundler # 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 diff --git a/lib/bundler/templates/Executable b/lib/bundler/templates/Executable index 9ff6f00898..b085c24da6 100644 --- a/lib/bundler/templates/Executable +++ b/lib/bundler/templates/Executable @@ -10,17 +10,6 @@ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("<%= relative_gemfile_path %>", __dir__) -bundle_binstub = File.expand_path("bundle", __dir__) - -if File.file?(bundle_binstub) - if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") - load(bundle_binstub) - else - abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. -Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") - end -end - require "rubygems" require "bundler/setup" diff --git a/lib/bundler/templates/Executable.bundler b/lib/bundler/templates/Executable.bundler deleted file mode 100644 index caa2021701..0000000000 --- a/lib/bundler/templates/Executable.bundler +++ /dev/null @@ -1,109 +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.match?(Gem::Version::ANCHORED_VERSION_PATTERN) - bundler_version = a - end - next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ - bundler_version = $1 - 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 %>", __dir__) - end - - def lockfile - lockfile = - case File.basename(gemfile) - when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") - 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_requirement - @bundler_requirement ||= - env_var_version || - cli_arg_version || - bundler_requirement_for(lockfile_version) - end - - def bundler_requirement_for(version) - return "#{Gem::Requirement.default}.a" unless version - - bundler_gem_version = Gem::Version.new(version) - - bundler_gem_version.approximate_recommendation - end - - def load_bundler! - ENV["BUNDLE_GEMFILE"] ||= gemfile - - activate_bundler - end - - def activate_bundler - gem_error = activation_error_handling do - gem "bundler", bundler_requirement - 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_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) - warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" - 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/newgem/CODE_OF_CONDUCT.md.tt b/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt index 67fe8cee79..633baebdd5 100644 --- a/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt +++ b/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt @@ -1,132 +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.): -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, caste, color, 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. -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall - community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or advances of - any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, - without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders 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, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official email address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -[INSERT CONTACT METHOD]. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of -actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or permanent -ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the -community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.1, available at -[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. - -Community Impact Guidelines were inspired by -[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. - -For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at -[https://www.contributor-covenant.org/translations][translations]. - -[homepage]: https://www.contributor-covenant.org -[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html -[Mozilla CoC]: https://github.com/mozilla/diversity -[FAQ]: https://www.contributor-covenant.org/faq -[translations]: https://www.contributor-covenant.org/translations +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 index f5a460c9bb..cd00f97e5a 100644 --- a/lib/bundler/templates/newgem/Cargo.toml.tt +++ b/lib/bundler/templates/newgem/Cargo.toml.tt @@ -5,3 +5,9 @@ [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 de82a63c5f..85dc593b8f 100644 --- a/lib/bundler/templates/newgem/Gemfile.tt +++ b/lib/bundler/templates/newgem/Gemfile.tt @@ -5,19 +5,20 @@ source "https://rubygems.org" # Specify your gem's dependencies in <%= config[:name] %>.gemspec gemspec -gem "rake", "~> 13.0" +gem "irb" +gem "rake", ">= 13.0" <%- if config[:ext] -%> gem "rake-compiler" <%- end -%> <%- if config[:test] -%> -gem "<%= config[:test] %>", "~> <%= config[:test_framework_version] %>" +gem "<%= config[:test] %>" <%- end -%> <%- if config[:linter] == "rubocop" -%> -gem "rubocop", "~> <%= config[:linter_version] %>" +gem "rubocop" <%- elsif config[:linter] == "standard" -%> -gem "standard", "~> <%= config[:linter_version] %>" +gem "standard" <%- end -%> diff --git a/lib/bundler/templates/newgem/Rakefile.tt b/lib/bundler/templates/newgem/Rakefile.tt index 172183d4b4..83f10009c7 100644 --- a/lib/bundler/templates/newgem/Rakefile.tt +++ b/lib/bundler/templates/newgem/Rakefile.tt @@ -59,6 +59,11 @@ Rake::ExtensionTask.new("<%= config[:underscored_name] %>", GEMSPEC) do |ext| end <% end -%> +<% if config[:ext] == "go" -%> +require "go_gem/rake_task" + +GoGem::RakeTask.new("<%= config[:underscored_name] %>") +<% end -%> <% end -%> <% if default_task_names.size == 1 -%> task default: <%= default_task_names.first.inspect %> diff --git a/lib/bundler/templates/newgem/circleci/config.yml.tt b/lib/bundler/templates/newgem/circleci/config.yml.tt index f40f029bf1..c4dd9d0647 100644 --- a/lib/bundler/templates/newgem/circleci/config.yml.tt +++ b/lib/bundler/templates/newgem/circleci/config.yml.tt @@ -7,6 +7,10 @@ jobs: 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' -%> @@ -17,6 +21,14 @@ jobs: 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: | diff --git a/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt b/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt index 0ebce0e4a0..a06166aee7 100644 --- a/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt +++ b/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt @@ -12,4 +12,11 @@ publish = false crate-type = ["cdylib"] [dependencies] -magnus = { version = "0.6.2" } +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-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/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.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 index ba234529a3..09ce97682d 100644 --- a/lib/bundler/templates/newgem/ext/newgem/src/lib.rs.tt +++ b/lib/bundler/templates/newgem/ext/newgem/src/lib.rs.tt @@ -1,7 +1,7 @@ use magnus::{function, prelude::*, Error, Ruby}; -fn hello(subject: String) -> String { - format!("Hello from Rust, {subject}!") +pub fn hello(subject: String) -> String { + format!("Hello {subject}, from Rust!") } #[magnus::init] @@ -10,3 +10,14 @@ fn init(ruby: &Ruby) -> Result<(), Error> { 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 index d1b5ae0534..cc8f04dd33 100644 --- a/lib/bundler/templates/newgem/github/workflows/main.yml.tt +++ b/lib/bundler/templates/newgem/github/workflows/main.yml.tt @@ -7,6 +7,9 @@ on: pull_request: +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest @@ -17,7 +20,9 @@ jobs: - '<%= RUBY_VERSION %>' steps: - - uses: actions/checkout@v4 + - 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 @@ -33,5 +38,11 @@ jobs: 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/gitlab-ci.yml.tt b/lib/bundler/templates/newgem/gitlab-ci.yml.tt index d2e1f33736..adbd70cbc0 100644 --- a/lib/bundler/templates/newgem/gitlab-ci.yml.tt +++ b/lib/bundler/templates/newgem/gitlab-ci.yml.tt @@ -6,6 +6,11 @@ default: - 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 @@ -14,5 +19,9 @@ example_job: 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 caf6e32f4a..3aedee0d25 100644 --- a/lib/bundler/templates/newgem/lib/newgem.rb.tt +++ b/lib/bundler/templates/newgem/lib/newgem.rb.tt @@ -2,7 +2,7 @@ require_relative "<%= File.basename(config[:namespaced_path]) %>/version" <%- if config[:ext] -%> -require_relative "<%= File.basename(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| -%> diff --git a/lib/bundler/templates/newgem/newgem.gemspec.tt b/lib/bundler/templates/newgem/newgem.gemspec.tt index ced300f379..1ab1c28f46 100644 --- a/lib/bundler/templates/newgem/newgem.gemspec.tt +++ b/lib/bundler/templates/newgem/newgem.gemspec.tt @@ -10,20 +10,23 @@ Gem::Specification.new do |spec| spec.summary = "TODO: Write a short summary, because RubyGems requires one." spec.description = "TODO: Write a longer description or delete this line." - spec.homepage = "TODO: Put your gem's website or public repo URL here." + spec.homepage = "<%= config[:homepage_uri] %>" <%- if config[:mit] -%> spec.license = "MIT" <%- end -%> spec.required_ruby_version = ">= <%= config[:required_ruby_version] %>" -<%- if config[:ext] == 'rust' -%> - spec.required_rubygems_version = ">= <%= config[:rust_builder_required_rubygems_version] %>" -<%- end -%> - 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"] = "TODO: Put your gem's public repo URL here." - spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." + spec.metadata["source_code_uri"] = "<%= config[:source_code_uri] %>" +<%- if config[:changelog] -%> + spec.metadata["changelog_uri"] = "<%= config[:changelog_uri] %>" +<%- 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" # 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. @@ -31,22 +34,25 @@ Gem::Specification.new do |spec| 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[bin/ test/ spec/ features/ .git <%= config[:ci_config_path] %>appveyor Gemfile]) + f.start_with?(*%w[<%= config[:ignore_paths].join(" ") %>]) end end spec.bindir = "exe" spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] -<%- if config[:ext] == 'c' || config[:ext] == 'rust' -%> +<%- if %w(c rust go).include?(config[:ext]) -%> spec.extensions = ["ext/<%= config[:underscored_name] %>/extconf.rb"] <%- end -%> # Uncomment to register a new dependency of your gem - # spec.add_dependency "example-gem", "~> 1.0" + # spec.add_dependency "example-gem", ">= 1.0" <%- if config[:ext] == 'rust' -%> - spec.add_dependency "rb_sys", "~> 0.9.91" + spec.add_dependency "rb_sys", ">= 0.9.128" +<%- end -%> +<%- 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://bundler.io/guides/creating_gem.html + # guide at: https://guides.rubygems.org/make-your-own-gem/ end diff --git a/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt b/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt index 82cada988c..7c6cde170b 100644 --- a/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt +++ b/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt @@ -5,7 +5,15 @@ RSpec.describe <%= config[:constant_name] %> 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/test/minitest/test_newgem.rb.tt b/lib/bundler/templates/newgem/test/minitest/test_newgem.rb.tt index 4b35a63071..844d3aff81 100644 --- a/lib/bundler/templates/newgem/test/minitest/test_newgem.rb.tt +++ b/lib/bundler/templates/newgem/test/minitest/test_newgem.rb.tt @@ -7,7 +7,13 @@ class <%= config[:minitest_constant_name] %> < Minitest::Test 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/ui/shell.rb b/lib/bundler/ui/shell.rb index 6df1512a5b..b836208da8 100644 --- a/lib/bundler/ui/shell.rb +++ b/lib/bundler/ui/shell.rb @@ -17,6 +17,7 @@ module Bundler @level = ENV["DEBUG"] ? "debug" : "info" @warning_history = [] @output_stream = :stdout + @thread_safe_logger_key = "logger_level_#{object_id}" end def add_color(string, *color) @@ -80,11 +81,11 @@ 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?(msg) @@ -97,11 +98,13 @@ 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) @@ -167,12 +170,13 @@ module Bundler 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 - @level = original + Thread.current.thread_variable_set(@thread_safe_logger_key, old_level) end def with_output_stream(symbol) diff --git a/lib/bundler/vendor/connection_pool/.document b/lib/bundler/vendor/connection_pool/.document deleted file mode 100644 index 0c43bbd6b3..0000000000 --- a/lib/bundler/vendor/connection_pool/.document +++ /dev/null @@ -1 +0,0 @@ -# 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 index 317088a866..e8aaf70016 100644 --- a/lib/bundler/vendor/connection_pool/lib/connection_pool.rb +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool.rb @@ -39,7 +39,7 @@ end # - :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} + DEFAULTS = {size: 5, timeout: 5, auto_reload_after_fork: true}.freeze def self.wrap(options, &block) Wrapper.new(options, &block) @@ -99,10 +99,14 @@ class Bundler::ConnectionPool @available = TimedStack.new(@size, &block) @key = :"pool-#{@available.object_id}" @key_count = :"pool-#{@available.object_id}-count" - INSTANCES[self] = self if INSTANCES + @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 @@ -116,20 +120,65 @@ class Bundler::ConnectionPool 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) + ::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 - @available.push(::Thread.current[@key]) + 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 @@ -146,7 +195,6 @@ class Bundler::ConnectionPool # 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 @@ -155,11 +203,16 @@ class Bundler::ConnectionPool # 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 @@ -169,6 +222,11 @@ class Bundler::ConnectionPool 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" 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 index 35d1d7cc35..026d2c5be2 100644 --- a/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb @@ -1,8 +1,8 @@ ## # The TimedStack manages a pool of homogeneous connections (or any resource -# you wish to manage). Connections are created lazily up to a given maximum +# you wish to manage). Connections are created lazily up to a given maximum # number. - +# # Examples: # # ts = TimedStack.new(1) { MyConnection.new } @@ -16,14 +16,12 @@ # 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 @@ -35,12 +33,12 @@ class Bundler::ConnectionPool::TimedStack end ## - # Returns +obj+ to the stack. +options+ is ignored in TimedStack but may be + # 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 @@ -52,14 +50,16 @@ class Bundler::ConnectionPool::TimedStack 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 + # 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. # - # +:timeout+ is the only checked entry in +options+ and is preferred over - # the +timeout+ argument (which will be removed in a future release). Other - # options may be used by subclasses that extend TimedStack. - + # @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 @@ -68,13 +68,22 @@ class Bundler::ConnectionPool::TimedStack @mutex.synchronize do loop do raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block - return fetch_connection(options) if connection_stored?(options) + if (conn = try_fetch_connection(options)) + return conn + end connection = try_create(options) return connection if connection to_wait = deadline - current_time - raise Bundler::ConnectionPool::TimeoutError, "Waited #{timeout} sec, #{length}/#{@max} available" if to_wait <= 0 + 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 @@ -85,7 +94,6 @@ class Bundler::ConnectionPool::TimedStack # 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 @@ -99,19 +107,49 @@ class Bundler::ConnectionPool::TimedStack end ## - # Returns +true+ if there are no available connections. + # 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 @@ -121,8 +159,17 @@ class Bundler::ConnectionPool::TimedStack ## # 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. + # 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 @@ -131,31 +178,48 @@ class Bundler::ConnectionPool::TimedStack # 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 + @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 connection_stored?(options) - conn = fetch_connection(options) + while (conn = try_fetch_connection(options)) + @created -= 1 unless @created == 0 @shutdown_block.call(conn) end - @created = 0 end ## # This is an extension point for TimedStack and is called with a mutex. # - # This method must return +obj+ to the stack. + # 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 + @que.push [obj, current_time] end ## @@ -163,7 +227,6 @@ class Bundler::ConnectionPool::TimedStack # # 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 diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb index 384d6fc977..2e9eebdbb6 100644 --- a/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb @@ -1,3 +1,3 @@ class Bundler::ConnectionPool - VERSION = "2.4.1" + VERSION = "2.5.5" end diff --git a/lib/bundler/vendor/fileutils/.document b/lib/bundler/vendor/fileutils/.document deleted file mode 100644 index 0c43bbd6b3..0000000000 --- a/lib/bundler/vendor/fileutils/.document +++ /dev/null @@ -1 +0,0 @@ -# Vendored files do not need to be documented diff --git a/lib/bundler/vendor/fileutils/lib/fileutils.rb b/lib/bundler/vendor/fileutils/lib/fileutils.rb index 4d7619ddcf..a11fdc7176 100644 --- a/lib/bundler/vendor/fileutils/lib/fileutils.rb +++ b/lib/bundler/vendor/fileutils/lib/fileutils.rb @@ -181,7 +181,7 @@ end # module Bundler::FileUtils # The version number. - VERSION = "1.7.3" + VERSION = "1.8.0" def self.private_module_function(name) #:nodoc: module_function name @@ -706,11 +706,12 @@ module Bundler::FileUtils # 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, noop: noop, verbose: verbose) + return ln_sr(src, dest, force: force, target_directory: target_directory, noop: noop, verbose: verbose) end - fu_output_message "ln -s#{force ? 'f' : ''} #{[src,dest].flatten.join ' '}" if verbose + 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 @@ -730,42 +731,37 @@ module Bundler::FileUtils # 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) - options = "#{force ? 'f' : ''}#{target_directory ? '' : 'T'}" - dest = File.path(dest) - srcs = Array(src) - link = proc do |s, target_dir_p = true| - s = File.path(s) - if target_dir_p - d = File.join(destdirs = dest, File.basename(s)) + 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 = File.dirname(d = dest) + destdirs ||= fu_split_path(dest) + real_ddirs ||= fu_split_path(File.realdirpath(dest)) end - destdirs = fu_split_path(File.realpath(destdirs)) - if fu_starting_path?(s) - srcdirs = fu_split_path((File.realdirpath(s) rescue File.expand_path(s))) - base = fu_relative_components_from(srcdirs, destdirs) - s = File.join(*base) + 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 - srcdirs = fu_clean_components(*fu_split_path(s)) - base = fu_relative_components_from(fu_split_path(Dir.pwd), destdirs) - while srcdirs.first&. == ".." and base.last&.!=("..") and !fu_starting_path?(base.last) - srcdirs.shift - base.pop - end - s = File.join(*base, *srcdirs) + 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 - fu_output_message "ln -s#{options} #{s} #{d}" if verbose + 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 - case srcs.size - when 0 - when 1 - link[srcs[0], target_directory && File.directory?(dest)] - else - srcs.each(&link) - end end module_function :ln_sr @@ -800,13 +796,13 @@ module Bundler::FileUtils # File.file?('dest1/dir1/t2.txt') # => true # File.file?('dest1/dir1/t3.txt') # => true # - # Keyword arguments: + # Optional arguments: # - # - <tt>dereference_root: true</tt> - dereferences +src+ if it is a symbolic link. - # - <tt>remove_destination: true</tt> - removes +dest+ before creating links. + # - +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 keyword argument <tt>remove_destination: true</tt> is not given. + # and optional argument +remove_destination+ is not given. # # Related: Bundler::FileUtils.ln (has different options). # @@ -1029,12 +1025,12 @@ module Bundler::FileUtils # directories, and symbolic links; # other file types (FIFO streams, device files, etc.) are not supported. # - # Keyword arguments: + # Optional arguments: # - # - <tt>dereference_root: true</tt> - if +src+ is a symbolic link, - # follows the link. - # - <tt>preserve: true</tt> - preserves file times. - # - <tt>remove_destination: true</tt> - removes +dest+ before copying files. + # - +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]. # @@ -1065,12 +1061,12 @@ module Bundler::FileUtils # Bundler::FileUtils.copy_file('src0.txt', 'dest0.txt') # File.file?('dest0.txt') # => true # - # Keyword arguments: + # Optional arguments: # - # - <tt>dereference: false</tt> - if +src+ is a symbolic link, - # does not follow the link. - # - <tt>preserve: true</tt> - preserves file times. - # - <tt>remove_destination: true</tt> - removes +dest+ before copying files. + # - +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]. # @@ -1491,7 +1487,8 @@ module Bundler::FileUtils # 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 @@ -2475,6 +2472,10 @@ module Bundler::FileUtils 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, (target_directory ? File.join(dest, File.basename(s)) : dest) @@ -2509,7 +2510,11 @@ module Bundler::FileUtils path = File.path(path) list = [] until (parent, base = File.split(path); parent == path or parent == ".") - list << base + if base != '..' and list.last == '..' and !(fu_have_symlink? && File.symlink?(path)) + list.pop + else + list << base + end path = parent end list << path @@ -2517,14 +2522,14 @@ module Bundler::FileUtils end private_module_function :fu_split_path - def fu_relative_components_from(target, base) #:nodoc: + def fu_common_components(target, base) #:nodoc: i = 0 while target[i]&.== base[i] i += 1 end - Array.new(base.size-i, '..').concat(target[i..-1]) + i end - private_module_function :fu_relative_components_from + private_module_function :fu_common_components def fu_clean_components(*comp) #:nodoc: comp.shift while comp.first == "." @@ -2534,7 +2539,7 @@ module Bundler::FileUtils while c = comp.shift if c == ".." and clean.last != ".." and !(fu_have_symlink? && File.symlink?(path)) clean.pop - path.chomp!(%r((?<=\A|/)[^/]+/\z), "") + path.sub!(%r((?<=\A|/)[^/]+/\z), "") else clean << c path << c << "/" diff --git a/lib/bundler/vendor/net-http-persistent/.document b/lib/bundler/vendor/net-http-persistent/.document deleted file mode 100644 index 0c43bbd6b3..0000000000 --- a/lib/bundler/vendor/net-http-persistent/.document +++ /dev/null @@ -1 +0,0 @@ -# Vendored files do not need to be documented 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 cfc0f48197..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,6 +1,10 @@ require_relative '../../../../../vendored_net_http' require_relative '../../../../../vendored_uri' -require 'cgi' # for escaping +begin + require 'cgi/escape' +rescue LoadError + require 'cgi/util' # for escaping +end require_relative '../../../../connection_pool/lib/connection_pool' autoload :OpenSSL, 'openssl' @@ -42,9 +46,8 @@ autoload :OpenSSL, 'openssl' # # 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 Gem::URI#request_uri not Gem::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 # # == TLS/SSL # @@ -60,6 +63,7 @@ autoload :OpenSSL, 'openssl' # #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 @@ -176,7 +180,7 @@ class Gem::Net::HTTP::Persistent ## # The version of Gem::Net::HTTP::Persistent you are using - VERSION = '4.0.4' + VERSION = '4.0.6' ## # Error class for errors raised by Gem::Net::HTTP::Persistent. Various @@ -268,6 +272,11 @@ class Gem::Net::HTTP::Persistent 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 @@ -587,6 +596,21 @@ class Gem::Net::HTTP::Persistent reconnect_ssl end + 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 + + 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 + ## # Creates a new connection for +uri+ @@ -605,47 +629,49 @@ class Gem::Net::HTTP::Persistent connection = @pool.checkout net_http_args - http = connection.http + begin + http = connection.http - connection.ressl @ssl_generation if - connection.ssl_generation != @ssl_generation + connection.ressl @ssl_generation if + connection.ssl_generation != @ssl_generation - if not http.started? then - ssl http if use_ssl - start http - elsif expired? connection then - reset connection - end + if not http.started? then + ssl http if use_ssl + start http + elsif expired? connection then + reset connection + end - 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=) + 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 - 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 + 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 - 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 + raise Error, "host down: #{address}:#{port}" + ensure + @pool.checkin net_http_args end - - raise Error, "host down: #{address}:#{port}" - ensure - @pool.checkin net_http_args end ## @@ -782,7 +808,7 @@ class Gem::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 @@ -953,7 +979,8 @@ class Gem::Net::HTTP::Persistent end ## - # Shuts down all connections + # Shuts down all connections. Attempting to checkout a connection after + # shutdown will raise an error. # # *NOTE*: Calling shutdown for can be dangerous! # @@ -965,6 +992,17 @@ class Gem::Net::HTTP::Persistent end ## + # Discard all existing connections. Subsequent checkouts will create + # new connections as needed. + # + # If any thread is still using a connection it may cause an error! Call + # #reload when you are completely done making requests! + + def reload + @pool.reload { |http| http.finish } + end + + ## # Enables SSL on +connection+ def ssl connection @@ -1021,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 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 index 214804fcd9..034fbe39b8 100644 --- 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 @@ -63,7 +63,8 @@ class Gem::Net::HTTP::Persistent::TimedStackMulti < Bundler::ConnectionPool::Tim if @created >= @max && @enqueued >= 1 oldest, = @lru.first @lru.delete oldest - @ques[oldest].pop + connection = @ques[oldest].pop + connection.close if connection.respond_to?(:close) @created -= 1 end diff --git a/lib/bundler/vendor/pub_grub/.document b/lib/bundler/vendor/pub_grub/.document deleted file mode 100644 index 0c43bbd6b3..0000000000 --- a/lib/bundler/vendor/pub_grub/.document +++ /dev/null @@ -1 +0,0 @@ -# Vendored files do not need to be documented 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 index dce20d37ad..491151ec0b 100644 --- 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 @@ -79,29 +79,17 @@ module Bundler::PubGrub dependencies_for(@root_package, @root_version) end - # Override me (maybe) - # - # If not overridden, the order returned by all_versions_for will be used - # - # Returns: Array of versions in preferred order - def sort_versions_by_preferred(package, sorted_versions) - indexes = @version_indexes[package] - sorted_versions.sort_by { |version| indexes[version] } - end - def initialize @root_package = Package.root @root_version = Package.root_version - @cached_versions = Hash.new do |h,k| + @sorted_versions = Hash.new do |h,k| if k == @root_package h[k] = [@root_version] else - h[k] = all_versions_for(k) + h[k] = all_versions_for(k).sort end end - @sorted_versions = Hash.new { |h,k| h[k] = @cached_versions[k].sort } - @version_indexes = Hash.new { |h,k| h[k] = @cached_versions[k].each.with_index.to_h } @cached_dependencies = Hash.new do |packages, package| if package == @root_package @@ -117,15 +105,7 @@ module Bundler::PubGrub end def versions_for(package, range=VersionRange.any) - versions = range.select_versions(@sorted_versions[package]) - - # Conditional avoids (among other things) calling - # sort_versions_by_preferred with the root package - if versions.size > 1 - sort_versions_by_preferred(package, versions) - else - versions - end + range.select_versions(@sorted_versions[package]) end def no_versions_incompatibility_for(_package, unsatisfied_term) @@ -164,7 +144,7 @@ module Bundler::PubGrub sorted_versions[high] end - range = VersionRange.new(min: low, max: high, include_min: true) + range = VersionRange.new(min: low, max: high, include_min: !low.nil?) self_constraint = VersionConstraint.new(package, range: range) 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/version_range.rb b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_range.rb index 8d73c3f7b5..49dcf716a3 100644 --- a/lib/bundler/vendor/pub_grub/lib/pub_grub/version_range.rb +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_range.rb @@ -76,6 +76,9 @@ module Bundler::PubGrub 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 @@ -311,10 +314,19 @@ module Bundler::PubGrub 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 - intersects?(other) || - (min == other.max && (include_min || other.include_max)) || - (max == other.min && (include_max || other.include_min)) + def contiguous_above?(other) + other.contiguous_below?(self) end def allows_all?(other) @@ -375,15 +387,15 @@ module Bundler::PubGrub 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) + low = -> { VersionRange.new(max: min, include_max: !include_min) } + high = -> { VersionRange.new(min: max, include_min: !include_max) } if !min - high + high.call elsif !max - low + low.call else - low.union(high) + low.call.union(high.call) 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 index 4caf6b355b..000923e99a 100644 --- a/lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb +++ b/lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb @@ -2,17 +2,20 @@ 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, logger: Bundler::PubGrub.logger) + 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| @@ -36,26 +39,25 @@ module Bundler::PubGrub # Returns true if there is more work to be done, false otherwise def work - return false if solved? - - next_package = choose_package_version - propagate(next_package) - - if solved? + 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 - false - else - true + return false end + + next_package = choose_package_version_from(unsatisfied_terms) + propagate(next_package) + + true end def solve - work until solved? + while work; end solution.decisions end @@ -105,29 +107,15 @@ module Bundler::PubGrub unsatisfied.package end - def next_package_to_try - solution.unsatisfied.min_by do |term| - package = term.package - range = term.constraint.range - matching_versions = source.versions_for(package, range) - higher_versions = source.versions_for(package, range.upper_invert) + def choose_package_version_from(unsatisfied_terms) + remaining = unsatisfied_terms.map { |t| [t.package, t.constraint.range] }.to_h - [matching_versions.count <= 1 ? 0 : 1, higher_versions.count] - end.package - end - - def choose_package_version - if solution.unsatisfied.empty? - logger.info "No packages unsatisfied. Solving complete!" - return nil - end + package, version = strategy.next_package_and_version(remaining) - package = next_package_to_try - unsatisfied_term = solution.unsatisfied.find { |t| t.package == package } - version = source.versions_for(package, unsatisfied_term.constraint.range).first 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 diff --git a/lib/bundler/vendor/securerandom/.document b/lib/bundler/vendor/securerandom/.document deleted file mode 100644 index 0c43bbd6b3..0000000000 --- a/lib/bundler/vendor/securerandom/.document +++ /dev/null @@ -1 +0,0 @@ -# Vendored files do not need to be documented diff --git a/lib/bundler/vendor/thor/.document b/lib/bundler/vendor/thor/.document deleted file mode 100644 index 0c43bbd6b3..0000000000 --- a/lib/bundler/vendor/thor/.document +++ /dev/null @@ -1 +0,0 @@ -# Vendored files do not need to be documented diff --git a/lib/bundler/vendor/thor/lib/thor.rb b/lib/bundler/vendor/thor/lib/thor.rb index bfd9f5c914..945bdbd551 100644 --- a/lib/bundler/vendor/thor/lib/thor.rb +++ b/lib/bundler/vendor/thor/lib/thor.rb @@ -625,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) 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 bccfbb6b85..d8c9863054 100644 --- a/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb +++ b/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb @@ -242,6 +242,35 @@ 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 @@ -267,11 +296,7 @@ class Bundler::Thor 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. Preserves indentation before @@ -348,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 @@ -357,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/parser/options.rb b/lib/bundler/vendor/thor/lib/thor/parser/options.rb index 734f5fe7e3..fe22d989e5 100644 --- a/lib/bundler/vendor/thor/lib/thor/parser/options.rb +++ b/lib/bundler/vendor/thor/lib/thor/parser/options.rb @@ -144,7 +144,7 @@ class Bundler::Thor def check_exclusive! opts = @assigns.keys # When option A and B are exclusive, if A and B are given at the same time, - # the diffrence of argument array size will decrease. + # 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}'"} diff --git a/lib/bundler/vendor/thor/lib/thor/runner.rb b/lib/bundler/vendor/thor/lib/thor/runner.rb index c7cc873131..f0ce6df96c 100644 --- a/lib/bundler/vendor/thor/lib/thor/runner.rb +++ b/lib/bundler/vendor/thor/lib/thor/runner.rb @@ -1,9 +1,8 @@ require_relative "../thor" require_relative "group" -require "yaml" require "digest/sha2" -require "pathname" +require "pathname" unless defined?(Pathname) class Bundler::Thor::Runner < Bundler::Thor #:nodoc: map "-T" => :list, "-i" => :install, "-u" => :update, "-v" => :version @@ -195,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 diff --git a/lib/bundler/vendor/thor/lib/thor/shell/basic.rb b/lib/bundler/vendor/thor/lib/thor/shell/basic.rb index b3e85733cb..da02b94227 100644 --- a/lib/bundler/vendor/thor/lib/thor/shell/basic.rb +++ b/lib/bundler/vendor/thor/lib/thor/shell/basic.rb @@ -314,7 +314,7 @@ class Bundler::Thor 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}") @@ -372,16 +372,12 @@ class Bundler::Thor Tempfile.open([File.basename(destination), File.extname(destination)], File.dirname(destination)) do |temp| temp.write content temp.rewind - system %(#{merge_tool} "#{temp.path}" "#{destination}") + system(merge_tool, temp.path, destination) end end def merge_tool #:nodoc: - @merge_tool ||= ENV["THOR_MERGE"] || git_merge_tool - end - - def git_merge_tool #:nodoc: - `git config merge.tool`.rstrip rescue "" + @merge_tool ||= ENV["THOR_MERGE"] || "git difftool --no-index" end end end diff --git a/lib/bundler/vendor/thor/lib/thor/version.rb b/lib/bundler/vendor/thor/lib/thor/version.rb index cd7b4f060e..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 = "1.3.2" + VERSION = "1.4.0" end diff --git a/lib/bundler/vendor/tsort/.document b/lib/bundler/vendor/tsort/.document deleted file mode 100644 index 0c43bbd6b3..0000000000 --- a/lib/bundler/vendor/tsort/.document +++ /dev/null @@ -1 +0,0 @@ -# Vendored files do not need to be documented diff --git a/lib/bundler/vendor/uri/.document b/lib/bundler/vendor/uri/.document deleted file mode 100644 index 0c43bbd6b3..0000000000 --- a/lib/bundler/vendor/uri/.document +++ /dev/null @@ -1 +0,0 @@ -# Vendored files do not need to be documented diff --git a/lib/bundler/vendor/uri/lib/uri/common.rb b/lib/bundler/vendor/uri/lib/uri/common.rb index 7cf81d27f6..38339119c5 100644 --- a/lib/bundler/vendor/uri/lib/uri/common.rb +++ b/lib/bundler/vendor/uri/lib/uri/common.rb @@ -13,19 +13,26 @@ 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 @@ -40,15 +47,15 @@ module Bundler::URI end self.parser = RFC3986_PARSER - def self.const_missing(const) + 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 RFC2396_PARSER.regexp[#{const.inspect}] explicitly.", uplevel: 1 if $VERBOSE + 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 RFC2396_Parser::#{const} explicitly.", uplevel: 1 if $VERBOSE + warn "Bundler::URI::#{const} is obsolete. Use Bundler::URI::RFC2396_Parser::#{const} explicitly.", uplevel: 1 if $VERBOSE value else super @@ -87,7 +94,41 @@ module Bundler::URI module_function :make_components_hash end - module Schemes + 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 @@ -100,7 +141,7 @@ module Bundler::URI # Note that after calling String#upcase on +scheme+, it must be a valid # constant name. def self.register_scheme(scheme, klass) - Schemes.const_set(scheme.to_s.upcase, klass) + Schemes.register(scheme, klass) end # Returns a hash of the defined schemes: @@ -118,14 +159,14 @@ module Bundler::URI # # Related: Bundler::URI.register_scheme. def self.scheme_list - Schemes.constants.map { |name| - [name.to_s.upcase, Schemes.const_get(name)] - }.to_h + 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+: @@ -144,12 +185,10 @@ module Bundler::URI # # => #<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 = scheme.to_s.upcase + const_name = Schemes.escape(scheme) uri_class = INITIAL_SCHEMES[const_name] - uri_class ||= if /\A[A-Z]\w*\z/.match?(const_name) && Schemes.const_defined?(const_name, false) - Schemes.const_get(const_name, false) - end + uri_class ||= Schemes.find(const_name) uri_class ||= default return uri_class.new(scheme, *arguments) @@ -191,7 +230,7 @@ module Bundler::URI # ["fragment", "top"]] # def self.split(uri) - DEFAULT_PARSER.split(uri) + PARSER.split(uri) end # Returns a new \Bundler::URI object constructed from the given string +uri+: @@ -201,11 +240,11 @@ module Bundler::URI # 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 ::escape string +uri+ + # It's recommended to first Bundler::URI::RFC2396_PARSER.escape string +uri+ # if it may contain invalid Bundler::URI characters. # def self.parse(uri) - DEFAULT_PARSER.parse(uri) + PARSER.parse(uri) end # Merges the given Bundler::URI strings +str+ @@ -261,7 +300,7 @@ module Bundler::URI # def self.extract(str, schemes = nil, &block) # :nodoc: warn "Bundler::URI.extract is obsolete", uplevel: 1 if $VERBOSE - DEFAULT_PARSER.extract(str, schemes, &block) + PARSER.extract(str, schemes, &block) end # @@ -298,14 +337,14 @@ module Bundler::URI # def self.regexp(schemes = nil)# :nodoc: warn "Bundler::URI.regexp is obsolete", uplevel: 1 if $VERBOSE - DEFAULT_PARSER.make_regexp(schemes) + PARSER.make_regexp(schemes) end TBLENCWWWCOMP_ = {} # :nodoc: 256.times do |i| TBLENCWWWCOMP_[-i.chr] = -('%%%02X' % i) end - TBLENCURICOMP_ = TBLENCWWWCOMP_.dup.freeze + TBLENCURICOMP_ = TBLENCWWWCOMP_.dup.freeze # :nodoc: TBLENCWWWCOMP_[' '] = '+' TBLENCWWWCOMP_.freeze TBLDECWWWCOMP_ = {} # :nodoc: @@ -403,6 +442,8 @@ module Bundler::URI _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 @@ -417,6 +458,8 @@ module Bundler::URI 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) @@ -855,6 +898,7 @@ 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> @@ -862,6 +906,8 @@ module Bundler # 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 diff --git a/lib/bundler/vendor/uri/lib/uri/file.rb b/lib/bundler/vendor/uri/lib/uri/file.rb index de347af6eb..21dd9ee535 100644 --- a/lib/bundler/vendor/uri/lib/uri/file.rb +++ b/lib/bundler/vendor/uri/lib/uri/file.rb @@ -47,7 +47,7 @@ module Bundler::URI # :path => '/ruby/src'}) # uri2.to_s # => "file://host.example.com/ruby/src" # - # uri3 = Bundler::URI::File.build({:path => Bundler::URI::escape('/path/my file.txt')}) + # 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) diff --git a/lib/bundler/vendor/uri/lib/uri/generic.rb b/lib/bundler/vendor/uri/lib/uri/generic.rb index 8b097fba99..30dab60903 100644 --- a/lib/bundler/vendor/uri/lib/uri/generic.rb +++ b/lib/bundler/vendor/uri/lib/uri/generic.rb @@ -73,7 +73,7 @@ module Bundler::URI # # 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::Escape.escape all Bundler::URI components and tries again. + # then it does Bundler::URI::RFC2396_PARSER.escape all Bundler::URI components and tries again. # def self.build2(args) begin @@ -126,9 +126,9 @@ module Bundler::URI end end else - component = self.class.component rescue ::Bundler::URI::Generic::COMPONENT + component = self.component rescue ::Bundler::URI::Generic::COMPONENT raise ArgumentError, - "expected Array of or Hash of components of #{self.class} (#{component.join(', ')})" + "expected Array of or Hash of components of #{self} (#{component.join(', ')})" end tmp << nil @@ -186,18 +186,18 @@ module Bundler::URI if arg_check self.scheme = scheme - self.userinfo = userinfo 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_userinfo(userinfo) self.set_host(host) self.set_port(port) + self.set_userinfo(userinfo) self.set_path(path) self.query = query self.set_opaque(opaque) @@ -284,7 +284,7 @@ module Bundler::URI # Returns the parser to be used. # - # Unless a Bundler::URI::Parser is defined, DEFAULT_PARSER is used. + # Unless the +parser+ is defined, DEFAULT_PARSER is used. # def parser if !defined?(@parser) || !@parser @@ -315,7 +315,7 @@ module Bundler::URI end # - # Checks the scheme +v+ component against the Bundler::URI::Parser Regexp for :SCHEME. + # Checks the scheme +v+ component against the +parser+ Regexp for :SCHEME. # def check_scheme(v) if v && parser.regexp[:SCHEME] !~ v @@ -385,7 +385,7 @@ module Bundler::URI # # Checks the user +v+ component for RFC2396 compliance - # and against the Bundler::URI::Parser Regexp for :USERINFO. + # and against the +parser+ Regexp for :USERINFO. # # Can not have a registry or opaque component defined, # with a user component defined. @@ -409,7 +409,7 @@ module Bundler::URI # # Checks the password +v+ component for RFC2396 compliance - # and against the Bundler::URI::Parser Regexp for :USERINFO. + # and against the +parser+ Regexp for :USERINFO. # # Can not have a registry or opaque component defined, # with a user component defined. @@ -511,7 +511,7 @@ module Bundler::URI user, password = split_userinfo(user) end @user = user - @password = password if password + @password = password [@user, @password] end @@ -522,7 +522,7 @@ module Bundler::URI # See also Bundler::URI::Generic.user=. # def set_user(v) - set_userinfo(v, @password) + set_userinfo(v, nil) v end protected :set_user @@ -574,6 +574,12 @@ module Bundler::URI @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 @@ -586,7 +592,7 @@ module Bundler::URI # # Checks the host +v+ component for RFC2396 compliance - # and against the Bundler::URI::Parser Regexp for :HOST. + # and against the +parser+ Regexp for :HOST. # # Can not have a registry or opaque component defined, # with a host component defined. @@ -615,6 +621,13 @@ module Bundler::URI 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 # @@ -639,6 +652,7 @@ module Bundler::URI def host=(v) check_host(v) set_host(v) + set_userinfo(nil) v end @@ -675,7 +689,7 @@ module Bundler::URI # # Checks the port +v+ component for RFC2396 compliance - # and against the Bundler::URI::Parser Regexp for :PORT. + # and against the +parser+ Regexp for :PORT. # # Can not have a registry or opaque component defined, # with a port component defined. @@ -729,6 +743,7 @@ module Bundler::URI def port=(v) check_port(v) set_port(v) + set_userinfo(nil) port end @@ -737,18 +752,18 @@ module Bundler::URI end private :check_registry - def set_registry(v) #:nodoc: + def set_registry(v) # :nodoc: raise InvalidURIError, "cannot set registry" end protected :set_registry - def registry=(v) + def registry=(v) # :nodoc: raise InvalidURIError, "cannot set registry" end # # Checks the path +v+ component for RFC2396 compliance - # and against the Bundler::URI::Parser Regexp + # and against the +parser+ Regexp # for :ABS_PATH and :REL_PATH. # # Can not have a opaque component defined, @@ -853,7 +868,7 @@ module Bundler::URI # # Checks the opaque +v+ component for RFC2396 compliance and - # against the Bundler::URI::Parser Regexp for :OPAQUE. + # against the +parser+ Regexp for :OPAQUE. # # Can not have a host, port, user, or path component defined, # with an opaque component defined. @@ -905,7 +920,7 @@ module Bundler::URI end # - # Checks the fragment +v+ component against the Bundler::URI::Parser Regexp for :FRAGMENT. + # Checks the fragment +v+ component against the +parser+ Regexp for :FRAGMENT. # # # == Args @@ -1121,7 +1136,7 @@ module Bundler::URI base = self.dup - authority = rel.userinfo || rel.host || rel.port + authority = rel.authority # RFC2396, Section 5.2, 2) if (rel.path.nil? || rel.path.empty?) && !authority && !rel.query @@ -1133,17 +1148,14 @@ module Bundler::URI base.fragment=(nil) # RFC2396, Section 5.2, 4) - if !authority - base.set_path(merge_path(base.path, rel.path)) if base.path && rel.path - else - # RFC2396, Section 5.2, 4) - base.set_path(rel.path) if rel.path + 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.set_userinfo(rel.userinfo) if rel.userinfo - base.set_host(rel.host) if rel.host - base.set_port(rel.port) if rel.port base.query = rel.query if rel.query base.fragment=(rel.fragment) if rel.fragment @@ -1392,10 +1404,12 @@ module Bundler::URI 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 && @@ -1438,7 +1452,7 @@ module Bundler::URI end end - def inspect + def inspect # :nodoc: "#<#{self.class} #{self}>" end @@ -1526,7 +1540,7 @@ module Bundler::URI else unless proxy_uri = env[name] if proxy_uri = env[name.upcase] - warn 'The environment variable HTTP_PROXY is discouraged. Use http_proxy.', uplevel: 1 + warn 'The environment variable HTTP_PROXY is discouraged. Please use http_proxy instead.', uplevel: 1 end end end diff --git a/lib/bundler/vendor/uri/lib/uri/http.rb b/lib/bundler/vendor/uri/lib/uri/http.rb index 6c34a469b7..9b217ee266 100644 --- a/lib/bundler/vendor/uri/lib/uri/http.rb +++ b/lib/bundler/vendor/uri/lib/uri/http.rb @@ -61,6 +61,18 @@ module Bundler::URI 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 # diff --git a/lib/bundler/vendor/uri/lib/uri/rfc2396_parser.rb b/lib/bundler/vendor/uri/lib/uri/rfc2396_parser.rb index c199f911c4..522113fe67 100644 --- a/lib/bundler/vendor/uri/lib/uri/rfc2396_parser.rb +++ b/lib/bundler/vendor/uri/lib/uri/rfc2396_parser.rb @@ -67,7 +67,7 @@ module Bundler::URI # # == Synopsis # - # Bundler::URI::Parser.new([opts]) + # Bundler::URI::RFC2396_Parser.new([opts]) # # == Args # @@ -86,7 +86,7 @@ module Bundler::URI # # == Examples # - # p = Bundler::URI::Parser.new(:ESCAPED => "(?:%[a-fA-F0-9]{2}|%u[a-fA-F0-9]{4})") + # 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 # @@ -108,12 +108,12 @@ module Bundler::URI # The Hash of patterns. # - # See also Bundler::URI::Parser.initialize_pattern. + # See also #initialize_pattern. attr_reader :pattern # The Hash of Regexp. # - # See also Bundler::URI::Parser.initialize_regexp. + # See also #initialize_regexp. attr_reader :regexp # Returns a split Bundler::URI against +regexp[:ABS_URI]+. @@ -202,8 +202,7 @@ module Bundler::URI # # == Usage # - # p = Bundler::URI::Parser.new - # p.parse("ldap://ldap.example.com/dc=example?user=john") + # 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) @@ -244,7 +243,7 @@ module Bundler::URI # If no +block+ given, then returns the result, # else it calls +block+ for each element in result. # - # See also Bundler::URI::Parser.make_regexp. + # See also #make_regexp. # def extract(str, schemes = nil) if block_given? @@ -263,7 +262,7 @@ module Bundler::URI unless schemes @regexp[:ABS_URI_REF] else - /(?=#{Regexp.union(*schemes)}:)#{@pattern[:X_ABS_URI]}/x + /(?=(?i:#{Regexp.union(*schemes).source}):)#{@pattern[:X_ABS_URI]}/x end end @@ -321,14 +320,14 @@ module Bundler::URI str.gsub(escaped) { [$&[1, 2]].pack('H2').force_encoding(enc) } end - @@to_s = Kernel.instance_method(:to_s) - if @@to_s.respond_to?(:bind_call) - def inspect - @@to_s.bind_call(self) + 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 - @@to_s.bind(self).call + def inspect # :nodoc: + TO_S.bind(self).call end end @@ -524,6 +523,8 @@ module Bundler::URI 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 diff --git a/lib/bundler/vendor/uri/lib/uri/version.rb b/lib/bundler/vendor/uri/lib/uri/version.rb index 4f4207f3d0..ad76308e81 100644 --- a/lib/bundler/vendor/uri/lib/uri/version.rb +++ b/lib/bundler/vendor/uri/lib/uri/version.rb @@ -1,6 +1,6 @@ module Bundler::URI # :stopdoc: - VERSION_CODE = '010002'.freeze - VERSION = VERSION_CODE.scan(/../).collect{|n| n.to_i}.join('.').freeze + VERSION = '1.1.1'.freeze + VERSION_CODE = VERSION.split('.').map{|s| s.rjust(2, '0')}.join.freeze # :startdoc: end diff --git a/lib/bundler/version.rb b/lib/bundler/version.rb index fa24b4966e..ca7bb0719a 100644 --- a/lib/bundler/version.rb +++ b/lib/bundler/version.rb @@ -1,13 +1,21 @@ # frozen_string_literal: false module Bundler - VERSION = "2.7.0.dev".freeze + VERSION = "4.1.0.dev".freeze def self.bundler_major_version - @bundler_major_version ||= VERSION.split(".").first.to_i + @bundler_major_version ||= gem_version.segments.first end 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/vlad.rb b/lib/bundler/vlad.rb index 6179d0e4eb..c3a3d949a6 100644 --- a/lib/bundler/vlad.rb +++ b/lib/bundler/vlad.rb @@ -1,17 +1,4 @@ # frozen_string_literal: true require_relative "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_relative "deployment" - -include Rake::DSL if defined? Rake::DSL - -namespace :vlad do - Bundler::Deployment.define_task(Rake::RemoteTask, :remote_task, roles: :app) -end +Bundler::SharedHelpers.feature_removed! "The Bundler task for Vlad" diff --git a/lib/bundler/worker.rb b/lib/bundler/worker.rb index 3ebd6f01db..77f4f004aa 100644 --- a/lib/bundler/worker.rb +++ b/lib/bundler/worker.rb @@ -22,6 +22,7 @@ module Bundler def initialize(size, name, func) @name = name @request_queue = Thread::Queue.new + @request_queue_with_priority = Thread::Queue.new @response_queue = Thread::Queue.new @func = func @size = size @@ -32,9 +33,10 @@ module Bundler # 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 @@ -52,7 +54,13 @@ module Bundler 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 @@ -88,7 +96,7 @@ module Bundler @threads = Array.new(@size) do |i| Thread.start { process_queue(i) }.tap do |thread| - thread.name = "#{name} Worker ##{i}" if thread.respond_to?(:name=) + thread.name = "#{name} Worker ##{i}" end rescue ThreadError => e creation_errors << e |
