diff --git a/NEWS.md b/NEWS.md index a2a1da19441609..e5eca2441cd127 100644 --- a/NEWS.md +++ b/NEWS.md @@ -137,11 +137,11 @@ They are still available on rubygems.org and can be installed with ### The following default gems are updated. * RubyGems 4.1.0.dev - * 4.0.3 to [v4.0.4][RubyGems-v4.0.4], [v4.0.5][RubyGems-v4.0.5], [v4.0.6][RubyGems-v4.0.6], [v4.0.7][RubyGems-v4.0.7], [v4.0.8][RubyGems-v4.0.8], [v4.0.9][RubyGems-v4.0.9], [v4.0.10][RubyGems-v4.0.10], [v4.0.11][RubyGems-v4.0.11], [v4.0.12][RubyGems-v4.0.12], [v4.0.13][RubyGems-v4.0.13], [v4.0.14][RubyGems-v4.0.14], [v4.0.15][RubyGems-v4.0.15], [v4.0.16][RubyGems-v4.0.16], [v4.0.17][RubyGems-v4.0.17] + * 4.0.3 to [v4.0.4][RubyGems-v4.0.4], [v4.0.5][RubyGems-v4.0.5], [v4.0.6][RubyGems-v4.0.6], [v4.0.7][RubyGems-v4.0.7], [v4.0.8][RubyGems-v4.0.8], [v4.0.9][RubyGems-v4.0.9], [v4.0.10][RubyGems-v4.0.10], [v4.0.11][RubyGems-v4.0.11], [v4.0.12][RubyGems-v4.0.12], [v4.0.13][RubyGems-v4.0.13], [v4.0.14][RubyGems-v4.0.14], [v4.0.15][RubyGems-v4.0.15], [v4.0.16][RubyGems-v4.0.16], [v4.0.17][RubyGems-v4.0.17], [v4.0.18][RubyGems-v4.0.18] * bundler 4.1.0.dev * 4.0.3 to [v4.0.4][bundler-v4.0.4], [v4.0.5][bundler-v4.0.5], [v4.0.6][bundler-v4.0.6], [v4.0.7][bundler-v4.0.7], [v4.0.8][bundler-v4.0.8], [v4.0.9][bundler-v4.0.9], [v4.0.10][bundler-v4.0.10], [v4.0.11][bundler-v4.0.11], [v4.0.12][bundler-v4.0.12], [v4.0.13][bundler-v4.0.13], [v4.0.14][bundler-v4.0.14], [v4.0.15][bundler-v4.0.15], [v4.0.16][bundler-v4.0.16], [v4.0.17][bundler-v4.0.17] * erb 6.0.7 - * 6.0.1 to [v6.0.1.1][erb-v6.0.1.1], [v6.0.2][erb-v6.0.2], [v6.0.3][erb-v6.0.3], [v6.0.4][erb-v6.0.4], [v6.0.5][erb-v6.0.5], [v6.0.6][erb-v6.0.6] + * 6.0.1 to [v6.0.1.1][erb-v6.0.1.1], [v6.0.2][erb-v6.0.2], [v6.0.3][erb-v6.0.3], [v6.0.4][erb-v6.0.4], [v6.0.5][erb-v6.0.5], [v6.0.6][erb-v6.0.6], [v6.0.7][erb-v6.0.7] * error_highlight 0.7.2 * ipaddr 1.2.9 * 1.2.8 to [v1.2.9][ipaddr-v1.2.9] @@ -178,7 +178,7 @@ They are still available on rubygems.org and can be installed with * net-imap 0.6.6 * 0.6.2 to [v0.6.3][net-imap-v0.6.3], [v0.6.4][net-imap-v0.6.4], [v0.6.4.1][net-imap-v0.6.4.1], [v0.6.5][net-imap-v0.6.5], [v0.6.6][net-imap-v0.6.6] * rbs 4.0.3 - * 3.10.0 to [v3.10.1][rbs-v3.10.1], [v3.10.2][rbs-v3.10.2], [v3.10.3][rbs-v3.10.3], [v3.10.4][rbs-v3.10.4], [v4.0.0.dev.5][rbs-v4.0.0.dev.5], [v4.0.0][rbs-v4.0.0], [v4.0.2][rbs-v4.0.2], [v4.0.3][rbs-v4.0.3] + * 3.10.0 to [v3.10.1][rbs-v3.10.1], [v3.10.2][rbs-v3.10.2], [v3.10.3][rbs-v3.10.3], [v3.10.4][rbs-v3.10.4], [v4.0.0.dev.1][rbs-v4.0.0.dev.1], [v4.0.0.dev.2][rbs-v4.0.0.dev.2], [v4.0.0.dev.3][rbs-v4.0.0.dev.3], [v4.0.0.dev.4][rbs-v4.0.0.dev.4], [v4.0.0.dev.5][rbs-v4.0.0.dev.5], [v4.0.0][rbs-v4.0.0], [v4.0.1.dev.1][rbs-v4.0.1.dev.1], [v4.0.1.dev.2][rbs-v4.0.1.dev.2], [v4.0.1][rbs-v4.0.1], [v4.0.2][rbs-v4.0.2], [v4.0.3][rbs-v4.0.3] * typeprof 0.32.0 * mutex_m 0.3.0 * bigdecimal 4.1.2 @@ -316,6 +316,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [RubyGems-v4.0.15]: https://github.com/rubygems/rubygems/releases/tag/v4.0.15 [RubyGems-v4.0.16]: https://github.com/rubygems/rubygems/releases/tag/v4.0.16 [RubyGems-v4.0.17]: https://github.com/rubygems/rubygems/releases/tag/v4.0.17 +[RubyGems-v4.0.18]: https://github.com/rubygems/rubygems/releases/tag/v4.0.18 [bundler-v4.0.4]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.4 [bundler-v4.0.5]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.5 [bundler-v4.0.6]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.6 @@ -336,6 +337,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [erb-v6.0.4]: https://github.com/ruby/erb/releases/tag/v6.0.4 [erb-v6.0.5]: https://github.com/ruby/erb/releases/tag/v6.0.5 [erb-v6.0.6]: https://github.com/ruby/erb/releases/tag/v6.0.6 +[erb-v6.0.7]: https://github.com/ruby/erb/releases/tag/v6.0.7 [ipaddr-v1.2.9]: https://github.com/ruby/ipaddr/releases/tag/v1.2.9 [json-v2.18.1]: https://github.com/ruby/json/releases/tag/v2.18.1 [json-v2.19.0]: https://github.com/ruby/json/releases/tag/v2.19.0 @@ -379,8 +381,15 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [rbs-v3.10.2]: https://github.com/ruby/rbs/releases/tag/v3.10.2 [rbs-v3.10.3]: https://github.com/ruby/rbs/releases/tag/v3.10.3 [rbs-v3.10.4]: https://github.com/ruby/rbs/releases/tag/v3.10.4 +[rbs-v4.0.0.dev.1]: https://github.com/ruby/rbs/releases/tag/v4.0.0.dev.1 +[rbs-v4.0.0.dev.2]: https://github.com/ruby/rbs/releases/tag/v4.0.0.dev.2 +[rbs-v4.0.0.dev.3]: https://github.com/ruby/rbs/releases/tag/v4.0.0.dev.3 +[rbs-v4.0.0.dev.4]: https://github.com/ruby/rbs/releases/tag/v4.0.0.dev.4 [rbs-v4.0.0.dev.5]: https://github.com/ruby/rbs/releases/tag/v4.0.0.dev.5 [rbs-v4.0.0]: https://github.com/ruby/rbs/releases/tag/v4.0.0 +[rbs-v4.0.1.dev.1]: https://github.com/ruby/rbs/releases/tag/v4.0.1.dev.1 +[rbs-v4.0.1.dev.2]: https://github.com/ruby/rbs/releases/tag/v4.0.1.dev.2 +[rbs-v4.0.1]: https://github.com/ruby/rbs/releases/tag/v4.0.1 [rbs-v4.0.2]: https://github.com/ruby/rbs/releases/tag/v4.0.2 [rbs-v4.0.3]: https://github.com/ruby/rbs/releases/tag/v4.0.3 [bigdecimal-v4.1.0]: https://github.com/ruby/bigdecimal/releases/tag/v4.1.0 diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 64ce8a91165b3c..0207773e8843cf 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -2420,6 +2420,24 @@ def initialize(a) port.receive == :aborted } +assert_equal 'ok', %q{ + long = Ractor.new { Ractor.receive } + Ractor.new(long) { |t| t.monitor(Ractor::Port.new) }.value + GC.start + 'ok' +} + +assert_equal 'ok', %q{ + long = Ractor.new { Ractor.receive } + 20.times do + Ractor.new(long) { |t| t.monitor(Ractor::Port.new) }.value + GC.start + end + long.send(:bye) + long.join + 'ok' +} + ## Ractor#join # Ractor#join returns self when the Ractor is terminated. diff --git a/doc/file/timestamps.md b/doc/file/timestamps.md index cd8b064e837571..24f63452d13a3f 100644 --- a/doc/file/timestamps.md +++ b/doc/file/timestamps.md @@ -9,12 +9,12 @@ the returned times may vary among filesystems, even on the same machine. These timestamps methods are: -| Name | Meaning | Changes | -|:--------------------------------:|----------------------------------------|-----------------------| -| [`birthtime`](#birth-time) | Create time. | Never. | -| [`mtime`](#modification-time) | Modification time. | When written. | -| [`atime`](#access-time) | Access time. | When read or written. | -| [`ctime`](#metadata-change-time) | Metadata-change time (or create time). | See below. | +| Name | Meaning | Changes | +|:--------------------------------:|----------------------------------------|---------------| +| [`birthtime`](#birth-time) | Create time. | Never. | +| [`mtime`](#modification-time) | Modification time. | When written. | +| [`atime`](#access-time) | Access time. | When read. | +| [`ctime`](#metadata-change-time) | Metadata-change time (or create time). | See below. | A method raises an exception if the filesystem does not support the corresponding timestamp. @@ -61,11 +61,8 @@ The modification time (along with the access time) may also be updated explicitl ## Access \Time -The access time for an entry is the time of the most recent read of or write to -the content of the entry, as reported by the underlying filesystem. - -Depending on a filesystem's settings, reading an entry may cause the access time -to be updated immediately, later, or never. +The access time for an entry is the time of the most recent read for the entry, +as reported by the underlying filesystem. Each of these methods returns the access time for an entry as a Time object: @@ -81,6 +78,41 @@ The access time (along with the modification time) may also be updated explicitl - Pathname#lutime. - Pathname#utime. +Depending on a filesystem's settings, reading an entry may cause the access time +to be updated immediately, later, or never; +thus in the tables below, some entries say "Filesystem-dependent." + +### File + +The access time for a file is commonly the most recent time the file was read, +or if never read, the time it was created: + +| Operation | Updates Access \Time | +|:------------------:|:--------------------:| +| Create | Yes | +| Read | Filesystem-dependent | +| Write | No | +| Rename | No | +| Move | No | +| Change permissions | No | +| Change ownership | No | + + +### Directory + +The access time for a directory is commonly the most recent time its entries were read, +or if never read, the time it was created: + +| Operation | Updates Access \Time | +|:------------------:|:--------------------:| +| Create | Yes | +| Read entries | Filesystem-dependent | +| Write entries | No | +| Rename | No | +| Move | No | +| Change permissions | No | +| Change ownership | No | + ## Metadata-Change \Time The metadata-change time for an entry is the time the entry last read. diff --git a/doc/language/ractor.md b/doc/language/ractor.md index 1592656217678f..059d9df76827b7 100644 --- a/doc/language/ractor.md +++ b/doc/language/ractor.md @@ -533,9 +533,7 @@ In order to dynamically define a method with `Module#define_method` that can be ```ruby class A - define_method :testing, &Ractor.shareable_proc do - p self - end + define_method(:testing, Ractor.shareable_proc { p self }) end Ractor.new do a = A.new diff --git a/ext/objspace/lib/objspace.rb b/ext/objspace/lib/objspace.rb index 47873f51126796..0bd5670b5e700c 100644 --- a/ext/objspace/lib/objspace.rb +++ b/ext/objspace/lib/objspace.rb @@ -70,7 +70,7 @@ def dump(obj, output: :string) # If _shapes_ is +false+, no shapes are dumped. # # To only dump objects allocated past a certain point you can combine _since_ and _shapes_: - # ObjectSpace.trace_object_allocations + # ObjectSpace.trace_object_allocations_start # GC.start # gc_generation = GC.count # shape_generation = RubyVM.stat(:next_shape_id) diff --git a/ext/openssl/ossl_rand.c b/ext/openssl/ossl_rand.c index 753f8b25f77fa5..a5db62a3610b6a 100644 --- a/ext/openssl/ossl_rand.c +++ b/ext/openssl/ossl_rand.c @@ -67,7 +67,7 @@ ossl_rand_add(VALUE self, VALUE str, VALUE entropy) static VALUE ossl_rand_load_file(VALUE self, VALUE filename) { - if(!RAND_load_file(StringValueCStr(filename), -1)) { + if (RAND_load_file(StringValueCStr(filename), -1) < 0) { ossl_raise(eRandomError, NULL); } return Qtrue; diff --git a/hash.c b/hash.c index 90d1c1ffd3f50b..c898327870bc79 100644 --- a/hash.c +++ b/hash.c @@ -32,6 +32,7 @@ #include "internal/class.h" #include "internal/cont.h" #include "internal/error.h" +#include "internal/gc.h" #include "internal/hash.h" #include "internal/object.h" #include "internal/proc.h" @@ -44,6 +45,7 @@ #include "ruby/st.h" #include "ruby/util.h" #include "ruby_assert.h" +#include "shape.h" #include "symbol.h" #include "ruby/thread_native.h" #include "ruby/ractor.h" @@ -1463,9 +1465,14 @@ hash_alloc(VALUE klass) #if USE_ZJIT size_t -rb_zjit_hash_new_size(void) -{ - return hash_slot_size(sizeof(st_table) > sizeof(ar_table)); +rb_zjit_hash_new_size(VALUE *flags_out) +{ + size_t size = hash_slot_size(sizeof(st_table) > sizeof(ar_table)); + // mimic rb_newobj() + shape_id_t shape_id = rb_shape_transition_slot_size(ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, + rb_gc_size_slot_size(size)); + *flags_out = T_HASH | ((VALUE)shape_id << SHAPE_FLAG_SHIFT); + return size; } #endif diff --git a/io_buffer.c b/io_buffer.c index e7a03fcb786b14..ae00daa7e641da 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -1819,15 +1819,15 @@ io_buffer_slice(int argc, VALUE *argv, VALUE self) * Transfers ownership of the underlying memory to a new buffer, causing the * current buffer to become uninitialized. * - * buffer = IO::Buffer.new('test') + * buffer = IO::Buffer.for('test') * other = buffer.transfer * other * # => - * # # + * # # * # 0x00000000 74 65 73 74 test * buffer * # => - * # # + * # # * buffer.null? * # => true */ diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb index b44fbc30964157..070cc095bdef5e 100644 --- a/lib/bundler/cli/common.rb +++ b/lib/bundler/cli/common.rb @@ -20,6 +20,17 @@ def self.print_post_install_message(name, msg) Bundler.ui.info msg end + def self.output_cooldown_skipped_summary(definition = Bundler.definition) + skipped = definition.cooldown_skipped + return if skipped.empty? + + Bundler.ui.info "The following gem versions were skipped by the cooldown setting:" + skipped.each do |entry| + days = entry[:available_in_days] + Bundler.ui.info " * #{entry[:name]} #{entry[:version]} (available in #{days} #{days == 1 ? "day" : "days"}), resolved #{entry[:resolved]} instead" + end + end + def self.output_fund_metadata_summary return if Bundler.settings["ignore_funding_requests"] definition = Bundler.definition diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb index cf21afbb06e062..aec64adb2c496f 100644 --- a/lib/bundler/cli/install.rb +++ b/lib/bundler/cli/install.rb @@ -64,6 +64,7 @@ def run end Bundler::CLI::Common.output_post_install_messages installer.post_install_messages + Bundler::CLI::Common.output_cooldown_skipped_summary(definition) if CLI::Common.clean_after_install? require_relative "clean" diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb index f6732c59254a09..eeea5ae1d51bd0 100644 --- a/lib/bundler/cli/lock.rb +++ b/lib/bundler/cli/lock.rb @@ -77,6 +77,8 @@ def run puts "Writing lockfile to #{file}" definition.write_lock(file, false) end + + Bundler::CLI::Common.output_cooldown_skipped_summary(definition) end Bundler.ui.output_stream = previous_output_stream diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb index 33f9ac87406a54..c4eb6b07a0f334 100644 --- a/lib/bundler/cli/update.rb +++ b/lib/bundler/cli/update.rb @@ -126,6 +126,7 @@ def run Bundler.ui.confirm "Bundle updated!" Bundler::CLI::Common.output_without_groups_message(:update) Bundler::CLI::Common.output_post_install_messages installer.post_install_messages + Bundler::CLI::Common.output_cooldown_skipped_summary Bundler::CLI::Common.output_fund_metadata_summary end diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index ac37929386592c..b997b7e4767bd5 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -372,6 +372,12 @@ def spec_git_paths sources.git_sources.filter_map {|s| File.realpath(s.path) if File.exist?(s.path) } end + # Versions excluded by cooldown during the last resolution, one entry per + # gem with the newest skipped version. Empty when no resolution ran. + def cooldown_skipped + @cooldown_skipped || [] + end + def groups dependencies.flat_map(&:groups).uniq end @@ -783,6 +789,8 @@ def start_resolution result = SpecSet.new(resolver.start) + @cooldown_skipped = resolver.cooldown_skipped + @resolved_bundler_version = result.find {|spec| spec.name == "bundler" }&.version @new_platforms.each do |platform| diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 972b33863f04aa..510ae09712c295 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -14,11 +14,15 @@ class Resolver require_relative "resolver/root" require_relative "resolver/strategy" + attr_reader :cooldown_skipped + def initialize(base, gem_version_promoter, most_specific_locked_platform = nil) @source_requirements = base.source_requirements @base = base @gem_version_promoter = gem_version_promoter @most_specific_locked_platform = most_specific_locked_platform + @cooldown_skipped = [] + @cooldown_skipped_specs = {} end def start @@ -36,6 +40,8 @@ def setup_solver root = Resolver::Root.new(name_for_explicit_dependency_source) root_version = Resolver::Candidate.new(0) + @cooldown_skipped_specs = {} + @all_specs = Hash.new do |specs, name| source = source_for(name) matches = source.specs.search(name) @@ -83,7 +89,9 @@ def solve_versions(root:, logger:) result = solver.solve resolved_specs = result.flat_map {|package, version| version.to_specs(package, @most_specific_locked_platform) } Override.attach(resolved_specs, @base.overrides) - SpecSet.new(resolved_specs).specs_with_additional_variants_from(@base.locked_specs) + spec_set = SpecSet.new(resolved_specs).specs_with_additional_variants_from(@base.locked_specs) + @cooldown_skipped = cooldown_skipped_summary(spec_set) + spec_set rescue Gem::PubGrub::SolveFailure => e incompatibility = e.incompatibility @@ -447,6 +455,7 @@ def cooldown_excluded_versions(specs) specs.each do |spec| next unless cooldown_excluded?(spec) excluded[[spec.name, spec.version]] = true + (@cooldown_skipped_specs ||= {})[[spec.name, spec.version]] ||= spec end excluded end @@ -489,6 +498,50 @@ def cooldown_now @cooldown_now ||= Time.now end + # Reports, per gem, the newest version that cooldown kept out of a + # successful resolution. A skipped version is only worth reporting when it + # is newer than the resolved version and satisfies every requirement the + # final resolution places on that gem, so we don't claim a version the + # resolver could never have picked anyway. + def cooldown_skipped_summary(spec_set) + return [] if @cooldown_skipped_specs.empty? + + requirements = Hash.new {|h, name| h[name] = [] } + @requirements.each {|dep| requirements[dep.name] << dep.requirement } + spec_set.each do |spec| + spec.dependencies.each {|dep| requirements[dep.name] << dep.requirement } + end + + resolved_versions = {} + spec_set.each do |spec| + version = resolved_versions[spec.name] + resolved_versions[spec.name] = spec.version if version.nil? || spec.version > version + end + + newest_skipped = {} + @cooldown_skipped_specs.each do |(name, version), spec| + resolved = resolved_versions[name] + next unless resolved && version > resolved + next unless requirements[name].all? {|req| req.satisfied_by?(version) } + newest = newest_skipped[name] + newest_skipped[name] = spec if newest.nil? || version > newest.version + end + + newest_skipped.values.sort_by(&:name).map do |spec| + { + name: spec.name, + version: spec.version, + resolved: resolved_versions[spec.name], + available_in_days: remaining_cooldown_days(spec), + } + end + end + + def remaining_cooldown_days(spec) + remaining = (spec.remote.effective_cooldown * 86_400) - (cooldown_now - spec.created_at) + [(remaining / 86_400.0).ceil, 1].max + end + def filter_remote_specs(specs, package) if package.prefer_local? local_specs = specs.select {|s| s.is_a?(StubSpecification) } diff --git a/lib/delegate.rb b/lib/delegate.rb index 0ff9797bdb2a61..e1f29925e782e9 100644 --- a/lib/delegate.rb +++ b/lib/delegate.rb @@ -252,6 +252,8 @@ def self.public_api # :nodoc: # and even to change the object being delegated to at a later time with # #__setobj__. # +# require 'date' +# # class User # def born_on # Date.new(1989, 9, 10) diff --git a/lib/rubygems/compact_index_client/http_fetcher.rb b/lib/rubygems/compact_index_client/http_fetcher.rb index cb7b485ee49791..be63807b7098b5 100644 --- a/lib/rubygems/compact_index_client/http_fetcher.rb +++ b/lib/rubygems/compact_index_client/http_fetcher.rb @@ -41,6 +41,11 @@ def fetch(uri, headers, redirects_remaining) raise Gem::RemoteFetcher::FetchError.new("redirecting but no redirect location was given", uri) unless location fetch(uri + location, headers, redirects_remaining - 1) + when Gem::Net::HTTPRangeNotSatisfiable + raise Gem::RemoteFetcher::FetchError.new("bad response #{response.message} #{response.code}", uri) unless headers.key?("Range") + + # The local cache is longer than the remote file, refetch it whole. + fetch(uri, headers.except("Range"), redirects_remaining) else raise Gem::RemoteFetcher::FetchError.new("bad response #{response.message} #{response.code}", uri) end diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index 50eecedb77276d..adee800051fc07 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -326,21 +326,30 @@ def authors=(value) ## # The license for this gem. # - # The license must be no more than 64 characters. - # - # This should just be the name of your license. The full text of the license - # should be inside of the gem (at the top level) when you build it. - # - # The simplest way is to specify the standard SPDX ID - # https://spdx.org/licenses/ for the license. + # The license must be no more than 64 characters, and should be a single + # SPDX license identifier from https://spdx.org/licenses/. # Ideally, you should pick one that is OSI (Open Source Initiative) # https://opensource.org/licenses/ approved. # # The most commonly used OSI-approved licenses are MIT and Apache-2.0. # GitHub also provides a license picker at https://choosealicense.com/. # - # You can also use a custom license file along with your gemspec and specify - # a LicenseRef-, where idstring is the name of the file containing + # The full text of the license should be inside of the gem (at the top + # level) when you build it. + # + # RubyGems validates the license against the SPDX license list when you + # run gem build and warns about unknown or deprecated identifiers. + # An identifier may carry a trailing + (this version or any later + # version) and a license exception joined with WITH, for example + # Apache-2.0 WITH LLVM-exception. + # + # Compound SPDX license expressions such as MIT OR Apache-2.0 are + # not currently supported. RubyGems treats the whole string as a single + # identifier and warns that it is invalid. For a gem available under more + # than one license, set each license as a separate entry with #licenses=. + # + # For a license that has no SPDX identifier, use Nonstandard, or + # LicenseRef- where idstring is the name of the file containing # the license text. # # You should specify a license for your gem so that people know how they are @@ -348,8 +357,6 @@ def authors=(value) # specifying a license means all rights are reserved; others have no right # to use the code for any purpose. # - # You can set multiple licenses with #licenses= - # # Usage: # spec.license = 'MIT' @@ -360,15 +367,20 @@ def license=(o) ## # The license(s) for the library. # - # Each license must be a short name, no more than 64 characters. + # Each entry must be a single SPDX license identifier, no more than 64 + # characters. Entries are validated independently, so a compound expression + # such as MIT OR Apache-2.0 is not valid as an entry. Listing the + # identifiers as separate array elements is currently the only way RubyGems + # supports declaring a dual- or multi-licensed gem. # - # This should just be the name of your license. The full - # text of the license should be inside of the gem when you build it. + # Note that the array itself does not state how the licenses combine. + # Include the full text of each license in the gem and describe the exact + # terms there. # # See #license= for more discussion # # Usage: - # spec.licenses = ['MIT', 'GPL-2.0'] + # spec.licenses = ['MIT', 'GPL-2.0-only'] def licenses=(licenses) @licenses = Array licenses diff --git a/ractor.c b/ractor.c index 556d663ba13db0..15132415bf9c4b 100644 --- a/ractor.c +++ b/ractor.c @@ -415,6 +415,9 @@ cancel_single_ractor_mode(void) RUBY_DEBUG_LOG("enable multi-ractor mode"); ruby_single_main_ractor = NULL; + rb_yjit_invalidate_single_ractor(); + rb_zjit_invalidate_single_ractor(); + rb_funcall(rb_cRactor, rb_intern("_activated"), 0); } @@ -601,8 +604,6 @@ ractor_create(rb_execution_context_t *ec, VALUE self, VALUE loc, VALUE name, VAL r->verbose = cr->verbose; r->debug = cr->debug; - rb_yjit_before_ractor_spawn(); - rb_zjit_before_ractor_spawn(); rb_thread_create_ractor(r, args, block); RB_GC_GUARD(rv); diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index 5825f91b5789bd..7cf6dde932b254 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -188,6 +188,84 @@ expect(the_bundle).to include_gems("ripe_gem 2.0.0") end + it "summarizes skipped versions at the end of bundle install" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(out).to include("The following gem versions were skipped by the cooldown setting:") + expect(out).to include("* ripe_gem 2.0.0 (available in 6 days), resolved 1.0.0 instead") + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "summarizes skipped versions at the end of bundle update" do + gemfile <<-G + source "https://gem.repo3", cooldown: 7 + 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", artifice: "compact_index_cooldown" + + expect(out).to include("The following gem versions were skipped by the cooldown setting:") + expect(out).to include("* ripe_gem 2.0.0 (available in 6 days), resolved 1.0.0 instead") + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "does not print a skip summary when cooldown is disabled" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 0", artifice: "compact_index_cooldown" + + expect(out).not_to include("skipped by the cooldown setting") + end + + it "does not print a skip summary for versions the Gemfile requirement rejects anyway" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem", "~> 1.0" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(out).not_to include("skipped by the cooldown setting") + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "does not print a skip summary when installing from an up-to-date lockfile" do + gemfile <<-G + source "https://gem.repo3", cooldown: 7 + gem "ripe_gem" + G + + bundle "install", artifice: "compact_index_cooldown" + expect(out).to include("skipped by the cooldown setting") + + bundle "install", artifice: "compact_index_cooldown" + expect(out).not_to include("skipped by the cooldown setting") + end + it "applies cooldown declared per-source in the Gemfile" do gemfile <<-G source "https://gem.repo3", cooldown: 7 diff --git a/spec/bundler/install/gems/compact_index_spec.rb b/spec/bundler/install/gems/compact_index_spec.rb index efc955b8432ecd..41d18280f24cfb 100644 --- a/spec/bundler/install/gems/compact_index_spec.rb +++ b/spec/bundler/install/gems/compact_index_spec.rb @@ -845,41 +845,6 @@ def start expect(the_bundle).to include_gems "myrack 1.0.0" end - it "performs full update of compact index info cache if range is not satisfiable" do - gemfile <<-G - source "#{source_uri}" - gem 'myrack', '0.9.1' - G - - bundle :install, artifice: "compact_index" - - cache_path = compact_index_cache_path.join("localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5") - - # We must remove the etag so that we don't ignore the range and get a 304 Not Modified. - myrack_info_etag_path = File.join(cache_path, "info-etags", "myrack-92f3313ce5721296f14445c3a6b9c073") - File.unlink(myrack_info_etag_path) if File.exist?(myrack_info_etag_path) - - myrack_info_path = File.join(cache_path, "info", "myrack") - expected_myrack_info_content = File.read(myrack_info_path) - - # Modify the cache files to make the range not satisfiable - File.open(myrack_info_path, "a") {|f| f << "0.9.2 |checksum:c55b525b421fd833a93171ad3d7f04528ca8e87d99ac273f8933038942a5888c" } - - # Update the Gemfile so the next install does its normal things - gemfile <<-G - source "#{source_uri}" - gem 'myrack', '1.0.0' - G - - # The cache files now being longer means the requested range is going to be not satisfiable - # Bundler must end up requesting the whole file to fix things up. - bundle :install, artifice: "compact_index_range_not_satisfiable" - - resulting_myrack_info_content = File.read(myrack_info_path) - - expect(resulting_myrack_info_content).to eq(expected_myrack_info_content) - end - it "fails gracefully when the source URI has an invalid scheme" do install_gemfile <<-G, raise_on_error: false source "htps://rubygems.org" @@ -964,14 +929,6 @@ def start end end - it "works when cache dir is world-writable" do - install_gemfile <<-G, artifice: "compact_index" - File.umask(0000) - source "#{source_uri}" - gem "myrack" - G - end - it "doesn't explode when the API dependencies are wrong" do install_gemfile <<-G, artifice: "compact_index_wrong_dependencies", env: { "DEBUG" => "true" }, raise_on_error: false source "#{source_uri}" diff --git a/spec/bundler/quality_es_spec.rb b/spec/bundler/quality_es_spec.rb deleted file mode 100644 index e68674c03094b7..00000000000000 --- a/spec/bundler/quality_es_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe "La biblioteca si misma" do - def check_for_expendable_words(filename) - failing_line_message = [] - useless_words = %w[ - básicamente - claramente - sólo - solamente - obvio - obviamente - fácil - fácilmente - sencillamente - simplemente - ] - pattern = /\b#{Regexp.union(useless_words)}\b/i - - File.readlines(File.expand_path(filename, source_root)).each_with_index do |line, number| - next unless word_found = pattern.match(line) - failing_line_message << "#{filename}:#{number.succ} contiene '#{word_found}'. Esta palabra tiene un significado subjetivo y es mejor obviarla en textos técnicos." - end - - failing_line_message unless failing_line_message.empty? - end - - def check_for_specific_pronouns(filename) - failing_line_message = [] - specific_pronouns = /\b(él|ella|ellos|ellas)\b/i - - File.readlines(File.expand_path(filename, source_root)).each_with_index do |line, number| - next unless word_found = specific_pronouns.match(line) - failing_line_message << "#{filename}:#{number.succ} contiene '#{word_found}'. Use pronombres más genéricos en la documentación." - end - - failing_line_message unless failing_line_message.empty? - end - - it "mantiene la calidad de lenguaje de la documentación" do - included = /ronn/ - error_messages = [] - man_tracked_files.each do |filename| - next unless filename&.match?(included) - error_messages << check_for_expendable_words(filename) - error_messages << check_for_specific_pronouns(filename) - end - expect(error_messages.compact).to be_well_formed - end - - it "mantiene la calidad de lenguaje de oraciones usadas en el código fuente" do - error_messages = [] - exempt = /vendor/ - lib_tracked_files.each do |filename| - next if filename&.match?(exempt) - error_messages << check_for_expendable_words(filename) - error_messages << check_for_specific_pronouns(filename) - end - expect(error_messages.compact).to be_well_formed - end -end diff --git a/spec/bundler/quality_spec.rb b/spec/bundler/quality_spec.rb deleted file mode 100644 index 16b7f18788076c..00000000000000 --- a/spec/bundler/quality_spec.rb +++ /dev/null @@ -1,261 +0,0 @@ -# frozen_string_literal: true - -require "set" - -RSpec.describe "The library itself" do - def check_for_git_merge_conflicts(filename) - merge_conflicts_regex = / - <<<<<<<| - =======| - >>>>>>> - /x - - failing_lines = [] - each_line(filename) do |line, number| - failing_lines << number + 1 if line&.match?(merge_conflicts_regex) - end - - return if failing_lines.empty? - "#{filename} has unresolved git merge conflicts on lines #{failing_lines.join(", ")}" - end - - def check_for_tab_characters(filename) - # Because Go uses hard tabs - return if filename.end_with?(".go.tt") - - failing_lines = [] - each_line(filename) do |line, number| - failing_lines << number + 1 if line.include?("\t") - end - - return if failing_lines.empty? - "#{filename} has tab characters on lines #{failing_lines.join(", ")}" - end - - def check_for_extra_spaces(filename) - failing_lines = [] - each_line(filename) do |line, number| - next if /^\s+#.*\s+\n$/.match?(line) - failing_lines << number + 1 if /\s+\n$/.match?(line) - end - - return if failing_lines.empty? - "#{filename} has spaces on the EOL on lines #{failing_lines.join(", ")}" - end - - def check_for_extraneous_quotes(filename) - failing_lines = [] - each_line(filename) do |line, number| - failing_lines << number + 1 if /\u{2019}/.match?(line) - end - - return if failing_lines.empty? - "#{filename} has an extraneous quote on lines #{failing_lines.join(", ")}" - end - - def check_for_expendable_words(filename) - failing_line_message = [] - useless_words = %w[ - actually - basically - clearly - just - obviously - really - simply - ] - pattern = /\b#{Regexp.union(useless_words)}\b/i - - each_line(filename) do |line, number| - next unless word_found = pattern.match(line) - failing_line_message << "#{filename}:#{number.succ} has '#{word_found}'. Avoid using these kinds of weak modifiers." - end - - failing_line_message unless failing_line_message.empty? - end - - def check_for_specific_pronouns(filename) - failing_line_message = [] - specific_pronouns = /\b(he|she|his|hers|him|her|himself|herself)\b/i - - each_line(filename) do |line, number| - next unless word_found = specific_pronouns.match(line) - failing_line_message << "#{filename}:#{number.succ} has '#{word_found}'. Use more generic pronouns in documentation." - end - - failing_line_message unless failing_line_message.empty? - end - - it "has no malformed whitespace" do - exempt = /\.gitmodules|fixtures|vendor|LICENSE|vcr_cassettes|rbreadline\.diff|index\.txt$/ - error_messages = [] - tracked_files.each do |filename| - next if filename&.match?(exempt) - error_messages << check_for_tab_characters(filename) - error_messages << check_for_extra_spaces(filename) - end - expect(error_messages.compact).to be_well_formed - end - - it "has no extraneous quotes" do - exempt = /vendor|vcr_cassettes|LICENSE|rbreadline\.diff/ - error_messages = [] - tracked_files.each do |filename| - next if filename&.match?(exempt) - error_messages << check_for_extraneous_quotes(filename) - end - expect(error_messages.compact).to be_well_formed - end - - it "does not include any unresolved merge conflicts" do - error_messages = [] - exempt = %r{lock/lockfile_spec|quality_spec|vcr_cassettes|\.ronn|lockfile_parser} - tracked_files.each do |filename| - next if filename&.match?(exempt) - error_messages << check_for_git_merge_conflicts(filename) - end - expect(error_messages.compact).to be_well_formed - end - - it "maintains language quality of the documentation" do - error_messages = [] - man_tracked_files.each do |filename| - error_messages << check_for_expendable_words(filename) - error_messages << check_for_specific_pronouns(filename) - end - expect(error_messages.compact).to be_well_formed - end - - it "maintains language quality of sentences used in source code" do - error_messages = [] - exempt = /vendor|vcr_cassettes|CODE_OF_CONDUCT/ - lib_tracked_files.each do |filename| - next if filename&.match?(exempt) - error_messages << check_for_expendable_words(filename) - error_messages << check_for_specific_pronouns(filename) - end - expect(error_messages.compact).to be_well_formed - end - - it "documents all used settings" do - exemptions = %w[ - gem.changelog - gem.ci - gem.coc - gem.linter - gem.mit - gem.bundle - gem.rubocop - gem.test - git.allow_insecure - inline - trust-policy - ] - - all_settings = Hash.new {|h, k| h[k] = [] } - documented_settings = [] - - Bundler::Settings::BOOL_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::BOOL_KEYS" } - Bundler::Settings::NUMBER_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::NUMBER_KEYS" } - Bundler::Settings::ARRAY_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::ARRAY_KEYS" } - Bundler::Settings::STRING_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::STRING_KEYS" } - - key_pattern = /([a-z\._-]+)/i - lib_tracked_files.each do |filename| - each_line(filename) do |line, number| - line.scan(/Bundler\.settings\[:#{key_pattern}\]/).flatten.each {|s| all_settings[s] << "referenced at `#{filename}:#{number.succ}`" } - end - end - settings_section = File.read(source_root.join("lib/bundler/man/bundle-config.1.ronn")).split(/^## /).find {|section| section.start_with?("LIST OF AVAILABLE KEYS") } - documented_settings = settings_section.scan(/^\* `#{key_pattern}`/).flatten - - documented_settings.each do |s| - all_settings.delete(s) - expect(exemptions.delete(s)).to be_nil, "setting #{s} was exempted but was actually documented" - end - - exemptions.each do |s| - expect(all_settings.delete(s)).to be_truthy, "setting #{s} was exempted but unused" - end - error_messages = all_settings.map do |setting, refs| - "The `#{setting}` setting is undocumented\n\t- #{refs.join("\n\t- ")}\n" - end - - expect(error_messages.sort).to be_well_formed - - expect(documented_settings).to be_sorted - end - - it "can still be built" do - with_built_bundler do |gem_path| - expect(File.exist?(gem_path)).to be true - end - end - - it "ships the correct set of files" do - git_list = tracked_files.reject {|f| f.start_with?("spec/") } - - gem_list = loaded_gemspec.files - gem_list.map! {|f| f.sub(%r{\Aexe/}, "libexec/") } if ruby_core? - - expect(git_list).to match_array(gem_list) - end - - it "does not contain any warnings" do - exclusions = %w[ - lib/bundler/capistrano.rb - lib/bundler/deployment.rb - lib/bundler/gem_tasks.rb - lib/bundler/vlad.rb - ] - files_to_require = lib_tracked_files.grep(/\.rb$/) - exclusions - files_to_require.reject! {|f| f.start_with?("lib/bundler/vendor") } - files_to_require.map! {|f| File.expand_path(f, source_root) } - files_to_require.sort! - sys_exec("ruby -w") do |input, _, _| - files_to_require.each do |f| - input.puts "require '#{f}'" - end - end - - warnings = stdboth.split("\n") - # ignore warnings around deprecated Object#=~ method in RubyGems - warnings.reject! {|w| w =~ %r{rubygems\/version.rb.*deprecated\ Object#=~} } - - expect(warnings).to be_well_formed - end - - it "does not use require internally, but require_relative" do - exempt = %r{templates/|\.5|\.1|vendor/} - all_bad_requires = [] - lib_tracked_files.each do |filename| - next if filename&.match?(exempt) - each_line(filename) do |line, number| - line.scan(/^ *require "bundler/).each { all_bad_requires << "#{filename}:#{number.succ}" } - end - end - - expect(all_bad_requires).to be_empty, "#{all_bad_requires.size} internal requires that should use `require_relative`: #{all_bad_requires}" - end - - # We don't want our artifice code to activate bundler, but it needs to use the - # namespaced implementation of `Net::HTTP`. So we duplicate the file in - # bundler that loads that. - it "keeps vendored_net_http spec code in sync with the lib implementation" do - lib_implementation_path = File.join(source_lib_dir, "bundler", "vendored_net_http.rb") - expect(File.exist?(lib_implementation_path)).to be_truthy - lib_code = File.read(lib_implementation_path) - - spec_implementation_path = File.join(spec_dir, "support", "vendored_net_http.rb") - expect(File.exist?(spec_implementation_path)).to be_truthy - spec_code = File.read(spec_implementation_path) - - expect(lib_code).to eq(spec_code) - end - - private - - def each_line(filename, &block) - File.readlines(File.expand_path(filename, source_root), encoding: "UTF-8").each_with_index(&block) - end -end diff --git a/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb deleted file mode 100644 index 8a7c4b79b0dcbe..00000000000000 --- a/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require_relative "helpers/compact_index" - -class CompactIndexRangeNotSatisfiable < CompactIndexAPI - get "/versions" do - if env["HTTP_RANGE"] - status 416 - else - etag_response do - file = tmp("versions.list") - FileUtils.rm_f(file) - file = CompactIndex::VersionsFile.new(file.to_s) - file.create(gems) - file.contents - end - end - end - - get "/info/:name" do - if env["HTTP_RANGE"] - status 416 - else - etag_response do - gem = gems.find {|g| g.name == params[:name] } - CompactIndex.info(gem ? gem.versions : []) - end - end - end -end - -require_relative "helpers/artifice" - -Artifice.activate_with(CompactIndexRangeNotSatisfiable) diff --git a/spec/bundler/support/matchers.rb b/spec/bundler/support/matchers.rb index efd56b44f68ffa..77b7d27e56e336 100644 --- a/spec/bundler/support/matchers.rb +++ b/spec/bundler/support/matchers.rb @@ -75,28 +75,6 @@ def self.define_compound_matcher(matcher, preconditions, &declarations) end end - RSpec::Matchers.define :be_sorted do - diffable - attr_reader :expected - match do |actual| - expected = block_arg ? actual.sort_by(&block_arg) : actual.sort - actual.==(expected).tap do - # HACK: since rspec won't show a diff when everything is a string - differ = RSpec::Support::Differ.new - @actual = differ.send(:object_to_string, actual) - @expected = differ.send(:object_to_string, expected) - end - end - end - - RSpec::Matchers.define :be_well_formed do - match(&:empty?) - - failure_message do |actual| - actual.join("\n") - end - end - define_compound_matcher :read_as, [exist] do |file_contents| diffable diff --git a/spec/bundler/support/shards.rb b/spec/bundler/support/shards.rb index e8e21ac842ad78..4cc96860b6348d 100644 --- a/spec/bundler/support/shards.rb +++ b/spec/bundler/support/shards.rb @@ -95,7 +95,6 @@ module Shards "spec/bundler/retry_spec.rb", "spec/bundler/installer/spec_installation_spec.rb", "spec/bundler/spec_set_spec.rb", - "spec/quality_es_spec.rb", "spec/bundler/index_spec.rb", "spec/other/cli_man_pages_spec.rb", ], @@ -175,7 +174,6 @@ module Shards "spec/commands/licenses_spec.rb", "spec/install/gemfile/lockfile_spec.rb", "spec/bundler/fetcher/dependency_spec.rb", - "spec/quality_spec.rb", "spec/bundler/remote_specification_spec.rb", "spec/install/process_lock_spec.rb", "spec/install/binstubs_spec.rb", diff --git a/test/ruby/test_process.rb b/test/ruby/test_process.rb index 7708f0d4225590..7b1ff61b569eaf 100644 --- a/test/ruby/test_process.rb +++ b/test/ruby/test_process.rb @@ -1251,6 +1251,20 @@ def test_spawn_trailing_backslash end end + def test_argv_backslash_before_space + return unless windows? + [ + ["AA\\ ", "BB"], + ["AA\\ ", "BB"], + ["AA\\\\ ", "BB"], + ["AA ", "BB"], # control: no backslash + ["AA\\", "BB"], # control: no space after the backslash + ].each do |args| + out = IO.popen([EnvUtil.rubybin, "-e", "STDOUT.binmode; print Marshal.dump(ARGV)", *args], "rb", &:read) + assert_equal(args, Marshal.load(out), "[Bug #22201] argv did not round-trip: #{args.inspect}") + end + end + def test_exec_wordsplit with_tmpchdir {|d| File.write("script", <<-'End') diff --git a/test/ruby/test_yjit.rb b/test/ruby/test_yjit.rb index 9b9a59c397f8e5..ad0ccbfae1310d 100644 --- a/test/ruby/test_yjit.rb +++ b/test/ruby/test_yjit.rb @@ -2020,6 +2020,13 @@ def assert_compiles( no_send_fallbacks: false, verify_ctx: false ) + # [Bug #22228] Run a major GC to collect objects registered during + # initialization that would otherwise cause unexpected YJIT side + # exits when GC.stress is enabled. + run_gc = <<~RUBY + GC.start + RUBY + reset_stats = <<~RUBY RubyVM::YJIT.runtime_stats RubyVM::YJIT.reset_stats! @@ -2048,6 +2055,7 @@ def collect_insns(iseq) _test_proc = -> { #{test_script} } + #{run_gc} #{reset_stats} result = _test_proc.call #{write_results} diff --git a/test/rubygems/test_gem_compact_index_client_cache_file.rb b/test/rubygems/test_gem_compact_index_client_cache_file.rb index f2e2633646b4a0..c770a4bb105097 100644 --- a/test/rubygems/test_gem_compact_index_client_cache_file.rb +++ b/test/rubygems/test_gem_compact_index_client_cache_file.rb @@ -139,6 +139,17 @@ def test_commit_after_close_raises end end + def test_write_with_permissive_umask_uses_default_mode + original_umask = File.umask(0o000) + + CacheFile.write(@path, "data") + + assert_equal "data", @path.read + assert_equal 0, @path.stat.mode & 0o022, "expected a fresh cache file to not be world-writable under a permissive umask" + ensure + File.umask(original_umask) + end + def test_write_preserves_permissions @path.binwrite "old" @path.chmod 0o400 diff --git a/test/rubygems/test_gem_compact_index_client_http_fetcher.rb b/test/rubygems/test_gem_compact_index_client_http_fetcher.rb index c563482dad8916..bb8c7f8727651c 100644 --- a/test/rubygems/test_gem_compact_index_client_http_fetcher.rb +++ b/test/rubygems/test_gem_compact_index_client_http_fetcher.rb @@ -34,6 +34,12 @@ def initialize end end + class FakeRangeNotSatisfiable < Gem::Net::HTTPRangeNotSatisfiable + def initialize + super("1.1", "416", "Range Not Satisfiable") + end + end + class FakeRemoteFetcher attr_reader :requests @@ -116,6 +122,35 @@ def test_call_raises_after_too_many_redirects assert_match(/too many redirects/, error.message) end + def test_call_retries_without_range_on_range_not_satisfiable + requests = [] + remote = Object.new + remote.define_singleton_method(:request) do |uri, request_class, &block| + request = request_class.new(uri) + block&.call(request) + requests << request + request["Range"] ? FakeRangeNotSatisfiable.new : FakeResponse.new("full data") + end + + fetcher = Gem::CompactIndexClient::HTTPFetcher.new("https://index.example", remote) + response = fetcher.call("versions", "Range" => "bytes=100-", "If-None-Match" => '"abc"') + + assert_equal "full data", response.body + assert_equal 2, requests.size + assert_nil requests.last["Range"] + assert_equal '"abc"', requests.last["If-None-Match"] + end + + def test_call_raises_on_range_not_satisfiable_without_range + fetcher, _remote = fetcher_for("https://index.example/versions" => FakeRangeNotSatisfiable.new) + + error = assert_raise Gem::RemoteFetcher::FetchError do + fetcher.call("versions") + end + + assert_match(/bad response Range Not Satisfiable 416/, error.message) + end + def test_call_raises_fetch_error_on_failure_response fetcher, _remote = fetcher_for("https://index.example/versions" => FakeNotFound.new) diff --git a/vm_eval.c b/vm_eval.c index 945c21f568fe7e..f1c8ba88b00b35 100644 --- a/vm_eval.c +++ b/vm_eval.c @@ -2442,7 +2442,7 @@ rb_obj_instance_exec(int argc, const VALUE *argv, VALUE self) * class Foo; end * * Foo.module_eval("puts __LINE__") # => 1 - * Foo.module_eval("puts __FILE__", nil, 10) # => 10 + * Foo.module_eval("puts __LINE__", nil, 10) # => 10 * * When a block is given, evaluates the block in the context * of +self+: diff --git a/win32/win32.c b/win32/win32.c index aaf54a76a7e4d4..f3cda7bf804718 100644 --- a/win32/win32.c +++ b/win32/win32.c @@ -1746,6 +1746,7 @@ w32_cmdvector(const WCHAR *cmd, char ***vec, UINT cp, rb_encoding *enc) *ptr = 0; done = 1; } + slashes = 0; break; case L'*': diff --git a/yjit.h b/yjit.h index 45a9f74c8692d6..481ee8b9b6350d 100644 --- a/yjit.h +++ b/yjit.h @@ -43,7 +43,7 @@ void rb_yjit_constant_state_changed(ID id); void rb_yjit_iseq_mark(void *payload); void rb_yjit_iseq_update_references(const rb_iseq_t *iseq); void rb_yjit_iseq_free(const rb_iseq_t *iseq); -void rb_yjit_before_ractor_spawn(void); +void rb_yjit_invalidate_single_ractor(void); void rb_yjit_constant_ic_update(const rb_iseq_t *const iseq, IC ic, unsigned insn_idx); void rb_yjit_tracing_invalidate_all(void); void rb_yjit_show_usage(int help, int highlight, unsigned int width, int columns); @@ -71,7 +71,7 @@ static inline void rb_yjit_constant_state_changed(ID id) {} static inline void rb_yjit_iseq_mark(void *payload) {} static inline void rb_yjit_iseq_update_references(const rb_iseq_t *iseq) {} static inline void rb_yjit_iseq_free(const rb_iseq_t *iseq) {} -static inline void rb_yjit_before_ractor_spawn(void) {} +static inline void rb_yjit_invalidate_single_ractor(void) {} static inline void rb_yjit_constant_ic_update(const rb_iseq_t *const iseq, IC ic, unsigned insn_idx) {} static inline void rb_yjit_tracing_invalidate_all(void) {} static inline void rb_yjit_lazy_push_frame(const VALUE *pc) {} diff --git a/yjit/src/invariants.rs b/yjit/src/invariants.rs index f3bb91ddc18f2e..71e22206797b3c 100644 --- a/yjit/src/invariants.rs +++ b/yjit/src/invariants.rs @@ -303,10 +303,11 @@ pub extern "C" fn rb_yjit_cme_invalidate(callee_cme: *const rb_callable_method_e }); } -/// Callback for when Ruby is about to spawn a ractor. In that case we need to -/// invalidate every block that is assuming single ractor mode. +/// Invalidate every block that assumes single-ractor mode. Called when Ruby +/// transitions from single-ractor to multi-ractor mode (i.e. a second ractor +/// is spawned). #[no_mangle] -pub extern "C" fn rb_yjit_before_ractor_spawn() { +pub extern "C" fn rb_yjit_invalidate_single_ractor() { // If YJIT isn't enabled, do nothing if !yjit_enabled_p() { return; diff --git a/zjit.h b/zjit.h index a99a105e06c286..cabef7ef2b81b5 100644 --- a/zjit.h +++ b/zjit.h @@ -86,14 +86,14 @@ void rb_zjit_iseq_update_references(void *payload); void rb_zjit_mark_all_writable(void); void rb_zjit_mark_all_executable(void); void rb_zjit_iseq_free(const rb_iseq_t *iseq); -void rb_zjit_before_ractor_spawn(void); +void rb_zjit_invalidate_single_ractor(void); void rb_zjit_tracing_invalidate_all(void); void rb_zjit_invalidate_no_singleton_class(VALUE klass); void rb_zjit_invalidate_root_box(void); void rb_zjit_jit_frame_update_references(zjit_jit_frame_t *jit_frame); void rb_zjit_materialize_frames(const rb_execution_context_t *ec, rb_control_frame_t *cfp); void rb_zjit_materialize_frames_for_longjmp(const rb_execution_context_t *ec, rb_control_frame_t *cfp); -size_t rb_zjit_hash_new_size(void); +size_t rb_zjit_hash_new_size(VALUE *flags_out); bool rb_zjit_class_allocate_instance_fastpath(VALUE klass, size_t *size_out, shape_id_t *shape_id_out); bool rb_zjit_str_resurrect_fastpath(VALUE str, bool chilled, size_t *size_out, VALUE *flags_out, long *len_out, size_t *byte_size_out); bool rb_zjit_array_dup_can_fastpath(VALUE ary, size_t *alloc_size_out, VALUE *flags_out, long *len_out); @@ -133,7 +133,7 @@ static inline void rb_zjit_bop_redefined(int redefined_flag, enum ruby_basic_ope static inline void rb_zjit_cme_invalidate(const rb_callable_method_entry_t *cme) {} static inline void rb_zjit_invalidate_no_ep_escape(const rb_iseq_t *iseq) {} static inline void rb_zjit_constant_state_changed(ID id) {} -static inline void rb_zjit_before_ractor_spawn(void) {} +static inline void rb_zjit_invalidate_single_ractor(void) {} static inline void rb_zjit_tracing_invalidate_all(void) {} static inline void rb_zjit_invalidate_no_singleton_class(VALUE klass) {} static inline void rb_zjit_invalidate_root_box(void) {} diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 27428f9d63a9aa..dace5d4b5dec5e 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -2475,11 +2475,11 @@ fn gen_new_hash( if elements.is_empty() { gen_prepare_leaf_call_with_gc(asm, state); - let alloc_size = unsafe { rb_zjit_hash_new_size() }; - let flags = RUBY_T_HASH as u64; + let mut flags = VALUE(0); + let alloc_size = unsafe { rb_zjit_hash_new_size(&mut flags) }; let klass = unsafe { rb_cHash }; - gc_fastpath::gc_fastpath_new_obj(jit, asm, alloc_size, flags, klass, + gc_fastpath::gc_fastpath_new_obj(jit, asm, alloc_size, flags.into(), klass, |asm, hash| { asm.store(Opnd::mem(VALUE_BITS, hash, RUBY_OFFSET_RHASH_IFNONE), Qnil.into()); }, @@ -2494,11 +2494,11 @@ fn gen_new_hash( let num_pairs = elements.len() / 2; let hash = if num_pairs <= RUBY_RHASH_AR_TABLE_MAX_SIZE as usize { - let alloc_size = unsafe { rb_zjit_hash_new_size() }; - let flags = RUBY_T_HASH as u64; + let mut flags = VALUE(0); + let alloc_size = unsafe { rb_zjit_hash_new_size(&mut flags) }; let klass = unsafe { rb_cHash }; - gc_fastpath::gc_fastpath_new_obj(jit, asm, alloc_size, flags, klass, + gc_fastpath::gc_fastpath_new_obj(jit, asm, alloc_size, flags.into(), klass, |asm, hash| { asm.store(Opnd::mem(VALUE_BITS, hash, RUBY_OFFSET_RHASH_IFNONE), Qnil.into()); }, diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 6b7325d8147592..b2c9bad57f93e5 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -3351,6 +3351,34 @@ fn test_new_hash_dynamic_sym_keys_gc_stress() { "#), @r#"[Hash, 2, [3], [3]]"#); } +// The NewHash inline-alloc fast path must bake the slot-size shape_id into the +// object flags. Without it, a cross-ractor move sizes the destination object +// from a zero shape_id, so the moved hash is allocated too small and its keys +// are corrupted. +#[test] +fn test_new_hash_sym_keys_ractor_move() { + eval(" + def create_hash + { an_object: Array.new, hi: true, bonjour: true } + end + "); + assert_contains_opcode("create_hash", YARVINSN_newhash); + assert_snapshot!(inspect(" + r = Ractor.new do + h = receive + 30.times { |i| h[i] = true } + h.keys.delete_if { |k| Integer === k } + end + + create_hash + create_hash + + h = create_hash + r.send(h, move: true) + r.value + "), @"[:an_object, :hi, :bonjour]"); +} + #[test] fn test_object_alloc_gc_stress() { eval(" diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 9e34613a338810..da77593f7b9619 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2272,7 +2272,7 @@ unsafe extern "C" { pub fn rb_iseq_label(iseq: *const rb_iseq_t) -> VALUE; pub fn rb_iseq_defined_string(type_: defined_type) -> VALUE; pub fn rb_zjit_profile_enable(iseq: *const rb_iseq_t); - pub fn rb_zjit_hash_new_size() -> usize; + pub fn rb_zjit_hash_new_size(flags_out: *mut VALUE) -> usize; pub fn rb_zjit_class_allocate_instance_fastpath( klass: VALUE, size_out: *mut usize, diff --git a/zjit/src/invariants.rs b/zjit/src/invariants.rs index 22cd3d2c73e852..6a4ad3ff5a2687 100644 --- a/zjit/src/invariants.rs +++ b/zjit/src/invariants.rs @@ -398,10 +398,11 @@ pub fn track_single_ractor_assumption( )); } -/// Callback for when Ruby is about to spawn a ractor. In that case we need to -/// invalidate every block that is assuming single ractor mode. +/// Invalidate every block that assumes single-ractor mode. Called when Ruby +/// transitions from single-ractor to multi-ractor mode (i.e. a second ractor +/// is spawned). #[unsafe(no_mangle)] -pub extern "C" fn rb_zjit_before_ractor_spawn() { +pub extern "C" fn rb_zjit_invalidate_single_ractor() { // If ZJIT isn't enabled, do nothing if !zjit_enabled_p() { return;