diff --git a/NEWS.md b/NEWS.md index 038f1a63c6a67b..8699bc0bff3faa 100644 --- a/NEWS.md +++ b/NEWS.md @@ -88,6 +88,7 @@ releases. * 4.0.0 to [v4.0.1][openssl-v4.0.1], [v4.0.2][openssl-v4.0.2] * prism 1.9.0 * 1.7.0 to [v1.8.0][prism-v1.8.0], [v1.8.1][prism-v1.8.1], [v1.9.0][prism-v1.9.0] +* psych 5.4.0 * resolv 0.7.1 * 0.7.0 to [v0.7.1][resolv-v0.7.1] * stringio 3.2.1.dev diff --git a/array.c b/array.c index 08d8d17c90f79f..6720406d89b2f9 100644 --- a/array.c +++ b/array.c @@ -30,6 +30,7 @@ #include "ruby/thread.h" #include "ruby/util.h" #include "ruby/ractor.h" +#include "shape.h" #include "vm_core.h" #include "builtin.h" @@ -909,6 +910,7 @@ init_fake_ary_flags(void) struct RArray fake_ary = {0}; fake_ary.basic.flags = T_ARRAY; VALUE ary = (VALUE)&fake_ary; + RBASIC_SET_SHAPE_ID(ary, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER); rb_ary_freeze(ary); return fake_ary.basic.flags; } @@ -2682,6 +2684,12 @@ ary_enum_length(VALUE ary, VALUE args, VALUE eobj) return rb_ary_length(ary); } +// These array primitives enable tight compatibility with the C implementation +// in terms of what method calls happen. They can use unchecked utilities such as +// FIX2LONG since unlike userland Ruby code, these methods cannot be traced with +// TracePoint (or ruby/debug.h APIs) and have their local variables changed from +// underneath them. + // Return true if the index is at or past the end of the array. VALUE rb_jit_ary_at_end(rb_execution_context_t *ec, VALUE self, VALUE index) diff --git a/benchmark/string_coderange_scan.yml b/benchmark/string_coderange_scan.yml new file mode 100644 index 00000000000000..d47bbd2b30313c --- /dev/null +++ b/benchmark/string_coderange_scan.yml @@ -0,0 +1,10 @@ +prelude: | + def unknown(s) = s.b.force_encoding("UTF-8") + multibyte = unknown("\u{00e9}" * 16384) # best case: every byte non-ASCII + alternating = unknown("\u{00e9}a" * 10922) # worst case: non-ASCII then ASCII + ascii = unknown("a" * 32768) # baseline + +benchmark: + coderange_multibyte: multibyte.dup.valid_encoding? + coderange_alternating: alternating.dup.valid_encoding? + coderange_ascii: ascii.dup.valid_encoding? diff --git a/class.c b/class.c index 69194ccab23756..02078cc9bc8baf 100644 --- a/class.c +++ b/class.c @@ -585,7 +585,15 @@ class_alloc0(enum ruby_value_type type, VALUE klass, bool boxable) VALUE flags = type | FL_SHAREABLE; if (boxable) flags |= RCLASS_BOXABLE; - NEWOBJ_OF(obj, struct RClass, klass, flags, alloc_size); + shape_id_t shape_id = ROOT_SHAPE_ID; + if (boxable) { + shape_id |= SHAPE_ID_LAYOUT_OTHER; + } + else { + shape_id |= SHAPE_ID_LAYOUT_RCLASS; + } + + struct RClass *obj = (struct RClass *)rb_newobj(GET_EC(), klass, flags, shape_id, true, alloc_size); obj->object_id = 0; diff --git a/depend b/depend index e61b6856704712..a2e8312298e7ea 100644 --- a/depend +++ b/depend @@ -80,6 +80,7 @@ array.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h array.$(OBJEXT): $(top_srcdir)/internal/serial.h array.$(OBJEXT): $(top_srcdir)/internal/set_table.h array.$(OBJEXT): $(top_srcdir)/internal/static_assert.h +array.$(OBJEXT): $(top_srcdir)/internal/struct.h array.$(OBJEXT): $(top_srcdir)/internal/variable.h array.$(OBJEXT): $(top_srcdir)/internal/vm.h array.$(OBJEXT): $(top_srcdir)/internal/warnings.h diff --git a/ext/psych/lib/psych/versions.rb b/ext/psych/lib/psych/versions.rb index 4c7a80d5c84274..6c1679bf65bd00 100644 --- a/ext/psych/lib/psych/versions.rb +++ b/ext/psych/lib/psych/versions.rb @@ -2,7 +2,7 @@ module Psych # The version of Psych you are using - VERSION = '5.3.1' + VERSION = '5.4.0' if RUBY_ENGINE == 'jruby' DEFAULT_SNAKEYAML_VERSION = '2.10'.freeze diff --git a/ext/psych/psych_parser.c b/ext/psych/psych_parser.c index d973496284f1d6..00a2207b58911c 100644 --- a/ext/psych/psych_parser.c +++ b/ext/psych/psych_parser.c @@ -32,9 +32,20 @@ static int io_reader(void * data, unsigned char *buf, size_t size, size_t *read) *read = 0; if(! NIL_P(string)) { - void * str = (void *)StringValuePtr(string); - *read = (size_t)RSTRING_LEN(string); - memcpy(buf, str, *read); + char * str = StringValuePtr(string); + size_t len = (size_t)RSTRING_LEN(string); + + /* IO#read(size) is documented to return at most `size` bytes, but a + * misbehaving IO-like object may return more. Clamp the copy to the + * buffer libyaml gave us to avoid writing past its end, rounding down + * to a character boundary so a multibyte character is never split. */ + if(len > size) { + rb_encoding * enc = rb_enc_get(string); + len = (size_t)(rb_enc_left_char_head(str, str + size, str + len, enc) - str); + } + + *read = len; + memcpy(buf, str, len); } return 1; diff --git a/gc.c b/gc.c index f0ec0f79efe692..2772f956387bfd 100644 --- a/gc.c +++ b/gc.c @@ -79,7 +79,6 @@ #undef LIST_HEAD /* ccan/list conflicts with BSD-origin sys/queue.h. */ #include "constant.h" -#include "darray.h" #include "debug_counter.h" #include "eval_intern.h" #include "gc/gc.h" @@ -1056,7 +1055,15 @@ rb_newobj(rb_execution_context_t *ec, VALUE klass, VALUE flags, shape_id_t shape VALUE rb_ec_newobj_of(rb_execution_context_t *ec, VALUE klass, VALUE flags, size_t size) { - return rb_newobj(ec, klass, flags, ROOT_SHAPE_ID, true, size); + VALUE type = flags & T_MASK; + RUBY_ASSERT(type != T_OBJECT); + RUBY_ASSERT(type != T_DATA); + RUBY_ASSERT(type != T_CLASS); + RUBY_ASSERT(type != T_MODULE); + RUBY_ASSERT(type != T_ICLASS); + (void)type; + + return rb_newobj(ec, klass, flags, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, true, size); } VALUE @@ -1068,13 +1075,13 @@ rb_newobj_of_with_shape(VALUE klass, VALUE flags, shape_id_t shape_id, size_t si VALUE rb_newobj_of(VALUE klass, VALUE flags, size_t size) { - return rb_newobj(GET_EC(), klass, flags, ROOT_SHAPE_ID, true, size); + return rb_newobj(GET_EC(), klass, flags, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, true, size); } static VALUE class_allocate_complex_instance(VALUE klass, uint32_t capacity) { - shape_id_t initial_shape_id = rb_shape_root(rb_gc_heap_id_for_size(sizeof(struct RObject))); + shape_id_t initial_shape_id = rb_shape_id_with_robject_layout(rb_shape_root(rb_gc_heap_id_for_size(sizeof(struct RObject)))); VALUE obj = rb_newobj_of_with_shape(klass, T_OBJECT, initial_shape_id, sizeof(struct RObject)); rb_obj_init_complex(obj, rb_st_init_numtable_with_size(capacity)); return obj; @@ -1099,7 +1106,7 @@ rb_class_allocate_instance(VALUE klass) // There might be a NEWOBJ tracepoint callback, and it may set fields. // So the shape must be passed to `NEWOBJ_OF`. - obj = rb_newobj_of_with_shape(klass, T_OBJECT, rb_shape_root(rb_gc_heap_id_for_size(size)), size); + obj = rb_newobj_of_with_shape(klass, T_OBJECT, rb_shape_id_with_robject_layout(rb_shape_root(rb_gc_heap_id_for_size(size))), size); #if RUBY_DEBUG VALUE *ptr = ROBJECT_FIELDS(obj); @@ -1149,7 +1156,7 @@ typed_data_alloc(VALUE klass, VALUE typed_flag, void *datap, const rb_data_type_ RBIMPL_NONNULL_ARG(type); if (klass) rb_data_object_check(klass); bool wb_protected = (type->flags & RUBY_FL_WB_PROTECTED) || !type->function.dmark; - VALUE obj = rb_newobj(GET_EC(), klass, T_DATA, ROOT_SHAPE_ID, wb_protected, size); + VALUE obj = rb_newobj(GET_EC(), klass, T_DATA, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_RDATA, wb_protected, size); rb_gc_register_pinning_obj(obj); @@ -4188,7 +4195,11 @@ vm_weak_table_gen_fields_foreach(st_data_t key, st_data_t value, st_data_t data) break; case ST_DELETE: - RBASIC_SET_SHAPE_ID((VALUE)key, ROOT_SHAPE_ID); + // When we're removing an object from the weak ref table, we need to + // set the shape on it so that the GC finalizer won't try to remove + // it again. A "root shape" indicates to the GC that this object + // has no fields on it, hence it won't be in the gen fields table. + RBASIC_SET_SHAPE_ID((VALUE)key, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER); return ST_DELETE; case ST_REPLACE: { diff --git a/imemo.c b/imemo.c index 3448a8dcd3c54f..796e078c89565f 100644 --- a/imemo.c +++ b/imemo.c @@ -141,7 +141,11 @@ rb_imemo_fields_new(VALUE owner, shape_id_t shape_id, bool shareable) size_t embedded_size = offsetof(struct rb_fields, as.embed) + capa * sizeof(VALUE); RUBY_ASSERT(rb_gc_size_allocatable_p(embedded_size)); VALUE fields = rb_imemo_new(imemo_fields, owner, embedded_size, shareable); - RBASIC_SET_SHAPE_ID(fields, shape_id); + // imemo fields objects should always have "RObject" layout. The + // layout in the shape describes the layout of the thing on which it is set. + // Imemo fields have the same layout as robject, therefore the layout + // should reflect that fact. + RBASIC_SET_SHAPE_ID(fields, rb_shape_id_with_robject_layout(shape_id)); RUBY_ASSERT(IMEMO_TYPE_P(fields, imemo_fields)); return fields; } @@ -152,7 +156,11 @@ rb_imemo_fields_new_complex(VALUE owner, shape_id_t shape_id, size_t capa, bool VALUE fields = rb_imemo_new(imemo_fields, owner, sizeof(struct rb_fields), shareable); IMEMO_OBJ_FIELDS(fields)->as.complex.table = st_init_numtable_with_size(capa); FL_SET_RAW(fields, OBJ_FIELD_HEAP); - RBASIC_SET_SHAPE_ID(fields, shape_id); + // imemo fields objects should always have "RObject" layout. The + // layout in the shape describes the layout of the thing on which it is set. + // Imemo fields have the same layout as robject, therefore the layout + // should reflect that fact. + RBASIC_SET_SHAPE_ID(fields, rb_shape_id_with_robject_layout(shape_id)); return fields; } @@ -177,7 +185,11 @@ rb_imemo_fields_new_complex_tbl(VALUE owner, shape_id_t shape_id, st_table *tbl, VALUE fields = rb_imemo_new(imemo_fields, owner, sizeof(struct rb_fields), shareable); IMEMO_OBJ_FIELDS(fields)->as.complex.table = tbl; FL_SET_RAW(fields, OBJ_FIELD_HEAP); - RBASIC_SET_SHAPE_ID(fields, shape_id); + // imemo fields objects should always have "RObject" layout. The + // layout in the shape describes the layout of the thing on which it is set. + // Imemo fields have the same layout as robject, therefore the layout + // should reflect that fact. + RBASIC_SET_SHAPE_ID(fields, rb_shape_id_with_robject_layout(shape_id)); st_foreach(tbl, imemo_fields_trigger_wb_i, (st_data_t)fields); return fields; } diff --git a/internal/gc.h b/internal/gc.h index ee2a0c28050a8a..8b136e6572022c 100644 --- a/internal/gc.h +++ b/internal/gc.h @@ -124,7 +124,7 @@ struct rb_objspace; /* in vm_core.h */ T *(var) = (T *)rb_ec_newobj_of((ec), (c), (f), s) #define NEWOBJ_OF(var, T, c, f, s) EC_NEWOBJ_OF(var, T, c, f, s, GET_EC()) #define UNPROTECTED_NEWOBJ_OF(var, T, c, f, s) \ - T *(var) = (T *)rb_newobj((GET_EC()), (c), (f), 0 /* ROOT_SHAPE_ID */, false, s) + T *(var) = (T *)rb_newobj((GET_EC()), (c), (f), ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, false, s) #ifndef RB_GC_OBJECT_METADATA_ENTRY_DEFINED # define RB_GC_OBJECT_METADATA_ENTRY_DEFINED diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 40a9dc35b82c85..9d8a68fff9b886 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -274,6 +274,7 @@ def remove(*gems) method_option "target-rbconfig", type: :string, banner: "Path to rbconfig.rb for the deployment target platform" method_option "without", type: :array, banner: "Exclude gems that are part of the specified named group (removed)." method_option "with", type: :array, banner: "Include gems that are part of the specified named group (removed)." + method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." def install %w[clean deployment frozen no-prune path shebang without with].each do |option| remembered_flag_deprecation(option) @@ -324,6 +325,7 @@ def install method_option "strict", type: :boolean, banner: "Do not allow any gem to be updated past latest --patch | --minor | --major" method_option "conservative", type: :boolean, banner: "Use bundle install conservative update behavior and do not allow shared dependencies to be updated." method_option "all", type: :boolean, banner: "Update everything." + method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." def update(*gems) require_relative "cli/update" Bundler.settings.temporary(no_install: false) do @@ -406,6 +408,7 @@ def binstubs(*gems) 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 @@ -436,6 +439,7 @@ def add(*gems) 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 diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb index d65ed68b4af87e..20f76b59d10a90 100644 --- a/lib/bundler/cli/add.rb +++ b/lib/bundler/cli/add.rb @@ -14,6 +14,9 @@ def initialize(options, gems) 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"] diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb index 2f332ff364404d..b44fbc30964157 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| diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb index 67feba84bdb06e..69affd1a109ab4 100644 --- a/lib/bundler/cli/install.rb +++ b/lib/bundler/cli/install.rb @@ -112,6 +112,9 @@ def normalize_settings 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"] diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb index ae827dbb4b97ca..465e56ada2cc78 100644 --- a/lib/bundler/cli/outdated.rb +++ b/lib/bundler/cli/outdated.rb @@ -26,6 +26,9 @@ def initialize(options, gems) 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 } @@ -203,6 +206,10 @@ def print_gem(current_spec, active_spec, dependency, groups) 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] @@ -219,6 +226,8 @@ def print_gem(current_spec, active_spec, dependency, groups) 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] @@ -227,6 +236,15 @@ def gem_column_for(current_spec, active_spec, dependency, groups) 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? diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb index 9cc90acc58536a..d92ffd995f3604 100644 --- a/lib/bundler/cli/update.rb +++ b/lib/bundler/cli/update.rb @@ -66,6 +66,8 @@ def run 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! diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index d67291514bd807..6e2638a8be4cc9 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -117,6 +117,10 @@ 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 @@ -131,9 +135,9 @@ def source(source, *args, &blk) 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 diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb index c06684657d598b..7c7ce107e205db 100644 --- a/lib/bundler/endpoint_specification.rb +++ b/lib/bundler/endpoint_specification.rb @@ -5,7 +5,7 @@ module Bundler class EndpointSpecification < Gem::Specification include MatchRemoteMetadata - attr_reader :name, :version, :platform, :checksum + attr_reader :name, :version, :platform, :checksum, :created_at attr_writer :dependencies attr_accessor :remote, :locked_platform @@ -145,6 +145,7 @@ def parse_metadata(data) unless data @required_ruby_version = nil @required_rubygems_version = nil + @created_at = nil return end @@ -161,6 +162,15 @@ def parse_metadata(data) @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 diff --git a/lib/bundler/man/bundle-add.1 b/lib/bundler/man/bundle-add.1 index 5b1e3e23406268..0956aa83f14f54 100644 --- a/lib/bundler/man/bundle-add.1 +++ b/lib/bundler/man/bundle-add.1 @@ -4,7 +4,7 @@ .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" @@ -53,6 +53,9 @@ Adds pessimistic declaration of version\. .TP \fB\-\-strict\fR Adds strict declaration of version\. +.TP +\fB\-\-cooldown=\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\. diff --git a/lib/bundler/man/bundle-add.1.ronn b/lib/bundler/man/bundle-add.1.ronn index 1741e8e3759d37..8c65af0cc00fd0 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` [--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 @@ -59,6 +59,11 @@ Adds the named gem to the [`Gemfile(5)`][Gemfile(5)] and run `bundle install`. * `--strict`: Adds strict declaration of version. +* `--cooldown=`: + Only consider gem versions published at least 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. diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index 61487ca55e7921..c055c8a415b060 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -69,168 +69,130 @@ The canonical form of this configuration is \fB"without"\fR\. To convert the can Any periods in the configuration keys must be replaced with two underscores when setting it via environment variables\. The configuration key \fBlocal\.rack\fR becomes the environment variable \fBBUNDLE_LOCAL__RACK\fR\. .SH "LIST OF AVAILABLE KEYS" The following is a list of all configuration keys and their purpose\. You can learn more about their operation in bundle install(1) \fIbundle\-install\.1\.html\fR\. -.TP -\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\. -.TP -\fBauto_install\fR (\fBBUNDLE_AUTO_INSTALL\fR) -Automatically run \fBbundle install\fR when gems are missing\. -.TP -\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\. -.TP -\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\. -.TP -\fBcache_all_platforms\fR (\fBBUNDLE_CACHE_ALL_PLATFORMS\fR) -Cache gems for all platforms\. -.TP -\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\. -.TP -\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\. -.TP -\fBconsole\fR (\fBBUNDLE_CONSOLE\fR) -The console that \fBbundle console\fR starts\. Defaults to \fBirb\fR\. -.TP -\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\. -.TP -\fBdeployment\fR (\fBBUNDLE_DEPLOYMENT\fR) -Equivalent to setting \fBfrozen\fR to \fBtrue\fR and \fBpath\fR to \fBvendor/bundle\fR\. -.TP -\fBdisable_checksum_validation\fR (\fBBUNDLE_DISABLE_CHECKSUM_VALIDATION\fR) -Allow installing gems even if they do not match the checksum provided by RubyGems\. -.TP -\fBdisable_exec_load\fR (\fBBUNDLE_DISABLE_EXEC_LOAD\fR) -Stop Bundler from using \fBload\fR to launch an executable in\-process in \fBbundle exec\fR\. -.TP -\fBdisable_local_branch_check\fR (\fBBUNDLE_DISABLE_LOCAL_BRANCH_CHECK\fR) -Allow Bundler to use a local git override without a branch specified in the Gemfile\. -.TP -\fBdisable_local_revision_check\fR (\fBBUNDLE_DISABLE_LOCAL_REVISION_CHECK\fR) -Allow Bundler to use a local git override without checking if the revision present in the lockfile is present in the repository\. -.TP -\fBdisable_shared_gems\fR (\fBBUNDLE_DISABLE_SHARED_GEMS\fR) -Stop Bundler from accessing gems installed to RubyGems' normal location\. -.TP -\fBdisable_version_check\fR (\fBBUNDLE_DISABLE_VERSION_CHECK\fR) -Stop Bundler from checking if a newer Bundler version is available on rubygems\.org\. -.TP -\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\. -.TP -\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\. -.TP -\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\. -.TP -\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\. -.TP -\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\. -.TP -\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\. -.TP -\fBignore_funding_requests\fR (\fBBUNDLE_IGNORE_FUNDING_REQUESTS\fR) -When set, no funding requests will be printed\. -.TP -\fBignore_messages\fR (\fBBUNDLE_IGNORE_MESSAGES\fR) -When set, no post install messages will be printed\. To silence a single gem, use dot notation like \fBignore_messages\.httparty true\fR\. -.TP -\fBinit_gems_rb\fR (\fBBUNDLE_INIT_GEMS_RB\fR) -Generate a \fBgems\.rb\fR instead of a \fBGemfile\fR when running \fBbundle init\fR\. -.TP -\fBjobs\fR (\fBBUNDLE_JOBS\fR) -The number of gems Bundler can download and install in parallel\. Defaults to the number of available processors\. -.TP -\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))\. -.TP -\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\. -.TP -\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 \fR\. -.TP -\fBno_install\fR (\fBBUNDLE_NO_INSTALL\fR) -Whether \fBbundle package\fR should skip installing gems\. -.TP -\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 \fR\. -.TP -\fBno_prune\fR (\fBBUNDLE_NO_PRUNE\fR) -Whether Bundler should leave outdated gems unpruned when caching\. -.TP -\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\. -.TP -\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\. -.TP -\fBpath\.system\fR (\fBBUNDLE_PATH__SYSTEM\fR) -Whether Bundler will install gems into the default system path (\fBGem\.dir\fR)\. -.TP -\fBplugins\fR (\fBBUNDLE_PLUGINS\fR) -Enable Bundler's experimental plugin system\. -.TP -\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\. -.TP -\fBredirect\fR (\fBBUNDLE_REDIRECT\fR) -The number of redirects allowed for network requests\. Defaults to \fB5\fR\. -.TP -\fBretry\fR (\fBBUNDLE_RETRY\fR) -The number of times to retry failed network requests\. Defaults to \fB3\fR\. -.TP -\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\. -.TP -\fBsilence_deprecations\fR (\fBBUNDLE_SILENCE_DEPRECATIONS\fR) -Whether Bundler should silence deprecation warnings for behavior that will be changed in the next major version\. -.TP -\fBsilence_root_warning\fR (\fBBUNDLE_SILENCE_ROOT_WARNING\fR) -Silence the warning Bundler prints when installing gems as root\. -.TP -\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\. -.TP -\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\. -.TP -\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\. -.TP -\fBssl_verify_mode\fR (\fBBUNDLE_SSL_VERIFY_MODE\fR) -The SSL verification mode Bundler uses when making HTTPS requests\. Defaults to verify peer\. -.TP -\fBsystem_bindir\fR (\fBBUNDLE_SYSTEM_BINDIR\fR) -The location where RubyGems installs binstubs\. Defaults to \fBGem\.bindir\fR\. -.TP -\fBtimeout\fR (\fBBUNDLE_TIMEOUT\fR) -The seconds allowed before timing out for network requests\. Defaults to \fB10\fR\. -.TP -\fBupdate_requires_all_flag\fR (\fBBUNDLE_UPDATE_REQUIRES_ALL_FLAG\fR) -Require passing \fB\-\-all\fR to \fBbundle update\fR when everything should be updated, and disallow passing no options to \fBbundle update\fR\. -.TP -\fBuser_agent\fR (\fBBUNDLE_USER_AGENT\fR) -The custom user agent fragment Bundler includes in API requests\. -.TP -\fBverbose\fR (\fBBUNDLE_VERBOSE\fR) -Whether Bundler should print verbose output\. Defaults to \fBfalse\fR, unless the \fB\-\-verbose\fR CLI flag is used\. -.TP -\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\. -.TP -\fBwith\fR (\fBBUNDLE_WITH\fR) -A space\-separated or \fB:\fR\-separated list of groups whose gems bundler should install\. -.TP -\fBwithout\fR (\fBBUNDLE_WITHOUT\fR) -A space\-separated or \fB:\fR\-separated list of groups whose gems bundler should not install\. +.IP "\(bu" 4 +\fBapi_request_size\fR (\fBBUNDLE_API_REQUEST_SIZE\fR): Configure how many dependencies to fetch when resolving the specifications\. This configuration is only used when fetching specifications from RubyGems servers that didn't implement the Compact Index API\. Defaults to 100\. +.IP "\(bu" 4 +\fBauto_install\fR (\fBBUNDLE_AUTO_INSTALL\fR): Automatically run \fBbundle install\fR when gems are missing\. +.IP "\(bu" 4 +\fBbin\fR (\fBBUNDLE_BIN\fR): If configured, \fBbundle binstubs\fR will install executables from gems in the bundle to the specified directory\. Otherwise it will create them in a \fBbin\fR directory relative to the Gemfile directory\. These executables run in Bundler's context\. If used, you might add this directory to your environment's \fBPATH\fR variable\. For instance, if the \fBrails\fR gem comes with a \fBrails\fR executable, \fBbundle binstubs\fR will create a \fBbin/rails\fR executable that ensures that all referred dependencies will be resolved using the bundled gems\. +.IP "\(bu" 4 +\fBcache_all\fR (\fBBUNDLE_CACHE_ALL\fR): Cache all gems, including path and git gems\. This needs to be explicitly before bundler 4, but will be the default on bundler 4\. +.IP "\(bu" 4 +\fBcache_all_platforms\fR (\fBBUNDLE_CACHE_ALL_PLATFORMS\fR): Cache gems for all platforms\. +.IP "\(bu" 4 +\fBcache_path\fR (\fBBUNDLE_CACHE_PATH\fR): The directory that bundler will place cached gems in when running \fBbundle package\fR, and that bundler will look in when installing gems\. Defaults to \fBvendor/cache\fR\. +.IP "\(bu" 4 +\fBclean\fR (\fBBUNDLE_CLEAN\fR): Whether Bundler should run \fBbundle clean\fR automatically after \fBbundle install\fR\. Defaults to \fBtrue\fR in Bundler 4, as long as \fBpath\fR is not explicitly configured\. +.IP "\(bu" 4 +\fBconsole\fR (\fBBUNDLE_CONSOLE\fR): The console that \fBbundle console\fR starts\. Defaults to \fBirb\fR\. +.IP "\(bu" 4 +\fBcooldown\fR (\fBBUNDLE_COOLDOWN\fR): Number of days a published gem version must age before bundler will resolve to it\. Defaults to unset (no cooldown)\. Pass \fB0\fR to disable cooldown for an individual run\. +.IP +The effective cooldown for any given gem is resolved from three layers, highest precedence first: +.IP "1." 4 +CLI flag \fB\-\-cooldown N\fR on \fBinstall\fR, \fBupdate\fR, \fBadd\fR, and \fBoutdated\fR\. +.IP "2." 4 +This setting (\fBbundle config set cooldown N\fR or \fBBUNDLE_COOLDOWN=N\fR)\. +.IP "3." 4 +The per\-source \fBcooldown:\fR keyword in the Gemfile, such as \fBsource "https://rubygems\.org", cooldown: 7\fR\. +.IP "" 0 +.IP +The CLI flag and this setting apply uniformly to every source, including ones declared with their own \fBcooldown:\fR value\. To keep a private registry permanently exempt while still cooling down public gems, declare \fBsource "https://internal", cooldown: 0\fR in the Gemfile; remember that \fB\-\-cooldown N\fR on the command line will still override it for that single run\. +.IP +Cooldown filtering depends on the gem server providing a per\-version \fBcreated_at\fR timestamp in the v2 compact\-index format\. Versions without that metadata \- older gem servers, historical entries that predate the v2 cutover on \fBrubygems\.org\fR, or private registries that still emit the v1 format \- are treated as outside the cooldown window and remain resolvable\. If you rely on cooldown for supply\-chain protection, confirm that the gem server emits \fBcreated_at\fR in its \fB/info/\fR responses\. +.IP "\(bu" 4 +\fBdefault_cli_command\fR (\fBBUNDLE_DEFAULT_CLI_COMMAND\fR): The command that running \fBbundle\fR without arguments should run\. Defaults to \fBcli_help\fR since Bundler 4, but can also be \fBinstall\fR which was the previous default\. +.IP "\(bu" 4 +\fBdeployment\fR (\fBBUNDLE_DEPLOYMENT\fR): Equivalent to setting \fBfrozen\fR to \fBtrue\fR and \fBpath\fR to \fBvendor/bundle\fR\. +.IP "\(bu" 4 +\fBdisable_checksum_validation\fR (\fBBUNDLE_DISABLE_CHECKSUM_VALIDATION\fR): Allow installing gems even if they do not match the checksum provided by RubyGems\. +.IP "\(bu" 4 +\fBdisable_exec_load\fR (\fBBUNDLE_DISABLE_EXEC_LOAD\fR): Stop Bundler from using \fBload\fR to launch an executable in\-process in \fBbundle exec\fR\. +.IP "\(bu" 4 +\fBdisable_local_branch_check\fR (\fBBUNDLE_DISABLE_LOCAL_BRANCH_CHECK\fR): Allow Bundler to use a local git override without a branch specified in the Gemfile\. +.IP "\(bu" 4 +\fBdisable_local_revision_check\fR (\fBBUNDLE_DISABLE_LOCAL_REVISION_CHECK\fR): Allow Bundler to use a local git override without checking if the revision present in the lockfile is present in the repository\. +.IP "\(bu" 4 +\fBdisable_shared_gems\fR (\fBBUNDLE_DISABLE_SHARED_GEMS\fR): Stop Bundler from accessing gems installed to RubyGems' normal location\. +.IP "\(bu" 4 +\fBdisable_version_check\fR (\fBBUNDLE_DISABLE_VERSION_CHECK\fR): Stop Bundler from checking if a newer Bundler version is available on rubygems\.org\. +.IP "\(bu" 4 +\fBforce_ruby_platform\fR (\fBBUNDLE_FORCE_RUBY_PLATFORM\fR): Ignore the current machine's platform and install only \fBruby\fR platform gems\. As a result, gems with native extensions will be compiled from source\. +.IP "\(bu" 4 +\fBfrozen\fR (\fBBUNDLE_FROZEN\fR): Disallow any automatic changes to \fBGemfile\.lock\fR\. Bundler commands will be blocked unless the lockfile can be installed exactly as written\. Usually this will happen when changing the \fBGemfile\fR manually and forgetting to update the lockfile through \fBbundle lock\fR or \fBbundle install\fR\. +.IP "\(bu" 4 +\fBgem\.github_username\fR (\fBBUNDLE_GEM__GITHUB_USERNAME\fR): Sets a GitHub username or organization to be used in the \fBREADME\fR and \fB\.gemspec\fR files when you create a new gem via \fBbundle gem\fR command\. It can be overridden by passing an explicit \fB\-\-github\-username\fR flag to \fBbundle gem\fR\. +.IP "\(bu" 4 +\fBgem\.push_key\fR (\fBBUNDLE_GEM__PUSH_KEY\fR): Sets the \fB\-\-key\fR parameter for \fBgem push\fR when using the \fBrake release\fR command with a private gemstash server\. +.IP "\(bu" 4 +\fBgemfile\fR (\fBBUNDLE_GEMFILE\fR): The name of the file that bundler should use as the \fBGemfile\fR\. This location of this file also sets the root of the project, which is used to resolve relative paths in the \fBGemfile\fR, among other things\. By default, bundler will search up from the current working directory until it finds a \fBGemfile\fR\. +.IP "\(bu" 4 +\fBglobal_gem_cache\fR (\fBBUNDLE_GLOBAL_GEM_CACHE\fR): Whether Bundler should cache all gems and compiled extensions globally, rather than locally to the configured installation path\. +.IP "\(bu" 4 +\fBignore_funding_requests\fR (\fBBUNDLE_IGNORE_FUNDING_REQUESTS\fR): When set, no funding requests will be printed\. +.IP "\(bu" 4 +\fBignore_messages\fR (\fBBUNDLE_IGNORE_MESSAGES\fR): When set, no post install messages will be printed\. To silence a single gem, use dot notation like \fBignore_messages\.httparty true\fR\. +.IP "\(bu" 4 +\fBinit_gems_rb\fR (\fBBUNDLE_INIT_GEMS_RB\fR): Generate a \fBgems\.rb\fR instead of a \fBGemfile\fR when running \fBbundle init\fR\. +.IP "\(bu" 4 +\fBjobs\fR (\fBBUNDLE_JOBS\fR): The number of gems Bundler can download and install in parallel\. Defaults to the number of available processors\. +.IP "\(bu" 4 +\fBlockfile\fR (\fBBUNDLE_LOCKFILE\fR): The path to the lockfile that bundler should use\. By default, Bundler adds \fB\.lock\fR to the end of the \fBgemfile\fR entry\. Can be set to \fBfalse\fR in the Gemfile to disable lockfile creation entirely (see gemfile(5))\. +.IP "\(bu" 4 +\fBlockfile_checksums\fR (\fBBUNDLE_LOCKFILE_CHECKSUMS\fR): Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources\. Defaults to true\. +.IP "\(bu" 4 +\fBno_build_extension\fR (\fBBUNDLE_NO_BUILD_EXTENSION\fR): Whether Bundler should skip building native extensions during installation\. When set, gems are installed without compiling their C extensions\. To build extensions later, unset this setting and run \fBbundle pristine \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 \fR\. +.IP "\(bu" 4 +\fBno_prune\fR (\fBBUNDLE_NO_PRUNE\fR): Whether Bundler should leave outdated gems unpruned when caching\. +.IP "\(bu" 4 +\fBonly\fR (\fBBUNDLE_ONLY\fR): A space\-separated list of groups to install only gems of the specified groups\. Please check carefully if you want to install also gems without a group, because they get put inside \fBdefault\fR group\. For example \fBonly test:default\fR will install all gems specified in test group and without one\. +.IP "\(bu" 4 +\fBpath\fR (\fBBUNDLE_PATH\fR): The location on disk where all gems in your bundle will be located regardless of \fB$GEM_HOME\fR or \fB$GEM_PATH\fR values\. Bundle gems not found in this location will be installed by \fBbundle install\fR\. When not set, Bundler install by default to a \fB\.bundle\fR directory relative to repository root in Bundler 4, and to the default system path (\fBGem\.dir\fR) before Bundler 4\. That means that before Bundler 4, Bundler shares this location with Rubygems, and \fBgem install \|\.\|\.\|\.\fR will have gems installed in the same location and therefore, gems installed without \fBpath\fR set will show up by calling \fBgem list\fR\. This will not be the case in Bundler 4\. +.IP "\(bu" 4 +\fBpath\.system\fR (\fBBUNDLE_PATH__SYSTEM\fR): Whether Bundler will install gems into the default system path (\fBGem\.dir\fR)\. +.IP "\(bu" 4 +\fBplugins\fR (\fBBUNDLE_PLUGINS\fR): Enable Bundler's experimental plugin system\. +.IP "\(bu" 4 +\fBprefer_patch\fR (\fBBUNDLE_PREFER_PATCH\fR): Prefer updating only to next patch version during updates\. Makes \fBbundle update\fR calls equivalent to \fBbundler update \-\-patch\fR\. +.IP "\(bu" 4 +\fBredirect\fR (\fBBUNDLE_REDIRECT\fR): The number of redirects allowed for network requests\. Defaults to \fB5\fR\. +.IP "\(bu" 4 +\fBretry\fR (\fBBUNDLE_RETRY\fR): The number of times to retry failed network requests\. Defaults to \fB3\fR\. +.IP "\(bu" 4 +\fBshebang\fR (\fBBUNDLE_SHEBANG\fR): The program name that should be invoked for generated binstubs\. Defaults to the ruby install name used to generate the binstub\. +.IP "\(bu" 4 +\fBsilence_deprecations\fR (\fBBUNDLE_SILENCE_DEPRECATIONS\fR): Whether Bundler should silence deprecation warnings for behavior that will be changed in the next major version\. +.IP "\(bu" 4 +\fBsilence_root_warning\fR (\fBBUNDLE_SILENCE_ROOT_WARNING\fR): Silence the warning Bundler prints when installing gems as root\. +.IP "\(bu" 4 +\fBsimulate_version\fR (\fBBUNDLE_SIMULATE_VERSION\fR): The virtual version Bundler should use for activating feature flags\. Can be used to simulate all the new functionality that will be enabled in a future major version\. +.IP "\(bu" 4 +\fBssl_ca_cert\fR (\fBBUNDLE_SSL_CA_CERT\fR): Path to a designated CA certificate file or folder containing multiple certificates for trusted CAs in PEM format\. +.IP "\(bu" 4 +\fBssl_client_cert\fR (\fBBUNDLE_SSL_CLIENT_CERT\fR): Path to a designated file containing a X\.509 client certificate and key in PEM format\. +.IP "\(bu" 4 +\fBssl_verify_mode\fR (\fBBUNDLE_SSL_VERIFY_MODE\fR): The SSL verification mode Bundler uses when making HTTPS requests\. Defaults to verify peer\. +.IP "\(bu" 4 +\fBsystem_bindir\fR (\fBBUNDLE_SYSTEM_BINDIR\fR): The location where RubyGems installs binstubs\. Defaults to \fBGem\.bindir\fR\. +.IP "\(bu" 4 +\fBtimeout\fR (\fBBUNDLE_TIMEOUT\fR): The seconds allowed before timing out for network requests\. Defaults to \fB10\fR\. +.IP "\(bu" 4 +\fBupdate_requires_all_flag\fR (\fBBUNDLE_UPDATE_REQUIRES_ALL_FLAG\fR): Require passing \fB\-\-all\fR to \fBbundle update\fR when everything should be updated, and disallow passing no options to \fBbundle update\fR\. +.IP "\(bu" 4 +\fBuser_agent\fR (\fBBUNDLE_USER_AGENT\fR): The custom user agent fragment Bundler includes in API requests\. +.IP "\(bu" 4 +\fBverbose\fR (\fBBUNDLE_VERBOSE\fR): Whether Bundler should print verbose output\. Defaults to \fBfalse\fR, unless the \fB\-\-verbose\fR CLI flag is used\. +.IP "\(bu" 4 +\fBversion\fR (\fBBUNDLE_VERSION\fR): The version of Bundler to use when running under Bundler environment\. Defaults to \fBlockfile\fR\. You can also specify \fBsystem\fR or \fBx\.y\.z\fR\. \fBlockfile\fR will use the Bundler version specified in the \fBGemfile\.lock\fR, \fBsystem\fR will use the system version of Bundler, and \fBx\.y\.z\fR will use the specified version of Bundler\. +.IP "\(bu" 4 +\fBwith\fR (\fBBUNDLE_WITH\fR): A space\-separated or \fB:\fR\-separated list of groups whose gems bundler should install\. +.IP "\(bu" 4 +\fBwithout\fR (\fBBUNDLE_WITHOUT\fR): A space\-separated or \fB:\fR\-separated list of groups whose gems bundler should not install\. +.IP "" 0 .SH "BUILD OPTIONS" You can use \fBbundle config\fR to give Bundler the flags to pass to the gem installer every time bundler tries to install a particular gem\. .P diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn index 9657e7414514ef..72f891b428d58b 100644 --- a/lib/bundler/man/bundle-config.1.ronn +++ b/lib/bundler/man/bundle-config.1.ronn @@ -137,6 +137,36 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). explicitly configured. * `console` (`BUNDLE_CONSOLE`): The console that `bundle console` starts. Defaults to `irb`. +* `cooldown` (`BUNDLE_COOLDOWN`): + Number of days a published gem version must age before bundler will + resolve to it. Defaults to unset (no cooldown). Pass `0` to disable + cooldown for an individual run. + + The effective cooldown for any given gem is resolved from three + layers, highest precedence first: + + 1. CLI flag `--cooldown N` on `install`, `update`, `add`, and + `outdated`. + 2. This setting (`bundle config set cooldown N` or + `BUNDLE_COOLDOWN=N`). + 3. The per-source `cooldown:` keyword in the Gemfile, such as + `source "https://rubygems.org", cooldown: 7`. + + The CLI flag and this setting apply uniformly to every source, + including ones declared with their own `cooldown:` value. To keep a + private registry permanently exempt while still cooling down public + gems, declare `source "https://internal", cooldown: 0` in the + Gemfile; remember that `--cooldown N` on the command line will + still override it for that single run. + + Cooldown filtering depends on the gem server providing a per-version + `created_at` timestamp in the v2 compact-index format. Versions + without that metadata - older gem servers, historical entries that + predate the v2 cutover on `rubygems.org`, or private registries that + still emit the v1 format - are treated as outside the cooldown + window and remain resolvable. If you rely on cooldown for + supply-chain protection, confirm that the gem server emits + `created_at` in its `/info/` 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 diff --git a/lib/bundler/man/bundle-install.1 b/lib/bundler/man/bundle-install.1 index a764d031ed12c3..801768c7ecefdc 100644 --- a/lib/bundler/man/bundle-install.1 +++ b/lib/bundler/man/bundle-install.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-install\fR \- Install the dependencies specified in your Gemfile .SH "SYNOPSIS" -\fBbundle install\fR [\-\-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] +\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 @@ -13,6 +13,9 @@ If a \fBGemfile\.lock\fR does exist, and you have not updated your Gemfile(5), B If a \fBGemfile\.lock\fR does exist, and you have updated your Gemfile(5), Bundler will use the dependencies in the \fBGemfile\.lock\fR for all gems that you did not update, but will re\-resolve the dependencies of gems that you did update\. You can find more information about this update process below under \fICONSERVATIVE UPDATING\fR\. .SH "OPTIONS" .TP +\fB\-\-cooldown=\fR +Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. See \fBcooldown\fR in bundle\-config(1) for details on the precedence between the CLI flag, Bundler config, and Gemfile per\-source settings\. +.TP \fB\-\-force\fR, \fB\-\-redownload\fR Force reinstalling every gem, even if already installed\. .TP diff --git a/lib/bundler/man/bundle-install.1.ronn b/lib/bundler/man/bundle-install.1.ronn index c7d88bfb73c3e4..56fd8bdf42a171 100644 --- a/lib/bundler/man/bundle-install.1.ronn +++ b/lib/bundler/man/bundle-install.1.ronn @@ -3,7 +3,8 @@ bundle-install(1) -- Install the dependencies specified in your Gemfile ## SYNOPSIS -`bundle install` [--force] +`bundle install` [--cooldown=NUMBER] + [--force] [--full-index] [--gemfile=GEMFILE] [--jobs=NUMBER] @@ -37,6 +38,13 @@ update process below under [CONSERVATIVE UPDATING][]. ## OPTIONS +* `--cooldown=`: + Only consider gem versions published at least days ago when + resolving. Pass `0` to disable cooldown for this run, overriding any + per-source or global configuration. See `cooldown` in bundle-config(1) + for details on the precedence between the CLI flag, Bundler config, + and Gemfile per-source settings. + * `--force`, `--redownload`: Force reinstalling every gem, even if already installed. diff --git a/lib/bundler/man/bundle-outdated.1 b/lib/bundler/man/bundle-outdated.1 index b739234d8daf07..c2f8086e241070 100644 --- a/lib/bundler/man/bundle-outdated.1 +++ b/lib/bundler/man/bundle-outdated.1 @@ -4,7 +4,7 @@ .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=\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" diff --git a/lib/bundler/man/bundle-outdated.1.ronn b/lib/bundler/man/bundle-outdated.1.ronn index 2c692c929be617..e5badac2e99484 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=`: + Annotate (rather than hide) versions that are still inside the + cooldown window of 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. diff --git a/lib/bundler/man/bundle-update.1 b/lib/bundler/man/bundle-update.1 index 6a749644e31a1d..94161083fc45f6 100644 --- a/lib/bundler/man/bundle-update.1 +++ b/lib/bundler/man/bundle-update.1 @@ -4,7 +4,7 @@ .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]] [\-\-force] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-quiet] [\-\-patch|\-\-minor|\-\-major] [\-\-pre] [\-\-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 @@ -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=\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 bfe381677c8efc..72fbf054d157eb 100644 --- a/lib/bundler/man/bundle-update.1.ronn +++ b/lib/bundler/man/bundle-update.1.ronn @@ -9,6 +9,7 @@ bundle-update(1) -- Update your gems to the latest available versions [--local] [--ruby] [--bundler[=VERSION]] + [--cooldown=NUMBER] [--force] [--full-index] [--gemfile=GEMFILE] @@ -91,6 +92,13 @@ gem. * `--conservative`: Use bundle install conservative update behavior and do not allow indirect dependencies to be updated. +* `--cooldown=`: + Only consider gem versions published at least 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/resolver.rb b/lib/bundler/resolver.rb index 096a349249168b..7e2ce321b6452a 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -203,6 +203,9 @@ def no_versions_incompatibility_for(package, unsatisfied_term) 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? @@ -372,6 +375,10 @@ def raise_not_found!(package) 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 @@ -415,7 +422,7 @@ def filter_matching_specs(specs, requirements) end def filter_specs(specs, package) - filter_remote_specs(filter_prereleases(specs, package), package) + filter_cooldown(filter_remote_specs(filter_prereleases(specs, package), package)) end def filter_prereleases(specs, package) @@ -424,6 +431,35 @@ def filter_prereleases(specs, package) specs.reject {|s| s.version.prerelease? } end + def filter_cooldown(specs) + return specs if specs.empty? + excluded = cooldown_excluded_specs(specs) + return specs if excluded.empty? + specs - excluded + end + + def cooldown_excluded_specs(specs) + specs.select {|spec| cooldown_excluded?(spec) } + end + + def cooldown_hint(specs) + excluded = cooldown_excluded_specs(specs) + return nil if excluded.empty? + "#{excluded.size} version#{"s" if excluded.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) if package.prefer_local? local_specs = specs.select {|s| s.is_a?(StubSpecification) } diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index 95b48da31e927a..fd77c2f7fc76d2 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -44,6 +44,7 @@ class Settings ].freeze NUMBER_KEYS = %w[ + cooldown jobs redirect retry diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb index 72f7dc771038ac..8094dcaa9d94ff 100644 --- a/lib/bundler/source/git/git_proxy.rb +++ b/lib/bundler/source/git/git_proxy.rb @@ -432,9 +432,14 @@ def capture(cmd, dir, ignore_err: false) 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"] - ["git", "-C", dir.to_s, *cmd] + return ["git", *opts, *cmd] unless dir + + ["git", "-C", dir.to_s, *opts, *cmd] end def extra_clone_args diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index d610ce3fdf92e0..66b36645199e85 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -16,6 +16,7 @@ class Rubygems < Source def initialize(options = {}) @options = options @remotes = [] + @remote_cooldowns = {} @dependency_names = [] @allow_remote = false @allow_cached = false @@ -25,7 +26,8 @@ def initialize(options = {}) @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 @@ -243,9 +245,14 @@ def cached_built_in_gem(spec, local: false) 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 @@ -266,7 +273,7 @@ def unmet_deps 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 diff --git a/lib/bundler/source/rubygems/remote.rb b/lib/bundler/source/rubygems/remote.rb index ed55912a9952be..3d847424b7aeb3 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,6 +14,16 @@ def initialize(uri) @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 diff --git a/lib/bundler/source_list.rb b/lib/bundler/source_list.rb index ac141299e5d010..ab7002d6e5b3d7 100644 --- a/lib/bundler/source_list.rb +++ b/lib/bundler/source_list.rb @@ -59,8 +59,8 @@ def add_plugin_source(source, options = {}) 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 diff --git a/lib/find.rb b/lib/find.rb index 8223eea4560319..d9b81eb92d1920 100644 --- a/lib/find.rb +++ b/lib/find.rb @@ -3,41 +3,50 @@ # find.rb: the Find module for processing all files under a given directory. # +# :markup: markdown # -# The +Find+ module supports the top-down traversal of a set of file paths. -# -# For example, to total the size of all files under your home directory, -# ignoring anything in a "dot" directory (e.g. $HOME/.ssh): -# -# require 'find' -# -# total_size = 0 -# -# Find.find(ENV["HOME"]) do |path| -# if FileTest.directory?(path) -# if File.basename(path).start_with?('.') -# Find.prune # Don't look any further into this directory. -# else -# next -# end -# else -# total_size += FileTest.size(path) -# end -# end -# +# \Module \Find supports the top-down traversal of entries in the file system. module Find # The version string VERSION = "0.2.0" + # :markup: markdown # - # Calls the associated block with the name of every file and directory listed - # as arguments, then recursively on their subdirectories, and so on. + # With a block given, performs a depth-first traversal of each given path in `paths`; + # calls the block with each found file or directory path: # - # Returns an enumerator if no block is given. + # ```ruby + # paths = [] + # Find.find('bin', 'jit') {|path| paths << path } + # paths + # # => + # # ["bin", + # # "bin/gem", + # # "jit", + # # "jit/Cargo.toml", + # # "jit/src", + # # "jit/src/lib.rs"] + # ``` # - # See the +Find+ module documentation for an example. + # Raises an exception if a given path cannot be read. # + # When keyword argument `ignore_error` is given as `true` (the default), + # certain exceptions during traversal are ignored (i.e., silently rescued): + # Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG, Errno::EINVAL; + # when given as `false`, no exceptions are rescued. + # + # Note that these exceptions may be ignored only in `Find` traversal code; + # an exception raised before traversal begins, + # or raised while in the block is not ignored. + # Each of the calls below raises an Errno::ENOENT exception that is not ignored: + # + # ```ruby + # Find.find('nosuch') { } + # Find.find('lib') {|entry| raise Errno::ENOENT } + # ``` + # + # With no block given, returns a new Enumerator. def find(*paths, ignore_error: true) # :yield: path block_given? or return enum_for(__method__, *paths, ignore_error: ignore_error) @@ -75,13 +84,26 @@ def find(*paths, ignore_error: true) # :yield: path nil end + # :markup: markdown + # + # call-seq: + # Find.prune + # + # This method is meaningful only within a block given with Find.find. # - # Skips the current file or directory, restarting the loop with the next - # entry. If the current file is a directory, that directory will not be - # recursively entered. Meaningful only within the block associated with - # Find::find. + # Inside such a block, + # "prunes" the traversed file tree by not descending into the current directory: # - # See the +Find+ module documentation for an example. + # ```ruby + # files = [] + # Find.find('.') do |path| + # Find.prune if File.basename(path) == 'test' + # next unless File.file?(path) && File.extname(path) == '.rb' + # files << path + # end + # files.size # => 6690 + # files.take(3) # => ["./KNOWNBUGS.rb", "./array.rb", "./ast.rb"] + # ``` # def prune throw :prune diff --git a/lib/rubygems/resolver/api_set/gem_parser.rb b/lib/rubygems/resolver/api_set/gem_parser.rb index 7dd9a89ebcdf18..4d827f49808841 100644 --- a/lib/rubygems/resolver/api_set/gem_parser.rb +++ b/lib/rubygems/resolver/api_set/gem_parser.rb @@ -13,7 +13,7 @@ def parse(line) private def parse_dependency(string) - dependency = string.split(":") + dependency = string.split(":", 2) dependency[-1] = dependency[-1].split("&") if dependency.size > 1 dependency[0] = -dependency[0] dependency diff --git a/object.c b/object.c index 8dd701ec5b22aa..2e962b1c3ce107 100644 --- a/object.c +++ b/object.c @@ -1555,10 +1555,10 @@ rb_true_to_s(VALUE obj) /* - * call-seq: - * true & object -> true or false + * call-seq: + * true & object -> true or false * - * Returns +false+ if +object+ is +false+ or +nil+, +true+ otherwise: + * Returns +false+ if +object+ is +false+ or +nil+, +true+ otherwise: * * true & Object.new # => true * true & false # => false diff --git a/ractor.c b/ractor.c index d611ca97c273df..f94c06cc738bf4 100644 --- a/ractor.c +++ b/ractor.c @@ -2015,7 +2015,7 @@ move_enter(VALUE obj, struct obj_traverse_replace_data *data) else { VALUE type = RB_BUILTIN_TYPE(obj); size_t slot_size = rb_gc_obj_slot_size(obj); - VALUE moved = rb_newobj(GET_EC(), 0, type, 0, wb_protected_types[type], slot_size); + VALUE moved = rb_newobj(GET_EC(), 0, type, RBASIC_SHAPE_ID(obj), wb_protected_types[type], slot_size); MEMZERO(((struct RBasic *)moved) + 1, char, slot_size - sizeof(struct RBasic)); data->replacement = (VALUE)moved; return traverse_cont; diff --git a/random.c b/random.c index b6c96f1b4d25ff..63f43a161ae252 100644 --- a/random.c +++ b/random.c @@ -566,36 +566,29 @@ fill_random_bytes_lib(void *buf, size_t size) static const HCRYPTPROV INVALID_HCRYPTPROV = (HCRYPTPROV)INVALID_HANDLE_VALUE; static void -release_crypt(void *p) +release_crypt(VALUE arg) { - HCRYPTPROV *ptr = p; + HCRYPTPROV *ptr = (void *)arg; HCRYPTPROV prov = (HCRYPTPROV)ATOMIC_PTR_EXCHANGE(*ptr, INVALID_HCRYPTPROV); if (prov && prov != INVALID_HCRYPTPROV) { CryptReleaseContext(prov, 0); } } -static const rb_data_type_t crypt_prov_type = { - "HCRYPTPROV", - {0, release_crypt,}, - 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_EMBEDDABLE -}; - static int fill_random_bytes_crypt(void *seed, size_t size) { static HCRYPTPROV perm_prov; HCRYPTPROV prov = perm_prov, old_prov; if (!prov) { - VALUE wrapper = TypedData_Wrap_Struct(0, &crypt_prov_type, 0); if (!CryptAcquireContext(&prov, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) { prov = INVALID_HCRYPTPROV; } old_prov = (HCRYPTPROV)ATOMIC_PTR_CAS(perm_prov, 0, prov); if (LIKELY(!old_prov)) { /* no other threads acquired */ if (prov != INVALID_HCRYPTPROV) { - DATA_PTR(wrapper) = (void *)prov; - rb_vm_register_global_object(wrapper); + /* register only once; perm_prov == 0 at the first call only */ + rb_set_end_proc(release_crypt, (VALUE)&perm_prov); } } else { /* another thread acquired */ @@ -607,7 +600,7 @@ fill_random_bytes_crypt(void *seed, size_t size) } if (prov == INVALID_HCRYPTPROV) return -1; while (size > 0) { - DWORD n = (size > (size_t)DWORD_MAX) ? DWORD_MAX : (DWORD)size; + DWORD n = (size > (size_t)DWORD_MAX) ? DWORD_MAX/2+1 : (DWORD)size; if (!CryptGenRandom(prov, n, seed)) return -1; seed = (char *)seed + n; size -= n; @@ -622,7 +615,7 @@ static int fill_random_bytes_bcrypt(void *seed, size_t size) { while (size > 0) { - ULONG n = (size > (size_t)ULONG_MAX) ? LONG_MAX : (ULONG)size; + ULONG n = (size > (size_t)ULONG_MAX) ? ULONG_MAX/2+1 : (ULONG)size; if (BCryptGenRandom(NULL, seed, n, BCRYPT_USE_SYSTEM_PREFERRED_RNG)) return -1; seed = (char *)seed + n; diff --git a/shape.c b/shape.c index 24f1394f6cd32f..7a02b230733043 100644 --- a/shape.c +++ b/shape.c @@ -409,10 +409,14 @@ rb_obj_shape_id(VALUE obj) if (BUILTIN_TYPE(obj) == T_CLASS || BUILTIN_TYPE(obj) == T_MODULE) { VALUE fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(obj); + shape_id_t base = ROOT_SHAPE_ID; if (fields_obj) { - return RBASIC_SHAPE_ID(fields_obj); + // Remove the layout from the fields object. We want to + // combine the shape of the fields object with the layout of the + // class / module object. + base = RBASIC_SHAPE_ID(fields_obj) & ~SHAPE_ID_LAYOUT_MASK; } - return ROOT_SHAPE_ID; + return rb_shape_layout(RBASIC_SHAPE_ID(obj)) | base; } return RBASIC_SHAPE_ID(obj); } @@ -697,7 +701,7 @@ rb_shape_transition_object_id(shape_id_t original_shape_id) bool dont_care; rb_shape_t *shape = get_next_shape_internal(RSHAPE(original_shape_id), id_object_id, SHAPE_OBJ_ID, &dont_care, true); if (!shape) { - return ROOT_COMPLEX_WITH_OBJ_ID | RSHAPE_FLAGS(original_shape_id); + return rb_shape_layout(original_shape_id) | ROOT_COMPLEX_WITH_OBJ_ID | RSHAPE_FLAGS(original_shape_id); } RUBY_ASSERT(shape); @@ -1225,17 +1229,55 @@ rb_shape_foreach_field(shape_id_t initial_shape_id, rb_shape_foreach_transition_ } #if RUBY_DEBUG +/* + * Get the layout of this object. The "layout" indicates what strategy + * we should use for fetching instance variables from `obj`. It's based + * on the C struct layout for each particular object. + * + * TODO: make Struct have a similar layout to RDATA + */ +static shape_id_t +rb_shape_expected_layout(VALUE obj) +{ + switch (BUILTIN_TYPE(obj)) { + case T_OBJECT: + return SHAPE_ID_LAYOUT_ROBJECT; + case T_CLASS: + case T_MODULE: + if (FL_TEST_RAW(obj, RCLASS_BOXABLE)) { + return SHAPE_ID_LAYOUT_OTHER; + } + return SHAPE_ID_LAYOUT_RCLASS; + case T_DATA: + return SHAPE_ID_LAYOUT_RDATA; + case T_IMEMO: + if (IMEMO_TYPE_P(obj, imemo_fields)) { + return SHAPE_ID_LAYOUT_ROBJECT; + } + return SHAPE_ID_LAYOUT_OTHER; + default: + return SHAPE_ID_LAYOUT_OTHER; + } +} + bool rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) { - if (shape_id == ROOT_SHAPE_ID) { - return true; - } - if (shape_id == INVALID_SHAPE_ID) { rb_bug("Can't set INVALID_SHAPE_ID on an object"); } + shape_id_t actual_layout = rb_shape_layout(rb_obj_shape_id(obj)); + shape_id_t expected_layout = rb_shape_expected_layout(obj); + if (actual_layout != expected_layout) { + rb_bug("shape_id layout mismatch: expected=%x actual=%x shape_id=%u obj=%s", + expected_layout, actual_layout, shape_id, rb_obj_info(obj)); + } + + if (shape_id == ROOT_SHAPE_ID) { + return true; + } + rb_shape_t *shape = RSHAPE(shape_id); bool has_object_id = false; @@ -1249,13 +1291,11 @@ rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) if (rb_shape_has_object_id(shape_id)) { if (!has_object_id) { - rb_p(obj); rb_bug("shape_id claim having obj_id but doesn't shape_id=%u, obj=%s", shape_id, rb_obj_info(obj)); } } else { if (has_object_id) { - rb_p(obj); rb_bug("shape_id claim not having obj_id but it does shape_id=%u, obj=%s", shape_id, rb_obj_info(obj)); } } @@ -1329,6 +1369,25 @@ shape_has_object_id_p(VALUE self) return RBOOL(rb_shape_has_object_id(shape_id)); } +static VALUE +shape_layout(VALUE self) +{ + shape_id_t shape_id = NUM2UINT(rb_struct_getmember(self, rb_intern("id"))); + + switch (rb_shape_layout(shape_id)) { + case SHAPE_ID_LAYOUT_ROBJECT: + return ID2SYM(rb_intern("robject")); + case SHAPE_ID_LAYOUT_RCLASS: + return ID2SYM(rb_intern("rclass")); + case SHAPE_ID_LAYOUT_RDATA: + return ID2SYM(rb_intern("rdata")); + case SHAPE_ID_LAYOUT_OTHER: + return ID2SYM(rb_intern("other")); + default: + rb_bug("unknown shape layout: %u", rb_shape_layout(shape_id)); + } +} + static VALUE parse_key(ID key) { @@ -1628,6 +1687,7 @@ Init_shape(void) rb_define_method(rb_cShape, "complex?", shape_complex, 0); rb_define_method(rb_cShape, "shape_frozen?", shape_frozen, 0); rb_define_method(rb_cShape, "has_object_id?", shape_has_object_id_p, 0); + rb_define_method(rb_cShape, "layout", shape_layout, 0); rb_define_const(rb_cShape, "SHAPE_ROOT", INT2NUM(SHAPE_ROOT)); rb_define_const(rb_cShape, "SHAPE_IVAR", INT2NUM(SHAPE_IVAR)); diff --git a/shape.h b/shape.h index 61fadca5bace2d..a319449988e8fc 100644 --- a/shape.h +++ b/shape.h @@ -27,12 +27,14 @@ STATIC_ASSERT(shape_id_num_bits, SHAPE_ID_NUM_BITS == sizeof(shape_id_t) * CHAR_ // 19-22 SHAPE_ID_HEAP_INDEX_MASK // index in rb_shape_tree.capacities. Allow to access slot size. // Currently always 0 except for T_OBJECT. -// 23 SHAPE_ID_FL_FROZEN +// 23 SHAPE_ID_FL_COMPLEX +// The object is backed by a `st_table`. +// 24 SHAPE_ID_FL_FROZEN // Whether the object is frozen or not. -// 24 SHAPE_ID_FL_HAS_OBJECT_ID +// 25 SHAPE_ID_FL_HAS_OBJECT_ID // Whether the object has an `SHAPE_OBJ_ID` transition. -// 25 SHAPE_ID_FL_COMPLEX -// The object is backed by a `st_table`. +// 26-27 SHAPE_ID_LAYOUT_MASK +// The object's physical field layout. enum shape_id_fl_type { #define RBIMPL_SHAPE_ID_FL(n) (1<<(SHAPE_ID_FL_USHIFT+n)) @@ -43,8 +45,26 @@ enum shape_id_fl_type { SHAPE_ID_FL_FROZEN = RBIMPL_SHAPE_ID_FL(1), SHAPE_ID_FL_HAS_OBJECT_ID = RBIMPL_SHAPE_ID_FL(2), + // Means IVs are found at an offset from the object's addr, or in a + // malloc allocated side table + SHAPE_ID_LAYOUT_ROBJECT = 0, + + // Means this object is a class/module that is NOT RCLASS_BOXABLE, and IV's + // are found in the fields_obj found on the rclass struct + SHAPE_ID_LAYOUT_RCLASS = RBIMPL_SHAPE_ID_FL(3), + + // Means this object is an RData or RTypedData and IVs are found in the + // fields_obj found on the RData/RTypedData struct + SHAPE_ID_LAYOUT_RDATA = RBIMPL_SHAPE_ID_FL(4), + + // Means this is a complicated object: boxable classes, structs, objects + // that store IVs on the geniv table + SHAPE_ID_LAYOUT_OTHER = SHAPE_ID_LAYOUT_RCLASS | SHAPE_ID_LAYOUT_RDATA, + + SHAPE_ID_LAYOUT_MASK = SHAPE_ID_LAYOUT_OTHER, + SHAPE_ID_FL_NON_CANONICAL_MASK = SHAPE_ID_FL_FROZEN | SHAPE_ID_FL_HAS_OBJECT_ID, - SHAPE_ID_FLAGS_MASK = SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_NON_CANONICAL_MASK | SHAPE_ID_FL_COMPLEX, + SHAPE_ID_FLAGS_MASK = SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_NON_CANONICAL_MASK | SHAPE_ID_FL_COMPLEX | SHAPE_ID_LAYOUT_MASK, #undef RBIMPL_SHAPE_ID_FL }; @@ -55,12 +75,13 @@ enum shape_id_mask { SHAPE_ID_HAS_IVAR_MASK = SHAPE_ID_FL_COMPLEX | (SHAPE_ID_OFFSET_MASK - 1), }; -// The interpreter doesn't care about frozen status, slot size or object id when reading ivars. +// The interpreter doesn't care about frozen status, slot size, or object id, and +// has its own checks for physical field layout when reading ivars. // So we normalize shape_id by clearing these bits to improve cache hits. // JITs however might care about some of it. -#define SHAPE_ID_READ_ONLY_MASK (~(SHAPE_ID_FL_FROZEN | SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID)) +#define SHAPE_ID_READ_ONLY_MASK (~(SHAPE_ID_FL_FROZEN | SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID | SHAPE_ID_LAYOUT_MASK)) // For write it's the same idea, but here we do care about frozen status. -#define SHAPE_ID_WRITE_MASK (~(SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID)) +#define SHAPE_ID_WRITE_MASK (~(SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID | SHAPE_ID_LAYOUT_MASK)) typedef uint32_t redblack_id_t; @@ -153,11 +174,18 @@ RBASIC_SET_SHAPE_ID_NO_CHECKS(VALUE obj, shape_id_t shape_id) #endif } +static inline shape_id_t +rb_shape_layout(shape_id_t shape_id) +{ + return shape_id & SHAPE_ID_LAYOUT_MASK; +} + static inline void RBASIC_SET_SHAPE_ID(VALUE obj, shape_id_t shape_id) { RUBY_ASSERT(!RB_SPECIAL_CONST_P(obj)); RUBY_ASSERT(!RB_TYPE_P(obj, T_IMEMO) || IMEMO_TYPE_P(obj, imemo_fields)); + RUBY_ASSERT(!IMEMO_TYPE_P(obj, imemo_fields) || rb_shape_layout(shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID_NO_CHECKS(obj, shape_id); @@ -232,6 +260,12 @@ rb_shape_canonical_p(shape_id_t shape_id) return !(shape_id & SHAPE_ID_FL_NON_CANONICAL_MASK); } +static inline shape_id_t +rb_shape_id_with_robject_layout(shape_id_t shape_id) +{ + return (shape_id & ~SHAPE_ID_LAYOUT_MASK) | SHAPE_ID_LAYOUT_ROBJECT; +} + static inline uint8_t rb_shape_heap_index(shape_id_t shape_id) { @@ -450,10 +484,10 @@ rb_shape_transition_frozen(shape_id_t shape_id) static inline shape_id_t rb_shape_transition_complex(shape_id_t shape_id) { - shape_id_t next_shape_id = ROOT_COMPLEX_SHAPE_ID; + shape_id_t next_shape_id = rb_shape_layout(shape_id) | ROOT_COMPLEX_SHAPE_ID; if (rb_shape_has_object_id(shape_id)) { - next_shape_id = ROOT_COMPLEX_WITH_OBJ_ID; + next_shape_id = rb_shape_layout(shape_id) | ROOT_COMPLEX_WITH_OBJ_ID; } uint8_t heap_index = rb_shape_heap_index(shape_id); diff --git a/signal.c b/signal.c index 5110ea4401a4be..f37aaf971ad997 100644 --- a/signal.c +++ b/signal.c @@ -1020,8 +1020,20 @@ check_reserved_signal_(const char *name, size_t name_len, int signo) #if __has_feature(address_sanitizer) || \ __has_feature(memory_sanitizer) || \ defined(HAVE_VALGRIND_MEMCHECK_H) - ruby_posix_signal(signo, SIG_DFL); +# define SANITIZING true +#else +# define SANITIZING false +#endif + +#ifdef SIGABRT +// Avoid infinite loop when already aborting +# define RECURSIVE (signo == SIGABRT) +#else +# define RECURSIVE false #endif + if (SANITIZING || RECURSIVE) ruby_signal(signo, SIG_DFL); +# undef SANITIZING +# undef RECURSIVE W(name, name_len); W(msg1, sizeof(msg1)); W(prev, strlen(prev)); diff --git a/spec/bundler/bundler/compact_index_client/parser_spec.rb b/spec/bundler/bundler/compact_index_client/parser_spec.rb index 6015f66f33a79b..6aa867f058f9f5 100644 --- a/spec/bundler/bundler/compact_index_client/parser_spec.rb +++ b/spec/bundler/bundler/compact_index_client/parser_spec.rb @@ -47,7 +47,7 @@ def set_info_data(name, value) INFO let(:c_info) { <<~INFO } 3.0.0 a:= 1.0.0,b:~> 2.0|checksum:ccc1,ruby:>= 2.7.0,rubygems:>= 3.0.0 - 3.3.3 a:>= 1.1.0,b:~> 2.0|checksum:ccc3,ruby:>= 3.0.0,rubygems:>= 3.2.3 + 3.3.3 a:>= 1.1.0,b:~> 2.0|checksum:ccc3,ruby:>= 3.0.0,rubygems:>= 3.2.3,created_at:2026-05-12T10:00:00Z INFO describe "#available?" do @@ -195,7 +195,7 @@ def set_info_data(name, value) "3.3.3", nil, [["a", [">= 1.1.0"]], ["b", ["~> 2.0"]]], - [["checksum", ["ccc3"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]]], + [["checksum", ["ccc3"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]], ["created_at", ["2026-05-12T10:00:00Z"]]], ], ] end diff --git a/spec/bundler/bundler/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb index a04528a57fc7d0..b6e67a312c0d41 100644 --- a/spec/bundler/bundler/dsl_spec.rb +++ b/spec/bundler/bundler/dsl_spec.rb @@ -367,6 +367,48 @@ end end + describe "#source with cooldown" do + before do + allow(@rubygems).to receive(:add_remote) + end + + it "accepts a non-negative integer" do + expect do + subject.source("https://rubygems.org", cooldown: 7) + end.not_to raise_error + end + + it "accepts 0 as an explicit disable" do + expect do + subject.source("https://rubygems.org", cooldown: 0) + end.not_to raise_error + end + + it "rejects a string" do + expect do + subject.source("https://rubygems.org", cooldown: "7") + end.to raise_error(Bundler::InvalidOption, /non-negative integer/) + end + + it "rejects a float" do + expect do + subject.source("https://rubygems.org", cooldown: 7.5) + end.to raise_error(Bundler::InvalidOption, /non-negative integer/) + end + + it "rejects a negative integer" do + expect do + subject.source("https://rubygems.org", cooldown: -7) + end.to raise_error(Bundler::InvalidOption, /non-negative integer/) + end + + it "rejects an array" do + expect do + subject.source("https://rubygems.org", cooldown: [7]) + end.to raise_error(Bundler::InvalidOption, /non-negative integer/) + end + end + describe "#override" do it "stores an Override for a gem with a version: operation" do subject.override("rails", version: ">= 8.0") diff --git a/spec/bundler/bundler/endpoint_specification_spec.rb b/spec/bundler/bundler/endpoint_specification_spec.rb index 6518f125ba5e52..4fbd59d48f23a3 100644 --- a/spec/bundler/bundler/endpoint_specification_spec.rb +++ b/spec/bundler/bundler/endpoint_specification_spec.rb @@ -46,6 +46,46 @@ ) end end + + context "when the metadata has created_at" do + let(:metadata) { { "created_at" => ["2026-05-12T10:00:00Z"] } } + + it "parses created_at as a Time" do + expect(subject.created_at).to eq(Time.utc(2026, 5, 12, 10, 0, 0)) + end + end + + context "when the metadata has a string created_at (older rubygems shape)" do + let(:metadata) { { "created_at" => "2026-05-12T10:00:00Z" } } + + it "still parses created_at" do + expect(subject.created_at).to eq(Time.utc(2026, 5, 12, 10, 0, 0)) + end + end + + context "when created_at is truncated (older rubygems splits on colons)" do + let(:metadata) { { "created_at" => "2026-05-12T10" } } + + it "leaves created_at as nil instead of raising" do + expect(subject.created_at).to be_nil + end + end + + context "when the metadata has no created_at" do + let(:metadata) { { "checksum" => ["abc"] } } + let(:spec_fetcher) { double(:spec_fetcher, uri: "https://rubygems.org") } + + it "leaves created_at as nil" do + allow(Bundler::Checksum).to receive(:from_api).and_return(nil) + expect(subject.created_at).to be_nil + end + end + + context "when the metadata is nil" do + it "leaves created_at as nil" do + expect(subject.created_at).to be_nil + end + end end describe "#required_ruby_version" do diff --git a/spec/bundler/bundler/resolver/cooldown_spec.rb b/spec/bundler/bundler/resolver/cooldown_spec.rb new file mode 100644 index 00000000000000..834842145fa615 --- /dev/null +++ b/spec/bundler/bundler/resolver/cooldown_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Resolver do + let(:resolver) { described_class.allocate } + + def remote(cooldown:) + instance_double(Bundler::Source::Rubygems::Remote, effective_cooldown: cooldown) + end + + def spec(created_at:, remote:) + Struct.new(:created_at, :remote).new(created_at, remote) + end + + describe "#filter_cooldown" do + let(:now) { Time.now } + + context "with a 7-day cooldown" do + let(:r) { remote(cooldown: 7) } + + it "rejects versions published within the window" do + recent = spec(created_at: now - (2 * 86_400), remote: r) + old = spec(created_at: now - (30 * 86_400), remote: r) + + expect(resolver.send(:filter_cooldown, [recent, old])).to eq([old]) + end + + it "keeps versions published exactly at the threshold" do + boundary = spec(created_at: now - (7 * 86_400), remote: r) + + expect(resolver.send(:filter_cooldown, [boundary])).to eq([boundary]) + end + + it "leaves rolling-delay history intact" do + # 7-day cooldown with frequent releases must still expose an older candidate. + in_cooldown = spec(created_at: now - 86_400, remote: r) + also_in_cooldown = spec(created_at: now - (3 * 86_400), remote: r) + eligible = spec(created_at: now - (10 * 86_400), remote: r) + + result = resolver.send(:filter_cooldown, [in_cooldown, also_in_cooldown, eligible]) + + expect(result).to eq([eligible]) + end + end + + context "when created_at is missing (blank metadata)" do + it "keeps the spec regardless of cooldown" do + s = spec(created_at: nil, remote: remote(cooldown: 7)) + + expect(resolver.send(:filter_cooldown, [s])).to eq([s]) + end + end + + context "when the remote has no cooldown" do + it "keeps every spec" do + s = spec(created_at: now - 3600, remote: remote(cooldown: nil)) + + expect(resolver.send(:filter_cooldown, [s])).to eq([s]) + end + end + + context "when cooldown is 0" do + it "keeps every spec (escape hatch)" do + s = spec(created_at: now - 3600, remote: remote(cooldown: 0)) + + expect(resolver.send(:filter_cooldown, [s])).to eq([s]) + end + end + + context "when the spec does not respond to created_at" do + it "keeps the spec" do + bare = Struct.new(:version).new("1.0.0") + + expect(resolver.send(:filter_cooldown, [bare])).to eq([bare]) + end + end + + context "when the spec has no remote" do + it "keeps the spec" do + s = spec(created_at: now - 86_400, remote: nil) + + expect(resolver.send(:filter_cooldown, [s])).to eq([s]) + end + end + + it "returns the same array when input is empty" do + expect(resolver.send(:filter_cooldown, [])).to eq([]) + end + end + + describe "#cooldown_hint" do + let(:now) { Time.now } + let(:r) { remote(cooldown: 7) } + + it "returns nil when no spec is excluded" do + expect(resolver.send(:cooldown_hint, [])).to be_nil + end + + it "returns nil when every spec is outside the cooldown window" do + eligible = [spec(created_at: now - (30 * 86_400), remote: r)] + + expect(resolver.send(:cooldown_hint, eligible)).to be_nil + end + + it "mentions the count and the bypass flag for one excluded version" do + excluded = [spec(created_at: now - 86_400, remote: r)] + + hint = resolver.send(:cooldown_hint, excluded) + + expect(hint).to match(/1 version excluded by the cooldown setting/) + expect(hint).to match(/--cooldown 0/) + end + + it "uses plural wording when multiple versions are excluded" do + excluded = Array.new(3) { spec(created_at: now - 86_400, remote: r) } + + expect(resolver.send(:cooldown_hint, excluded)).to match(/3 versions excluded/) + end + end +end diff --git a/spec/bundler/bundler/settings_spec.rb b/spec/bundler/bundler/settings_spec.rb index e91e1641b3da20..5e1aaaa5551109 100644 --- a/spec/bundler/bundler/settings_spec.rb +++ b/spec/bundler/bundler/settings_spec.rb @@ -119,6 +119,11 @@ settings.set_local :ssl_verify_mode, "1" expect(settings[:ssl_verify_mode]).to be 1 end + + it "coerces cooldown to integer" do + settings.set_local :cooldown, "7" + expect(settings[:cooldown]).to be 7 + end end context "when it's not possible to create the settings directory" do diff --git a/spec/bundler/bundler/source/rubygems/remote_spec.rb b/spec/bundler/bundler/source/rubygems/remote_spec.rb index f2214ca8fe129a..27430d4a3bb72e 100644 --- a/spec/bundler/bundler/source/rubygems/remote_spec.rb +++ b/spec/bundler/bundler/source/rubygems/remote_spec.rb @@ -169,4 +169,39 @@ def remote(uri) end end end + + describe "#cooldown" do + it "is nil by default" do + expect(remote(uri_no_auth).cooldown).to be_nil + end + + it "returns the value passed to the constructor" do + r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7) + expect(r.cooldown).to eq(7) + end + end + + describe "#effective_cooldown" do + it "returns the per-remote value when no override is set" do + r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7) + expect(r.effective_cooldown).to eq(7) + end + + it "returns nil when neither override nor per-remote value is set" do + expect(remote(uri_no_auth).effective_cooldown).to be_nil + end + + it "settings override per-remote value" do + r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7) + Bundler.settings.temporary(cooldown: 14) do + expect(r.effective_cooldown).to eq(14) + end + end + + it "settings override even when per-remote value is absent" do + Bundler.settings.temporary(cooldown: 14) do + expect(remote(uri_no_auth).effective_cooldown).to eq(14) + end + end + end end diff --git a/spec/bundler/bundler/source_list_spec.rb b/spec/bundler/bundler/source_list_spec.rb index 3ed58b867d9641..61bd99b063b4f6 100644 --- a/spec/bundler/bundler/source_list_spec.rb +++ b/spec/bundler/bundler/source_list_spec.rb @@ -129,6 +129,12 @@ Gem::URI("https://rubygems.org/"), ] end + + it "records the per-remote cooldown when supplied" do + source_list.add_global_rubygems_remote("https://othersource.org", cooldown: 7) + expect(returned_source.cooldown_for(Gem::URI("https://othersource.org/"))).to eq(7) + expect(returned_source.cooldown_for(Gem::URI("https://rubygems.org/"))).to be_nil + end end describe "#add_plugin_source" do diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb new file mode 100644 index 00000000000000..1368726c2c7722 --- /dev/null +++ b/spec/bundler/install/cooldown_spec.rb @@ -0,0 +1,246 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with the cooldown setting" do + before do + build_repo2 + end + + context "Gemfile DSL" do + it "accepts `source ..., cooldown: N` without error" do + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo2", cooldown: 5 + gem "myrack" + G + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "accepts `cooldown: 0` to disable cooldown for a source" do + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo2", cooldown: 0 + gem "myrack" + G + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + end + + context "CLI flag" do + before do + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + end + + it "accepts --cooldown N on install" do + bundle "install --cooldown 7", artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "accepts --cooldown 0 as an escape hatch" do + bundle "install --cooldown 0", artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "rejects a negative --cooldown value" do + bundle "install --cooldown=-7", artifice: "compact_index", raise_on_error: false + + expect(err).to match(/non-negative integer/) + end + end + + context "configuration" do + it "reads BUNDLE_COOLDOWN as an integer" do + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + bundle "install", env: { "BUNDLE_COOLDOWN" => "7" }, artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "reads `bundle config set cooldown N`" do + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + bundle "config set cooldown 7" + bundle "install", artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + end + + context "end-to-end with v2 compact index" do + before do + now = Time.now.utc + build_repo3 do + build_gem "ripe_gem", "1.0.0" do |s| + s.date = now - (30 * 86_400) + end + build_gem "ripe_gem", "2.0.0" do |s| + s.date = now - (1 * 86_400) + end + end + end + + it "excludes versions within the cooldown window" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "selects the latest version when --cooldown 0 is passed" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 0", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + + it "applies cooldown declared per-source in the Gemfile" do + gemfile <<-G + source "https://gem.repo3", cooldown: 7 + gem "ripe_gem" + G + + bundle "install", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "is overridden by CLI --cooldown when Gemfile sets a different per-source value" do + gemfile <<-G + source "https://gem.repo3", cooldown: 0 + gem "ripe_gem" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "bypasses cooldown when bundle install uses an existing lockfile" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + + it "annotates in-cooldown versions in bundle outdated table output" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem", "1.0.0" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem (= 1.0.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --cooldown 7", artifice: "compact_index_cooldown", raise_on_error: false + + expect(out).to match(/ripe_gem.*\(cooldown \d+d\)/) + end + + it "annotates in-cooldown versions in bundle outdated --parseable output" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem", "1.0.0" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem (= 1.0.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --cooldown 7 --parseable", artifice: "compact_index_cooldown", raise_on_error: false + + expect(out).to match(/ripe_gem.*in cooldown for \d+ more day/) + end + + it "surfaces a cooldown hint when bundle update filters every candidate" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update ripe_gem --cooldown 99999", artifice: "compact_index_cooldown", raise_on_error: false + + expect(err).to match(/excluded by the cooldown setting/) + expect(err).to match(/--cooldown 0/) + end + end +end diff --git a/spec/bundler/support/artifice/compact_index_cooldown.rb b/spec/bundler/support/artifice/compact_index_cooldown.rb new file mode 100644 index 00000000000000..85e3173c989cac --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_cooldown.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index_cooldown" +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexCooldownAPI) diff --git a/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb b/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb new file mode 100644 index 00000000000000..9920fd2c9520cb --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "compact_index" + +class CompactIndexCooldownAPI < CompactIndexAPI + helpers do + def build_gem_version(spec, deps, checksum) + created_at = spec.date&.utc&.iso8601 + CompactIndex::GemVersionV2.new(spec.version.version, spec.platform.to_s, checksum, nil, + deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s, created_at) + end + end +end diff --git a/spec/bundler/support/command_execution.rb b/spec/bundler/support/command_execution.rb index 979a46549a3a96..e2915b996d9d3d 100644 --- a/spec/bundler/support/command_execution.rb +++ b/spec/bundler/support/command_execution.rb @@ -72,7 +72,7 @@ def failure? attr_reader :failure_reason def normalize(string) - string.force_encoding(Encoding::UTF_8).strip.gsub("\r\n", "\n") + string.dup.force_encoding(Encoding::UTF_8).scrub.strip.gsub("\r\n", "\n") end end end diff --git a/spec/bundler/support/rubygems_ext.rb b/spec/bundler/support/rubygems_ext.rb index 8e3d84212d31fd..812dc4deaa9cd1 100644 --- a/spec/bundler/support/rubygems_ext.rb +++ b/spec/bundler/support/rubygems_ext.rb @@ -110,7 +110,9 @@ def install_vendored_compact_index url = "https://raw.githubusercontent.com/rubygems/rubygems.org/#{ref}/#{path}" target = target_root.join(path) FileUtils.mkdir_p(File.dirname(target)) - File.write(target, URI.parse(url).open(&:read)) + tmp = "#{target}.tmp" + File.write(tmp, URI.parse(url).open(&:read)) + File.rename(tmp, target) end end end diff --git a/spec/bundler/support/shards.rb b/spec/bundler/support/shards.rb index 580997eb72df69..25563c765c004d 100644 --- a/spec/bundler/support/shards.rb +++ b/spec/bundler/support/shards.rb @@ -143,6 +143,8 @@ module Shards "spec/bundler/ci_detector_spec.rb", ], shard_d: [ + "spec/bundler/resolver/cooldown_spec.rb", + "spec/install/cooldown_spec.rb", "spec/commands/outdated_spec.rb", "spec/commands/update_spec.rb", "spec/lock/lockfile_spec.rb", diff --git a/string.c b/string.c index 6865b0d8e658f3..134e1254318f74 100644 --- a/string.c +++ b/string.c @@ -630,7 +630,7 @@ static VALUE setup_fake_str(struct RString *fake_str, const char *name, long len, int encidx) { fake_str->basic.flags = T_STRING|RSTRING_NOEMBED|STR_NOFREE|STR_FAKESTR; - RBASIC_SET_SHAPE_ID((VALUE)fake_str, ROOT_SHAPE_ID); + RBASIC_SET_SHAPE_ID((VALUE)fake_str, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER); if (!name) { RUBY_ASSERT_ALWAYS(len == 0); @@ -713,6 +713,10 @@ search_nonascii(const char *p, const char *e) { const char *s, *t; + if (p < e && !ISASCII(*p)) { + return p; + } + #if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) # if SIZEOF_UINTPTR_T == 8 # define NONASCII_MASK UINT64_C(0x8080808080808080) @@ -2297,26 +2301,22 @@ enc_strlen(const char *p, const char *e, rb_encoding *enc, int cr) c = 0; if (ENC_CODERANGE_CLEAN_P(cr)) { while (p < e) { - if (ISASCII(*p)) { - q = search_nonascii(p, e); - if (!q) - return c + (e - p); - c += q - p; - p = q; - } + q = search_nonascii(p, e); + if (!q) + return c + (e - p); + c += q - p; + p = q; p += rb_enc_fast_mbclen(p, e, enc); c++; } } else { while (p < e) { - if (ISASCII(*p)) { - q = search_nonascii(p, e); - if (!q) - return c + (e - p); - c += q - p; - p = q; - } + q = search_nonascii(p, e); + if (!q) + return c + (e - p); + c += q - p; + p = q; p += rb_enc_mbclen(p, e, enc); c++; } @@ -2354,15 +2354,13 @@ rb_enc_strlen_cr(const char *p, const char *e, rb_encoding *enc, int *cr) else if (rb_enc_asciicompat(enc)) { c = 0; while (p < e) { - if (ISASCII(*p)) { - q = search_nonascii(p, e); - if (!q) { - if (!*cr) *cr = ENC_CODERANGE_7BIT; - return c + (e - p); - } - c += q - p; - p = q; + q = search_nonascii(p, e); + if (!q) { + if (!*cr) *cr = ENC_CODERANGE_7BIT; + return c + (e - p); } + c += q - p; + p = q; ret = rb_enc_precise_mbclen(p, e, enc); if (MBCLEN_CHARFOUND_P(ret)) { *cr |= ENC_CODERANGE_VALID; @@ -3020,16 +3018,14 @@ str_nth_len(const char *p, const char *e, long *nthp, rb_encoding *enc) *nthp = nth; return (char *)e; } - if (ISASCII(*p)) { - p2 = search_nonascii(p, e2); - if (!p2) { - nth -= e2 - p; - *nthp = nth; - return (char *)e2; - } - nth -= p2 - p; - p = p2; + p2 = search_nonascii(p, e2); + if (!p2) { + nth -= e2 - p; + *nthp = nth; + return (char *)e2; } + nth -= p2 - p; + p = p2; n = rb_enc_mbclen(p, e, enc); p += n; nth--; @@ -11850,17 +11846,11 @@ enc_str_scrub(rb_encoding *enc, VALUE str, VALUE repl, int cr) else if (MBCLEN_CHARFOUND_P(ret)) { cr = ENC_CODERANGE_VALID; p += MBCLEN_CHARFOUND_LEN(ret); - /* - * After a valid multibyte character, skip the following ASCII run. - * If the next byte is already non-ASCII, search_nonascii would only - * rediscover p after its word-at-a-time setup. - */ - if (p < e && ISASCII(*p)) { - p = search_nonascii(p, e); - if (!p) { - p = e; - break; - } + /* After a multibyte character, fast-skip the following ASCII run. */ + p = search_nonascii(p, e); + if (!p) { + p = e; + break; } } else if (MBCLEN_INVALID_P(ret)) { diff --git a/test/psych/test_parser.rb b/test/psych/test_parser.rb index c1e0abb89d0806..4ca4d63d80c483 100644 --- a/test/psych/test_parser.rb +++ b/test/psych/test_parser.rb @@ -198,6 +198,48 @@ def test_parse_io assert_called :end_stream end + def test_parse_io_returns_more_bytes_than_requested + # An IO-like source whose #read returns more bytes than the size it was + # asked for must not overflow libyaml's read buffer. + io = Object.new + def io.external_encoding; Encoding::UTF_8 end + def io.read len + return nil if @done + @done = true + "--- a\n" + ("#" * (len + (1 << 20))) + end + + # CRuby clamps the over-read and parses; JRuby's parser rejects the + # over-reading IO with an IOError. Either way there is no overflow. + begin + @parser.parse io + rescue IOError + return + end + assert_called :start_stream + assert_called :scalar + assert_called :end_stream + end + + def test_parse_io_returns_more_bytes_than_requested_multibyte + # The over-read is rounded down to a character boundary so a multibyte + # character is never split when the copy is clamped. + io = Object.new + def io.external_encoding; Encoding::UTF_8 end + def io.read len + return nil if @done + @done = true + "--- a\n#" + ("あ" * (len + (1 << 20))) + end + + begin + @parser.parse io + rescue IOError + return + end + assert_called :scalar + end + def test_syntax_error assert_raise(Psych::SyntaxError) do @parser.parse("---\n\"foo\"\n\"bar\"\n") diff --git a/test/ruby/test_shapes.rb b/test/ruby/test_shapes.rb index ef5dbd9fb15ee5..bace69658adfb5 100644 --- a/test/ruby/test_shapes.rb +++ b/test/ruby/test_shapes.rb @@ -1067,6 +1067,37 @@ def test_new_obj_has_t_object_shape assert_nil shape.parent end + def test_shape_layout + assert_equal :robject, RubyVM::Shape.of(TestObject.new).layout + + if ENV["RUBY_BOX"] + assert_equal :other, RubyVM::Shape.of(Kernel).layout + assert_equal :other, RubyVM::Shape.of(String).layout + else + assert_equal :rclass, RubyVM::Shape.of(Kernel).layout + assert_equal :rclass, RubyVM::Shape.of(String).layout + end + + assert_equal :rclass, RubyVM::Shape.of(Class.new).layout + assert_equal :rclass, RubyVM::Shape.of(Module.new).layout + + klass = Class.new + assert_equal :rclass, RubyVM::Shape.of(klass).layout + klass.instance_variable_set(:@a, 123) + assert_equal :rclass, RubyVM::Shape.of(klass).layout + + assert_equal :rdata, RubyVM::Shape.of(Thread.current).layout + assert_equal :rdata, RubyVM::Shape.of(lambda {}).layout + + assert_equal :other, RubyVM::Shape.of(Struct.new(:x).new(1)).layout + assert_equal :other, RubyVM::Shape.of([]).layout + assert_equal :other, RubyVM::Shape.of("hello").layout + assert_equal :other, RubyVM::Shape.of(/foo/).layout + assert_equal :other, RubyVM::Shape.of(2..3).layout + assert_equal :other, RubyVM::Shape.of(2**67).layout + assert_equal :other, RubyVM::Shape.of(:"aaroniscool#{123}").layout + end + def test_str_has_root_shape assert_shape_equal(RubyVM::Shape.root_shape, RubyVM::Shape.of("")) end diff --git a/test/rubygems/test_gem_commands_owner_command.rb b/test/rubygems/test_gem_commands_owner_command.rb index cf0fe521a21518..f6d4d03f84963a 100644 --- a/test/rubygems/test_gem_commands_owner_command.rb +++ b/test/rubygems/test_gem_commands_owner_command.rb @@ -399,7 +399,6 @@ def test_with_webauthn_enabled_success end def test_with_webauthn_enabled_failure - pend "Flaky on TruffleRuby" if RUBY_ENGINE == "truffleruby" response_success = "Owner added successfully." server = Gem::MockTCPServer.new error = Gem::WebauthnVerificationError.new("Something went wrong") @@ -417,7 +416,8 @@ def test_with_webauthn_enabled_failure end end - assert_match @stub_fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key + webauthn_verification_request = @stub_fetcher.requests.find {|req| req.path == "/api/v1/webauthn_verification" } + assert_match webauthn_verification_request["Authorization"], Gem.configuration.rubygems_api_key assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @stub_ui.output diff --git a/variable.c b/variable.c index 857d870413881f..687fa03631b6bb 100644 --- a/variable.c +++ b/variable.c @@ -1343,7 +1343,7 @@ rb_free_generic_ivar(VALUE obj) } } } - RBASIC_SET_SHAPE_ID(obj, ROOT_SHAPE_ID); + RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | ROOT_SHAPE_ID); } } @@ -1395,7 +1395,7 @@ rb_obj_set_fields(VALUE obj, VALUE fields_obj, ID field_name, VALUE original_fie } } - RBASIC_SET_SHAPE_ID(obj, RBASIC_SHAPE_ID(fields_obj)); + RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | RBASIC_SHAPE_ID(fields_obj)); } void @@ -1710,6 +1710,7 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) size_t trailing_fields = new_fields_count - removed_index; MEMMOVE(&fields[removed_index], &fields[removed_index + 1], VALUE, trailing_fields); + RUBY_ASSERT(rb_shape_layout(next_shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID(fields_obj, next_shape_id); if (FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP)) { @@ -1732,7 +1733,7 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) } } - RBASIC_SET_SHAPE_ID(obj, next_shape_id); + RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | next_shape_id); if (fields_obj != original_fields_obj) { switch (type) { case T_OBJECT: @@ -1843,7 +1844,7 @@ imemo_fields_set(VALUE owner, VALUE fields_obj, shape_id_t target_shape_id, ID f RUBY_ASSERT(field_name); st_insert(table, (st_data_t)field_name, (st_data_t)val); RB_OBJ_WRITTEN(fields_obj, Qundef, val); - RBASIC_SET_SHAPE_ID(fields_obj, target_shape_id); + RBASIC_SET_SHAPE_ID(fields_obj, rb_shape_id_with_robject_layout(target_shape_id)); } else { attr_index_t index = RSHAPE_INDEX(target_shape_id); @@ -1855,7 +1856,7 @@ imemo_fields_set(VALUE owner, VALUE fields_obj, shape_id_t target_shape_id, ID f RB_OBJ_WRITE(fields_obj, &table[index], val); if (index >= RSHAPE_LEN(current_shape_id)) { - RBASIC_SET_SHAPE_ID(fields_obj, target_shape_id); + RBASIC_SET_SHAPE_ID(fields_obj, rb_shape_id_with_robject_layout(target_shape_id)); } } @@ -2010,11 +2011,12 @@ rb_obj_freeze_inline(VALUE x) RB_FL_UNSET_RAW(x, FL_USER2 | FL_USER3); // STR_CHILLED } + // rb_obj_freeze_inline(String) shape_id_t shape_id = rb_obj_shape_transition_frozen(x); switch (BUILTIN_TYPE(x)) { case T_CLASS: case T_MODULE: - RBASIC_SET_SHAPE_ID(RCLASS_WRITABLE_ENSURE_FIELDS_OBJ(x), shape_id); + rb_obj_freeze_inline(RCLASS_WRITABLE_ENSURE_FIELDS_OBJ(x)); // FIXME: How to do multi-shape? RBASIC_SET_SHAPE_ID(x, shape_id); break; @@ -2303,7 +2305,7 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) } if (!RSHAPE_LEN(dest_shape_id)) { - RBASIC_SET_SHAPE_ID(dest, dest_shape_id); + RBASIC_SET_SHAPE_ID(dest, rb_shape_layout(RBASIC_SHAPE_ID(dest)) | dest_shape_id); return; } @@ -4636,6 +4638,7 @@ class_fields_ivar_set(VALUE klass, VALUE fields_obj, ID id, VALUE val, bool conc } if (new_ivar) { + RUBY_ASSERT(rb_shape_layout(next_shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID(fields_obj, next_shape_id); } } @@ -4658,6 +4661,7 @@ class_fields_ivar_set(VALUE klass, VALUE fields_obj, ID id, VALUE val, bool conc RB_OBJ_WRITTEN(fields_obj, Qundef, val); if (fields_obj != original_fields_obj) { + RUBY_ASSERT(rb_shape_layout(next_shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID(fields_obj, next_shape_id); } } @@ -4684,7 +4688,7 @@ class_ivar_set(VALUE obj, ID id, VALUE val, bool *new_ivar) // TODO: What should we set as the T_CLASS shape_id? // In most case we can replicate the single `fields_obj` shape // but in namespaced case? Perhaps INVALID_SHAPE_ID? - RBASIC_SET_SHAPE_ID(obj, RBASIC_SHAPE_ID(new_fields_obj)); + RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | RBASIC_SHAPE_ID(new_fields_obj)); return index; } @@ -4709,7 +4713,7 @@ rb_fields_tbl_copy(VALUE dst, VALUE src) VALUE fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(src); if (fields_obj) { RCLASS_WRITABLE_SET_FIELDS_OBJ(dst, rb_imemo_fields_clone(fields_obj)); - RBASIC_SET_SHAPE_ID(dst, RBASIC_SHAPE_ID(src)); + RBASIC_SET_SHAPE_ID(dst, rb_shape_layout(RBASIC_SHAPE_ID(dst)) | RBASIC_SHAPE_ID(src)); } } diff --git a/vm_insnhelper.c b/vm_insnhelper.c index ac0d81092d890d..f515662bf07765 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -1412,7 +1412,8 @@ vm_setivar_class(VALUE obj, VALUE val, rb_setivar_cache cache) RB_OBJ_WRITE(fields_obj, &rb_imemo_fields_ptr(fields_obj)[cache.index], val); if (shape_id != dest_shape_id) { - RBASIC_SET_SHAPE_ID(obj, dest_shape_id); + // The dest_shape_id comes from the fields_obj + RBASIC_SET_SHAPE_ID(obj, SHAPE_ID_LAYOUT_RCLASS | (dest_shape_id & ~SHAPE_ID_LAYOUT_MASK)); RBASIC_SET_SHAPE_ID(fields_obj, dest_shape_id); } @@ -1437,7 +1438,9 @@ vm_setivar_default(VALUE obj, ID id, VALUE val, rb_setivar_cache cache) if (shape_id != dest_shape_id) { RBASIC_SET_SHAPE_ID(obj, dest_shape_id); - RBASIC_SET_SHAPE_ID(fields_obj, dest_shape_id); + // The dest_shape_id comes from the owner, but fields_obj must always + // have layout RObject, so give the fields_object the right layout. + RBASIC_SET_SHAPE_ID(fields_obj, rb_shape_id_with_robject_layout(dest_shape_id)); } RB_DEBUG_COUNTER_INC(ivar_set_ic_hit); diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index bd832acc9604cd..08c502b0d84e47 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1491,8 +1491,13 @@ pub const SHAPE_ID_HEAP_INDEX_MASK: shape_id_fl_type = 7864320; pub const SHAPE_ID_FL_COMPLEX: shape_id_fl_type = 8388608; pub const SHAPE_ID_FL_FROZEN: shape_id_fl_type = 16777216; pub const SHAPE_ID_FL_HAS_OBJECT_ID: shape_id_fl_type = 33554432; +pub const SHAPE_ID_LAYOUT_ROBJECT: shape_id_fl_type = 0; +pub const SHAPE_ID_LAYOUT_RCLASS: shape_id_fl_type = 67108864; +pub const SHAPE_ID_LAYOUT_RDATA: shape_id_fl_type = 134217728; +pub const SHAPE_ID_LAYOUT_OTHER: shape_id_fl_type = 201326592; +pub const SHAPE_ID_LAYOUT_MASK: shape_id_fl_type = 201326592; pub const SHAPE_ID_FL_NON_CANONICAL_MASK: shape_id_fl_type = 50331648; -pub const SHAPE_ID_FLAGS_MASK: shape_id_fl_type = 66584576; +pub const SHAPE_ID_FLAGS_MASK: shape_id_fl_type = 267911168; pub type shape_id_fl_type = u32; pub const CONST_DEPRECATED: rb_const_flag_t = 256; pub const CONST_VISIBILITY_MASK: rb_const_flag_t = 255; diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 6911129ad34896..c2ae897d9aa9cb 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -5195,18 +5195,32 @@ impl Function { } } Insn::FixnumAdd { left, right, .. } => { + match (self.type_of(left).fixnum_value(), self.type_of(right).fixnum_value()) { + (Some(0), _) => { self.make_equal_to(insn_id, right); continue; } + (_, Some(0)) => { self.make_equal_to(insn_id, left); continue; } + _ => {} + } self.fold_fixnum_bop(insn_id, left, right, |l, r| match (l, r) { (Some(l), Some(r)) => l.checked_add(r), _ => None, }) } Insn::FixnumSub { left, right, .. } => { + match (self.type_of(left).fixnum_value(), self.type_of(right).fixnum_value()) { + (_, Some(0)) => { self.make_equal_to(insn_id, left); continue; } + _ => {} + } self.fold_fixnum_bop(insn_id, left, right, |l, r| match (l, r) { (Some(l), Some(r)) => l.checked_sub(r), _ => None, }) } Insn::FixnumMult { left, right, .. } => { + match (self.type_of(left).fixnum_value(), self.type_of(right).fixnum_value()) { + (Some(1), _) => { self.make_equal_to(insn_id, right); continue; } + (_, Some(1)) => { self.make_equal_to(insn_id, left); continue; } + _ => {} + } self.fold_fixnum_bop(insn_id, left, right, |l, r| match (l, r) { (Some(l), Some(r)) => l.checked_mul(r), (Some(0), _) | (_, Some(0)) => Some(0), @@ -5214,6 +5228,10 @@ impl Function { }) } Insn::FixnumDiv { left, right, .. } => { + match (self.type_of(left).fixnum_value(), self.type_of(right).fixnum_value()) { + (_, Some(1)) => { self.make_equal_to(insn_id, left); continue; } + _ => {} + } self.fold_fixnum_bop(insn_id, left, right, |l, r| match (l, r) { (Some(l), Some(r)) if l == (RUBY_FIXNUM_MIN as i64) && r == -1 => None, // Avoid Fixnum overflow (Some(_l), Some(r)) if r == 0 => None, // Avoid Divide by zero. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 74c0af710e7363..53f65db4ccd502 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -117,6 +117,36 @@ mod hir_opt_tests { "); } + #[test] + fn test_fold_fixnum_add_zero() { + eval(" + def test(n) + 0 + n + 0 + end + test 1; test 2 + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :n@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :n@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + v14:Fixnum[0] = Const Value(0) + PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) + v32:Fixnum = GuardType v10, Fixnum + CheckInterrupts + Return v32 + "); + } + #[test] fn test_fold_fixnum_sub() { eval(" @@ -171,6 +201,36 @@ mod hir_opt_tests { "); } + #[test] + fn test_fold_fixnum_sub_zero() { + eval(" + def test(n) + n - 0 + end + test 1; test 2 + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :n@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :n@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + v15:Fixnum[0] = Const Value(0) + PatchPoint MethodRedefined(Integer@0x1008, -@0x1010, cme:0x1018) + v26:Fixnum = GuardType v10, Fixnum + CheckInterrupts + Return v26 + "); + } + #[test] fn test_fold_fixnum_mult() { eval(" @@ -226,9 +286,40 @@ mod hir_opt_tests { v46:Fixnum[0] = Const Value(0) v47:Fixnum[0] = Const Value(0) PatchPoint MethodRedefined(Integer@0x1008, +@0x1040, cme:0x1048) - v48:Fixnum[0] = Const Value(0) CheckInterrupts - Return v48 + Return v47 + "); + } + + #[test] + fn test_fold_fixnum_mult_one() { + eval(" + def test(n) + 1 * n + n * 1 + end + test 1; test 2 + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :n@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :n@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + v14:Fixnum[1] = Const Value(1) + PatchPoint MethodRedefined(Integer@0x1008, *@0x1010, cme:0x1018) + v36:Fixnum = GuardType v10, Fixnum + PatchPoint MethodRedefined(Integer@0x1008, +@0x1040, cme:0x1048) + v45:Fixnum = FixnumAdd v36, v36 + CheckInterrupts + Return v45 "); } @@ -340,6 +431,36 @@ mod hir_opt_tests { "); } + #[test] + fn test_fold_fixnum_div_one() { + eval(" + def test(n) + n / 1 + end + test 1; test 2 + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :n@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :n@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + v15:Fixnum[1] = Const Value(1) + PatchPoint MethodRedefined(Integer@0x1008, /@0x1010, cme:0x1018) + v26:Fixnum = GuardType v10, Fixnum + CheckInterrupts + Return v26 + "); + } + #[test] fn test_fold_fixnum_mod_zero_by_zero() { eval(" @@ -16519,9 +16640,8 @@ mod hir_opt_tests { v139:BasicObject = LoadField v138, :var@0x1040 PatchPoint MethodRedefined(Integer@0x1048, +@0x1050, cme:0x1058) v179:Fixnum = GuardType v139, Fixnum - v180:Fixnum = FixnumAdd v17, v179 PatchPoint NoEPEscape(test) - v185:Fixnum = FixnumAdd v180, v179 + v185:Fixnum = FixnumAdd v179, v179 v190:Fixnum = FixnumAdd v185, v179 v195:Fixnum = FixnumAdd v190, v179 v200:Fixnum = FixnumAdd v195, v179