From c6e3e8c594a3f5442bad059e976b7f8971f2b622 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 14 Jul 2026 06:05:56 -0700 Subject: [PATCH 01/47] ZJIT: Refine inline budget estimates (#17765) The first version of the inliner used the "high water mark" of HIR instructions ever allocated. This PR estimates the number of live instructions that we care about in `infer_types`. When inlining, it also bumps the estimate by the delta of new HIR instructions from inlining the callee. `infer_types` is somewhat arbitrarily chosen because it runs frequently between other passes and I don't want a whole other pass. Result: slightly different inlining behavior. On Lobste.rs, we go from ~5400 inlining rejected due to budget down to ~3900. --- zjit/src/hir.rs | 37 ++++++++++++++++++++++++++++++------- zjit/src/options.rs | 17 +++-------------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 5fd2369537dd7e..658614caaf97b7 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1868,6 +1868,19 @@ impl Insn { abstract_heaps::Allocator.includes(self.effects_of().write_bits()) } + + fn counts_against_inlining_budget(&self) -> bool { + match self { + // Don't count metadata-only instructions. + Insn::Comment { .. } + | Insn::IncrCounter { .. } + | Insn::IncrCounterPtr { .. } + | Insn::Snapshot { .. } + | Insn::PatchPoint { .. } + => false, + _ => true, + } + } } /// Print adaptor for [`Insn`]. See [`PtrPrintMap`]. @@ -2710,6 +2723,10 @@ pub struct Function { /// fulfilling `(0..=opt_num)` optional parameters. jit_entry_blocks: Vec, profiles: Option, + /// Rough estimate for the number of (actually executable) instructions in the function. Does + /// not count Snapshot, PatchPoint, etc. + /// Currently updated by `infer_types` as a heuristic but that is not a guarantee. + num_instructions: usize, } /// The kind of a value an ISEQ returns @@ -2842,6 +2859,7 @@ impl Function { jit_entry_blocks: vec![], param_types: vec![], profiles: None, + num_instructions: 0, } } @@ -3386,10 +3404,14 @@ impl Function { let rpo = self.reverse_post_order(); loop { let mut changed = false; + let mut num_instructions = 0; for &block in &rpo { if !reachable.get(block) { continue; } for i in 0..self.blocks[block.0].insns.len() { let insn_id = self.blocks[block.0].insns[i]; + if self.insns[insn_id.0].counts_against_inlining_budget() { + num_instructions += 1; + } // Instructions without output, including branch instructions, can't be targets // of make_equal_to, so we don't need find() here. let insn_type = match &self.insns[insn_id.0] { @@ -3438,6 +3460,7 @@ impl Function { } } if !changed { + self.num_instructions = num_instructions; break; } } @@ -4982,14 +5005,11 @@ impl Function { return false; } - // Per-caller cumulative budget. self.insns is append-only (InsnIds are stable - // indices), so its len() is a high-water mark of total HIR instructions ever - // allocated for this function — not the final compiled size. Once that count - // crosses the budget, every further callee is rejected and the optimization - // fixed-point loop reaches its terminal iteration. See `Options::inline_budget` - // for the full unit/semantics caveat. + // Per-caller cumulative budget. Once that count crosses the budget, every further callee + // is rejected and the optimization fixed-point loop reaches its terminal iteration. See + // `Options::inline_budget` for the full unit/semantics caveat. let budget = get_option!(inline_budget); - if budget != INLINE_BUDGET_UNLIMITED && self.insns.len() > budget { + if budget != INLINE_BUDGET_UNLIMITED && self.num_instructions > budget { incr_counter!(inline_reject_budget_exceeded); return false; } @@ -5164,6 +5184,9 @@ impl Function { } }; + // Bump the rough estimate of new instructions added by inlining this function + self.num_instructions += self.insns.len() - pre_insns_len; + // Past the point of no return: commit the inlining. incr_counter!(inline_method_count); did_inline = true; diff --git a/zjit/src/options.rs b/zjit/src/options.rs index 55c9728a1944f1..bffecaeb99b0fe 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -149,27 +149,16 @@ pub struct Options { pub inline_threshold: InlineThreshold, /// Per-caller cumulative size budget for inlining, measured as the caller - /// `Function`'s `insns.len()` at the moment `should_inline` is consulted (during + /// `Function::num_instructions` at the moment `should_inline` is consulted (during /// `inline_methods` inside the `optimize()` fixed-point loop). Once a caller has /// grown past this many HIR instructions, `should_inline` rejects further callees, /// bounding runaway code-size growth from depth-N inlining (and providing the /// optimization fixed-point loop's effective terminating condition). /// `INLINE_BUDGET_UNLIMITED` disables the budget. /// - /// Caveat on the unit: `self.insns` is append-only across the whole pipeline — - /// `InsnId`s are stable indices into it, so passes never shrink it. `len()` is - /// therefore a high-water mark of total HIR instructions ever allocated for the - /// function, including ones that later optimization passes mark dead via - /// `eliminate_dead_code` or alias away via `union_find`. By the time - /// `should_inline` runs in a given fixed-point iteration, `self.insns.len()` has - /// already been bumped by `iseq_to_hir`'s initial build, then by `type_specialize` - /// and the trivial `inline` pass, and (in iterations 2+) by every prior pass in - /// the loop including the previous round's `inline_methods`. It is a useful proxy - /// for "compile work done" but not for "size of the compiled output". - /// /// Note: this is a different unit than `inline_threshold` — that field is callee - /// YARV bytecode words; this one is caller HIR instructions, allocation high-water - /// mark. They aren't directly comparable; YARV → HIR typically expands roughly 1-3x. + /// YARV bytecode words; this one is caller HIR instructions. They aren't directly comparable; + /// YARV → HIR typically expands roughly 1-3x. pub inline_budget: InlineBudget, /// Set of qualified method names (e.g. `Class#method`, `Module::Class.method`) that From 8bcc8101bea1385c9291573ffa172f8983a7b683 Mon Sep 17 00:00:00 2001 From: Shugo Maeda Date: Tue, 14 Jul 2026 22:52:32 +0900 Subject: [PATCH 02/47] gc: remember re-embedded objects when compaction changes slot size When gc_move relocates an object to a different slot size, rb_gc_obj_changed_slot_size may re-embed it: the fields move from the fields_obj into the object itself and the object references them directly. The write-barrier history for those (possibly young) values lived on the discarded fields_obj, so an old unremembered object then violates the O->Y invariant, which gc_verify_internal_consistency reports as a WB miss. Remember the object on such moves. Remembering inside rb_gc_obj_changed_slot_size (as #17866 tried) does not work for this path: gc_move calls it before the destination's age bits are restored, so RVALUE_OLD_P is false there, and gc_move overwrites the destination's remembered bits afterwards anyway. Co-Authored-By: Claude Fable 5 --- gc/default/default.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gc/default/default.c b/gc/default/default.c index f8f4ab2259b34c..cee550df6b6c79 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -7610,6 +7610,14 @@ gc_move(rb_objspace_t *objspace, VALUE src, VALUE dest, struct heap_page *src_pa } RVALUE_AGE_SET(dest, age); + + /* A re-embedded object (rb_gc_obj_changed_slot_size) references its + * former fields_obj's contents directly; the write-barrier history + * lived on the discarded fields_obj, so remember the object. */ + if (src_slot_size != slot_size && age >= RVALUE_OLD_AGE && !remembered) { + rgengc_remember(objspace, dest); + } + /* Assign forwarding address */ RMOVED(src)->flags = T_MOVED; RMOVED(src)->dummy = Qundef; From 307a771532e46fd236c9ccc1fb5ea862815aae4a Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 14 Jul 2026 10:01:59 -0700 Subject: [PATCH 03/47] ZJIT: Fold defined?(yield) for inlined methods (#17861) * ZJIT: Fold defined?(yield) when inlining method with blockiseq We know this statically. * ZJIT: Fold negative case of defined?(yield) too Leave handling non-blockiseq arguments for later (right now they should never appear in SendDirect instructions). --- zjit/src/hir.rs | 38 ++++++++++---- zjit/src/hir/opt_tests.rs | 106 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 11 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 658614caaf97b7..a4821d3aa62ca7 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -5103,6 +5103,9 @@ impl Function { let SendDirectData { recv, cme, iseq, args, kw_bits, jit_entry_idx, block: call_block, state, .. } = *data; // SendDirect invariant: block is either None or BlockIseq. // BlockArg is rejected upstream during type specialization. + // TODO(max): If we accept BlockArg here, we need to change the folding of Defined + // in HIR construction for the defined opcode to check the send flags of the method + // being inlined, too. let blockiseq: Option = call_block.map(|bh| match bh { BlockHandler::BlockIseq(bi) => bi, BlockHandler::BlockArg => unreachable!("BlockArg in SendDirect"), @@ -8239,18 +8242,31 @@ fn add_iseq_to_hir( // Similar to gen_is_block_given Insn::Const { val: Const::Value(Qnil) } } else { - // For DEFINED_YIELD, codegen materializes the local EP inline (similar to - // gen_is_block_given) to check for a block handler. Precompute the lexical - // distance from this iseq up to local_iseq so codegen does not have to - // walk the parent chain. Any DEFINED_YIELD reaching this branch has a - // method local_iseq by construction -- the above branch has already - // diverted the non-method case to Qnil. - let lep_level = if op_type == DEFINED_YIELD as usize { - get_lvar_level(iseq) + if op_type == DEFINED_YIELD as usize && matches!(mode, AddIseqMode::Inlined { .. }) { + // If we are inlining a method that has a blockiseq handler, we can fold Defined(DEFINED_YIELD). + // TODO(max): If we handle non-blockiseq block arguments such as + // &:symbol or just &block forwarding, we need to revisit this and + // check flags. + let has_block = matches!(mode, AddIseqMode::Inlined { blockiseq: Some(_), .. }); + if has_block { + Insn::Const { val: Const::Value(pushval) } + } else { + Insn::Const { val: Const::Value(Qnil) } + } } else { - 0 - }; - Insn::Defined { op_type, obj, pushval, v, lep_level, state: exit_id } + // For DEFINED_YIELD, codegen materializes the local EP inline (similar to + // gen_is_block_given) to check for a block handler. Precompute the lexical + // distance from this iseq up to local_iseq so codegen does not have to + // walk the parent chain. Any DEFINED_YIELD reaching this branch has a + // method local_iseq by construction -- the above branch has already + // diverted the non-method case to Qnil. + let lep_level = if op_type == DEFINED_YIELD as usize { + get_lvar_level(iseq) + } else { + 0 + }; + Insn::Defined { op_type, obj, pushval, v, lep_level, state: exit_id } + } }; state.stack_push(fun.push_insn(block, insn)); } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index b75bd87f178346..58589cbb7d07ba 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -17218,6 +17218,112 @@ mod hir_opt_tests { "); } + #[test] + fn test_inline_with_block_folds_defined_yield() { + eval(r" + def foo = defined?(yield) + def test = foo { |x| x } + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile + PushInlineFrame v18 (0x1038) + v27:StringExact[VALUE(0x1040)] = Const Value(VALUE(0x1040)) + CheckInterrupts + PopInlineFrame + Return v27 + "); + } + + #[test] + fn test_inline_without_block_folds_defined_yield() { + eval(r" + def foo = defined?(yield) + def test = foo + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile + PushInlineFrame v18 (0x1038) + v27:NilClass = Const Value(nil) + CheckInterrupts + PopInlineFrame + Return v27 + "); + } + + #[test] + fn test_inline_array_each_with_block_folds_defined_yield() { + set_inline_threshold(100); + eval(r" + def test = [1, 2, 3].each { |x| x } + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v11:ArrayExact = ArrayDup v10 + PatchPoint NoSingletonClass(Array@0x1008) + PatchPoint MethodRedefined(Array@0x1008, each@0x1010, cme:0x1018) + PushInlineFrame v11 (0x1040) + v51:Fixnum[0] = Const Value(0) + Jump bb10(v11, v51) + bb10(v64:ArrayExact, v65:Fixnum): + v69:CInt64 = ArrayLength v64 + v70:Fixnum = BoxFixnum v69 + v71:BoolExact = FixnumGe v65, v70 + v73:CBool = Test v71 + CondBranch v73, bb13(), bb9(v64, v65) + bb13(): + CheckInterrupts + PopInlineFrame + Return v64 + bb9(v86:ArrayExact, v87:Fixnum): + v92:CInt64 = UnboxFixnum v87 + v93:BasicObject = ArrayAref v86, v92 + v95:CPtr = GetEP 0 + v96:CInt64 = LoadField v95, :VM_ENV_DATA_INDEX_SPECVAL@0x1048 + v97:CInt64[-4] = Const CInt64(-4) + v98:CInt64 = IntAnd v96, v97 + v99:BasicObject = InvokeBlockIseqDirect (0x1050), v98, v93 + v103:Fixnum[1] = Const Value(1) + v104:Fixnum = FixnumAdd v87, v103 + PatchPoint NoEPEscape(each) + Jump bb10(v86, v104) + "); + } + #[test] fn test_delete_duplicate_store() { eval(" From 1d363d6f415275e41e32be81e87b719635b1c019 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 14 Jul 2026 10:02:19 -0700 Subject: [PATCH 04/47] ZJIT: Add small tool to analyze perf map (#17734) It tells you total size per symbol, average size per instance of symbol, and total count per symbol. Can be used with `--zjit-perf=hir`: ``` plum% ruby ../tool/zjit_analyze_perf_map_size.rb /tmp/perf-16653.map 12.1 MB total; 277.5 B each side-exit 2.2 MB total; 16.9 B each PatchPoint 2.1 MB total; 197.4 B each SendDirect 1.3 MB total; 10.7 B each PadPatchPoint 973.6 KB total; 56.5 B each GuardType 952.6 KB total; 97.7 B each HasType 910.4 KB total; 111.2 B each Send 705.0 KB total; 218.0 B each CCallWithFrame 689.9 KB total; 149.0 B each PushInlineFrame 559.8 KB total; 247.5 B each CCallVariadic 440.4 KB total; 32.0 B each EntryPoint 387.9 KB total; 17.8 B each CheckInterrupts 356.8 KB total; 66.7 B each GetIvar 326.3 KB total; 11.0 B each Jump 198.2 KB total; 8.0 B each CondBranch 167.6 KB total; 22.3 B each Return 163.5 KB total; 4.8 B each LoadField 149.0 KB total; 23.6 B each IsBitEqual 142.4 KB total; 100.0 B each HashAref 135.9 KB total; 16.3 B each Test 138705 PatchPoint 128879 PadPatchPoint 124942 Snapshot 45884 side-exit 40535 Const 34701 LoadField 30364 Jump 25234 CondBranch 22264 CheckInterrupts 17657 GuardType 16101 LoadArg 14081 EntryPoint 12892 RefineType 11106 SendDirect 9980 HasType 8541 Test 8385 Send 7693 Return 6938 GuardBitEquals 6699 LoadSelf plum% ``` or with just `--zjit-perf`: ``` plum% ruby ../tool/zjit_analyze_perf_map_size.rb /tmp/perf-22517.map 770.7 KB total; 192.7 KB each _app_views_stories__listdetail_html_erb__2872691580871714137_14696@/Users/emacs/Documents/code/yjit-bench/benchmarks/lobsters/app/views/stories/_listdetail.html.erb:1 452.0 KB total; 150.7 KB each _app_views_layouts_application_html_erb__3422508349557286544_14376@/Users/emacs/Documents/code/yjit-bench/benchmarks/lobsters/app/views/layouts/application.html.erb:1 232.9 KB total; 232.9 KB each _app_views_comments__comment_html_erb___3513273811879470227_16232@/Users/emacs/Documents/code/yjit-bench/benchmarks/lobsters/app/views/comments/_comment.html.erb:1 200.9 KB total; 50.2 KB each block-in initialize@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/erubi-1.13.1/lib/erubi.rb:135 194.0 KB total; 64.7 KB each parse@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/tzinfo-2.0.6/lib/tzinfo/data_sources/zoneinfo_reader.rb:345 191.8 KB total; 191.8 KB each _app_views_comments__comment_html_erb___3513273811879470227_15016@/Users/emacs/Documents/code/yjit-bench/benchmarks/lobsters/app/views/comments/_comment.html.erb:1 167.4 KB total; 167.4 KB each _app_views_comments__comment_html_erb___3513273811879470227_16488@/Users/emacs/Documents/code/yjit-bench/benchmarks/lobsters/app/views/comments/_comment.html.erb:1 165.9 KB total; 165.9 KB each _app_views_stories__listdetail_html_erb__2872691580871714137_15008@/Users/emacs/Documents/code/yjit-bench/benchmarks/lobsters/app/views/stories/_listdetail.html.erb:1 124.3 KB total; 31.1 KB each build_arel@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activerecord-8.1.1/lib/active_record/relation/query_methods.rb:1751 120.9 KB total; 30.2 KB each _delegator_method@/Users/emacs/.rubies/ruby-zjit-rel-stats/lib/ruby/4.1.0+4/forwardable.rb:202 119.5 KB total; 29.9 KB each perform_query@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activerecord-8.1.1/lib/active_record/connection_adapters/sqlite3/database_statements.rb:87 111.6 KB total; 55.8 KB each _app_views_stories_show_html_erb__2313005407901203172_15000@/Users/emacs/Documents/code/yjit-bench/benchmarks/lobsters/app/views/stories/show.html.erb:1 103.8 KB total; 25.9 KB each block-in generate@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activesupport-8.1.1/lib/active_support/delegation.rb:72 98.3 KB total; 98.3 KB each _app_views_users_show_html_erb___3973630128665404112_14592@/Users/emacs/Documents/code/yjit-bench/benchmarks/lobsters/app/views/users/show.html.erb:1 91.0 KB total; 45.5 KB each normalize_options@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/bundler-4.0.12/lib/bundler/dsl.rb:394 85.9 KB total; 21.5 KB each parse@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/tzinfo-2.0.6/lib/tzinfo/data_sources/posix_time_zone_parser.rb:37 74.5 KB total; 37.3 KB each generate@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activesupport-8.1.1/lib/active_support/delegation.rb:22 69.0 KB total; 17.3 KB each determine_template@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/actionview-8.1.1/lib/action_view/renderer/template_renderer.rb:17 67.5 KB total; 16.9 KB each block-in expand_from_hash@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activerecord-8.1.1/lib/active_record/relation/predicate_builder.rb:88 65.2 KB total; 16.3 KB each block-in derive_offsets@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/tzinfo-2.0.6/lib/tzinfo/data_sources/zoneinfo_reader.rb:118 17 __callbacks@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activesupport-8.1.1/lib/active_support/callbacks.rb:70 12 _layout@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/actionview-8.1.1/lib/action_view/layouts.rb:329 5 __callbacks=@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activesupport-8.1.1/lib/active_support/callbacks.rb:70 5 default_url_options@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/actionpack-8.1.1/lib/action_dispatch/routing/url_for.rb:100 4 scope@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activerecord-8.1.1/lib/active_record/associations/association.rb:108 4 default_render@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/actionpack-8.1.1/lib/action_controller/metal/implicit_render.rb:38 4 scope@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activerecord-8.1.1/lib/active_record/associations/association_scope.rb:22 4 get_method_for_class@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/actionpack-8.1.1/lib/action_dispatch/routing/polymorphic_routes.rb:348 4 visit_Arel_Nodes_SqlLiteral@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activerecord-8.1.1/lib/arel/visitors/to_sql.rb:755 4 block-in preprocess_order_args@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activerecord-8.1.1/lib/active_record/relation/query_methods.rb:2105 4 strictly_lower?@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/bundler-4.0.12/lib/bundler/vendor/pub_grub/lib/pub_grub/version_range.rb:179 4 choose_compatible@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/bundler-4.0.12/lib/bundler/lazy_specification.rb:236 4 column_types@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activerecord-8.1.1/lib/active_record/result.rb:168 4 local_search@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/bundler-4.0.12/lib/bundler/index.rb:65 4 method_defined_within?@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activerecord-8.1.1/lib/active_record/attribute_methods.rb:187 4 join_scopes@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activerecord-8.1.1/lib/active_record/reflection.rb:227 4 block-in expand_from_hash@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activerecord-8.1.1/lib/active_record/relation/predicate_builder.rb:88 4 block-in convert_dot_notation_to_hash@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activerecord-8.1.1/lib/active_record/relation/predicate_builder.rb:166 4 block-in references@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/activerecord-8.1.1/lib/active_record/relation/predicate_builder.rb:29 4 block-in materialize_dependencies@/Users/emacs/.gem/ruby/ruby-zjit-rel-stats/gems/bundler-4.0.12/lib/bundler/spec_set.rb:246 plum% ``` --- tool/zjit_analyze_perf_map_size.rb | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tool/zjit_analyze_perf_map_size.rb diff --git a/tool/zjit_analyze_perf_map_size.rb b/tool/zjit_analyze_perf_map_size.rb new file mode 100644 index 00000000000000..48b72e4864f820 --- /dev/null +++ b/tool/zjit_analyze_perf_map_size.rb @@ -0,0 +1,40 @@ +#!/usr/bin/env ruby + +required_ruby_version = Gem::Version.new("3.4.0") +raise "Ruby version #{required_ruby_version} or higher is required" if Gem::Version.new(RUBY_VERSION) < required_ruby_version + +PERF_MAP = ARGV[0] || raise("Expected perf map as first argument") + +sizes = Hash.new(0) +counts = Hash.new(0) +File.foreach(PERF_MAP) do |line| + address, size, name = line.split(" ", 3) + name.delete_prefix!("ZJIT: ") + name.delete_suffix!("\n") + sizes[name] += size.to_i(16) + counts[name] += 1 +end + +total_size = sizes.values.sum + +def pretty_size bytes + u = 0 + s = 1024 + while bytes >= s || -bytes >= s + bytes /= s.to_f + u += 1 + end + "#{bytes.round(1)} #{'​KMGTPEZY'[u]}B" +end + +n = 20 + +sizes.sort_by { |name, size| -size }.first(n).each do |name, size| + puts "#{pretty_size(size)} total (#{(size/total_size.to_f*100).round(1)}%); #{pretty_size(size/counts[name].to_f)} each #{name}" +end + +puts + +counts.sort_by { |name, count| -count }.first(n).each do |name, count| + puts "#{count} #{name}" +end From 7facc9f2a172a753ba1973240a866afe44efcbfc Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Tue, 14 Jul 2026 11:59:03 -0400 Subject: [PATCH 05/47] [ruby/prism] Fix assume warning That attribute is C++23, not C23 https://github.com/ruby/prism/commit/5e4f45712e --- prism/compiler/assume.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/prism/compiler/assume.h b/prism/compiler/assume.h index 32e68f829745b8..256e3873b88d7c 100644 --- a/prism/compiler/assume.h +++ b/prism/compiler/assume.h @@ -10,12 +10,12 @@ * range analysis so it can prune impossible paths. Use it to communicate an * invariant the caller guarantees but that the compiler cannot otherwise prove. */ -#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L /* C23 or later */ && !defined(__APPLE__) - #define PRISM_ASSUME(expr_) [[assume(expr_)]] -#elif defined(__clang__) +#if defined(__clang__) #define PRISM_ASSUME(expr_) __builtin_assume(expr_) #elif defined(_MSC_VER) #define PRISM_ASSUME(expr_) __assume(expr_) +#elif defined(__GNUC__) && (__GNUC__ >= 13) + #define PRISM_ASSUME(expr_) __attribute__((assume(expr_))) #elif defined(__GNUC__) #define PRISM_ASSUME(expr_) ((expr_) ? (void) 0 : __builtin_unreachable()) #else From be2a2eea8db2f8c48067711216cd347d029dda06 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Wed, 15 Jul 2026 04:23:56 +0900 Subject: [PATCH 06/47] Relax opt_getconstant_path IC assertion under multi-ractor rb_vm_opt_getconstant_path() asserts on an inline constant cache hit that the cached value equals a freshly recomputed constant chain: VM_ASSERT(val == vm_get_ev_const_chain(ec, segments)); On a non-main ractor an IC hit is only allowed for shareable values (IMEMO_CONST_CACHE_SHAREABLE). Reassigning a shareable constant is legal from a non-main ractor, and it invalidates ICs asynchronously via rb_clear_constant_cache_for_id() (ic->entry = NULL) without locking the reader side. So the hit path can load a still-valid, previously-assigned shareable value while another ractor concurrently reassigns the constant; the subsequent recomputation then observes the newer value and the two legitimately differ (a benign TOCTOU). Both values were validly assigned to the constant at some point, so no correctness or memory-safety issue results -- only this debug-only assertion trips. Guard the equality check with rb_multi_ractor_p(), mirroring the existing workaround in vm_cc_invalidate() (vm_callinfo.h). Reproducible with several ractors reassigning the same frozen constant in a loop; NDEBUG builds are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- vm_insnhelper.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 1abf7dd122aefa..f7b007d814e47a 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -6548,7 +6548,11 @@ rb_vm_opt_getconstant_path(rb_execution_context_t *ec, rb_control_frame_t *const if (ice && vm_ic_hit_p(ice, GET_EP())) { val = ice->value; - VM_ASSERT(val == vm_get_ev_const_chain(ec, segments)); + // On a non-main ractor another ractor can concurrently reassign a + // shareable constant (which invalidates ICs asynchronously), so the + // cached-but-still-shareable value may legitimately differ from a + // freshly recomputed chain. Only assert equality when single-ractor. + VM_ASSERT(val == vm_get_ev_const_chain(ec, segments) || rb_multi_ractor_p()); } else { ruby_vm_constant_cache_misses++; From 39564b895764c0d43370d1404ba0026f874078ed Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Tue, 14 Jul 2026 19:02:53 +0000 Subject: [PATCH 07/47] dtoa.c: load the 5-powers cache pointers atomically pow5mult's b_cache publishes new cache nodes with ATOMIC_PTR_CAS (release), but both readers -- the fast-path check and the recheck before allocating -- were plain loads racing with that CAS: a data race in the C11 sense, and what TSan reports as `race:pow5mult` under parallel Ractor float<->string conversion. Harmless on current targets (the reader dereferences through the loaded pointer, so address dependency orders the accesses), but easy to make formally correct: read the cache head/next through RUBY_ATOMIC_PTR_LOAD. The lock-free CAS scheme is unchanged. Co-Authored-By: Claude Fable 5 --- missing/dtoa.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/missing/dtoa.c b/missing/dtoa.c index ba8cd46ebd9484..2a460959b07953 100644 --- a/missing/dtoa.c +++ b/missing/dtoa.c @@ -511,6 +511,9 @@ extern double rnd_prod(double, double), rnd_quot(double, double); #ifndef ATOMIC_PTR_CAS #define ATOMIC_PTR_CAS(var, old, new) ((var) = (new), (void *)(old)) #endif +#ifndef RUBY_ATOMIC_PTR_LOAD +#define RUBY_ATOMIC_PTR_LOAD(var) (var) +#endif #ifndef LIKELY #define LIKELY(x) (x) #endif @@ -863,10 +866,10 @@ pow5mult(Bigint *b, int k) } #define b_cache(var, addr, new_expr) \ - if ((var = addr) != 0) {} else { \ + if ((var = RUBY_ATOMIC_PTR_LOAD(addr)) != 0) {} else { \ Bigint *tmp = 0; \ ACQUIRE_DTOA_LOCK(1); \ - if (!(var = addr) && (var = (new_expr)) != 0) { \ + if (!(var = RUBY_ATOMIC_PTR_LOAD(addr)) && (var = (new_expr)) != 0) { \ var->next = 0; \ tmp = ATOMIC_PTR_CAS(addr, NULL, var); \ } \ From 243e640982ce387d2a037fd227b0d721c61ca72e Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 14 Jul 2026 21:18:41 +0200 Subject: [PATCH 08/47] gc.c: re-embed T_OBJECT during `gc_compact_finish` Doing it in `rb_gc_obj_changed_slot_size` is wrong as the object could still be moved back because of `invalidate_moved_plane`. --- gc.c | 33 +++++++++++++++++++-------------- test/ruby/test_gc_compact.rb | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/gc.c b/gc.c index 650e4ea4ff0a57..e1c14dceb5394b 100644 --- a/gc.c +++ b/gc.c @@ -377,15 +377,6 @@ void rb_gc_obj_changed_slot_size(VALUE obj, size_t slot_size) { shape_id_t shape_id = rb_obj_shape_transition_capacity(obj, rb_shape_capacity_for_slot_size(slot_size)); - - if (RB_TYPE_P(obj, T_OBJECT) && FL_TEST_RAW(obj, ROBJECT_HEAP) && !rb_obj_shape_complex_p(obj)) { - VALUE *embedded_fields = ROBJECT_EMBEDDED_FIELDS(obj); - VALUE *extended_fields = ROBJECT_FIELDS(obj); - MEMCPY(embedded_fields, extended_fields, VALUE, RSHAPE_LEN(RBASIC_SHAPE_ID(obj))); - FL_UNSET_RAW(obj, ROBJECT_HEAP); - shape_id = rb_shape_transition_robject(shape_id); - } - RBASIC_SET_FULL_SHAPE_ID(obj, shape_id); } @@ -3668,15 +3659,29 @@ gc_ref_update_array(void *objspace, VALUE v) static void gc_ref_update_object(void *objspace, VALUE v) { + RUBY_ASSERT(rb_gc_obj_slot_size(v) == rb_obj_shape_slot_size(v)); + shape_id_t shape_id = RBASIC_SHAPE_ID(v); + if (FL_TEST_RAW(v, ROBJECT_HEAP)) { UPDATE_IF_MOVED(objspace, ROBJECT(v)->as.extended); - } - else { - VALUE *ptr = ROBJECT_FIELDS(v); - for (uint32_t i = 0; i < ROBJECT_FIELDS_COUNT(v); i++) { - UPDATE_IF_MOVED(objspace, ptr[i]); + + if (!rb_shape_complex_p(shape_id) && rb_shape_embedded_capacity(shape_id) >= RSHAPE_LEN(shape_id)) { + VALUE *embedded_fields = ROBJECT_EMBEDDED_FIELDS(v); + VALUE *extended_fields = ROBJECT_FIELDS(v); + MEMCPY(embedded_fields, extended_fields, VALUE, RSHAPE_LEN(shape_id)); + FL_UNSET_RAW(v, ROBJECT_HEAP); + RBASIC_SET_FULL_SHAPE_ID(v, rb_shape_transition_robject(shape_id)); + rb_gc_writebarrier_remember(v); + } + else { + return; } } + + VALUE *ptr = ROBJECT_FIELDS(v); + for (uint32_t i = 0; i < ROBJECT_FIELDS_COUNT(v); i++) { + UPDATE_IF_MOVED(objspace, ptr[i]); + } } void diff --git a/test/ruby/test_gc_compact.rb b/test/ruby/test_gc_compact.rb index cb5e9d6ccb4b12..4d735bb26371e2 100644 --- a/test/ruby/test_gc_compact.rb +++ b/test/ruby/test_gc_compact.rb @@ -485,4 +485,24 @@ def test_moving_complex_generic_ivar assert_equal("hello", obj.instance_variable_get(:@str)) RUBY end + + def test_object_reembedding + assert_separately([], <<~'RUBY') + GC.auto_compact = true + + objs = [] + 30.times.map { Class.new }.map do |k| + 50.times.each do + obj = k.new + rand(0..30).times do |i| + obj.instance_variable_set(:"@v#{i}", i) + end + objs << obj + end + end + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + assert :ok + RUBY + end end From 71ee81f7d99200041e8b0812028fbefd540aa48a Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 14 Jul 2026 14:49:55 +0200 Subject: [PATCH 09/47] imemo.c: embed the `st_table` inside `rb_fields` Followup: https://github.com/ruby/ruby/pull/17631 Now that complex `RObject` always have an IMEMO/fields, we no longer need to have an external `st_table` to replicate the memory layout of `RObject`. --- depend | 1 + gc.c | 6 ++-- imemo.c | 53 ++++++++++++----------------- internal/imemo.h | 21 ++++-------- internal/variable.h | 2 +- object.c | 12 ++++--- shape.c | 67 +++++++++++++++++++++---------------- shape.h | 22 +++++++----- test/ruby/test_object_id.rb | 4 ++- variable.c | 64 ++++++++++------------------------- variable.h | 1 - 11 files changed, 115 insertions(+), 138 deletions(-) diff --git a/depend b/depend index 8db2d7a6759f4f..697a9a5eb78c6c 100644 --- a/depend +++ b/depend @@ -16680,6 +16680,7 @@ shape.$(OBJEXT): $(top_srcdir)/internal/object.h shape.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h shape.$(OBJEXT): $(top_srcdir)/internal/serial.h shape.$(OBJEXT): $(top_srcdir)/internal/set_table.h +shape.$(OBJEXT): $(top_srcdir)/internal/st.h shape.$(OBJEXT): $(top_srcdir)/internal/static_assert.h shape.$(OBJEXT): $(top_srcdir)/internal/string.h shape.$(OBJEXT): $(top_srcdir)/internal/struct.h diff --git a/gc.c b/gc.c index e1c14dceb5394b..29fe600f27c797 100644 --- a/gc.c +++ b/gc.c @@ -1101,9 +1101,9 @@ rb_newobj_of(VALUE klass, VALUE flags, size_t size) static VALUE class_allocate_complex_instance(VALUE klass, uint32_t capacity) { - shape_id_t initial_shape_id = rb_shape_transition_robject(0); - 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)); + VALUE obj = rb_newobj_of_with_shape(klass, T_OBJECT, rb_shape_transition_extended(ROOT_COMPLEX_SHAPE_ID), sizeof(struct RObject)); + VALUE fields_obj = rb_imemo_fields_new_complex(obj, ROOT_COMPLEX_SHAPE_ID, capacity, false); + ROBJECT_SET_EXTENDED(obj, fields_obj); return obj; } diff --git a/imemo.c b/imemo.c index 06a94d0d19a628..f416e219ed175b 100644 --- a/imemo.c +++ b/imemo.c @@ -131,11 +131,11 @@ rb_imemo_cdhash_new(size_t size, const struct st_hash_type *type) } static VALUE -imemo_fields_new(VALUE owner, VALUE flags, shape_id_t shape_id, size_t size, bool is_shareable) +imemo_fields_new(VALUE owner, shape_id_t shape_id, size_t size, bool is_shareable) { RUBY_ASSERT(rb_gc_size_allocatable_p(size)); - flags |= T_IMEMO | (imemo_fields << FL_USHIFT) | (is_shareable ? FL_SHAREABLE : 0); + VALUE flags = T_IMEMO | (imemo_fields << FL_USHIFT) | (is_shareable ? FL_SHAREABLE : 0); // 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 @@ -150,7 +150,7 @@ rb_imemo_fields_new(VALUE owner, shape_id_t shape_id, bool shareable) size_t capa = RSHAPE(shape_id)->capacity; size_t embedded_size = offsetof(struct rb_fields, as.embed) + capa * sizeof(VALUE); - VALUE fields = imemo_fields_new(owner, 0, shape_id, embedded_size, shareable); + VALUE fields = imemo_fields_new(owner, shape_id, embedded_size, shareable); RUBY_ASSERT(IMEMO_TYPE_P(fields, imemo_fields)); RUBY_ASSERT(rb_shape_embedded_capacity(RBASIC_SHAPE_ID(fields)) >= capa); @@ -158,20 +158,19 @@ rb_imemo_fields_new(VALUE owner, shape_id_t shape_id, bool shareable) } VALUE -rb_imemo_fields_new_complex(VALUE owner, shape_id_t shape_id, size_t capa, bool shareable) +rb_imemo_fields_new_complex_empty(VALUE owner) { - st_table *tbl = st_init_numtable_with_size(capa); - VALUE fields = imemo_fields_new(owner, OBJ_FIELD_HEAP, shape_id, sizeof(struct rb_fields), shareable); - IMEMO_OBJ_FIELDS(fields)->as.complex.table = tbl; - return fields; + return imemo_fields_new(owner, ROOT_SHAPE_ID, sizeof(struct rb_fields), false); } -static int -imemo_fields_trigger_wb_i(st_data_t key, st_data_t value, st_data_t arg) +VALUE +rb_imemo_fields_new_complex(VALUE owner, shape_id_t shape_id, size_t capa, bool shareable) { - VALUE field_obj = (VALUE)arg; - RB_OBJ_WRITTEN(field_obj, Qundef, (VALUE)value); - return ST_CONTINUE; + st_table tbl; + st_init_existing_numtable_with_size(&tbl, capa); + VALUE fields = imemo_fields_new(owner, shape_id, sizeof(struct rb_fields), shareable); + MEMCPY(&IMEMO_OBJ_FIELDS(fields)->as.complex.table, &tbl, st_table, 1); + return fields; } static int @@ -181,33 +180,26 @@ imemo_fields_complex_wb_i(st_data_t key, st_data_t value, st_data_t arg) return ST_CONTINUE; } -VALUE -rb_imemo_fields_new_complex_tbl(VALUE owner, shape_id_t shape_id, st_table *tbl, bool shareable) -{ - VALUE fields = imemo_fields_new(owner, OBJ_FIELD_HEAP, shape_id, sizeof(struct rb_fields), shareable); - IMEMO_OBJ_FIELDS(fields)->as.complex.table = tbl; - st_foreach(tbl, imemo_fields_trigger_wb_i, (st_data_t)fields); - return fields; -} - VALUE rb_imemo_fields_clone(VALUE fields_obj) { shape_id_t shape_id = RBASIC_SHAPE_ID(fields_obj); + VALUE owner = rb_imemo_fields_owner(fields_obj); VALUE clone; if (rb_shape_complex_p(shape_id)) { st_table *src_table = rb_imemo_fields_complex_tbl(fields_obj); - st_table *dest_table = xcalloc(1, sizeof(st_table)); - clone = rb_imemo_fields_new_complex_tbl(rb_imemo_fields_owner(fields_obj), shape_id, dest_table, false /* TODO: check */); - + // We start with ROOT_SHAPE_ID so that if GC trigger in `st_replace` it won't try + // to mark an uninitialized table. + clone = imemo_fields_new(owner, ROOT_SHAPE_ID, sizeof(struct rb_fields), false /* TODO: check */); + st_table *dest_table = rb_imemo_fields_complex_tbl(clone); st_replace(dest_table, src_table); - st_foreach(dest_table, imemo_fields_complex_wb_i, (st_data_t)clone); + RBASIC_SET_FULL_SHAPE_ID(clone, shape_id); } else { - clone = rb_imemo_fields_new(rb_imemo_fields_owner(fields_obj), shape_id, false /* TODO: check */); + clone = rb_imemo_fields_new(owner, shape_id, false /* TODO: check */); VALUE *fields = rb_imemo_fields_ptr(clone); attr_index_t fields_count = RSHAPE_LEN(shape_id); MEMCPY(fields, rb_imemo_fields_ptr(fields_obj), VALUE, fields_count); @@ -297,7 +289,7 @@ rb_imemo_memsize(VALUE obj) break; case imemo_fields: if (rb_obj_shape_complex_p(obj)) { - size += st_memsize(IMEMO_OBJ_FIELDS(obj)->as.complex.table); + size += st_memsize(rb_imemo_fields_complex_tbl(obj)) - sizeof(st_table); } break; @@ -620,9 +612,8 @@ rb_free_const_table(struct rb_id_table *tbl) static inline void imemo_fields_free(struct rb_fields *fields) { - if (FL_TEST_RAW((VALUE)fields, OBJ_FIELD_HEAP)) { - RUBY_ASSERT(rb_shape_complex_p(RBASIC_SHAPE_ID((VALUE)fields))); - st_free_table(fields->as.complex.table); + if (rb_obj_shape_complex_p((VALUE)fields)) { + st_free_table(&fields->as.complex.table); } } diff --git a/internal/imemo.h b/internal/imemo.h index a66ed1161b424b..0bd9fc0bfb50d0 100644 --- a/internal/imemo.h +++ b/internal/imemo.h @@ -245,8 +245,7 @@ struct rb_fields { VALUE fields[1]; } embed; struct { - // TODO: the st_table should be embedded - st_table *table; + st_table table; } complex; } as; }; @@ -281,7 +280,7 @@ rb_imemo_subclasses_entries(VALUE v) VALUE rb_imemo_fields_new(VALUE owner, /* shape_id_t */ uint32_t shape_id, bool shareable); VALUE rb_imemo_subclasses_new(uint32_t capacity); VALUE rb_imemo_fields_new_complex(VALUE owner, /* shape_id_t */ uint32_t shape_id, size_t capa, bool shareable); -VALUE rb_imemo_fields_new_complex_tbl(VALUE owner, /* shape_id_t */ uint32_t shape_id, st_table *tbl, bool shareable); +VALUE rb_imemo_fields_new_complex_empty(VALUE owner); VALUE rb_imemo_fields_clone(VALUE fields_obj); void rb_imemo_fields_clear(VALUE fields_obj); @@ -303,15 +302,9 @@ rb_imemo_fields_ptr(VALUE fields_obj) RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields) || RB_TYPE_P(fields_obj, T_OBJECT)); if (UNLIKELY(FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP))) { - if (RB_TYPE_P(fields_obj, T_OBJECT)) { - fields_obj = ROBJECT(fields_obj)->as.extended; - } + RUBY_ASSERT(RB_TYPE_P(fields_obj, T_OBJECT)); + fields_obj = ROBJECT(fields_obj)->as.extended; RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields)); - - // This will go away once we embed the `st_table` inside `IMEMO/fields`. - if (UNLIKELY(FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP))) { - return (VALUE *)IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table; - } } return IMEMO_OBJ_FIELDS(fields_obj)->as.embed.fields; @@ -325,13 +318,13 @@ rb_imemo_fields_complex_tbl(VALUE fields_obj) } RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields)); - RUBY_ASSERT(FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP)); + RUBY_ASSERT(!FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP)); // Some codepaths unconditionally access the fields_ptr, and assume it can be used as st_table if the // shape is complex. - RUBY_ASSERT((st_table *)rb_imemo_fields_ptr(fields_obj) == IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table); + RUBY_ASSERT((st_table *)rb_imemo_fields_ptr(fields_obj) == &IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table); - return IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table; + return &IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table; } #endif /* INTERNAL_IMEMO_H */ diff --git a/internal/variable.h b/internal/variable.h index 70142815e89710..d743a7033fd68f 100644 --- a/internal/variable.h +++ b/internal/variable.h @@ -47,8 +47,8 @@ void rb_gvar_box_dynamic(const char *name); */ VALUE rb_mod_set_temporary_name(VALUE, VALUE); +void rb_obj_replace_fields(VALUE obj, VALUE fields_obj); void rb_obj_copy_ivs_to_hash_table(VALUE obj, st_table *table); -void rb_obj_init_complex(VALUE obj, st_table *table); void rb_evict_ivars_to_hash(VALUE obj); VALUE rb_obj_field_get(VALUE obj, shape_id_t target_shape_id); void rb_ivar_set_internal(VALUE obj, ID id, VALUE val); diff --git a/object.c b/object.c index 3b382bcc5c0a7b..89785f3b1d438c 100644 --- a/object.c +++ b/object.c @@ -339,7 +339,11 @@ rb_obj_copy_ivar(VALUE dest, VALUE obj) shape_id_t src_shape_id = RBASIC_SHAPE_ID(obj); if (rb_shape_complex_p(src_shape_id)) { - rb_shape_copy_complex_ivars(dest, obj, src_shape_id, ROBJECT_FIELDS_HASH(obj)); + VALUE fields_obj = ROBJECT_FIELDS_OBJ(obj); + VALUE clone = rb_imemo_fields_new_complex_empty(dest); + rb_shape_copy_complex_ivars(clone, fields_obj); + ROBJECT_SET_EXTENDED(dest, clone); + RBASIC_SET_SHAPE_ID_WITH_LAYOUT(dest, ROOT_COMPLEX_SHAPE_ID, SHAPE_ID_LAYOUT_EXTENDED); return; } @@ -348,10 +352,10 @@ rb_obj_copy_ivar(VALUE dest, VALUE obj) shape_id_t dest_shape_id = rb_shape_rebuild(initial_shape_id, src_shape_id); if (UNLIKELY(rb_shape_complex_p(dest_shape_id))) { - st_table *table = rb_st_init_numtable_with_size(src_num_ivs); + VALUE fields_obj = rb_imemo_fields_new_complex(dest, dest_shape_id, rb_ivar_count(obj), false); + st_table *table = rb_imemo_fields_complex_tbl(fields_obj); rb_obj_copy_ivs_to_hash_table(obj, table); - rb_obj_init_complex(dest, table); - + rb_obj_replace_fields(dest, fields_obj); return; } diff --git a/shape.c b/shape.c index 8c1955fc939c43..28f079a369ca78 100644 --- a/shape.c +++ b/shape.c @@ -9,6 +9,7 @@ #include "internal/object.h" #include "internal/symbol.h" #include "internal/variable.h" +#include "internal/st.h" #include "variable.h" #include @@ -1147,16 +1148,21 @@ rb_shape_copy_fields(VALUE dest, VALUE *dest_buf, shape_id_t dest_shape_id, VALU } void -rb_shape_copy_complex_ivars(VALUE dest, VALUE obj, shape_id_t src_shape_id, st_table *fields_table) +rb_shape_copy_complex_ivars(VALUE dest, VALUE src) { - // obj is COMPLEX so we can copy its iv_hash - st_table *table = st_copy(fields_table); - if (rb_shape_has_object_id(src_shape_id)) { - st_data_t id = (st_data_t)id_object_id; - st_delete(table, &id, NULL); + RUBY_ASSERT(IMEMO_TYPE_P(src, imemo_fields)); + RUBY_ASSERT(IMEMO_TYPE_P(dest, imemo_fields)); + + shape_id_t dest_shape_id = RBASIC_SHAPE_ID(src); + st_table *dest_table = rb_imemo_fields_complex_tbl(dest); + st_replace(dest_table, rb_imemo_fields_complex_tbl(src)); + + if (rb_shape_has_object_id(dest_shape_id)) { + st_data_t stkey = (st_data_t)id_object_id; + st_delete(dest_table, &stkey, NULL); } - rb_obj_init_complex(dest, table); - rb_gc_writebarrier_remember(dest); + + RBASIC_SET_SHAPE_ID(dest, ROOT_COMPLEX_SHAPE_ID); } size_t @@ -1295,36 +1301,39 @@ rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) rb_shape_t *shape = RSHAPE(shape_id); - bool has_object_id = false; - while (shape->parent_offset != INVALID_SHAPE_ID) { - if (shape->type == SHAPE_OBJ_ID) { - has_object_id = true; - break; - } - shape = RSHAPE(shape->parent_offset); - } - - if (rb_shape_has_object_id(shape_id)) { - if (!has_object_id) { - 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_bug("shape_id claim not having obj_id but it does shape_id=%u, obj=%s", shape_id, rb_obj_info(obj)); - } - } - // Make sure SHAPE_ID_HAS_IVAR_MASK is valid. if (rb_shape_complex_p(shape_id)) { RUBY_ASSERT(shape_id & SHAPE_ID_HAS_IVAR_MASK); // Ensure complex object don't appear as embedded - if (RB_TYPE_P(obj, T_OBJECT) || IMEMO_TYPE_P(obj, imemo_fields)) { + if (RB_TYPE_P(obj, T_OBJECT)) { RUBY_ASSERT(FL_TEST_RAW(obj, ROBJECT_HEAP)); } + else if (IMEMO_TYPE_P(obj, imemo_fields)) { + RUBY_ASSERT(!FL_TEST_RAW(obj, ROBJECT_HEAP)); + } } else { + bool has_object_id = false; + while (shape->parent_offset != INVALID_SHAPE_ID) { + if (shape->type == SHAPE_OBJ_ID) { + has_object_id = true; + break; + } + shape = RSHAPE(shape->parent_offset); + } + + if (rb_shape_has_object_id(shape_id)) { + if (!has_object_id) { + 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_bug("shape_id claim not having obj_id but it does shape_id=%u, obj=%s", shape_id, rb_obj_info(obj)); + } + } + attr_index_t ivar_count = RSHAPE_LEN(shape_id); if (has_object_id) { ivar_count--; diff --git a/shape.h b/shape.h index 51ebf4f72d762f..2c67d33123188a 100644 --- a/shape.h +++ b/shape.h @@ -267,7 +267,7 @@ shape_id_t rb_shape_transition_add_ivar_no_warnings(shape_id_t shape_id, ID id, shape_id_t rb_shape_object_id(shape_id_t original_shape_id); shape_id_t rb_shape_rebuild(shape_id_t initial_shape_id, shape_id_t dest_shape_id); void rb_shape_copy_fields(VALUE dest, VALUE *dest_buf, shape_id_t dest_shape_id, VALUE *src_buf, shape_id_t src_shape_id); -void rb_shape_copy_complex_ivars(VALUE dest, VALUE obj, shape_id_t src_shape_id, st_table *fields_table); +void rb_shape_copy_complex_ivars(VALUE dest, VALUE src); static inline bool rb_shape_frozen_p(shape_id_t shape_id) @@ -497,10 +497,22 @@ rb_obj_using_gen_fields_table_p(VALUE obj) return rb_obj_gen_fields_p(obj); } +static inline shape_id_t +rb_shape_transition_layout(shape_id_t shape_id, shape_id_t layout) +{ + return (shape_id & (~SHAPE_ID_LAYOUT_MASK)) | layout; +} + static inline shape_id_t rb_shape_transition_robject(shape_id_t shape_id) { - return (shape_id & ~SHAPE_ID_LAYOUT_MASK) | SHAPE_ID_LAYOUT_ROBJECT; + return rb_shape_transition_layout(shape_id, SHAPE_ID_LAYOUT_ROBJECT); +} + +static inline shape_id_t +rb_shape_transition_extended(shape_id_t shape_id) +{ + return rb_shape_transition_layout(shape_id, SHAPE_ID_LAYOUT_EXTENDED); } static inline shape_id_t @@ -548,12 +560,6 @@ rb_shape_transition_slot_size(shape_id_t shape_id, size_t slot_size) return rb_shape_transition_capacity(shape_id, rb_shape_capacity_for_slot_size(slot_size)); } -static inline shape_id_t -rb_shape_transition_layout(shape_id_t shape_id, shape_id_t layout) -{ - return (shape_id & (~SHAPE_ID_LAYOUT_MASK)) | layout; -} - shape_id_t rb_shape_transition_object_id(shape_id_t shape_id); static inline shape_id_t diff --git a/test/ruby/test_object_id.rb b/test/ruby/test_object_id.rb index 034674e5be1a47..acbd96572e175d 100644 --- a/test/ruby/test_object_id.rb +++ b/test/ruby/test_object_id.rb @@ -2,8 +2,10 @@ require "securerandom" class TestObjectId < Test::Unit::TestCase + SomeClass = Class.new + def setup - @obj = Object.new + @obj = SomeClass.new end def test_dup_new_id diff --git a/variable.c b/variable.c index bcd8c32ea2246b..6e76e14bd4f684 100644 --- a/variable.c +++ b/variable.c @@ -1606,49 +1606,35 @@ rb_attr_get(VALUE obj, ID id) void rb_obj_copy_fields_to_hash_table(VALUE obj, st_table *table); static VALUE imemo_fields_complex_from_obj(VALUE owner, VALUE source_fields_obj, shape_id_t shape_id); -static shape_id_t -obj_transition_complex(VALUE obj, st_table *table) -{ - RUBY_ASSERT(!rb_obj_shape_complex_p(obj)); - RUBY_ASSERT(!RB_TYPE_P(obj, T_IMEMO)); - - shape_id_t shape_id = rb_obj_shape_transition_complex(obj); - VALUE fields_obj = rb_imemo_fields_new_complex_tbl(obj, shape_id, table, RB_OBJ_SHAREABLE_P(obj)); - - rb_obj_set_fields(obj, fields_obj, 0, 0); - RBASIC_SET_SHAPE_ID(obj, shape_id); - - RUBY_ASSERT(FL_TEST_RAW(fields_obj, ROBJECT_HEAP)); - RUBY_ASSERT(rb_obj_shape_complex_p(obj)); - RUBY_ASSERT(rb_obj_shape_complex_p(fields_obj)); - - return shape_id; -} - // Copy all object fields, including ivars and internal object_id, etc static shape_id_t rb_evict_fields_to_hash(VALUE obj) { + RUBY_ASSERT(RB_TYPE_P(obj, T_OBJECT)); RUBY_ASSERT(!rb_obj_shape_complex_p(obj)); - st_table *table = st_init_numtable_with_size(RSHAPE_LEN(RBASIC_SHAPE_ID(obj))); + shape_id_t new_shape_id = rb_obj_shape_transition_complex(obj); + VALUE fields_obj = rb_imemo_fields_new_complex(obj, new_shape_id, RSHAPE_LEN(RBASIC_SHAPE_ID(obj)), false); + st_table *table = rb_imemo_fields_complex_tbl(fields_obj); rb_obj_copy_fields_to_hash_table(obj, table); - shape_id_t new_shape_id = obj_transition_complex(obj, table); + ROBJECT_SET_EXTENDED(obj, fields_obj); + RBASIC_SET_FULL_SHAPE_ID(obj, rb_shape_transition_extended(new_shape_id)); - RUBY_ASSERT(rb_obj_shape_complex_p(obj)); return new_shape_id; } void rb_evict_ivars_to_hash(VALUE obj) { + RUBY_ASSERT(RB_TYPE_P(obj, T_OBJECT)); RUBY_ASSERT(!rb_obj_shape_complex_p(obj)); - st_table *table = st_init_numtable_with_size(rb_ivar_count(obj)); - - // Evacuate all previous values from shape into id_table + shape_id_t new_shape_id = rb_obj_shape_transition_complex(obj); + VALUE fields_obj = rb_imemo_fields_new_complex(obj, new_shape_id, rb_ivar_count(obj), false); + st_table *table = rb_imemo_fields_complex_tbl(fields_obj); rb_obj_copy_ivs_to_hash_table(obj, table); - obj_transition_complex(obj, table); + ROBJECT_SET_EXTENDED(obj, fields_obj); + RBASIC_SET_FULL_SHAPE_ID(obj, rb_shape_transition_extended(new_shape_id)); RUBY_ASSERT(rb_obj_shape_complex_p(obj)); } @@ -1783,23 +1769,6 @@ rb_attr_delete(VALUE obj, ID id) return rb_ivar_delete(obj, id, Qnil); } -void -rb_obj_init_complex(VALUE obj, st_table *table) -{ - // This method is meant to be called on newly allocated object. - RUBY_ASSERT(rb_shape_canonical_p(RBASIC_SHAPE_ID(obj))); - RUBY_ASSERT(RSHAPE_LEN(RBASIC_SHAPE_ID(obj)) == 0); - - if (rb_obj_shape_complex_p(obj)) { - shape_id_t shape_id = rb_obj_shape_transition_complex(obj); - VALUE fields_obj = rb_imemo_fields_new_complex_tbl(obj, shape_id, table, false); - ROBJECT_SET_EXTENDED(obj, fields_obj); - } - else { - obj_transition_complex(obj, table); - } -} - static int imemo_fields_complex_from_obj_i(ID key, VALUE val, st_data_t arg) { @@ -2317,7 +2286,9 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) } if (rb_shape_complex_p(src_shape_id)) { - rb_shape_copy_complex_ivars(dest, obj, src_shape_id, rb_imemo_fields_complex_tbl(fields_obj)); + VALUE clone = rb_imemo_fields_new_complex_empty(dest); + rb_shape_copy_complex_ivars(clone, fields_obj); + rb_obj_set_fields(dest, clone, 0, 0); return; } @@ -2329,9 +2300,10 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) dest_shape_id = rb_shape_rebuild(initial_shape_id, src_shape_id); if (UNLIKELY(rb_shape_complex_p(dest_shape_id))) { - st_table *table = rb_st_init_numtable_with_size(src_num_ivs); + new_fields_obj = rb_imemo_fields_new_complex(dest, dest_shape_id, rb_ivar_count(obj), false); + st_table *table = rb_imemo_fields_complex_tbl(new_fields_obj); rb_obj_copy_ivs_to_hash_table(obj, table); - rb_obj_init_complex(dest, table); + rb_obj_replace_fields(dest, new_fields_obj); return; } } diff --git a/variable.h b/variable.h index f2afead9d3a8dd..034f95f949561c 100644 --- a/variable.h +++ b/variable.h @@ -12,7 +12,6 @@ #include "shape.h" -void rb_copy_complex_ivars(VALUE dest, VALUE obj, shape_id_t src_shape_id, st_table *fields_table); VALUE rb_obj_fields(VALUE obj, ID field_name); static inline VALUE From 6f0548a0bdd207a22a8f7644a97442e71eac542f Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 14 Jul 2026 17:54:47 +0200 Subject: [PATCH 10/47] Simplify `rb_obj_copy_ivar` and `rb_copy_generic_ivar` By handling complex shapes in `rb_shape_rebuild` we can merge the two complex cases (when `src` is complex and when `dest` becomes complex). --- object.c | 10 ---------- shape.c | 12 ++++++++++-- variable.c | 26 +++++++------------------- 3 files changed, 17 insertions(+), 31 deletions(-) diff --git a/object.c b/object.c index 89785f3b1d438c..52b80e1e2d27e2 100644 --- a/object.c +++ b/object.c @@ -337,16 +337,6 @@ rb_obj_copy_ivar(VALUE dest, VALUE obj) } shape_id_t src_shape_id = RBASIC_SHAPE_ID(obj); - - if (rb_shape_complex_p(src_shape_id)) { - VALUE fields_obj = ROBJECT_FIELDS_OBJ(obj); - VALUE clone = rb_imemo_fields_new_complex_empty(dest); - rb_shape_copy_complex_ivars(clone, fields_obj); - ROBJECT_SET_EXTENDED(dest, clone); - RBASIC_SET_SHAPE_ID_WITH_LAYOUT(dest, ROOT_COMPLEX_SHAPE_ID, SHAPE_ID_LAYOUT_EXTENDED); - return; - } - shape_id_t initial_shape_id = RBASIC_SHAPE_ID(dest); RUBY_ASSERT(RSHAPE_TYPE_P(initial_shape_id, SHAPE_ROOT)); diff --git a/shape.c b/shape.c index 28f079a369ca78..a0b12b4759d9c1 100644 --- a/shape.c +++ b/shape.c @@ -1094,8 +1094,16 @@ shape_rebuild(rb_shape_t *initial_shape, rb_shape_t *dest_shape) shape_id_t rb_shape_rebuild(shape_id_t initial_shape_id, shape_id_t dest_shape_id) { - RUBY_ASSERT(!rb_shape_complex_p(initial_shape_id)); - RUBY_ASSERT(!rb_shape_complex_p(dest_shape_id)); + RUBY_ASSERT(RSHAPE_TYPE_P(initial_shape_id, SHAPE_ROOT)); + + if (RB_UNLIKELY(rb_shape_complex_p(initial_shape_id))) { + // The class has been marked as too complex. + return initial_shape_id; + } + + if (RB_UNLIKELY(rb_shape_complex_p(dest_shape_id))) { + return rb_shape_transition_complex(initial_shape_id); + } shape_id_t next_shape_id; // The shape has a SHAPE_OBJ_ID edge, it needs to be rebuilt. diff --git a/variable.c b/variable.c index 6e76e14bd4f684..ace3df650ac5df 100644 --- a/variable.c +++ b/variable.c @@ -2285,27 +2285,15 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) goto clear; } - if (rb_shape_complex_p(src_shape_id)) { - VALUE clone = rb_imemo_fields_new_complex_empty(dest); - rb_shape_copy_complex_ivars(clone, fields_obj); - rb_obj_set_fields(dest, clone, 0, 0); - return; - } - - shape_id_t dest_shape_id = src_shape_id; shape_id_t initial_shape_id = rb_obj_shape_id(dest); + shape_id_t dest_shape_id = rb_shape_rebuild(initial_shape_id, src_shape_id); - if (!rb_shape_canonical_p(src_shape_id)) { - RUBY_ASSERT(RSHAPE_TYPE_P(initial_shape_id, SHAPE_ROOT)); - - dest_shape_id = rb_shape_rebuild(initial_shape_id, src_shape_id); - if (UNLIKELY(rb_shape_complex_p(dest_shape_id))) { - new_fields_obj = rb_imemo_fields_new_complex(dest, dest_shape_id, rb_ivar_count(obj), false); - st_table *table = rb_imemo_fields_complex_tbl(new_fields_obj); - rb_obj_copy_ivs_to_hash_table(obj, table); - rb_obj_replace_fields(dest, new_fields_obj); - return; - } + if (UNLIKELY(rb_shape_complex_p(dest_shape_id))) { + new_fields_obj = rb_imemo_fields_new_complex(dest, dest_shape_id, rb_ivar_count(obj), false); + st_table *table = rb_imemo_fields_complex_tbl(new_fields_obj); + rb_obj_copy_ivs_to_hash_table(obj, table); + rb_obj_replace_fields(dest, new_fields_obj); + return; } if (!RSHAPE_LEN(dest_shape_id)) { From 6c23075086bde1612e6aab61b878606f07508d64 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 14 Jul 2026 18:04:03 +0200 Subject: [PATCH 11/47] rb_copy_generic_ivar: cleanup goto statement It used to be that multiple paths would need to clear the generic ivar reference, but it's no longer the case. --- variable.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/variable.c b/variable.c index ace3df650ac5df..27fab5c591b536 100644 --- a/variable.c +++ b/variable.c @@ -2282,7 +2282,8 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) if (fields_obj) { unsigned long src_num_ivs = rb_ivar_count(fields_obj); if (!src_num_ivs) { - goto clear; + rb_free_generic_ivar(dest); + return; } shape_id_t initial_shape_id = rb_obj_shape_id(dest); @@ -2308,10 +2309,6 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) rb_obj_replace_fields(dest, new_fields_obj); } - return; - - clear: - rb_free_generic_ivar(dest); } void From 24337e3cbe03e5f575cc338f11787924d725755a Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 14 Jul 2026 19:38:15 +0200 Subject: [PATCH 12/47] Refactor `rb_obj_copy_ivar` Share more code with `rb_copy_generic_ivar`. --- depend | 1 - internal/variable.h | 2 +- object.c | 5 +---- shape.c | 19 ------------------- shape.h | 1 - variable.c | 20 +++++--------------- 6 files changed, 7 insertions(+), 41 deletions(-) diff --git a/depend b/depend index 697a9a5eb78c6c..8db2d7a6759f4f 100644 --- a/depend +++ b/depend @@ -16680,7 +16680,6 @@ shape.$(OBJEXT): $(top_srcdir)/internal/object.h shape.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h shape.$(OBJEXT): $(top_srcdir)/internal/serial.h shape.$(OBJEXT): $(top_srcdir)/internal/set_table.h -shape.$(OBJEXT): $(top_srcdir)/internal/st.h shape.$(OBJEXT): $(top_srcdir)/internal/static_assert.h shape.$(OBJEXT): $(top_srcdir)/internal/string.h shape.$(OBJEXT): $(top_srcdir)/internal/struct.h diff --git a/internal/variable.h b/internal/variable.h index d743a7033fd68f..1b3c01f546f6bd 100644 --- a/internal/variable.h +++ b/internal/variable.h @@ -49,7 +49,7 @@ VALUE rb_mod_set_temporary_name(VALUE, VALUE); void rb_obj_replace_fields(VALUE obj, VALUE fields_obj); void rb_obj_copy_ivs_to_hash_table(VALUE obj, st_table *table); -void rb_evict_ivars_to_hash(VALUE obj); +VALUE rb_obj_complex_fields_build(VALUE obj); VALUE rb_obj_field_get(VALUE obj, shape_id_t target_shape_id); void rb_ivar_set_internal(VALUE obj, ID id, VALUE val); void rb_ivar_foreach_buffered(VALUE obj, int (*func)(ID name, VALUE val, st_data_t arg), st_data_t arg); diff --git a/object.c b/object.c index 52b80e1e2d27e2..033b3dc35fb5de 100644 --- a/object.c +++ b/object.c @@ -342,10 +342,7 @@ rb_obj_copy_ivar(VALUE dest, VALUE obj) shape_id_t dest_shape_id = rb_shape_rebuild(initial_shape_id, src_shape_id); if (UNLIKELY(rb_shape_complex_p(dest_shape_id))) { - VALUE fields_obj = rb_imemo_fields_new_complex(dest, dest_shape_id, rb_ivar_count(obj), false); - st_table *table = rb_imemo_fields_complex_tbl(fields_obj); - rb_obj_copy_ivs_to_hash_table(obj, table); - rb_obj_replace_fields(dest, fields_obj); + rb_obj_replace_fields(dest, rb_obj_complex_fields_build(obj)); return; } diff --git a/shape.c b/shape.c index a0b12b4759d9c1..0004d4cc56764f 100644 --- a/shape.c +++ b/shape.c @@ -9,7 +9,6 @@ #include "internal/object.h" #include "internal/symbol.h" #include "internal/variable.h" -#include "internal/st.h" #include "variable.h" #include @@ -1155,24 +1154,6 @@ rb_shape_copy_fields(VALUE dest, VALUE *dest_buf, shape_id_t dest_shape_id, VALU } } -void -rb_shape_copy_complex_ivars(VALUE dest, VALUE src) -{ - RUBY_ASSERT(IMEMO_TYPE_P(src, imemo_fields)); - RUBY_ASSERT(IMEMO_TYPE_P(dest, imemo_fields)); - - shape_id_t dest_shape_id = RBASIC_SHAPE_ID(src); - st_table *dest_table = rb_imemo_fields_complex_tbl(dest); - st_replace(dest_table, rb_imemo_fields_complex_tbl(src)); - - if (rb_shape_has_object_id(dest_shape_id)) { - st_data_t stkey = (st_data_t)id_object_id; - st_delete(dest_table, &stkey, NULL); - } - - RBASIC_SET_SHAPE_ID(dest, ROOT_COMPLEX_SHAPE_ID); -} - size_t rb_shape_edges_count(shape_id_t shape_id) { diff --git a/shape.h b/shape.h index 2c67d33123188a..c2cf158c72e775 100644 --- a/shape.h +++ b/shape.h @@ -267,7 +267,6 @@ shape_id_t rb_shape_transition_add_ivar_no_warnings(shape_id_t shape_id, ID id, shape_id_t rb_shape_object_id(shape_id_t original_shape_id); shape_id_t rb_shape_rebuild(shape_id_t initial_shape_id, shape_id_t dest_shape_id); void rb_shape_copy_fields(VALUE dest, VALUE *dest_buf, shape_id_t dest_shape_id, VALUE *src_buf, shape_id_t src_shape_id); -void rb_shape_copy_complex_ivars(VALUE dest, VALUE src); static inline bool rb_shape_frozen_p(shape_id_t shape_id) diff --git a/variable.c b/variable.c index 27fab5c591b536..f78c35cac1cc23 100644 --- a/variable.c +++ b/variable.c @@ -1623,20 +1623,13 @@ rb_evict_fields_to_hash(VALUE obj) return new_shape_id; } -void -rb_evict_ivars_to_hash(VALUE obj) +VALUE +rb_obj_complex_fields_build(VALUE obj) { - RUBY_ASSERT(RB_TYPE_P(obj, T_OBJECT)); - RUBY_ASSERT(!rb_obj_shape_complex_p(obj)); - - shape_id_t new_shape_id = rb_obj_shape_transition_complex(obj); - VALUE fields_obj = rb_imemo_fields_new_complex(obj, new_shape_id, rb_ivar_count(obj), false); + VALUE fields_obj = rb_imemo_fields_new_complex(obj, ROOT_COMPLEX_SHAPE_ID, rb_ivar_count(obj), false); st_table *table = rb_imemo_fields_complex_tbl(fields_obj); rb_obj_copy_ivs_to_hash_table(obj, table); - ROBJECT_SET_EXTENDED(obj, fields_obj); - RBASIC_SET_FULL_SHAPE_ID(obj, rb_shape_transition_extended(new_shape_id)); - - RUBY_ASSERT(rb_obj_shape_complex_p(obj)); + return fields_obj; } static VALUE @@ -2290,10 +2283,7 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) shape_id_t dest_shape_id = rb_shape_rebuild(initial_shape_id, src_shape_id); if (UNLIKELY(rb_shape_complex_p(dest_shape_id))) { - new_fields_obj = rb_imemo_fields_new_complex(dest, dest_shape_id, rb_ivar_count(obj), false); - st_table *table = rb_imemo_fields_complex_tbl(new_fields_obj); - rb_obj_copy_ivs_to_hash_table(obj, table); - rb_obj_replace_fields(dest, new_fields_obj); + rb_obj_replace_fields(dest, rb_obj_complex_fields_build(obj)); return; } From 944d936d7c13a71e01e13bbe3d4a6d2d00803f98 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 14 Jul 2026 20:18:54 +0200 Subject: [PATCH 13/47] variable.c: Refactor too_complex evacuation paths Now share more code between the various cases: - Whether to copy all fields or just ivars - Whether to add or remove some capacity. --- internal/variable.h | 1 - variable.c | 89 +++++++++++++++++---------------------------- 2 files changed, 33 insertions(+), 57 deletions(-) diff --git a/internal/variable.h b/internal/variable.h index 1b3c01f546f6bd..461676e6fae300 100644 --- a/internal/variable.h +++ b/internal/variable.h @@ -48,7 +48,6 @@ void rb_gvar_box_dynamic(const char *name); VALUE rb_mod_set_temporary_name(VALUE, VALUE); void rb_obj_replace_fields(VALUE obj, VALUE fields_obj); -void rb_obj_copy_ivs_to_hash_table(VALUE obj, st_table *table); VALUE rb_obj_complex_fields_build(VALUE obj); VALUE rb_obj_field_get(VALUE obj, shape_id_t target_shape_id); void rb_ivar_set_internal(VALUE obj, ID id, VALUE val); diff --git a/variable.c b/variable.c index f78c35cac1cc23..7edeb57c9343ba 100644 --- a/variable.c +++ b/variable.c @@ -1603,33 +1603,19 @@ rb_attr_get(VALUE obj, ID id) return rb_ivar_lookup(obj, id, Qnil); } -void rb_obj_copy_fields_to_hash_table(VALUE obj, st_table *table); -static VALUE imemo_fields_complex_from_obj(VALUE owner, VALUE source_fields_obj, shape_id_t shape_id); +static VALUE imemo_fields_evacutate_to_complex(VALUE owner, VALUE source_fields_obj, shape_id_t shape_id, int extra_capa); -// Copy all object fields, including ivars and internal object_id, etc static shape_id_t -rb_evict_fields_to_hash(VALUE obj) +rb_obj_convert_too_complex(VALUE obj, VALUE fields_obj, shape_id_t shape_id) { RUBY_ASSERT(RB_TYPE_P(obj, T_OBJECT)); RUBY_ASSERT(!rb_obj_shape_complex_p(obj)); - shape_id_t new_shape_id = rb_obj_shape_transition_complex(obj); - VALUE fields_obj = rb_imemo_fields_new_complex(obj, new_shape_id, RSHAPE_LEN(RBASIC_SHAPE_ID(obj)), false); - st_table *table = rb_imemo_fields_complex_tbl(fields_obj); - rb_obj_copy_fields_to_hash_table(obj, table); - ROBJECT_SET_EXTENDED(obj, fields_obj); - RBASIC_SET_FULL_SHAPE_ID(obj, rb_shape_transition_extended(new_shape_id)); - - return new_shape_id; -} - -VALUE -rb_obj_complex_fields_build(VALUE obj) -{ - VALUE fields_obj = rb_imemo_fields_new_complex(obj, ROOT_COMPLEX_SHAPE_ID, rb_ivar_count(obj), false); - st_table *table = rb_imemo_fields_complex_tbl(fields_obj); - rb_obj_copy_ivs_to_hash_table(obj, table); - return fields_obj; + shape_id = rb_shape_transition_complex(shape_id); + VALUE new_fields_obj = imemo_fields_evacutate_to_complex(obj, fields_obj, shape_id, 1); + ROBJECT_SET_EXTENDED(obj, new_fields_obj); + RBASIC_SET_SHAPE_ID_WITH_LAYOUT(obj, shape_id, SHAPE_ID_LAYOUT_EXTENDED); + return shape_id; } static VALUE @@ -1676,7 +1662,7 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) if (UNLIKELY(rb_shape_complex_p(next_shape_id))) { if (UNLIKELY(!rb_shape_complex_p(old_shape_id))) { - fields_obj = imemo_fields_complex_from_obj(obj, fields_obj, next_shape_id); + fields_obj = imemo_fields_evacutate_to_complex(obj, fields_obj, next_shape_id, -1); } st_data_t key = id; if (!st_delete(rb_imemo_fields_complex_tbl(fields_obj), &key, (st_data_t *)&val)) { @@ -1776,16 +1762,31 @@ imemo_fields_complex_from_obj_i(ID key, VALUE val, st_data_t arg) } static VALUE -imemo_fields_complex_from_obj(VALUE owner, VALUE source_fields_obj, shape_id_t shape_id) +imemo_fields_complex_from_obj(VALUE owner, VALUE source, shape_id_t shape_id, bool ivar_only, int extra_capa) { - attr_index_t len = source_fields_obj ? RSHAPE_LEN(RBASIC_SHAPE_ID(source_fields_obj)) : 0; - VALUE fields_obj = rb_imemo_fields_new_complex(owner, shape_id, len + 1, RB_OBJ_SHAREABLE_P(owner)); + attr_index_t len = source ? RSHAPE_LEN(RBASIC_SHAPE_ID(source)) : 0; + int capa = (len + extra_capa); + RUBY_ASSERT(capa >= 0); - rb_field_foreach(source_fields_obj, imemo_fields_complex_from_obj_i, (st_data_t)fields_obj, false); + VALUE fields_obj = rb_imemo_fields_new_complex(owner, shape_id, capa, RB_OBJ_SHAREABLE_P(owner)); + + rb_field_foreach(source, imemo_fields_complex_from_obj_i, (st_data_t)fields_obj, ivar_only); return fields_obj; } +static VALUE +imemo_fields_evacutate_to_complex(VALUE owner, VALUE source, shape_id_t shape_id, int extra_capa) +{ + return imemo_fields_complex_from_obj(owner, source, shape_id, false, extra_capa); +} + +VALUE +rb_obj_complex_fields_build(VALUE obj) +{ + return imemo_fields_complex_from_obj(obj, obj, ROOT_COMPLEX_SHAPE_ID, true, 0); +} + static VALUE imemo_fields_copy_append(VALUE owner, VALUE source_fields_obj, shape_id_t current_shape_id, shape_id_t target_shape_id, VALUE val) { @@ -1823,7 +1824,7 @@ imemo_fields_set(VALUE owner, VALUE fields_obj, shape_id_t target_shape_id, ID f } } else { - fields_obj = imemo_fields_complex_from_obj(owner, original_fields_obj, target_shape_id); + fields_obj = imemo_fields_evacutate_to_complex(owner, original_fields_obj, target_shape_id, 1); current_shape_id = target_shape_id; } @@ -1892,40 +1893,19 @@ generic_ivar_set(VALUE obj, ID id, VALUE val) return generic_field_set(obj, target_shape_id, id, val); } -static int -rb_obj_copy_ivs_to_hash_table_i(ID key, VALUE val, st_data_t arg) -{ - RUBY_ASSERT(!st_lookup((st_table *)arg, (st_data_t)key, NULL)); - - st_add_direct((st_table *)arg, (st_data_t)key, (st_data_t)val); - return ST_CONTINUE; -} - -void -rb_obj_copy_ivs_to_hash_table(VALUE obj, st_table *table) -{ - rb_ivar_foreach(obj, rb_obj_copy_ivs_to_hash_table_i, (st_data_t)table); -} - -void -rb_obj_copy_fields_to_hash_table(VALUE obj, st_table *table) -{ - rb_field_foreach(obj, rb_obj_copy_ivs_to_hash_table_i, (st_data_t)table, false); -} - static attr_index_t obj_field_set(VALUE obj, shape_id_t target_shape_id, ID field_name, VALUE val) { + // may be T_OBJECT or imemo_fields + VALUE fields_obj = ROBJECT_FIELDS_OBJ(obj); shape_id_t current_shape_id = RBASIC_SHAPE_ID(obj); if (UNLIKELY(rb_shape_complex_p(target_shape_id))) { if (UNLIKELY(!rb_shape_complex_p(current_shape_id))) { - current_shape_id = rb_evict_fields_to_hash(obj); + current_shape_id = rb_obj_convert_too_complex(obj, fields_obj, current_shape_id); + fields_obj = ROBJECT_FIELDS_OBJ(obj); } - // may be T_OBJECT or imemo_fields - VALUE fields_obj = ROBJECT_FIELDS_OBJ(obj); - RUBY_ASSERT(rb_obj_shape_complex_p(obj)); RUBY_ASSERT(rb_obj_shape_complex_p(fields_obj)); @@ -1947,9 +1927,6 @@ obj_field_set(VALUE obj, shape_id_t target_shape_id, ID field_name, VALUE val) else { attr_index_t index = RSHAPE_INDEX(target_shape_id); - // may be T_OBJECT or imemo_fields - VALUE fields_obj = ROBJECT_FIELDS_OBJ(obj); - if (index < RSHAPE_LEN(current_shape_id)) { // Replace existing value; RB_OBJ_WRITE(fields_obj, &rb_imemo_fields_ptr(fields_obj)[index], val); @@ -4591,7 +4568,7 @@ class_fields_ivar_set(VALUE klass, VALUE fields_obj, ID id, VALUE val, bool conc next_shape_id = generic_shape_ivar(fields_obj, id, &new_ivar); if (UNLIKELY(rb_shape_complex_p(next_shape_id))) { - fields_obj = imemo_fields_complex_from_obj(klass, fields_obj, next_shape_id); + fields_obj = imemo_fields_evacutate_to_complex(klass, fields_obj, next_shape_id, 1); goto complex; } From 4bd94ccbbd60d1d5cd19de2aba08b1ef4feadc6e Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 14 Jul 2026 22:35:47 +0200 Subject: [PATCH 14/47] variable.c: only filter out `object_id`, not all internal ivars --- shape.c | 10 ++++------ shape.h | 1 + variable.c | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/shape.c b/shape.c index 0004d4cc56764f..c54c86b937c923 100644 --- a/shape.c +++ b/shape.c @@ -33,8 +33,6 @@ #define MAX_SHAPE_ID (INVALID_SHAPE_ID - 1) #define ANCESTOR_SEARCH_MAX_DEPTH 2 -static ID id_object_id; - // Should be on its own cache line static RUBY_ALIGNAS(128) rb_atomic_t redblack_cache_size; @@ -701,7 +699,7 @@ rb_shape_transition_object_id(shape_id_t original_shape_id) bool dont_care; rb_shape_t *shape = NULL; if (LIKELY(original_shape->next_field_index < rb_shape_max_capacity())) { - shape = get_next_shape_internal(original_shape, id_object_id, SHAPE_OBJ_ID, &dont_care, true); + shape = get_next_shape_internal(original_shape, rb_shape_tree.id_object_id, SHAPE_OBJ_ID, &dont_care, true); } if (!shape) { return rb_shape_layout(original_shape_id) | ROOT_COMPLEX_WITH_OBJ_ID | RSHAPE_FLAGS(original_shape_id); @@ -1612,7 +1610,7 @@ Init_default_shapes(void) rb_memerror(); } - id_object_id = rb_make_internal_id(); + rb_shape_tree.id_object_id = rb_make_internal_id(); #ifdef HAVE_MMAP size_t shape_cache_mmap_size = rb_size_mul_or_raise(REDBLACK_CACHE_SIZE, sizeof(redblack_node_t), rb_eRuntimeError); @@ -1647,11 +1645,11 @@ Init_default_shapes(void) RUBY_ASSERT(!(SHAPE_OFFSET(root) & SHAPE_ID_HAS_IVAR_MASK)); bool dontcare; - rb_shape_t *root_with_obj_id = get_next_shape_internal(root, id_object_id, SHAPE_OBJ_ID, &dontcare, true); + rb_shape_t *root_with_obj_id = get_next_shape_internal(root, rb_shape_tree.id_object_id, SHAPE_OBJ_ID, &dontcare, true); RUBY_ASSERT(root_with_obj_id); RUBY_ASSERT(SHAPE_OFFSET(root_with_obj_id) == ROOT_SHAPE_WITH_OBJ_ID); RUBY_ASSERT(root_with_obj_id->type == SHAPE_OBJ_ID); - RUBY_ASSERT(root_with_obj_id->edge_name == id_object_id); + RUBY_ASSERT(root_with_obj_id->edge_name == rb_shape_tree.id_object_id); RUBY_ASSERT(root_with_obj_id->next_field_index == 1); RUBY_ASSERT(!(SHAPE_OFFSET(root_with_obj_id) & SHAPE_ID_HAS_IVAR_MASK)); (void)root_with_obj_id; diff --git a/shape.h b/shape.h index c2cf158c72e775..f885bab74f6d85 100644 --- a/shape.h +++ b/shape.h @@ -134,6 +134,7 @@ enum shape_flags { typedef struct { rb_shape_t *shape_list; attr_index_t max_capacity; + ID id_object_id; } rb_shape_tree_t; RUBY_SYMBOL_EXPORT_BEGIN diff --git a/variable.c b/variable.c index 7edeb57c9343ba..c29054b57652d2 100644 --- a/variable.c +++ b/variable.c @@ -2185,7 +2185,7 @@ each_hash_iv(st_data_t id, st_data_t val, st_data_t data) { struct iv_itr_data * itr_data = (struct iv_itr_data *)data; rb_ivar_foreach_callback_func *callback = itr_data->func; - if (is_internal_id((ID)id)) { + if ((ID)id == rb_shape_tree.id_object_id) { return ST_CONTINUE; } return callback((ID)id, (VALUE)val, itr_data->arg); From 9a3be8a74c3048c9e6f114073fdd9713b62e25c7 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 14 Jul 2026 14:43:34 -0700 Subject: [PATCH 15/47] ZJIT: Rename RData shape layout to Extended The layout is shared by T_DATA objects and overflowed RObjects, so name the Rust variant after SHAPE_ID_LAYOUT_EXTENDED rather than one of its users. --- zjit/src/cruby.rs | 4 ++-- zjit/src/hir.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 9024971d1ac61d..74f8e676a8acd4 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -265,7 +265,7 @@ pub struct ShapeId(pub u32); pub enum ShapeLayout { RObject, RClass, - RData, + Extended, Other, } @@ -288,7 +288,7 @@ impl ShapeId { match self.0 & SHAPE_ID_LAYOUT_MASK { SHAPE_ID_LAYOUT_ROBJECT => ShapeLayout::RObject, SHAPE_ID_LAYOUT_RCLASS => ShapeLayout::RClass, - SHAPE_ID_LAYOUT_EXTENDED => ShapeLayout::RData, + SHAPE_ID_LAYOUT_EXTENDED => ShapeLayout::Extended, SHAPE_ID_LAYOUT_OTHER => ShapeLayout::Other, layout => unreachable!("unknown shape layout bits: {layout:#x}"), } diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index a4821d3aa62ca7..c46598c70f0830 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -5397,7 +5397,7 @@ impl Function { let layout = recv_type.shape().layout(); match layout { - ShapeLayout::RClass | ShapeLayout::RData => { + ShapeLayout::RClass | ShapeLayout::Extended => { let offset = if layout == ShapeLayout::RClass { RCLASS_OFFSET_PRIME_FIELDS_OBJ } else { @@ -5464,7 +5464,7 @@ impl Function { ShapeLayout::RObject => { // OK } - ShapeLayout::RData => { + ShapeLayout::Extended => { // FIXME: we side exit for now as we're missing SHAPE_ID_FL_PRIVATE_MASK handling. return Err(Counter::setivar_fallback_not_t_object); } @@ -5522,7 +5522,7 @@ impl Function { ShapeLayout::RObject => { // AKA embedded (self_val, true) }, - ShapeLayout::RData => { // AKA extended + ShapeLayout::Extended => { let fields = self.load_field(block, self_val, FieldName::as_heap, ROBJECT_OFFSET_AS_HEAP_FIELDS, types::BasicObject); (fields, false) }, From 09e19a51ee00509ed132f5ecad2a83bf51463d0f Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 14 Jul 2026 14:50:14 -0700 Subject: [PATCH 16/47] ZJIT: Optimize setivar on extended RObjects Directly update existing instance variables in the fields object used by overflowed RObjects. Keep shape-changing writes on the generic path so the owner and fields object retain their private layout and capacity bits. --- zjit/src/hir.rs | 24 +++++++++++----------- zjit/src/hir/opt_tests.rs | 43 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index c46598c70f0830..5f82369717eef3 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -5460,18 +5460,11 @@ impl Function { return Err(Counter::setivar_fallback_immediate); } - match profiled_type.shape().layout() { - ShapeLayout::RObject => { - // OK - } - ShapeLayout::Extended => { - // FIXME: we side exit for now as we're missing SHAPE_ID_FL_PRIVATE_MASK handling. - return Err(Counter::setivar_fallback_not_t_object); - } - _ => { - return Err(Counter::setivar_fallback_not_t_object); - } - } + let extended_robject = match profiled_type.shape().layout() { + ShapeLayout::RObject => false, + ShapeLayout::Extended if profiled_type.flags().is_t_object() => true, + _ => return Err(Counter::setivar_fallback_not_t_object), + }; assert!(profiled_type.shape().is_valid()); if profiled_type.shape().is_frozen() { @@ -5485,6 +5478,13 @@ impl Function { let mut ivar_index: attr_index_t = 0; let mut next_shape = profiled_type.shape(); if !unsafe { rb_shape_get_iv_index(profiled_type.shape().0, id, &mut ivar_index) } { + // Updating the fields object's shape requires preserving its private layout and + // capacity bits, which can differ from the owning RObject's. Existing ivars do not + // change either shape, so they can still use the fast path. + if extended_robject { + return Err(Counter::setivar_fallback_shape_transition); + } + // Current shape does not contain this ivar; do a shape transition. let current_shape_id = profiled_type.shape(); let class = profiled_type.class(); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 58589cbb7d07ba..54f76507358a9f 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -6703,6 +6703,49 @@ mod hir_opt_tests { "); } + #[test] + fn test_specialize_monomorphic_setivar_on_extended_robject() { + let obj = eval(r#" + class ExtendedSetIvar + def test(value) + @v0 = value + end + end + + OBJ = ExtendedSetIvar.new + 10.times { |i| OBJ.instance_variable_set(:"@v#{i}", i) } + OBJ.test(10) + TEST = ExtendedSetIvar.instance_method(:test) + OBJ + "#); + assert!(!obj.embedded_p()); + + assert_snapshot!(hir_string_proc("TEST"), @" + fn test@:4: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :value@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :value@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint SingleRactorMode + v17:HeapBasicObject = GuardType v9, HeapBasicObject + v18:CShape = LoadField v17, :shape_id@0x1001 + v19:CShape[0x1002] = GuardBitEquals v18, CShape(0x1002) recompile + v20:BasicObject = LoadField v17, :as_heap@0x1003 + StoreField v20, :@v0@0x1003, v10 + WriteBarrier v20, v10 + CheckInterrupts + Return v10 + "); + } + #[test] fn test_specialize_monomorphic_setivar_with_shape_transition() { eval(" From 8f0c0b3d00387f1654a70333866e9dd524e70f0d Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 14 Jul 2026 15:35:58 -0700 Subject: [PATCH 17/47] ZJIT: Read from correct CFP in forward fallback (#17875) Lightweight frames need C code to use CFP_ISEQ(...). --- vm_args.c | 2 +- zjit/src/codegen_tests.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/vm_args.c b/vm_args.c index 3d67c6540a3b48..bf5dc3cadb80e2 100644 --- a/vm_args.c +++ b/vm_args.c @@ -1184,7 +1184,7 @@ vm_caller_setup_fwd_args(const rb_execution_context_t *ec, rb_control_frame_t *r CALL_INFO site_ci = cd->ci; VALUE bh = Qundef; - RUBY_ASSERT(ISEQ_BODY(ISEQ_BODY(GET_ISEQ())->local_iseq)->param.flags.forwardable); + RUBY_ASSERT(ISEQ_BODY(ISEQ_BODY(CFP_ISEQ(reg_cfp))->local_iseq)->param.flags.forwardable); CALL_INFO caller_ci = (CALL_INFO)TOPN(0); unsigned int site_argc = vm_ci_argc(site_ci); diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 09faf5814daa96..e53f265202d351 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -7145,3 +7145,33 @@ fn test_send_no_profiles_with_disabled_specialized_instruction() { fn test_array_each_is_defined_in_ruby() { assert_snapshot!(inspect("Array.instance_method(:each).source_location&.first"), @r#""""#); } + +#[test] +fn test_forward_fallback_with_lightweight_frame_reads_cfp() { + assert_snapshot!(inspect(r#" + class Base + def foo(...) + "base" + end + end + + class Child < Base + def foo(...) + bar do + super + end + end + + def bar + yield + end + end + + c = Child.new + 100.times do + Array.new(50) { |n| n * n } + c.foo(1, 2, 3) + end + :done + "#), @":done"); +} From a42d327517fa53ae86efe7d5a59f4113cb1fef7f Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 14 Jul 2026 17:33:47 -0700 Subject: [PATCH 18/47] ZJIT: Skip interrupt checks on forward branches (#17878) Match YJIT by checking interrupts only on backward jump, branchif, branchunless, and branchnil instructions. Forward branches cannot form loops and do not need polling. --- zjit/src/hir.rs | 14 +- zjit/src/hir/opt_tests.rs | 724 ++++++++++++++++++-------------------- zjit/src/hir/tests.rs | 339 +++++++++--------- 3 files changed, 514 insertions(+), 563 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 5f82369717eef3..ace63943053ce4 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -8430,10 +8430,10 @@ fn add_iseq_to_hir( } } YARVINSN_branchunless | YARVINSN_branchunless_without_ints => { - if opcode == YARVINSN_branchunless { + let offset = get_arg(pc, 0).as_i64(); + if opcode == YARVINSN_branchunless && offset < 0 { fun.push_insn(block, Insn::CheckInterrupts { state: exit_id }); } - let offset = get_arg(pc, 0).as_i64(); let val = state.stack_pop()?; let test_id = fun.push_insn(block, Insn::Test { val }); let target_idx = insn_idx_at_offset(insn_idx, offset); @@ -8458,10 +8458,10 @@ fn add_iseq_to_hir( queue.push_back((state.clone(), target, target_idx, local_inval)); } YARVINSN_branchif | YARVINSN_branchif_without_ints => { - if opcode == YARVINSN_branchif { + let offset = get_arg(pc, 0).as_i64(); + if opcode == YARVINSN_branchif && offset < 0 { fun.push_insn(block, Insn::CheckInterrupts { state: exit_id }); } - let offset = get_arg(pc, 0).as_i64(); let val = state.stack_pop()?; let test_id = fun.push_insn(block, Insn::Test { val }); let target_idx = insn_idx_at_offset(insn_idx, offset); @@ -8487,10 +8487,10 @@ fn add_iseq_to_hir( queue.push_back((state.clone(), target, target_idx, local_inval)); } YARVINSN_branchnil | YARVINSN_branchnil_without_ints => { - if opcode == YARVINSN_branchnil { + let offset = get_arg(pc, 0).as_i64(); + if opcode == YARVINSN_branchnil && offset < 0 { fun.push_insn(block, Insn::CheckInterrupts { state: exit_id }); } - let offset = get_arg(pc, 0).as_i64(); let val = state.stack_pop()?; let test_id = fun.push_insn(block, Insn::HasType { val, expected: types::NilClass }); let target_idx = insn_idx_at_offset(insn_idx, offset); @@ -8552,7 +8552,7 @@ fn add_iseq_to_hir( } YARVINSN_jump | YARVINSN_jump_without_ints => { let offset = get_arg(pc, 0).as_i64(); - if opcode == YARVINSN_jump { + if opcode == YARVINSN_jump && offset < 0 { fun.push_insn(block, Insn::CheckInterrupts { state: exit_id }); } let target_idx = insn_idx_at_offset(insn_idx, offset); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 54f76507358a9f..c374e98c85c9e1 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -52,9 +52,9 @@ mod hir_opt_tests { Jump bb3(v5, v6) bb3(v8:BasicObject, v9:NilClass): v13:TrueClass = Const Value(true) + v24:Fixnum[3] = Const Value(3) CheckInterrupts - v25:Fixnum[3] = Const Value(3) - Return v25 + Return v24 "); } @@ -84,9 +84,9 @@ mod hir_opt_tests { Jump bb3(v5, v6) bb3(v8:BasicObject, v9:NilClass): v13:FalseClass = Const Value(false) + v34:Fixnum[4] = Const Value(4) CheckInterrupts - v35:Fixnum[4] = Const Value(4) - Return v35 + Return v34 "); } @@ -957,10 +957,9 @@ mod hir_opt_tests { v10:Fixnum[1] = Const Value(1) v12:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1000, <@0x1008, cme:0x1010) - v42:TrueClass = Const Value(true) + v23:Fixnum[3] = Const Value(3) CheckInterrupts - v24:Fixnum[3] = Const Value(3) - Return v24 + Return v23 "); } @@ -989,10 +988,9 @@ mod hir_opt_tests { v10:Fixnum[1] = Const Value(1) v12:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1000, <=@0x1008, cme:0x1010) - v58:TrueClass = Const Value(true) + v35:Fixnum[3] = Const Value(3) CheckInterrupts - v37:Fixnum[3] = Const Value(3) - Return v37 + Return v35 "); } @@ -1021,10 +1019,9 @@ mod hir_opt_tests { v10:Fixnum[2] = Const Value(2) v12:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1000, >@0x1008, cme:0x1010) - v42:TrueClass = Const Value(true) + v23:Fixnum[3] = Const Value(3) CheckInterrupts - v24:Fixnum[3] = Const Value(3) - Return v24 + Return v23 "); } @@ -1053,10 +1050,9 @@ mod hir_opt_tests { v10:Fixnum[2] = Const Value(2) v12:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1000, >=@0x1008, cme:0x1010) - v58:TrueClass = Const Value(true) + v35:Fixnum[3] = Const Value(3) CheckInterrupts - v37:Fixnum[3] = Const Value(3) - Return v37 + Return v35 "); } @@ -1085,10 +1081,9 @@ mod hir_opt_tests { v10:Fixnum[1] = Const Value(1) v12:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1000, ==@0x1008, cme:0x1010) - v42:FalseClass = Const Value(false) + v32:Fixnum[4] = Const Value(4) CheckInterrupts - v33:Fixnum[4] = Const Value(4) - Return v33 + Return v32 "); } @@ -1117,10 +1112,9 @@ mod hir_opt_tests { v10:Fixnum[2] = Const Value(2) v12:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1000, ==@0x1008, cme:0x1010) - v42:TrueClass = Const Value(true) + v23:Fixnum[3] = Const Value(3) CheckInterrupts - v24:Fixnum[3] = Const Value(3) - Return v24 + Return v23 "); } @@ -1150,10 +1144,9 @@ mod hir_opt_tests { v12:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1000, !=@0x1008, cme:0x1010) PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_EQ) - v43:TrueClass = Const Value(true) + v23:Fixnum[3] = Const Value(3) CheckInterrupts - v24:Fixnum[3] = Const Value(3) - Return v24 + Return v23 "); } @@ -1183,10 +1176,9 @@ mod hir_opt_tests { v12:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1000, !=@0x1008, cme:0x1010) PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_EQ) - v43:FalseClass = Const Value(false) + v32:Fixnum[4] = Const Value(4) CheckInterrupts - v33:Fixnum[4] = Const Value(4) - Return v33 + Return v32 "); } @@ -5046,12 +5038,11 @@ mod hir_opt_tests { v12:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) v14:NilClass = Const Value(nil) PatchPoint MethodRedefined(C@0x1008, new@0x1009, cme:0x1010) - v45:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) + v44:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, initialize@0x1038, cme:0x1040) - v50:NilClass = Const Value(nil) CheckInterrupts - Return v45 + Return v44 "); } @@ -5083,19 +5074,19 @@ mod hir_opt_tests { v14:NilClass = Const Value(nil) v17:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(C@0x1008, new@0x1009, cme:0x1010) - v48:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) + v47:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, initialize@0x1038, cme:0x1040) - PushInlineFrame v48 (0x1068), v17 - v66:CShape = LoadField v48, :shape_id@0x1070 - v67:CShape[0x1071] = GuardBitEquals v66, CShape(0x1071) recompile - StoreField v48, :@x@0x1072, v17 - WriteBarrier v48, v17 - v70:CShape[0x1073] = Const CShape(0x1073) - StoreField v48, :shape_id@0x1070, v70 + PushInlineFrame v47 (0x1068), v17 + v65:CShape = LoadField v47, :shape_id@0x1070 + v66:CShape[0x1071] = GuardBitEquals v65, CShape(0x1071) recompile + StoreField v47, :@x@0x1072, v17 + WriteBarrier v47, v17 + v69:CShape[0x1073] = Const CShape(0x1073) + StoreField v47, :shape_id@0x1070, v69 CheckInterrupts PopInlineFrame - Return v48 + Return v47 "); } @@ -5121,12 +5112,11 @@ mod hir_opt_tests { v12:ClassSubclass[Object@0x1008] = Const Value(VALUE(0x1008)) v14:NilClass = Const Value(nil) PatchPoint MethodRedefined(Object@0x1008, new@0x1009, cme:0x1010) - v45:ObjectExact = ObjectAllocClass Object:VALUE(0x1008) + v44:ObjectExact = ObjectAllocClass Object:VALUE(0x1008) PatchPoint NoSingletonClass(Object@0x1008) PatchPoint MethodRedefined(Object@0x1008, initialize@0x1038, cme:0x1040) - v50:NilClass = Const Value(nil) CheckInterrupts - Return v45 + Return v44 "); } @@ -5152,12 +5142,11 @@ mod hir_opt_tests { v12:ClassSubclass[BasicObject@0x1008] = Const Value(VALUE(0x1008)) v14:NilClass = Const Value(nil) PatchPoint MethodRedefined(BasicObject@0x1008, new@0x1009, cme:0x1010) - v45:BasicObjectExact = ObjectAllocClass BasicObject:VALUE(0x1008) + v44:BasicObjectExact = ObjectAllocClass BasicObject:VALUE(0x1008) PatchPoint NoSingletonClass(BasicObject@0x1008) PatchPoint MethodRedefined(BasicObject@0x1008, initialize@0x1038, cme:0x1040) - v50:NilClass = Const Value(nil) CheckInterrupts - Return v45 + Return v44 "); } @@ -5183,29 +5172,29 @@ mod hir_opt_tests { v12:ClassSubclass[Hash@0x1008] = Const Value(VALUE(0x1008)) v14:NilClass = Const Value(nil) PatchPoint MethodRedefined(Hash@0x1008, new@0x1009, cme:0x1010) - v45:HashExact = ObjectAllocClass Hash:VALUE(0x1008) - v46:Fixnum[0] = Const Value(0) + v44:HashExact = ObjectAllocClass Hash:VALUE(0x1008) + v45:Fixnum[0] = Const Value(0) PatchPoint NoSingletonClass(Hash@0x1008) PatchPoint MethodRedefined(Hash@0x1008, initialize@0x1038, cme:0x1040) - v98:Fixnum[0] = Const Value(0) - v99:NilClass = Const Value(nil) - PushInlineFrame v45 (0x1068), v46 - v65:TrueClass = Const Value(true) - v83:CPtr = GetEP 0 - v84:CUInt64 = LoadField v83, :VM_ENV_DATA_INDEX_FLAGS@0x1070 - v85:CBool = IsBlockParamModified v84 - CondBranch v85, bb11(), bb12() + v97:Fixnum[0] = Const Value(0) + v98:NilClass = Const Value(nil) + PushInlineFrame v44 (0x1068), v45 + v64:TrueClass = Const Value(true) + v82:CPtr = GetEP 0 + v83:CUInt64 = LoadField v82, :VM_ENV_DATA_INDEX_FLAGS@0x1070 + v84:CBool = IsBlockParamModified v83 + CondBranch v84, bb11(), bb12() bb11(): - v87:BasicObject = LoadField v83, :block@0x1071 - Jump bb13(v87) + v86:BasicObject = LoadField v82, :block@0x1071 + Jump bb13(v86) bb12(): - v89:BasicObject = GetBlockParam :block, l0, EP@4 - Jump bb13(v89) - bb13(v82:BasicObject): - v92:BasicObject = InvokeBuiltin rb_hash_init, v45, v46, v65, v65, v82 + v88:BasicObject = GetBlockParam :block, l0, EP@4 + Jump bb13(v88) + bb13(v81:BasicObject): + v91:BasicObject = InvokeBuiltin rb_hash_init, v44, v45, v64, v64, v81 CheckInterrupts PopInlineFrame - Return v45 + Return v44 "); assert_snapshot!(inspect("test"), @"{}"); } @@ -5234,9 +5223,9 @@ mod hir_opt_tests { v17:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Array@0x1008, new@0x1009, cme:0x1010) PatchPoint MethodRedefined(Class@0x1038, new@0x1009, cme:0x1010) - v56:BasicObject = CCallVariadic v12, :Array.new@0x1040, v17 + v55:BasicObject = CCallVariadic v12, :Array.new@0x1040, v17 CheckInterrupts - Return v56 + Return v55 "); } @@ -5265,10 +5254,10 @@ mod hir_opt_tests { v19:HeapBasicObject = ObjectAlloc v12 PatchPoint NoSingletonClass(Set@0x1008) PatchPoint MethodRedefined(Set@0x1008, initialize@0x1038, cme:0x1040) - v48:SetExact = GuardType v19, SetExact recompile - v49:BasicObject = CCallVariadic v48, :Set#initialize@0x1068 + v47:SetExact = GuardType v19, SetExact recompile + v48:BasicObject = CCallVariadic v47, :Set#initialize@0x1068 CheckInterrupts - Return v48 + Return v47 "); } @@ -5295,9 +5284,9 @@ mod hir_opt_tests { v14:NilClass = Const Value(nil) PatchPoint MethodRedefined(String@0x1008, new@0x1009, cme:0x1010) PatchPoint MethodRedefined(Class@0x1038, new@0x1009, cme:0x1010) - v53:BasicObject = CCallVariadic v12, :String.new@0x1040 + v52:BasicObject = CCallVariadic v12, :String.new@0x1040 CheckInterrupts - Return v53 + Return v52 "); } @@ -5325,12 +5314,12 @@ mod hir_opt_tests { v17:StringExact[VALUE(0x1010)] = Const Value(VALUE(0x1010)) v18:StringExact = StringCopy v17 PatchPoint MethodRedefined(Regexp@0x1008, new@0x1018, cme:0x1020) - v49:RegexpExact = ObjectAllocClass Regexp:VALUE(0x1008) + v48:RegexpExact = ObjectAllocClass Regexp:VALUE(0x1008) PatchPoint NoSingletonClass(Regexp@0x1008) PatchPoint MethodRedefined(Regexp@0x1008, initialize@0x1048, cme:0x1050) - v54:BasicObject = CCallVariadic v49, :Regexp#initialize@0x1078, v18 + v53:BasicObject = CCallVariadic v48, :Regexp#initialize@0x1078, v18 CheckInterrupts - Return v49 + Return v48 "); } @@ -5594,33 +5583,32 @@ mod hir_opt_tests { v9:BasicObject = LoadArg :block@2 Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): - CheckInterrupts - v19:CBool = Test v12 - v20:Falsy = RefineType v12, Falsy - CondBranch v19, bb5(), bb4(v11, v20, v13) + v18:CBool = Test v12 + v19:Falsy = RefineType v12, Falsy + CondBranch v18, bb5(), bb4(v11, v19, v13) bb5(): - v22:Truthy = RefineType v12, Truthy - v25:Fixnum[0] = Const Value(0) - v29:CPtr = GetEP 0 - v30:CUInt64 = LoadField v29, :VM_ENV_DATA_INDEX_FLAGS@0x1002 - v31:CBool = IsBlockParamModified v30 - CondBranch v31, bb6(), bb7() + v21:Truthy = RefineType v12, Truthy + v24:Fixnum[0] = Const Value(0) + v28:CPtr = GetEP 0 + v29:CUInt64 = LoadField v28, :VM_ENV_DATA_INDEX_FLAGS@0x1002 + v30:CBool = IsBlockParamModified v29 + CondBranch v30, bb6(), bb7() bb6(): - v33:BasicObject = LoadField v29, :block@0x1003 - Jump bb8(v33, v33) + v32:BasicObject = LoadField v28, :block@0x1003 + Jump bb8(v32, v32) bb7(): - v35:CInt64 = LoadField v29, :VM_ENV_DATA_INDEX_SPECVAL@0x1004 - v36:CInt64[0] = GuardBitEquals v35, CInt64(0) recompile - v37:NilClass = Const Value(nil) - Jump bb8(v37, v13) - bb8(v27:BasicObject, v28:BasicObject): - v40:BasicObject = Send v25, &block, :then, v27 # SendFallbackReason: Send: block argument is not nil + v34:CInt64 = LoadField v28, :VM_ENV_DATA_INDEX_SPECVAL@0x1004 + v35:CInt64[0] = GuardBitEquals v34, CInt64(0) recompile + v36:NilClass = Const Value(nil) + Jump bb8(v36, v13) + bb8(v26:BasicObject, v27:BasicObject): + v39:BasicObject = Send v24, &block, :then, v26 # SendFallbackReason: Send: block argument is not nil CheckInterrupts - Return v40 - bb4(v45:BasicObject, v46:Falsy, v47:BasicObject): - v51:StaticSymbol[:skip] = Const Value(VALUE(0x1008)) + Return v39 + bb4(v44:BasicObject, v45:Falsy, v46:BasicObject): + v50:StaticSymbol[:skip] = Const Value(VALUE(0x1008)) CheckInterrupts - Return v51 + Return v50 "); } @@ -7569,10 +7557,9 @@ mod hir_opt_tests { v6:NilClass = Const Value(nil) Jump bb3(v5, v6) bb3(v8:BasicObject, v9:NilClass): - v13:NilClass = Const Value(nil) + v20:NilClass = Const Value(nil) CheckInterrupts - v21:NilClass = Const Value(nil) - Return v21 + Return v20 "); } @@ -7599,8 +7586,8 @@ mod hir_opt_tests { Jump bb3(v5, v6) bb3(v8:BasicObject, v9:NilClass): v13:Fixnum[1] = Const Value(1) - CheckInterrupts PatchPoint MethodRedefined(Integer@0x1000, itself@0x1008, cme:0x1010) + CheckInterrupts Return v13 "); } @@ -8430,38 +8417,36 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :a@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - CondBranch v16, bb6(), bb4(v9, v17) + v15:CBool = Test v10 + v16:Falsy = RefineType v10, Falsy + CondBranch v15, bb6(), bb4(v9, v16) bb6(): - v19:Truthy = RefineType v10, Truthy - v21:FalseClass = Const Value(false) - CheckInterrupts - Jump bb5(v9, v19, v21) - bb4(v25:BasicObject, v26:Falsy): - v29:NilClass = Const Value(nil) - Jump bb5(v25, v26, v29) - bb5(v31:BasicObject, v32:BasicObject, v33:Falsy): - v38:CBool = HasType v33, FalseClass - CondBranch v38, bb8(), bb9() + v18:Truthy = RefineType v10, Truthy + v20:FalseClass = Const Value(false) + Jump bb5(v9, v18, v20) + bb4(v23:BasicObject, v24:Falsy): + v27:NilClass = Const Value(nil) + Jump bb5(v23, v24, v27) + bb5(v29:BasicObject, v30:BasicObject, v31:Falsy): + v36:CBool = HasType v31, FalseClass + CondBranch v36, bb8(), bb9() bb8(): PatchPoint MethodRedefined(FalseClass@0x1008, !@0x1010, cme:0x1018) - v59:TrueClass = Const Value(true) - Jump bb7(v59) + v57:TrueClass = Const Value(true) + Jump bb7(v57) bb9(): - v44:CBool = HasType v33, NilClass - CondBranch v44, bb10(), bb11() + v42:CBool = HasType v31, NilClass + CondBranch v42, bb10(), bb11() bb10(): PatchPoint MethodRedefined(NilClass@0x1040, !@0x1010, cme:0x1018) - v62:TrueClass = Const Value(true) - Jump bb7(v62) + v60:TrueClass = Const Value(true) + Jump bb7(v60) bb11(): - v50:BasicObject = Send v33, :! # SendFallbackReason: SendWithoutBlock: polymorphic fallback - Jump bb7(v50) - bb7(v37:BasicObject): + v48:BasicObject = Send v31, :! # SendFallbackReason: SendWithoutBlock: polymorphic fallback + Jump bb7(v48) + bb7(v35:BasicObject): CheckInterrupts - Return v37 + Return v35 "); } @@ -10078,7 +10063,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v72:NilClass = Const Value(nil) + v71:NilClass = Const Value(nil) PushInlineFrame v18 (0x1038) v28:CPtr = GetEP 0 v29:CUInt64 = LoadField v28, :VM_ENV_DATA_INDEX_FLAGS@0x1040 @@ -10091,36 +10076,35 @@ mod hir_opt_tests { v34:CInt64 = LoadField v28, :VM_ENV_DATA_INDEX_SPECVAL@0x1042 v35:CInt64[0] = GuardBitEquals v34, CInt64(0) recompile v36:NilClass = Const Value(nil) - Jump bb9(v36, v72) + Jump bb9(v36, v71) bb9(v26:BasicObject, v27:BasicObject): - CheckInterrupts - v40:CBool = Test v26 - CondBranch v40, bb10(), bb6(v18, v27) + v39:CBool = Test v26 + CondBranch v39, bb10(), bb6(v18, v27) bb10(): - v47:CPtr = GetEP 0 - v48:CUInt64 = LoadField v47, :VM_ENV_DATA_INDEX_FLAGS@0x1040 - v49:CBool = IsBlockParamModified v48 - CondBranch v49, bb11(), bb12() + v46:CPtr = GetEP 0 + v47:CUInt64 = LoadField v46, :VM_ENV_DATA_INDEX_FLAGS@0x1040 + v48:CBool = IsBlockParamModified v47 + CondBranch v48, bb11(), bb12() bb11(): - v51:BasicObject = LoadField v47, :blk@0x1041 - Jump bb13(v51, v51) + v50:BasicObject = LoadField v46, :blk@0x1041 + Jump bb13(v50, v50) bb12(): - v53:CInt64 = LoadField v47, :VM_ENV_DATA_INDEX_SPECVAL@0x1042 - v54:CInt64 = GuardAnyBitSet v53, CUInt64(1) recompile - v55:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1048)) - Jump bb13(v55, v27) - bb13(v45:BasicObject, v46:BasicObject): - v58:BasicObject = Send v45, :call # SendFallbackReason: SendWithoutBlock: no profile data available - CheckInterrupts - Jump bb4(v58) - bb6(v63:ObjectSubclass[class_exact*:Object@VALUE(0x1000)], v64:BasicObject): - v67:Fixnum[42] = Const Value(42) - CheckInterrupts - Jump bb4(v67) - bb4(v73:BasicObject): + v52:CInt64 = LoadField v46, :VM_ENV_DATA_INDEX_SPECVAL@0x1042 + v53:CInt64 = GuardAnyBitSet v52, CUInt64(1) recompile + v54:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1048)) + Jump bb13(v54, v27) + bb13(v44:BasicObject, v45:BasicObject): + v57:BasicObject = Send v44, :call # SendFallbackReason: SendWithoutBlock: no profile data available + CheckInterrupts + Jump bb4(v57) + bb6(v62:ObjectSubclass[class_exact*:Object@VALUE(0x1000)], v63:BasicObject): + v66:Fixnum[42] = Const Value(42) + CheckInterrupts + Jump bb4(v66) + bb4(v72:BasicObject): PopInlineFrame CheckInterrupts - Return v73 + Return v72 "); } @@ -10149,12 +10133,12 @@ mod hir_opt_tests { PatchPoint SingleRactorMode PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile - v49:NilClass = Const Value(nil) + v48:NilClass = Const Value(nil) PushInlineFrame v19 (0x1038) + v43:Fixnum[42] = Const Value(42) CheckInterrupts - v44:Fixnum[42] = Const Value(42) PopInlineFrame - Return v44 + Return v43 "); } @@ -15064,16 +15048,15 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint NoSingletonClass(C@0x1000) PatchPoint MethodRedefined(C@0x1000, class@0x1008, cme:0x1010) - v43:ObjectSubclass[class_exact:C] = GuardType v6, ObjectSubclass[class_exact:C] recompile - v44:ClassSubclass[C@0x1000] = Const Value(VALUE(0x1000)) + v42:ObjectSubclass[class_exact:C] = GuardType v6, ObjectSubclass[class_exact:C] recompile + v43:ClassSubclass[C@0x1000] = Const Value(VALUE(0x1000)) v13:StaticSymbol[:_lex_actions] = Const Value(VALUE(0x1038)) v15:TrueClass = Const Value(true) PatchPoint MethodRedefined(Class@0x1040, respond_to?@0x1048, cme:0x1050) PatchPoint MethodRedefined(Class@0x1040, _lex_actions@0x1078, cme:0x1080) - v50:TrueClass = Const Value(true) + v25:StaticSymbol[:CORRECT] = Const Value(VALUE(0x10a8)) CheckInterrupts - v26:StaticSymbol[:CORRECT] = Const Value(VALUE(0x10a8)) - Return v26 + Return v25 "); } @@ -15263,36 +15246,35 @@ mod hir_opt_tests { StoreField v11, :formatted@0x1004, v17 Jump bb3(v9, v10, v13, v15, v17) bb3(v20:BasicObject, v21:BasicObject, v22:BasicObject, v23:BasicObject, v24:NilClass): - CheckInterrupts SetLocal :formatted, l0, EP@3, v21 PatchPoint SingleRactorMode - v48:HeapBasicObject = GuardType v20, HeapBasicObject - v49:CShape = LoadField v48, :shape_id@0x1005 - v50:CShape[0x1006] = Const CShape(0x1006) - v51:CBool = IsBitEqual v49, v50 - CondBranch v51, bb7(), bb8() + v47:HeapBasicObject = GuardType v20, HeapBasicObject + v48:CShape = LoadField v47, :shape_id@0x1005 + v49:CShape[0x1006] = Const CShape(0x1006) + v50:CBool = IsBitEqual v48, v49 + CondBranch v50, bb7(), bb8() bb7(): - StoreField v48, :@formatted@0x1007, v21 - WriteBarrier v48, v21 + StoreField v47, :@formatted@0x1007, v21 + WriteBarrier v47, v21 Jump bb6() bb8(): - v56:CShape[0x1008] = GuardBitEquals v49, CShape(0x1008) recompile - StoreField v48, :@formatted@0x1007, v21 - WriteBarrier v48, v21 - v60:CShape[0x1006] = Const CShape(0x1006) - StoreField v48, :shape_id@0x1005, v60 + v55:CShape[0x1008] = GuardBitEquals v48, CShape(0x1008) recompile + StoreField v47, :@formatted@0x1007, v21 + WriteBarrier v47, v21 + v59:CShape[0x1006] = Const CShape(0x1006) + StoreField v47, :shape_id@0x1005, v59 Jump bb6() bb6(): - v66:ClassSubclass[VMFrozenCore] = Const Value(VALUE(0x1010)) + v65:ClassSubclass[VMFrozenCore] = Const Value(VALUE(0x1010)) PatchPoint MethodRedefined(Class@0x1018, lambda@0x1020, cme:0x1028) - v82:BasicObject = CCallWithFrame v66, :RubyVM::FrozenCore.lambda@0x1050, block=0x1058 - v69:CPtr = GetEP 0 - v70:BasicObject = LoadField v69, :a@0x1001 - v71:BasicObject = LoadField v69, :_b@0x1002 - v72:BasicObject = LoadField v69, :_c@0x1003 - v73:BasicObject = LoadField v69, :formatted@0x1004 + v81:BasicObject = CCallWithFrame v65, :RubyVM::FrozenCore.lambda@0x1050, block=0x1058 + v68:CPtr = GetEP 0 + v69:BasicObject = LoadField v68, :a@0x1001 + v70:BasicObject = LoadField v68, :_b@0x1002 + v71:BasicObject = LoadField v68, :_c@0x1003 + v72:BasicObject = LoadField v68, :formatted@0x1004 CheckInterrupts - Return v82 + Return v81 "); } @@ -16674,19 +16656,18 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - CondBranch v16, bb7(), bb6(v9, v17) + v15:CBool = Test v10 + v16:Falsy = RefineType v10, Falsy + CondBranch v15, bb7(), bb6(v9, v16) bb7(): - v19:Truthy = RefineType v10, Truthy + v18:Truthy = RefineType v10, Truthy + v35:Fixnum[3] = Const Value(3) CheckInterrupts - v38:Fixnum[3] = Const Value(3) - Return v38 - bb6(v43:BasicObject, v44:Falsy): - v48:Fixnum[6] = Const Value(6) + Return v35 + bb6(v40:BasicObject, v41:Falsy): + v45:Fixnum[6] = Const Value(6) CheckInterrupts - Return v48 + Return v45 "); } @@ -17168,38 +17149,37 @@ mod hir_opt_tests { StoreField v42, :kwsplat@0x1006, v48 Jump bb7(v40, v41, v44, v46, v48) bb7(v73:BasicObject, v74:BasicObject, v75:BasicObject, v76:BasicObject, v77:NilClass): - CheckInterrupts - v83:CBool = Test v75 - v84:Truthy = RefineType v75, Truthy - CondBranch v83, bb8(v73, v74, v84, v76, v77), bb11() + v82:CBool = Test v75 + v83:Truthy = RefineType v75, Truthy + CondBranch v82, bb8(v73, v74, v83, v76, v77), bb11() bb11(): - v86:Falsy = RefineType v75, Falsy + v85:Falsy = RefineType v75, Falsy PatchPoint MethodRedefined(Object@0x1010, lambda@0x1018, cme:0x1020) - v132:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v73, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile - v133:BasicObject = CCallWithFrame v132, :Kernel#lambda@0x1048, block=0x1050 - v90:CPtr = GetEP 0 - v91:BasicObject = LoadField v90, :list@0x1001 - v93:BasicObject = LoadField v90, :iter_method@0x1005 - v94:BasicObject = LoadField v90, :kwsplat@0x1006 - SetLocal :sep, l0, EP@5, v133 - Jump bb8(v132, v91, v133, v93, v94) - bb8(v98:BasicObject, v99:BasicObject, v100:BasicObject, v101:BasicObject, v102:BasicObject): + v131:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v73, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile + v132:BasicObject = CCallWithFrame v131, :Kernel#lambda@0x1048, block=0x1050 + v89:CPtr = GetEP 0 + v90:BasicObject = LoadField v89, :list@0x1001 + v92:BasicObject = LoadField v89, :iter_method@0x1005 + v93:BasicObject = LoadField v89, :kwsplat@0x1006 + SetLocal :sep, l0, EP@5, v132 + Jump bb8(v131, v90, v132, v92, v93) + bb8(v97:BasicObject, v98:BasicObject, v99:BasicObject, v100:BasicObject, v101:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1058, CONST) - v108:HashExact[VALUE(0x1060)] = Const Value(VALUE(0x1060)) - SetLocal :kwsplat, l0, EP@3, v108 - v113:CPtr = GetEP 0 - v114:BasicObject = LoadField v113, :list@0x1001 - v116:CPtr = GetEP 0 - v117:BasicObject = LoadField v116, :iter_method@0x1005 - v119:BasicObject = Send v114, 0x1068, :__send__, v117 # SendFallbackReason: Send: unsupported method type Optimized - v120:CPtr = GetEP 0 - v121:BasicObject = LoadField v120, :list@0x1001 - v122:BasicObject = LoadField v120, :sep@0x1002 - v123:BasicObject = LoadField v120, :iter_method@0x1005 - v124:BasicObject = LoadField v120, :kwsplat@0x1006 + v107:HashExact[VALUE(0x1060)] = Const Value(VALUE(0x1060)) + SetLocal :kwsplat, l0, EP@3, v107 + v112:CPtr = GetEP 0 + v113:BasicObject = LoadField v112, :list@0x1001 + v115:CPtr = GetEP 0 + v116:BasicObject = LoadField v115, :iter_method@0x1005 + v118:BasicObject = Send v113, 0x1068, :__send__, v116 # SendFallbackReason: Send: unsupported method type Optimized + v119:CPtr = GetEP 0 + v120:BasicObject = LoadField v119, :list@0x1001 + v121:BasicObject = LoadField v119, :sep@0x1002 + v122:BasicObject = LoadField v119, :iter_method@0x1005 + v123:BasicObject = LoadField v119, :kwsplat@0x1006 CheckInterrupts - Return v119 + Return v118 "); } @@ -17591,26 +17571,25 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :flag@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - CondBranch v16, bb5(), bb4(v9, v17) + v15:CBool = Test v10 + v16:Falsy = RefineType v10, Falsy + CondBranch v15, bb5(), bb4(v9, v16) bb5(): - v19:Truthy = RefineType v10, Truthy - v23:Fixnum[42] = Const Value(42) + v18:Truthy = RefineType v10, Truthy + v22:Fixnum[42] = Const Value(42) PatchPoint MethodRedefined(Object@0x1008, greet_recompile@0x1010, cme:0x1018) - v43:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v9, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile - PushInlineFrame v43 (0x1040), v23 + v42:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v9, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile + PushInlineFrame v42 (0x1040), v22 PatchPoint MethodRedefined(Integer@0x1048, to_s@0x1050, cme:0x1058) - v64:StringExact = CCallVariadic v23, :Integer#to_s@0x1080 + v63:StringExact = CCallVariadic v22, :Integer#to_s@0x1080 CheckInterrupts PopInlineFrame - Return v64 - bb4(v30:BasicObject, v31:Falsy): - v35:StringExact[VALUE(0x1088)] = Const Value(VALUE(0x1088)) - v36:StringExact = StringCopy v35 + Return v63 + bb4(v29:BasicObject, v30:Falsy): + v34:StringExact[VALUE(0x1088)] = Const Value(VALUE(0x1088)) + v35:StringExact = StringCopy v34 CheckInterrupts - Return v36 + Return v35 "); } @@ -17664,21 +17643,20 @@ mod hir_opt_tests { v9:BasicObject = LoadArg :block@2 Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): - CheckInterrupts - v19:CBool = Test v12 - v20:Falsy = RefineType v12, Falsy - CondBranch v19, bb5(), bb4(v11, v20, v13) + v18:CBool = Test v12 + v19:Falsy = RefineType v12, Falsy + CondBranch v18, bb5(), bb4(v11, v19, v13) bb5(): - v22:Truthy = RefineType v12, Truthy - v26:Fixnum[42] = Const Value(42) - v29:BasicObject = Send v11, &block, :passthrough_recompile_blockarg, v26, v13 # SendFallbackReason: Send: block argument is not nil + v21:Truthy = RefineType v12, Truthy + v25:Fixnum[42] = Const Value(42) + v28:BasicObject = Send v11, &block, :passthrough_recompile_blockarg, v25, v13 # SendFallbackReason: Send: block argument is not nil CheckInterrupts - Return v29 - bb4(v34:BasicObject, v35:Falsy, v36:BasicObject): - v40:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v41:StringExact = StringCopy v40 + Return v28 + bb4(v33:BasicObject, v34:Falsy, v35:BasicObject): + v39:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v40:StringExact = StringCopy v39 CheckInterrupts - Return v41 + Return v40 "); } @@ -17724,21 +17702,20 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :flag@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - CondBranch v16, bb5(), bb4(v9, v17) + v15:CBool = Test v10 + v16:Falsy = RefineType v10, Falsy + CondBranch v15, bb5(), bb4(v9, v16) bb5(): - v19:Truthy = RefineType v10, Truthy - v23:Fixnum[42] = Const Value(42) - v25:BasicObject = Send v9, :greet_final, v23 # SendFallbackReason: SendWithoutBlock: no profile data available + v18:Truthy = RefineType v10, Truthy + v22:Fixnum[42] = Const Value(42) + v24:BasicObject = Send v9, :greet_final, v22 # SendFallbackReason: SendWithoutBlock: no profile data available CheckInterrupts - Return v25 - bb4(v30:BasicObject, v31:Falsy): - v35:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v36:StringExact = StringCopy v35 + Return v24 + bb4(v29:BasicObject, v30:Falsy): + v34:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v35:StringExact = StringCopy v34 CheckInterrupts - Return v36 + Return v35 "); } @@ -17933,52 +17910,50 @@ mod hir_opt_tests { Jump bb3(v5, v6) bb3(v8:HeapBasicObject, v9:NilClass): v13:Fixnum[0] = Const Value(0) - CheckInterrupts Jump bb6(v8, v13) - bb6(v19:HeapBasicObject, v20:Fixnum): - v24:Fixnum[10] = Const Value(10) + bb6(v18:HeapBasicObject, v19:Fixnum): + v23:Fixnum[10] = Const Value(10) PatchPoint MethodRedefined(Integer@0x1000, <@0x1008, cme:0x1010) - v96:BoolExact = FixnumLt v20, v24 + v94:BoolExact = FixnumLt v19, v23 CheckInterrupts - v30:CBool = Test v96 - CondBranch v30, bb4(v19, v20), bb7() - bb4(v40:HeapBasicObject, v41:Fixnum): + v29:CBool = Test v94 + CondBranch v29, bb4(v18, v19), bb7() + bb4(v39:HeapBasicObject, v40:Fixnum): PatchPoint SingleRactorMode - v47:CShape = LoadField v40, :shape_id@0x1038 - v49:CShape[0x1039] = Const CShape(0x1039) - v50:CBool = IsBitEqual v47, v49 - CondBranch v50, bb9(), bb10() + v46:CShape = LoadField v39, :shape_id@0x1038 + v48:CShape[0x1039] = Const CShape(0x1039) + v49:CBool = IsBitEqual v46, v48 + CondBranch v49, bb9(), bb10() bb9(): - v52:BasicObject = LoadField v40, :@levar@0x103a - Jump bb8(v52) + v51:BasicObject = LoadField v39, :@levar@0x103a + Jump bb8(v51) bb10(): - v54:CShape[0x103b] = GuardBitEquals v47, CShape(0x103b) recompile - v56:NilClass = Const Value(nil) - Jump bb8(v56) - bb8(v48:BasicObject): - CheckInterrupts - v60:CBool = Test v48 - CondBranch v60, bb5(v40, v41), bb12() + v53:CShape[0x103b] = GuardBitEquals v46, CShape(0x103b) recompile + v55:NilClass = Const Value(nil) + Jump bb8(v55) + bb8(v47:BasicObject): + v58:CBool = Test v47 + CondBranch v58, bb5(v39, v40), bb12() bb12(): PatchPoint NoEPEscape(set_value_loop) PatchPoint SingleRactorMode - v70:CShape = LoadField v40, :shape_id@0x1038 - v71:CShape[0x103b] = GuardBitEquals v70, CShape(0x103b) recompile - StoreField v40, :@levar@0x103a, v41 - WriteBarrier v40, v41 - v74:CShape[0x1039] = Const CShape(0x1039) - StoreField v40, :shape_id@0x1038, v74 - Jump bb5(v40, v41) - bb5(v78:HeapBasicObject, v79:Fixnum): + v68:CShape = LoadField v39, :shape_id@0x1038 + v69:CShape[0x103b] = GuardBitEquals v68, CShape(0x103b) recompile + StoreField v39, :@levar@0x103a, v40 + WriteBarrier v39, v40 + v72:CShape[0x1039] = Const CShape(0x1039) + StoreField v39, :shape_id@0x1038, v72 + Jump bb5(v39, v40) + bb5(v76:HeapBasicObject, v77:Fixnum): PatchPoint NoEPEscape(set_value_loop) - v86:Fixnum[1] = Const Value(1) + v84:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1000, +@0x103c, cme:0x1040) - v100:Fixnum = FixnumAdd v79, v86 - Jump bb6(v78, v100) + v98:Fixnum = FixnumAdd v77, v84 + Jump bb6(v76, v98) bb7(): - v35:NilClass = Const Value(nil) + v34:NilClass = Const Value(nil) CheckInterrupts - Return v35 + Return v34 "); } @@ -18681,19 +18656,17 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Symbol@0x1008, ==@0x1010, cme:0x1018) - v48:StaticSymbol = GuardType v12, StaticSymbol recompile - v49:CBool = IsBitEqual v48, v13 - v50:BoolExact = BoxBool v49 - CheckInterrupts - CondBranch v49, bb5(), bb4(v11, v48, v13) + v47:StaticSymbol = GuardType v12, StaticSymbol recompile + v48:CBool = IsBitEqual v47, v13 + CondBranch v48, bb5(), bb4(v11, v47, v13) bb5(): - v29:Fixnum[3] = Const Value(3) + v28:Fixnum[3] = Const Value(3) CheckInterrupts - Return v29 - bb4(v34:BasicObject, v35:StaticSymbol, v36:BasicObject): - v40:Fixnum[4] = Const Value(4) + Return v28 + bb4(v33:BasicObject, v34:StaticSymbol, v35:BasicObject): + v39:Fixnum[4] = Const Value(4) CheckInterrupts - Return v40 + Return v39 "); } @@ -18961,22 +18934,21 @@ mod hir_opt_tests { PushInlineFrame v23 (0x1040), v10 v32:Fixnum[0] = Const Value(0) PatchPoint MethodRedefined(Integer@0x1048, <@0x1050, cme:0x1058) - v63:Fixnum = GuardType v10, Fixnum recompile - v64:BoolExact = FixnumLt v63, v32 - CheckInterrupts - v38:CBool = Test v64 - CondBranch v38, bb7(), bb6(v23, v63) + v62:Fixnum = GuardType v10, Fixnum recompile + v63:BoolExact = FixnumLt v62, v32 + v37:CBool = Test v63 + CondBranch v37, bb7(), bb6(v23, v62) bb7(): - v43:Fixnum[0] = Const Value(0) + v42:Fixnum[0] = Const Value(0) CheckInterrupts - Jump bb4(v43) - bb6(v48:ObjectSubclass[class_exact*:Object@VALUE(0x1008)], v49:Fixnum): + Jump bb4(v42) + bb6(v47:ObjectSubclass[class_exact*:Object@VALUE(0x1008)], v48:Fixnum): CheckInterrupts - Jump bb4(v49) - bb4(v57:Fixnum): + Jump bb4(v48) + bb4(v56:Fixnum): PopInlineFrame CheckInterrupts - Return v57 + Return v56 "); } @@ -19830,26 +19802,25 @@ mod hir_opt_tests { v22:NilClass = Const Value(nil) PatchPoint MethodRedefined(Object@0x1008, add_optkw_dyn@0x1010, cme:0x1018) v25:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v9, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile - v64:Fixnum[1] = Const Value(1) + v63:Fixnum[1] = Const Value(1) PushInlineFrame v25 (0x1040), v10, v22 - v34:BoolExact = FixnumBitCheck v64, 0 - CheckInterrupts - v37:CBool = Test v34 - CondBranch v37, bb6(v25, v10, v22, v64), bb7() + v34:BoolExact = FixnumBitCheck v63, 0 + v36:CBool = Test v34 + CondBranch v36, bb6(v25, v10, v22, v63), bb7() bb7(): - v43:Fixnum[2] = Const Value(2) + v42:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1048, *@0x1050, cme:0x1058) - v71:Fixnum = GuardType v10, Fixnum recompile - v72:Fixnum = FixnumMult v71, v43 - Jump bb6(v25, v71, v72, v64) - bb6(v49:ObjectSubclass[class_exact*:Object@VALUE(0x1008)], v50:BasicObject, v51:NilClass|Fixnum, v52:Fixnum[1]): + v70:Fixnum = GuardType v10, Fixnum recompile + v71:Fixnum = FixnumMult v70, v42 + Jump bb6(v25, v70, v71, v63) + bb6(v48:ObjectSubclass[class_exact*:Object@VALUE(0x1008)], v49:BasicObject, v50:NilClass|Fixnum, v51:Fixnum[1]): PatchPoint MethodRedefined(Integer@0x1048, +@0x1080, cme:0x1088) - v75:Fixnum = GuardType v50, Fixnum recompile - v76:Fixnum = GuardType v51, Fixnum - v77:Fixnum = FixnumAdd v75, v76 + v74:Fixnum = GuardType v49, Fixnum recompile + v75:Fixnum = GuardType v50, Fixnum + v76:Fixnum = FixnumAdd v74, v75 CheckInterrupts PopInlineFrame - Return v77 + Return v76 "); } @@ -20148,84 +20119,84 @@ mod hir_opt_tests { v17:Fixnum[1] = Const Value(1) v19:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Point@0x1008, new@0x1009, cme:0x1010) - v91:ObjectSubclass[class_exact:Point] = ObjectAllocClass Point:VALUE(0x1008) + v89:ObjectSubclass[class_exact:Point] = ObjectAllocClass Point:VALUE(0x1008) PatchPoint NoSingletonClass(Point@0x1008) PatchPoint MethodRedefined(Point@0x1008, initialize@0x1038, cme:0x1040) - PushInlineFrame v91 (0x1068), v17, v19 - v123:CShape = LoadField v91, :shape_id@0x1070 - v124:CShape[0x1071] = GuardBitEquals v123, CShape(0x1071) recompile - StoreField v91, :@x@0x1072, v17 - WriteBarrier v91, v17 - v127:CShape[0x1073] = Const CShape(0x1073) - StoreField v91, :shape_id@0x1070, v127 + PushInlineFrame v89 (0x1068), v17, v19 + v121:CShape = LoadField v89, :shape_id@0x1070 + v122:CShape[0x1071] = GuardBitEquals v121, CShape(0x1071) recompile + StoreField v89, :@x@0x1072, v17 + WriteBarrier v89, v17 + v125:CShape[0x1073] = Const CShape(0x1073) + StoreField v89, :shape_id@0x1070, v125 PatchPoint NoEPEscape(initialize) PatchPoint SingleRactorMode - StoreField v91, :@y@0x1074, v19 - WriteBarrier v91, v19 - v142:CShape[0x1075] = Const CShape(0x1075) - StoreField v91, :shape_id@0x1070, v142 + StoreField v89, :@y@0x1074, v19 + WriteBarrier v89, v19 + v140:CShape[0x1075] = Const CShape(0x1075) + StoreField v89, :shape_id@0x1070, v140 CheckInterrupts PopInlineFrame PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1078, Point) - v47:ClassSubclass[Point@0x1008] = Const Value(VALUE(0x1008)) - v49:NilClass = Const Value(nil) - v52:Fixnum[1] = Const Value(1) - v54:Fixnum[2] = Const Value(2) + v46:ClassSubclass[Point@0x1008] = Const Value(VALUE(0x1008)) + v48:NilClass = Const Value(nil) + v51:Fixnum[1] = Const Value(1) + v53:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Point@0x1008, new@0x1009, cme:0x1010) - v101:ObjectSubclass[class_exact:Point] = ObjectAllocClass Point:VALUE(0x1008) + v99:ObjectSubclass[class_exact:Point] = ObjectAllocClass Point:VALUE(0x1008) PatchPoint NoSingletonClass(Point@0x1008) PatchPoint MethodRedefined(Point@0x1008, initialize@0x1038, cme:0x1040) - PushInlineFrame v101 (0x1068), v52, v54 - v163:CShape = LoadField v101, :shape_id@0x1070 - v164:CShape[0x1071] = GuardBitEquals v163, CShape(0x1071) recompile - StoreField v101, :@x@0x1072, v52 - WriteBarrier v101, v52 - v167:CShape[0x1073] = Const CShape(0x1073) - StoreField v101, :shape_id@0x1070, v167 + PushInlineFrame v99 (0x1068), v51, v53 + v161:CShape = LoadField v99, :shape_id@0x1070 + v162:CShape[0x1071] = GuardBitEquals v161, CShape(0x1071) recompile + StoreField v99, :@x@0x1072, v51 + WriteBarrier v99, v51 + v165:CShape[0x1073] = Const CShape(0x1073) + StoreField v99, :shape_id@0x1070, v165 PatchPoint NoEPEscape(initialize) PatchPoint SingleRactorMode - StoreField v101, :@y@0x1074, v54 - WriteBarrier v101, v54 - v182:CShape[0x1075] = Const CShape(0x1075) - StoreField v101, :shape_id@0x1070, v182 + StoreField v99, :@y@0x1074, v53 + WriteBarrier v99, v53 + v180:CShape[0x1075] = Const CShape(0x1075) + StoreField v99, :shape_id@0x1070, v180 CheckInterrupts PopInlineFrame PatchPoint NoSingletonClass(Point@0x1008) PatchPoint MethodRedefined(Point@0x1008, ==@0x1080, cme:0x1088) - PushInlineFrame v91 (0x1068), v101 + PushInlineFrame v89 (0x1068), v99 PatchPoint SingleRactorMode - v201:CShape = LoadField v91, :shape_id@0x1070 - v202:CShape[0x1075] = GuardBitEquals v201, CShape(0x1075) recompile - v203:BasicObject = LoadField v91, :@x@0x1072 + v199:CShape = LoadField v89, :shape_id@0x1070 + v200:CShape[0x1075] = GuardBitEquals v199, CShape(0x1075) recompile + v201:BasicObject = LoadField v89, :@x@0x1072 PatchPoint NoEPEscape(==) PatchPoint MethodRedefined(Point@0x1008, x@0x10b0, cme:0x10b8) PatchPoint MethodRedefined(Integer@0x10e0, ==@0x1080, cme:0x10e8) - v258:Fixnum = GuardType v203, Fixnum recompile - v260:BoolExact = FixnumEq v258, v52 - v215:CBool = Test v260 - v216:FalseClass = RefineType v260, Falsy - CondBranch v215, bb19(), bb18(v91, v101, v216) + v255:Fixnum = GuardType v201, Fixnum recompile + v257:BoolExact = FixnumEq v255, v51 + v212:CBool = Test v257 + v213:FalseClass = RefineType v257, Falsy + CondBranch v212, bb19(), bb18(v89, v99, v213) bb19(): PatchPoint SingleRactorMode - v223:CShape = LoadField v91, :shape_id@0x1070 - v224:CShape[0x1075] = GuardBitEquals v223, CShape(0x1075) recompile - v225:BasicObject = LoadField v91, :@y@0x1074 + v220:CShape = LoadField v89, :shape_id@0x1070 + v221:CShape[0x1075] = GuardBitEquals v220, CShape(0x1075) recompile + v222:BasicObject = LoadField v89, :@y@0x1074 PatchPoint NoEPEscape(==) PatchPoint NoSingletonClass(Point@0x1008) PatchPoint MethodRedefined(Point@0x1008, y@0x1110, cme:0x1118) - v265:CShape = LoadField v101, :shape_id@0x1070 - v266:CShape[0x1075] = GuardBitEquals v265, CShape(0x1075) recompile - v267:BasicObject = LoadField v101, :@y@0x1074 + v262:CShape = LoadField v99, :shape_id@0x1070 + v263:CShape[0x1075] = GuardBitEquals v262, CShape(0x1075) recompile + v264:BasicObject = LoadField v99, :@y@0x1074 PatchPoint MethodRedefined(Integer@0x10e0, ==@0x1080, cme:0x10e8) - v270:Fixnum = GuardType v225, Fixnum recompile - v271:Fixnum = GuardType v267, Fixnum - v272:BoolExact = FixnumEq v270, v271 - Jump bb18(v91, v101, v272) - bb18(v235:ObjectSubclass[class_exact:Point], v236:ObjectSubclass[class_exact:Point], v237:BoolExact): + v267:Fixnum = GuardType v222, Fixnum recompile + v268:Fixnum = GuardType v264, Fixnum + v269:BoolExact = FixnumEq v267, v268 + Jump bb18(v89, v99, v269) + bb18(v232:ObjectSubclass[class_exact:Point], v233:ObjectSubclass[class_exact:Point], v234:BoolExact): CheckInterrupts PopInlineFrame - Return v237 + Return v234 "); } @@ -20299,18 +20270,17 @@ mod hir_opt_tests { v33:Fixnum[7] = Const Value(7) v35:BasicObject = Send v14, :seven, v21, v23, v25, v27, v29, v31, v33 # SendFallbackReason: Too many arguments for LIR PatchPoint NoEPEscape(test) - CheckInterrupts - v43:CBool = Test v15 - v44:Falsy = RefineType v15, Falsy - CondBranch v43, bb5(), bb4(v13, v14, v44, v35) + v42:CBool = Test v15 + v43:Falsy = RefineType v15, Falsy + CondBranch v42, bb5(), bb4(v13, v14, v43, v35) bb5(): - v46:Truthy = RefineType v15, Truthy + v45:Truthy = RefineType v15, Truthy CheckInterrupts Return v35 - bb4(v53:BasicObject, v54:BasicObject, v55:Falsy, v56:BasicObject): - v60:NilClass = Const Value(nil) + bb4(v52:BasicObject, v53:BasicObject, v54:Falsy, v55:BasicObject): + v59:NilClass = Const Value(nil) CheckInterrupts - Return v60 + Return v59 "); } } diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 574cf60b1a1051..d6557c05244f0a 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -405,19 +405,18 @@ pub(crate) mod hir_build_tests { PatchPoint StableConstantNames(0x1008, Integer) v20:ClassSubclass[Integer@0x1010] = Const Value(VALUE(0x1010)) v22:BasicObject = CheckMatch v10, v20, CASE + v24:CBool = Test v22 + v25:Truthy = RefineType v22, Truthy + CondBranch v24, bb4(v9, v10, v14, v10), bb5() + bb4(v37:BasicObject, v38:BasicObject, v39:NilClass, v40:BasicObject): + v45:Fixnum[1] = Const Value(1) CheckInterrupts - v25:CBool = Test v22 - v26:Truthy = RefineType v22, Truthy - CondBranch v25, bb4(v9, v10, v14, v10), bb5() - bb4(v38:BasicObject, v39:BasicObject, v40:NilClass, v41:BasicObject): - v46:Fixnum[1] = Const Value(1) - CheckInterrupts - Return v46 + Return v45 bb5(): - v28:Falsy = RefineType v22, Falsy - v33:Fixnum[2] = Const Value(2) + v27:Falsy = RefineType v22, Falsy + v32:Fixnum[2] = Const Value(2) CheckInterrupts - Return v33 + Return v32 "); } @@ -452,19 +451,18 @@ pub(crate) mod hir_build_tests { v16:ArrayExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v17:ArrayExact = ArrayDup v16 v19:BasicObject = CheckMatch v10, v17, CASE|ARRAY + v21:CBool = Test v19 + v22:Truthy = RefineType v19, Truthy + CondBranch v21, bb4(v9, v10, v10), bb5() + bb4(v33:BasicObject, v34:BasicObject, v35:BasicObject): + v40:Fixnum[1] = Const Value(1) CheckInterrupts - v22:CBool = Test v19 - v23:Truthy = RefineType v19, Truthy - CondBranch v22, bb4(v9, v10, v10), bb5() - bb4(v34:BasicObject, v35:BasicObject, v36:BasicObject): - v41:Fixnum[1] = Const Value(1) - CheckInterrupts - Return v41 + Return v40 bb5(): - v25:Falsy = RefineType v19, Falsy - v29:Fixnum[2] = Const Value(2) + v24:Falsy = RefineType v19, Falsy + v28:Fixnum[2] = Const Value(2) CheckInterrupts - Return v29 + Return v28 "); } @@ -497,19 +495,18 @@ pub(crate) mod hir_build_tests { v12:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v13:ArrayExact = ArrayDup v12 v15:BasicObject = CheckMatch v10, v13, WHEN|ARRAY + v17:CBool = Test v15 + v18:Truthy = RefineType v15, Truthy + CondBranch v17, bb4(v6), bb5() + bb4(v28:BasicObject): + v32:Fixnum[1] = Const Value(1) CheckInterrupts - v18:CBool = Test v15 - v19:Truthy = RefineType v15, Truthy - CondBranch v18, bb4(v6), bb5() - bb4(v29:BasicObject): - v33:Fixnum[1] = Const Value(1) - CheckInterrupts - Return v33 + Return v32 bb5(): - v21:Falsy = RefineType v15, Falsy - v24:Fixnum[2] = Const Value(2) + v20:Falsy = RefineType v15, Falsy + v23:Fixnum[2] = Const Value(2) CheckInterrupts - Return v24 + Return v23 "); } @@ -1378,19 +1375,18 @@ pub(crate) mod hir_build_tests { Jump bb3(v4) bb3(v6:BasicObject): v10:TrueClass|NilClass = DefinedIvar v6, :@foo - CheckInterrupts - v13:CBool = Test v10 - v14:NilClass = RefineType v10, Falsy - CondBranch v13, bb5(), bb4(v6) + v12:CBool = Test v10 + v13:NilClass = RefineType v10, Falsy + CondBranch v12, bb5(), bb4(v6) bb5(): - v16:TrueClass = RefineType v10, Truthy - v19:Fixnum[3] = Const Value(3) + v15:TrueClass = RefineType v10, Truthy + v18:Fixnum[3] = Const Value(3) CheckInterrupts - Return v19 - bb4(v24:BasicObject): - v28:Fixnum[4] = Const Value(4) + Return v18 + bb4(v23:BasicObject): + v27:Fixnum[4] = Const Value(4) CheckInterrupts - Return v28 + Return v27 "); } @@ -1496,19 +1492,18 @@ pub(crate) mod hir_build_tests { v7:BasicObject = LoadArg :cond@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - CondBranch v16, bb5(), bb4(v9, v17) + v15:CBool = Test v10 + v16:Falsy = RefineType v10, Falsy + CondBranch v15, bb5(), bb4(v9, v16) bb5(): - v19:Truthy = RefineType v10, Truthy - v22:Fixnum[3] = Const Value(3) + v18:Truthy = RefineType v10, Truthy + v21:Fixnum[3] = Const Value(3) CheckInterrupts - Return v22 - bb4(v27:BasicObject, v28:Falsy): - v32:Fixnum[4] = Const Value(4) + Return v21 + bb4(v26:BasicObject, v27:Falsy): + v31:Fixnum[4] = Const Value(4) CheckInterrupts - Return v32 + Return v31 "); } @@ -1540,21 +1535,19 @@ pub(crate) mod hir_build_tests { v9:NilClass = Const Value(nil) Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:NilClass): - CheckInterrupts - v19:CBool = Test v12 - v20:Falsy = RefineType v12, Falsy - CondBranch v19, bb6(), bb4(v11, v20, v13) + v18:CBool = Test v12 + v19:Falsy = RefineType v12, Falsy + CondBranch v18, bb6(), bb4(v11, v19, v13) bb6(): - v22:Truthy = RefineType v12, Truthy - v25:Fixnum[3] = Const Value(3) - CheckInterrupts - Jump bb5(v11, v22, v25) - bb4(v30:BasicObject, v31:Falsy, v32:NilClass): - v36:Fixnum[4] = Const Value(4) - Jump bb5(v30, v31, v36) - bb5(v39:BasicObject, v40:BasicObject, v41:Fixnum): + v21:Truthy = RefineType v12, Truthy + v24:Fixnum[3] = Const Value(3) + Jump bb5(v11, v21, v24) + bb4(v28:BasicObject, v29:Falsy, v30:NilClass): + v34:Fixnum[4] = Const Value(4) + Jump bb5(v28, v29, v34) + bb5(v37:BasicObject, v38:BasicObject, v39:Fixnum): CheckInterrupts - Return v41 + Return v39 "); } @@ -1879,26 +1872,25 @@ pub(crate) mod hir_build_tests { bb3(v10:BasicObject, v11:NilClass, v12:NilClass): v16:Fixnum[0] = Const Value(0) v20:Fixnum[10] = Const Value(10) - CheckInterrupts Jump bb5(v10, v16, v20) - bb5(v26:BasicObject, v27:BasicObject, v28:BasicObject): - v32:Fixnum[0] = Const Value(0) - v35:BasicObject = Send v28, :>, v32 # SendFallbackReason: Uncategorized(opt_gt) - CheckInterrupts - v38:CBool = Test v35 - v39:Truthy = RefineType v35, Truthy - CondBranch v38, bb4(v26, v27, v28), bb6() - bb4(v51:BasicObject, v52:BasicObject, v53:BasicObject): - v58:Fixnum[1] = Const Value(1) - v61:BasicObject = Send v52, :+, v58 # SendFallbackReason: Uncategorized(opt_plus) - v66:Fixnum[1] = Const Value(1) - v69:BasicObject = Send v53, :-, v66 # SendFallbackReason: Uncategorized(opt_minus) - Jump bb5(v51, v61, v69) + bb5(v25:BasicObject, v26:BasicObject, v27:BasicObject): + v31:Fixnum[0] = Const Value(0) + v34:BasicObject = Send v27, :>, v31 # SendFallbackReason: Uncategorized(opt_gt) + CheckInterrupts + v37:CBool = Test v34 + v38:Truthy = RefineType v34, Truthy + CondBranch v37, bb4(v25, v26, v27), bb6() + bb4(v50:BasicObject, v51:BasicObject, v52:BasicObject): + v57:Fixnum[1] = Const Value(1) + v60:BasicObject = Send v51, :+, v57 # SendFallbackReason: Uncategorized(opt_plus) + v65:Fixnum[1] = Const Value(1) + v68:BasicObject = Send v52, :-, v65 # SendFallbackReason: Uncategorized(opt_minus) + Jump bb5(v50, v60, v68) bb6(): - v41:Falsy = RefineType v35, Falsy - v43:NilClass = Const Value(nil) + v40:Falsy = RefineType v34, Falsy + v42:NilClass = Const Value(nil) CheckInterrupts - Return v27 + Return v26 "); } @@ -1957,19 +1949,18 @@ pub(crate) mod hir_build_tests { Jump bb3(v5, v6) bb3(v8:BasicObject, v9:NilClass): v13:TrueClass = Const Value(true) - CheckInterrupts - v19:CBool[true] = Test v13 - v20 = RefineType v13, Falsy - CondBranch v19, bb5(), bb4(v8, v20) + v18:CBool[true] = Test v13 + v19 = RefineType v13, Falsy + CondBranch v18, bb5(), bb4(v8, v19) bb5(): - v22:TrueClass = RefineType v13, Truthy - v25:Fixnum[3] = Const Value(3) + v21:TrueClass = RefineType v13, Truthy + v24:Fixnum[3] = Const Value(3) CheckInterrupts - Return v25 - bb4(v30, v31): - v35 = Const Value(4) + Return v24 + bb4(v29, v30): + v34 = Const Value(4) CheckInterrupts - Return v35 + Return v34 "); } @@ -2824,14 +2815,13 @@ pub(crate) mod hir_build_tests { bb6(): v17:HeapBasicObject = ObjectAlloc v10 v19:BasicObject = Send v17, :initialize # SendFallbackReason: Uncategorized(opt_send_without_block) - CheckInterrupts Jump bb5(v6, v17, v19) - bb4(v23:BasicObject, v24:NilClass, v25:BasicObject): - v28:BasicObject = Send v25, :new # SendFallbackReason: Uncategorized(opt_send_without_block) - Jump bb5(v23, v28, v24) - bb5(v31:BasicObject, v32:BasicObject, v33:BasicObject): + bb4(v22:BasicObject, v23:NilClass, v24:BasicObject): + v27:BasicObject = Send v24, :new # SendFallbackReason: Uncategorized(opt_send_without_block) + Jump bb5(v22, v27, v23) + bb5(v30:BasicObject, v31:BasicObject, v32:BasicObject): CheckInterrupts - Return v32 + Return v31 "); } @@ -4873,17 +4863,16 @@ pub(crate) mod hir_build_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v17:CBool = HasType v10, NilClass - v18:NilClass = Const Value(nil) - CondBranch v17, bb4(v9, v18, v18), bb5() + v16:CBool = HasType v10, NilClass + v17:NilClass = Const Value(nil) + CondBranch v16, bb4(v9, v17, v17), bb5() bb5(): - v20:NotNil = RefineType v10, NotNil - v22:BasicObject = Send v20, :itself # SendFallbackReason: Uncategorized(opt_send_without_block) - Jump bb4(v9, v20, v22) - bb4(v24:BasicObject, v25:BasicObject, v26:BasicObject): + v19:NotNil = RefineType v10, NotNil + v21:BasicObject = Send v19, :itself # SendFallbackReason: Uncategorized(opt_send_without_block) + Jump bb4(v9, v19, v21) + bb4(v23:BasicObject, v24:BasicObject, v25:BasicObject): CheckInterrupts - Return v26 + Return v25 "); } @@ -4914,27 +4903,25 @@ pub(crate) mod hir_build_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - CondBranch v16, bb6(), bb4(v9, v17) + v15:CBool = Test v10 + v16:Falsy = RefineType v10, Falsy + CondBranch v15, bb6(), bb4(v9, v16) bb6(): - v19:Truthy = RefineType v10, Truthy - CheckInterrupts - v25:CBool[false] = HasType v19, NilClass - v26:NilClass = Const Value(nil) - CondBranch v25, bb5(v9, v26, v26), bb7() + v18:Truthy = RefineType v10, Truthy + v23:CBool[false] = HasType v18, NilClass + v24:NilClass = Const Value(nil) + CondBranch v23, bb5(v9, v24, v24), bb7() bb7(): - v28:Truthy = RefineType v19, NotNil - v30:BasicObject = Send v28, :itself # SendFallbackReason: Uncategorized(opt_send_without_block) + v26:Truthy = RefineType v18, NotNil + v28:BasicObject = Send v26, :itself # SendFallbackReason: Uncategorized(opt_send_without_block) CheckInterrupts - Return v30 - bb4(v35:BasicObject, v36:Falsy): - v40:Fixnum[4] = Const Value(4) - Jump bb5(v35, v36, v40) - bb5(v42:BasicObject, v43:Falsy, v44:Fixnum[4]): + Return v28 + bb4(v33:BasicObject, v34:Falsy): + v38:Fixnum[4] = Const Value(4) + Jump bb5(v33, v34, v38) + bb5(v40:BasicObject, v41:Falsy, v42:Fixnum[4]): CheckInterrupts - Return v44 + Return v42 "); } @@ -4971,39 +4958,36 @@ pub(crate) mod hir_build_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - CondBranch v16, bb7(), bb6(v9, v17) + v15:CBool = Test v10 + v16:Falsy = RefineType v10, Falsy + CondBranch v15, bb7(), bb6(v9, v16) bb7(): - v19:Truthy = RefineType v10, Truthy - CheckInterrupts - v24:CBool[true] = Test v19 - v25 = RefineType v19, Falsy - CondBranch v24, bb8(), bb5(v9, v25) + v18:Truthy = RefineType v10, Truthy + v22:CBool[true] = Test v18 + v23 = RefineType v18, Falsy + CondBranch v22, bb8(), bb5(v9, v23) bb8(): - v27:Truthy = RefineType v19, Truthy - CheckInterrupts - v32:CBool[true] = Test v27 - v33 = RefineType v27, Falsy - CondBranch v32, bb9(), bb4(v9, v33) + v25:Truthy = RefineType v18, Truthy + v29:CBool[true] = Test v25 + v30 = RefineType v25, Falsy + CondBranch v29, bb9(), bb4(v9, v30) bb9(): - v35:Truthy = RefineType v27, Truthy - v38:Fixnum[3] = Const Value(3) + v32:Truthy = RefineType v25, Truthy + v35:Fixnum[3] = Const Value(3) CheckInterrupts - Return v38 - bb4(v63, v64): - v68 = Const Value(4) + Return v35 + bb4(v60, v61): + v65 = Const Value(4) CheckInterrupts - Return v68 - bb5(v53, v54): - v58 = Const Value(5) + Return v65 + bb5(v50, v51): + v55 = Const Value(5) CheckInterrupts - Return v58 - bb6(v43:BasicObject, v44:Falsy): - v48:Fixnum[6] = Const Value(6) + Return v55 + bb6(v40:BasicObject, v41:Falsy): + v45:Fixnum[6] = Const Value(6) CheckInterrupts - Return v48 + Return v45 "); } @@ -5152,19 +5136,18 @@ pub(crate) mod hir_build_tests { v43:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) Jump bb8(v43, v22) bb8(v33:BasicObject, v34:BasicObject): - CheckInterrupts - v47:CBool = Test v33 - v48:Falsy = RefineType v33, Falsy - CondBranch v47, bb9(), bb4(v18, v19, v20, v21, v34, v27) + v46:CBool = Test v33 + v47:Falsy = RefineType v33, Falsy + CondBranch v46, bb9(), bb4(v18, v19, v20, v21, v34, v27) bb9(): - v50:Truthy = RefineType v33, Truthy - v54:BasicObject = InvokeBlock v27 # SendFallbackReason: InvokeBlock: not yet specialized - v57:BasicObject = InvokeBuiltin dir_s_close, v18, v27 + v49:Truthy = RefineType v33, Truthy + v53:BasicObject = InvokeBlock v27 # SendFallbackReason: InvokeBlock: not yet specialized + v56:BasicObject = InvokeBuiltin dir_s_close, v18, v27 CheckInterrupts - Return v54 - bb4(v63:BasicObject, v64:BasicObject, v65:BasicObject, v66:BasicObject, v67:BasicObject, v68:BasicObject): + Return v53 + bb4(v62:BasicObject, v63:BasicObject, v64:BasicObject, v65:BasicObject, v66:BasicObject, v67:BasicObject): CheckInterrupts - Return v68 + Return v67 "); } @@ -5295,19 +5278,18 @@ pub(crate) mod hir_build_tests { v17:Fixnum[0] = Const Value(0) v19:Fixnum[1] = Const Value(1) v22:BasicObject = Send v10, :[], v17, v19 # SendFallbackReason: Uncategorized(opt_send_without_block) + v25:CBool = Test v22 + v26:Truthy = RefineType v22, Truthy + CondBranch v25, bb4(v9, v10, v14, v10, v17, v19, v26), bb5() + bb4(v40:BasicObject, v41:BasicObject, v42:NilClass, v43:BasicObject, v44:Fixnum[0], v45:Fixnum[1], v46:Truthy): CheckInterrupts - v26:CBool = Test v22 - v27:Truthy = RefineType v22, Truthy - CondBranch v26, bb4(v9, v10, v14, v10, v17, v19, v27), bb5() - bb4(v41:BasicObject, v42:BasicObject, v43:NilClass, v44:BasicObject, v45:Fixnum[0], v46:Fixnum[1], v47:Truthy): - CheckInterrupts - Return v47 + Return v46 bb5(): - v29:Falsy = RefineType v22, Falsy - v32:Fixnum[2] = Const Value(2) - v35:BasicObject = Send v10, :[]=, v17, v19, v32 # SendFallbackReason: Uncategorized(opt_send_without_block) + v28:Falsy = RefineType v22, Falsy + v31:Fixnum[2] = Const Value(2) + v34:BasicObject = Send v10, :[]=, v17, v19, v31 # SendFallbackReason: Uncategorized(opt_send_without_block) CheckInterrupts - Return v32 + Return v31 "); } @@ -5844,19 +5826,18 @@ pub(crate) mod hir_build_tests { Jump bb3(v7, v8, v10) bb3(v12:BasicObject, v13:BasicObject, v14:BasicObject): v17:BoolExact = FixnumBitCheck v14, 0 - CheckInterrupts - v20:CBool = Test v17 - v21:TrueClass = RefineType v17, Truthy - CondBranch v20, bb4(v12, v13, v14), bb5() + v19:CBool = Test v17 + v20:TrueClass = RefineType v17, Truthy + CondBranch v19, bb4(v12, v13, v14), bb5() bb5(): - v23:FalseClass = RefineType v17, Falsy - v25:Fixnum[1] = Const Value(1) - v27:Fixnum[1] = Const Value(1) - v30:BasicObject = Send v25, :+, v27 # SendFallbackReason: Uncategorized(opt_plus) - Jump bb4(v12, v30, v14) - bb4(v33:BasicObject, v34:BasicObject, v35:BasicObject): + v22:FalseClass = RefineType v17, Falsy + v24:Fixnum[1] = Const Value(1) + v26:Fixnum[1] = Const Value(1) + v29:BasicObject = Send v24, :+, v26 # SendFallbackReason: Uncategorized(opt_plus) + Jump bb4(v12, v29, v14) + bb4(v32:BasicObject, v33:BasicObject, v34:BasicObject): CheckInterrupts - Return v34 + Return v33 "); } From 6d6e9a93b191d788dda57506b4a23afc01f0329c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 14 Jul 2026 14:35:51 +0900 Subject: [PATCH 19/47] Remove unreferenced OPT_GLOBAL_METHOD_CACHE and OPT_UNIFY_ALL_COMBINATION The global method cache was removed by b9007b6c548f91e88fd3f2ffa23de740431fa969 for Ruby 3.0 [Feature #16614], and OPT_UNIFY_ALL_COMBINATION lost its last consumer when the old insns2vm was replaced by tool/ruby_vm in 2017. Nothing reads these macros anymore. Co-Authored-By: Claude Opus 4.8 --- vm_opts.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/vm_opts.h b/vm_opts.h index ce47745b1102de..8b8b47fa675985 100644 --- a/vm_opts.h +++ b/vm_opts.h @@ -44,7 +44,6 @@ /* VM running option */ #define OPT_CHECKED_RUN 1 #define OPT_INLINE_METHOD_CACHE 1 -#define OPT_GLOBAL_METHOD_CACHE 1 #ifndef OPT_IC_FOR_IVAR #define OPT_IC_FOR_IVAR 1 @@ -53,7 +52,6 @@ /* architecture independent, affects generated code */ #define OPT_OPERANDS_UNIFICATION 1 #define OPT_INSTRUCTIONS_UNIFICATION 0 -#define OPT_UNIFY_ALL_COMBINATION 0 /* misc */ #ifndef OPT_SUPPORT_JOKE From ff0f4d4df36a9fa05ca9b7bd760c4d264d98a6c6 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 14 Jul 2026 14:33:34 +0900 Subject: [PATCH 20/47] CI: drop redundant --with-gmp macOS job gmp is enabled by default and setup/macos passes --with-gmp-dir to every job, so the plain check job already links -lgmp and the dedicated --with-gmp job duplicated it. Trims one of ten macOS jobs per event against the org-wide macOS concurrency cap of 50. Co-Authored-By: Claude Fable 5 --- .github/workflows/macos.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index c9c5af3b1952b9..191e3a8988bac8 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -30,12 +30,12 @@ jobs: - test_task: check os: macos-15 configure_args: '--with-gcc=gcc-14' + # No dedicated --with-gmp job: gmp is enabled by default and + # setup/macos passes --with-gmp-dir to every job, so each check + # here already builds and tests with -lgmp. - test_task: check os: macos-26 configure_args: '--with-jemalloc --with-opt-dir=$(brew --prefix jemalloc)' - - test_task: check - os: macos-26 - configure_args: '--with-gmp' - test_task: test-all test_opts: --repeat-count=2 os: macos-26 From ad57e69d906e7dc06db563ffa23933ac017e2cfd Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 14 Jul 2026 14:45:19 +0900 Subject: [PATCH 21/47] CI: fold cargo test into a YJIT macOS make job The dedicated cargo job occupied a runner from the org-wide macOS concurrency pool of 50 for less than a minute of work. Run the Rust unit tests as steps of the --enable-yjit entry instead, which stays shorter than the --enable-yjit=dev long pole. The steps are guarded with !cancelled() so a Ruby-side test failure does not mask the Rust result. Co-Authored-By: Claude Fable 5 --- .github/workflows/yjit-macos.yml | 52 ++++++++++++++------------------ 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/.github/workflows/yjit-macos.yml b/.github/workflows/yjit-macos.yml index fadcaeac30cc80..a17bffcf41e709 100644 --- a/.github/workflows/yjit-macos.yml +++ b/.github/workflows/yjit-macos.yml @@ -32,35 +32,6 @@ env: CARGO_HTTP_MULTIPLEXING: false jobs: - cargo: - name: cargo test - - runs-on: macos-26 - - if: >- - ${{!(false - || contains(github.event.head_commit.message, '[DOC]') - || contains(github.event.pull_request.title, '[DOC]') - || contains(github.event.pull_request.labels.*.name, 'Documentation') - || (github.event.pull_request.user.login == 'dependabot[bot]' && !startsWith(github.head_ref, 'dependabot/cargo')) - )}} - - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - run: RUST_BACKTRACE=1 cargo test - working-directory: yjit - - # Also compile and test with all features enabled - - run: RUST_BACKTRACE=1 cargo test --all-features - working-directory: yjit - - # Check that we can build in release mode too - - run: cargo build --release - working-directory: yjit - make: strategy: matrix: @@ -68,6 +39,10 @@ jobs: - test_task: 'check' configure: '--enable-yjit' yjit_opts: '--yjit' + # Run the Rust unit tests on one entry only; a separate job + # would occupy another runner from the org-wide macOS + # concurrency pool just for ~1 minute of cargo. + cargo: true - test_task: 'check' configure: '--enable-yjit=dev' yjit_opts: '--yjit-call-threshold=1 --yjit-verify-ctx --yjit-code-gc' @@ -177,6 +152,25 @@ jobs: if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }} continue-on-error: ${{ matrix.continue-on-skipped_tests || false }} + # Run even when an earlier test step failed, so a Ruby-side failure + # does not mask the Rust unit test result. + - name: cargo test + run: RUST_BACKTRACE=1 cargo test + working-directory: src/yjit + if: ${{ matrix.cargo && !cancelled() }} + + # Also compile and test with all features enabled + - name: cargo test --all-features + run: RUST_BACKTRACE=1 cargo test --all-features + working-directory: src/yjit + if: ${{ matrix.cargo && !cancelled() }} + + # Check that we can build in release mode too + - name: cargo build --release + run: cargo build --release + working-directory: src/yjit + if: ${{ matrix.cargo && !cancelled() }} + - name: Dump crash logs if: ${{ failure() }} continue-on-error: true From e7142e1c4530e61282223187818fd460f8acda28 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 15 Jul 2026 08:14:11 +0900 Subject: [PATCH 22/47] CI: trim the tarball-test macOS matrix on PR and push The full macOS fan-out (10 jobs, about 117 runner-minutes) repeats test tasks that macos.yml already runs from the git tree on every event; what is tarball-specific is only that the packaged tarball configures, builds and installs. Run a single check on macos-26 for pull requests and pushes, and keep the full matrix for workflow_dispatch, which is how the nightly schedule and release verification invoke it. Only macOS is trimmed: its org-wide concurrency pool of 50 runners saturates during peak hours and has been delaying release jobs, while Linux and Windows share a pool with headroom. Co-Authored-By: Claude Fable 5 --- .github/workflows/tarball-macos.yml | 24 +++++++++++++++--------- .github/workflows/tarball-test.yml | 5 +++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tarball-macos.yml b/.github/workflows/tarball-macos.yml index 2b3ac7ab37b6ae..1302b653f0c401 100644 --- a/.github/workflows/tarball-macos.yml +++ b/.github/workflows/tarball-macos.yml @@ -12,6 +12,11 @@ on: required: false type: boolean default: false + full-matrix: + description: 'Run the full test matrix (nightly schedule and release verification). Off, the default, runs a single smoke check.' + required: false + type: boolean + default: false secrets: SIMPLER_ALERTS_URL: required: false @@ -24,16 +29,17 @@ permissions: jobs: macos: strategy: + # The smoke matrix keeps only what is tarball-specific: that the + # packaged tarball configures, builds and installs. macos.yml runs the + # same test tasks from the git tree on every PR/push already. + # macos-15-intel: the runner image ships a pre-installed Ruby under + # /usr/local whose gems dir collides with the default prefix on Intel. + # Use an isolated prefix so RDoc generation does not scan that stale + # default-gem stub. matrix: - test_task: [check, test-bundled-gems, test-bundler-parallel] - os: [macos-26, macos-15, macos-14] - include: - - os: macos-15-intel - test_task: check - # The runner image ships a pre-installed Ruby under /usr/local whose - # gems dir collides with the default prefix on Intel. Use an isolated - # prefix so RDoc generation does not scan that stale default-gem stub. - prefix: /tmp/ruby-prefix + test_task: ${{ inputs.full-matrix && fromJSON('["check", "test-bundled-gems", "test-bundler-parallel"]') || fromJSON('["check"]') }} + os: ${{ inputs.full-matrix && fromJSON('["macos-26", "macos-15", "macos-14"]') || fromJSON('["macos-26"]') }} + include: ${{ inputs.full-matrix && fromJSON('[{"os":"macos-15-intel", "test_task":"check", "prefix":"/tmp/ruby-prefix"}]') || fromJSON('[]') }} fail-fast: false runs-on: ${{ matrix.os }} env: diff --git a/.github/workflows/tarball-test.yml b/.github/workflows/tarball-test.yml index 814304eb2a7b95..f27f842332ba4f 100644 --- a/.github/workflows/tarball-test.yml +++ b/.github/workflows/tarball-test.yml @@ -83,6 +83,11 @@ jobs: uses: ./.github/workflows/tarball-macos.yml with: archname: snapshot-${{ needs.tarball.outputs.branch }} + # Full fan-out only for workflow_dispatch (nightly schedule and release + # verification); pull requests and pushes run a single smoke check. + # Only macOS is trimmed: its org-wide concurrency pool of 50 runners + # saturates during peak hours, while Linux and Windows have headroom. + full-matrix: ${{ github.event_name == 'workflow_dispatch' }} notify-release-channel: ${{ github.event_name == 'workflow_dispatch' && inputs.notify-release-channel || false }} secrets: SIMPLER_ALERTS_URL: ${{ secrets.SIMPLER_ALERTS_URL }} From b1f50425ccc1be34d686805daf85d38adde6f8ff Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 3 Jul 2026 15:13:12 +0900 Subject: [PATCH 23/47] [ruby/rubygems] Split install/gemfile/git_spec.rb for parallel execution This was by far the heaviest bundler spec file (~2500 example-seconds on Windows). Parallel spec runs distribute work per file, so a single huge file caps the achievable wall-clock time. Move its describe blocks verbatim into three files, keeping example count and structure unchanged. https://github.com/ruby/rubygems/commit/a50eb931e5 Co-Authored-By: Claude Fable 5 --- .../install/gemfile/git_extensions_spec.rb | 609 ++++++++ .../install/gemfile/git_overrides_spec.rb | 633 +++++++++ spec/bundler/install/gemfile/git_spec.rb | 1236 ----------------- spec/bundler/support/shards.rb | 2 + 4 files changed, 1244 insertions(+), 1236 deletions(-) create mode 100644 spec/bundler/install/gemfile/git_extensions_spec.rb create mode 100644 spec/bundler/install/gemfile/git_overrides_spec.rb diff --git a/spec/bundler/install/gemfile/git_extensions_spec.rb b/spec/bundler/install/gemfile/git_extensions_spec.rb new file mode 100644 index 00000000000000..ff540955ef34f2 --- /dev/null +++ b/spec/bundler/install/gemfile/git_extensions_spec.rb @@ -0,0 +1,609 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with git sources" do + describe "switching sources" do + it "doesn't explode when switching Path to Git sources" do + build_gem "foo", "1.0", to_system: true do |s| + s.write "lib/foo.rb", "raise 'fail'" + end + build_lib "foo", "1.0", path: lib_path("bar/foo") + build_git "bar", "1.0", path: lib_path("bar") do |s| + s.add_dependency "foo" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "bar", :path => "#{lib_path("bar")}" + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "bar", :git => "#{lib_path("bar")}" + G + + expect(the_bundle).to include_gems "foo 1.0", "bar 1.0" + end + + it "doesn't explode when switching Gem to Git source" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack-obama" + gem "myrack", "1.0.0" + G + + build_git "myrack", "1.0" do |s| + s.write "lib/new_file.rb", "puts 'USING GIT'" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack-obama" + gem "myrack", "1.0.0", :git => "#{lib_path("myrack-1.0")}" + G + + run "require 'new_file'" + expect(out).to eq("USING GIT") + end + + it "doesn't explode when removing an explicit exact version from a git gem with dependencies" do + build_lib "activesupport", "7.1.4", path: lib_path("rails/activesupport") + build_git "rails", "7.1.4", path: lib_path("rails") do |s| + s.add_dependency "activesupport", "= 7.1.4" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", "7.1.4", :git => "#{lib_path("rails")}" + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", :git => "#{lib_path("rails")}" + G + + expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4" + end + + it "doesn't explode when adding an explicit ref to a git gem with dependencies" do + lib_root = lib_path("rails") + + build_lib "activesupport", "7.1.4", path: lib_root.join("activesupport") + build_git "rails", "7.1.4", path: lib_root do |s| + s.add_dependency "activesupport", "= 7.1.4" + end + + old_revision = revision_for(lib_root) + update_git "rails", "7.1.4", path: lib_root + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", "7.1.4", :git => "#{lib_root}" + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", :git => "#{lib_root}", :ref => "#{old_revision}" + G + + expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4" + end + end + + describe "bundle install after the remote has been updated" do + it "installs" do + build_git "valim" + + install_gemfile <<-G + source "https://gem.repo1" + gem "valim", :git => "#{lib_path("valim-1.0")}" + G + + old_revision = revision_for(lib_path("valim-1.0")) + update_git "valim" + new_revision = revision_for(lib_path("valim-1.0")) + + old_lockfile = File.read(bundled_app_lock) + lockfile(bundled_app_lock, old_lockfile.gsub(/revision: #{old_revision}/, "revision: #{new_revision}")) + + bundle "install" + + run <<-R + require "valim" + puts VALIM_PREV_REF + R + + expect(out).to eq(old_revision) + end + + it "gives a helpful error message when the remote ref no longer exists" do + build_git "foo" + revision = revision_for(lib_path("foo-1.0")) + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{revision}" + G + expect(out).to_not match(/Revision.*does not exist/) + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "deadbeef" + G + expect(err).to include("Revision deadbeef does not exist in the repository") + end + + it "gives a helpful error message when the remote branch no longer exists" do + build_git "foo" + + install_gemfile <<-G, env: { "LANG" => "en" }, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "deadbeef" + G + + expect(err).to include("Revision deadbeef does not exist in the repository") + end + end + + describe "bundle install with deployment mode configured and git sources" do + it "works" do + build_git "valim", path: lib_path("valim") + + install_gemfile <<-G + source "https://gem.repo1" + gem "valim", "= 1.0", :git => "#{lib_path("valim")}" + G + + pristine_system_gems + + bundle_config "deployment true" + bundle :install + end + end + + describe "gem install hooks" do + it "runs pre-install hooks" do + build_git "foo" + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + Gem.pre_install_hooks << lambda do |inst| + STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + requires: [lib_path("install_hooks.rb")] + expect(err_without_deprecations).to eq("Ran pre-install hook: foo-1.0") + end + + it "runs post-install hooks" do + build_git "foo" + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + Gem.post_install_hooks << lambda do |inst| + STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + requires: [lib_path("install_hooks.rb")] + expect(err_without_deprecations).to eq("Ran post-install hook: foo-1.0") + end + + it "complains if the install hook fails" do + build_git "foo" + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + Gem.pre_install_hooks << lambda do |inst| + false + end + H + end + + bundle :install, requires: [lib_path("install_hooks.rb")], raise_on_error: false + expect(err).to include("failed for foo-1.0") + end + end + + context "with an extension" do + it "installs the extension" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = 'YES'" + end + end + RUBY + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + expect(out).to eq("YES") + + run <<-R + puts $:.grep(/ext/) + R + expect(out).to include(Pathname.glob(default_bundle_path("bundler/gems/extensions/**/foo-1.0-*")).first.to_s) + end + + it "does not use old extension after ref changes" do + git_reader = build_git "foo", no_default: true do |s| + s.extensions = ["ext/extconf.rb"] + s.write "ext/extconf.rb", <<-RUBY + require "mkmf" + create_makefile("foo") + RUBY + s.write "ext/foo.c", "void Init_foo() {}" + end + + 2.times do |i| + File.open(git_reader.path.join("ext/foo.c"), "w") do |file| + file.write <<-C + #include "ruby.h" + VALUE foo(VALUE self) { return INT2FIX(#{i}); } + void Init_foo() { rb_define_global_function("foo", &foo, 0); } + C + end + git("commit -m \"commit for iteration #{i}\" ext/foo.c", git_reader.path) + + git_commit_sha = git_reader.ref_for("HEAD") + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{git_commit_sha}" + G + + run <<-R + require 'foo' + puts foo + R + + expect(out).to eq(i.to_s) + end + end + + it "does not prompt to gem install if extension fails" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + raise + end + RUBY + end + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + expect(err).to end_with(<<-M.strip) +An error occurred while installing foo (1.0), and Bundler cannot continue. + +In Gemfile: + foo + M + expect(out).not_to include("gem install foo") + end + + it "does not reinstall the extension" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + cur_time = Time.now.to_f.to_s + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = \#{cur_time}" + end + end + RUBY + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + + installed_time = out + expect(installed_time).to match(/\A\d+\.\d+\z/) + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + expect(out).to eq(installed_time) + end + + it "does not reinstall the extension when changing another gem" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + cur_time = Time.now.to_f.to_s + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = \#{cur_time}" + end + end + RUBY + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + + installed_time = out + expect(installed_time).to match(/\A\d+\.\d+\z/) + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + expect(out).to eq(installed_time) + end + + it "does reinstall the extension when changing refs" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + cur_time = Time.now.to_f.to_s + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = \#{cur_time}" + end + end + RUBY + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + + installed_time = out + + update_git("foo", branch: "branch2") + + expect(installed_time).to match(/\A\d+\.\d+\z/) + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "branch2" + G + + run <<-R + require 'foo' + puts FOO + R + expect(out).not_to eq(installed_time) + + installed_time = out + + update_git("foo") + bundle "update foo" + + run <<-R + require 'foo' + puts FOO + R + expect(out).not_to eq(installed_time) + end + end + + it "ignores git environment variables" do + build_git "xxxxxx" do |s| + s.executables = "xxxxxxbar" + end + + Bundler::SharedHelpers.with_clean_git_env do + ENV["GIT_DIR"] = "bar" + ENV["GIT_WORK_TREE"] = "bar" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("xxxxxx-1.0")}" do + gem 'xxxxxx' + end + G + + expect(ENV["GIT_DIR"]).to eq("bar") + expect(ENV["GIT_WORK_TREE"]).to eq("bar") + end + end + + describe "without git installed" do + it "prints a better error message when installing" do + gemfile <<-G + source "https://gem.repo1" + + gem "rake", git: "https://github.com/ruby/rake" + G + + lockfile <<-L + GIT + remote: https://github.com/ruby/rake + revision: 5c60da8644a9e4f655e819252e3b6ca77f42b7af + specs: + rake (13.0.6) + + GEM + remote: https://rubygems.org/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rake! + + BUNDLED WITH + #{Bundler::VERSION} + L + + with_path_as("") do + bundle "install", raise_on_error: false + end + expect(err). + to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git") + end + + it "prints a better error message when updating" do + build_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + + with_path_as("") do + bundle "update", all: true, raise_on_error: false + end + expect(err). + to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git") + end + + it "doesn't need git in the new machine if an installed git gem is copied to another machine" do + build_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + bundle_config_global "path vendor/bundle" + bundle :install + pristine_system_gems + + bundle "install", env: { "PATH" => "" } + expect(out).to_not include("You need to install git to be able to use gems from git repositories.") + end + end + + describe "when the git source is overridden with a local git repo" do + before do + bundle_config_global "local.foo #{lib_path("foo")}" + end + + describe "and git output is colorized" do + before do + File.open("#{ENV["HOME"]}/.gitconfig", "w") do |f| + f.write("[color]\n\tui = always\n") + end + end + + it "installs successfully" do + build_git "foo", "1.0", path: lib_path("foo") + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}", :branch => "main" + G + + bundle :install + expect(the_bundle).to include_gems "foo 1.0" + end + end + end + + context "git sources that include credentials" do + context "that are username and password" do + let(:credentials) { "user1:password1" } + + it "does not display the password" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + git "https://#{credentials}@github.com/company/private-repo" do + gem "foo" + end + G + + expect(stdboth).to_not include("password1") + expect(out).to include("Fetching https://user1@github.com/company/private-repo") + end + end + + context "that is an oauth token" do + let(:credentials) { "oauth_token" } + + it "displays the oauth scheme but not the oauth token" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + git "https://#{credentials}:x-oauth-basic@github.com/company/private-repo" do + gem "foo" + end + G + + expect(stdboth).to_not include("oauth_token") + expect(out).to include("Fetching https://x-oauth-basic@github.com/company/private-repo") + end + end + end +end diff --git a/spec/bundler/install/gemfile/git_overrides_spec.rb b/spec/bundler/install/gemfile/git_overrides_spec.rb new file mode 100644 index 00000000000000..750d16c8685509 --- /dev/null +++ b/spec/bundler/install/gemfile/git_overrides_spec.rb @@ -0,0 +1,633 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with git sources" do + describe "when specifying local override" do + it "uses the local repository instead of checking a new one out" do + build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" + end + + gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install + + run "require 'myrack'" + expect(out).to eq("LOCAL") + end + + it "chooses the local repository on runtime" do + build_git "myrack", "0.8" + + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + + update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + run "require 'myrack'" + expect(out).to eq("LOCAL") + end + + it "unlocks the source when the dependencies have changed while switching to the local" do + build_git "myrack", "0.8" + + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + + update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "myrack.gemspec", build_spec("myrack", "0.8") { runtime "rspec", "> 0" }.first.to_ruby + s.write "lib/myrack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install + run "require 'myrack'" + expect(out).to eq("LOCAL") + end + + it "updates specs on runtime" do + system_gems "nokogiri-1.4.2" + + build_git "myrack", "0.8" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + lockfile0 = File.read(bundled_app_lock) + + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.add_dependency "nokogiri", "1.4.2" + end + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + run "require 'myrack'" + + lockfile1 = File.read(bundled_app_lock) + expect(lockfile1).not_to eq(lockfile0) + end + + it "updates ref on install" do + build_git "myrack", "0.8" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + lockfile0 = File.read(bundled_app_lock) + + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + update_git "myrack", "0.8", path: lib_path("local-myrack") + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install + + lockfile1 = File.read(bundled_app_lock) + expect(lockfile1).not_to eq(lockfile0) + end + + it "explodes and gives correct solution if given path does not exist on install" do + build_git "myrack", "0.8" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/Cannot use local override for myrack-0.8 because #{Regexp.escape(lib_path("local-myrack").to_s)} does not exist/) + + solution = "config unset local.myrack" + expect(err).to match(/Run `bundle #{solution}` to remove the local override/) + + bundle solution + bundle :install + + expect(err).to be_empty + end + + it "explodes and gives correct solution if branch is not given on install" do + build_git "myrack", "0.8" + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/Cannot use local override for myrack-0.8 at #{Regexp.escape(lib_path("local-myrack").to_s)} because :branch is not specified in Gemfile/) + + solution = "config unset local.myrack" + expect(err).to match(/Specify a branch or run `bundle #{solution}` to remove the local override/) + + bundle solution + bundle :install + + expect(err).to be_empty + end + + it "does not explode if disable_local_branch_check is given" do + build_git "myrack", "0.8" + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle %(config set disable_local_branch_check true) + bundle :install + expect(out).to match(/Bundle complete!/) + end + + it "explodes on different branches on install" do + build_git "myrack", "0.8" + + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + + update_git "myrack", "0.8", path: lib_path("local-myrack"), branch: "another" do |s| + s.write "lib/myrack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/is using branch another but Gemfile specifies main/) + end + + it "explodes on invalid revision on install" do + build_git "myrack", "0.8" + + build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/The Gemfile lock is pointing to revision \w+/) + end + + it "does not explode on invalid revision on install" do + build_git "myrack", "0.8" + + build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle %(config set disable_local_revision_check true) + bundle :install + expect(out).to match(/Bundle complete!/) + end + end + + describe "specified inline" do + # TODO: Figure out how to write this test so that it is not flaky depending + # on the current network situation. + # it "supports private git URLs" do + # gemfile <<-G + # gem "thingy", :git => "git@notthere.fallingsnow.net:somebody/thingy.git" + # G + # + # bundle :install + # + # # p out + # # p err + # puts err unless err.empty? # This spec fails randomly every so often + # err.should include("notthere.fallingsnow.net") + # err.should include("ssh") + # end + + it "installs from git even if a newer gem is available elsewhere" do + build_git "myrack", "0.8" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" + G + + expect(the_bundle).to include_gems "myrack 0.8" + end + + it "installs dependencies from git even if a newer gem is available elsewhere" do + system_gems "myrack-1.0.0" + + build_lib "myrack", "1.0", path: lib_path("nested/bar") do |s| + s.write "lib/myrack.rb", "puts 'WIN OVERRIDE'" + end + + build_git "foo", path: lib_path("nested") do |s| + s.add_dependency "myrack", "= 1.0" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("nested")}" + G + + run "require 'myrack'" + expect(out).to eq("WIN OVERRIDE") + end + + it "correctly unlocks when changing to a git source" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + G + + build_git "myrack", path: lib_path("myrack") + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0", :git => "#{lib_path("myrack")}" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "correctly unlocks when changing to a git source without versions" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + build_git "myrack", "1.2", path: lib_path("myrack") + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack")}" + G + + expect(the_bundle).to include_gems "myrack 1.2" + end + end + + describe "block syntax" do + it "pulls all gems from a git block" do + build_lib "omg", path: lib_path("hi2u/omg") + build_lib "hi2u", path: lib_path("hi2u") + + install_gemfile <<-G + source "https://gem.repo1" + path "#{lib_path("hi2u")}" do + gem "omg" + gem "hi2u" + end + G + + expect(the_bundle).to include_gems "omg 1.0", "hi2u 1.0" + end + end + + it "uses a ref if specified" do + build_git "foo" + @revision = revision_for(lib_path("foo-1.0")) + update_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{@revision}" + G + + run <<-RUBY + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + + it "correctly handles cases with invalid gemspecs" do + build_git "foo" do |s| + s.summary = nil + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "rails 2.3.2" + end + + it "runs the gemspec in the context of its parent directory" do + build_lib "bar", path: lib_path("foo/bar"), gemspec: false do |s| + s.write lib_path("foo/bar/lib/version.rb"), %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + $:.unshift Dir.pwd + require 'lib/version' + Gem::Specification.new do |s| + s.name = 'bar' + s.author = 'no one' + s.version = BAR_VERSION + s.summary = 'Bar' + s.files = Dir["lib/**/*.rb"] + end + G + end + + build_git "foo", path: lib_path("foo") do |s| + s.write "bin/foo", "" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "bar", :git => "#{lib_path("foo")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems "bar 1.0" + expect(the_bundle).to include_gems "rails 2.3.2" + end + + it "runs the gemspec in the context of its parent directory, when using local overrides" do + build_git "foo", path: lib_path("foo"), gemspec: false do |s| + s.write lib_path("foo/lib/foo/version.rb"), %(FOO_VERSION = '1.0') + s.write "foo.gemspec", <<-G + $:.unshift Dir.pwd + require 'lib/foo/version' + Gem::Specification.new do |s| + s.name = 'foo' + s.author = 'no one' + s.version = FOO_VERSION + s.summary = 'Foo' + s.files = Dir["lib/**/*.rb"] + end + G + end + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "https://github.com/gems/foo", branch: "main" + G + + bundle %(config set local.foo #{lib_path("foo")}) + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "installs from git even if a rubygems gem is present" do + build_gem "foo", "1.0", path: lib_path("fake_foo"), to_system: true do |s| + s.write "lib/foo.rb", "raise 'FAIL'" + end + + build_git "foo", "1.0" + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "fakes the gem out if there is no gemspec" do + build_git "foo", gemspec: false + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems("foo 1.0") + expect(the_bundle).to include_gems("rails 2.3.2") + end + + it "catches git errors and spits out useful output" do + gemfile <<-G + source "https://gem.repo1" + gem "foo", "1.0", :git => "omgomg" + G + + bundle :install, raise_on_error: false + + expect(err).to include("Git error:") + expect(err).to include("fatal") + expect(err).to include("omgomg") + end + + it "works when the gem path has spaces in it" do + build_git "foo", path: lib_path("foo space-1.0") + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo space-1.0")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "handles repos that have been force-pushed" do + build_git "forced", "1.0" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("forced-1.0")}" do + gem 'forced' + end + G + expect(the_bundle).to include_gems "forced 1.0" + + update_git "forced" do |s| + s.write "lib/forced.rb", "FORCED = '1.1'" + end + + bundle "update", all: true + expect(the_bundle).to include_gems "forced 1.1" + + git("reset --hard HEAD^", lib_path("forced-1.0")) + + bundle "update", all: true + expect(the_bundle).to include_gems "forced 1.0" + end + + it "ignores submodules if :submodule is not passed" do + # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ + system(*%W[git config --global protocol.file.allow always]) + + build_git "submodule", "1.0" + build_git "has_submodule", "1.0" do |s| + s.add_dependency "submodule" + end + git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") + git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + git "#{lib_path("has_submodule-1.0")}" do + gem "has_submodule" + end + G + expect(err).to match(%r{submodule >= 0 could not be found in rubygems repository https://gem.repo1/ or installed locally}) + + expect(the_bundle).not_to include_gems "has_submodule 1.0" + end + + it "handles repos with submodules" do + # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ + system(*%W[git config --global protocol.file.allow always]) + + build_git "submodule", "1.0" + build_git "has_submodule", "1.0" do |s| + s.add_dependency "submodule" + end + git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") + git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("has_submodule-1.0")}", :submodules => true do + gem "has_submodule" + end + G + + expect(the_bundle).to include_gems "has_submodule 1.0" + end + + it "does not warn when deiniting submodules" do + # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ + system(*%W[git config --global protocol.file.allow always]) + + build_git "submodule", "1.0" + build_git "has_submodule", "1.0" + + git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") + git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("has_submodule-1.0")}" do + gem "has_submodule" + end + G + expect(err).to be_empty + + expect(the_bundle).to include_gems "has_submodule 1.0" + expect(the_bundle).to_not include_gems "submodule 1.0" + end + + it "handles implicit updates when modifying the source info" do + git = build_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}" do + gem "foo" + end + G + + update_git "foo" + update_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}", :ref => "#{git.ref_for("HEAD^")}" do + gem "foo" + end + G + + run <<-RUBY + require 'foo' + puts "WIN" if FOO_PREV_REF == '#{git.ref_for("HEAD^^")}' + RUBY + + expect(out).to eq("WIN") + end + + it "does not do a remote fetch if the revision is cached locally" do + build_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + FileUtils.rm_r(lib_path("foo-1.0")) + + bundle "install" + expect(out).not_to match(/updating/i) + end + + it "doesn't blow up if bundle install is run twice in a row" do + build_git "foo" + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + bundle "install" + bundle "install" + end + + it "prints a friendly error if a file blocks the git repo" do + build_git "foo" + + FileUtils.mkdir_p(default_bundle_path) + FileUtils.touch(default_bundle_path("bundler")) + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + expect(last_command).to be_failure + expect(err).to include("Bundler could not install a gem because it " \ + "needs to create a directory, but a file exists " \ + "- #{default_bundle_path("bundler")}") + end + + it "does not duplicate git gem sources" do + build_lib "foo", path: lib_path("nested/foo") + build_lib "bar", path: lib_path("nested/bar") + + build_git "foo", path: lib_path("nested") + build_git "bar", path: lib_path("nested") + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("nested")}" + gem "bar", :git => "#{lib_path("nested")}" + G + + expect(File.read(bundled_app_lock).scan("GIT").size).to eq(1) + end +end diff --git a/spec/bundler/install/gemfile/git_spec.rb b/spec/bundler/install/gemfile/git_spec.rb index 5fa740f4e1c68b..bed0a261a95404 100644 --- a/spec/bundler/install/gemfile/git_spec.rb +++ b/spec/bundler/install/gemfile/git_spec.rb @@ -513,1240 +513,4 @@ end end end - - describe "when specifying local override" do - it "uses the local repository instead of checking a new one out" do - build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| - s.write "lib/myrack.rb", "puts :LOCAL" - end - - gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle :install - - run "require 'myrack'" - expect(out).to eq("LOCAL") - end - - it "chooses the local repository on runtime" do - build_git "myrack", "0.8" - - FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - - update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| - s.write "lib/myrack.rb", "puts :LOCAL" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - run "require 'myrack'" - expect(out).to eq("LOCAL") - end - - it "unlocks the source when the dependencies have changed while switching to the local" do - build_git "myrack", "0.8" - - FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - - update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| - s.write "myrack.gemspec", build_spec("myrack", "0.8") { runtime "rspec", "> 0" }.first.to_ruby - s.write "lib/myrack.rb", "puts :LOCAL" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle :install - run "require 'myrack'" - expect(out).to eq("LOCAL") - end - - it "updates specs on runtime" do - system_gems "nokogiri-1.4.2" - - build_git "myrack", "0.8" - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - lockfile0 = File.read(bundled_app_lock) - - FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| - s.add_dependency "nokogiri", "1.4.2" - end - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - run "require 'myrack'" - - lockfile1 = File.read(bundled_app_lock) - expect(lockfile1).not_to eq(lockfile0) - end - - it "updates ref on install" do - build_git "myrack", "0.8" - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - lockfile0 = File.read(bundled_app_lock) - - FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - update_git "myrack", "0.8", path: lib_path("local-myrack") - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle :install - - lockfile1 = File.read(bundled_app_lock) - expect(lockfile1).not_to eq(lockfile0) - end - - it "explodes and gives correct solution if given path does not exist on install" do - build_git "myrack", "0.8" - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle :install, raise_on_error: false - expect(err).to match(/Cannot use local override for myrack-0.8 because #{Regexp.escape(lib_path("local-myrack").to_s)} does not exist/) - - solution = "config unset local.myrack" - expect(err).to match(/Run `bundle #{solution}` to remove the local override/) - - bundle solution - bundle :install - - expect(err).to be_empty - end - - it "explodes and gives correct solution if branch is not given on install" do - build_git "myrack", "0.8" - FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle :install, raise_on_error: false - expect(err).to match(/Cannot use local override for myrack-0.8 at #{Regexp.escape(lib_path("local-myrack").to_s)} because :branch is not specified in Gemfile/) - - solution = "config unset local.myrack" - expect(err).to match(/Specify a branch or run `bundle #{solution}` to remove the local override/) - - bundle solution - bundle :install - - expect(err).to be_empty - end - - it "does not explode if disable_local_branch_check is given" do - build_git "myrack", "0.8" - FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle %(config set disable_local_branch_check true) - bundle :install - expect(out).to match(/Bundle complete!/) - end - - it "explodes on different branches on install" do - build_git "myrack", "0.8" - - FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - - update_git "myrack", "0.8", path: lib_path("local-myrack"), branch: "another" do |s| - s.write "lib/myrack.rb", "puts :LOCAL" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle :install, raise_on_error: false - expect(err).to match(/is using branch another but Gemfile specifies main/) - end - - it "explodes on invalid revision on install" do - build_git "myrack", "0.8" - - build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| - s.write "lib/myrack.rb", "puts :LOCAL" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle :install, raise_on_error: false - expect(err).to match(/The Gemfile lock is pointing to revision \w+/) - end - - it "does not explode on invalid revision on install" do - build_git "myrack", "0.8" - - build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| - s.write "lib/myrack.rb", "puts :LOCAL" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle %(config set disable_local_revision_check true) - bundle :install - expect(out).to match(/Bundle complete!/) - end - end - - describe "specified inline" do - # TODO: Figure out how to write this test so that it is not flaky depending - # on the current network situation. - # it "supports private git URLs" do - # gemfile <<-G - # gem "thingy", :git => "git@notthere.fallingsnow.net:somebody/thingy.git" - # G - # - # bundle :install - # - # # p out - # # p err - # puts err unless err.empty? # This spec fails randomly every so often - # err.should include("notthere.fallingsnow.net") - # err.should include("ssh") - # end - - it "installs from git even if a newer gem is available elsewhere" do - build_git "myrack", "0.8" - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}" - G - - expect(the_bundle).to include_gems "myrack 0.8" - end - - it "installs dependencies from git even if a newer gem is available elsewhere" do - system_gems "myrack-1.0.0" - - build_lib "myrack", "1.0", path: lib_path("nested/bar") do |s| - s.write "lib/myrack.rb", "puts 'WIN OVERRIDE'" - end - - build_git "foo", path: lib_path("nested") do |s| - s.add_dependency "myrack", "= 1.0" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("nested")}" - G - - run "require 'myrack'" - expect(out).to eq("WIN OVERRIDE") - end - - it "correctly unlocks when changing to a git source" do - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", "0.9.1" - G - - build_git "myrack", path: lib_path("myrack") - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", "1.0.0", :git => "#{lib_path("myrack")}" - G - - expect(the_bundle).to include_gems "myrack 1.0.0" - end - - it "correctly unlocks when changing to a git source without versions" do - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack" - G - - build_git "myrack", "1.2", path: lib_path("myrack") - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack")}" - G - - expect(the_bundle).to include_gems "myrack 1.2" - end - end - - describe "block syntax" do - it "pulls all gems from a git block" do - build_lib "omg", path: lib_path("hi2u/omg") - build_lib "hi2u", path: lib_path("hi2u") - - install_gemfile <<-G - source "https://gem.repo1" - path "#{lib_path("hi2u")}" do - gem "omg" - gem "hi2u" - end - G - - expect(the_bundle).to include_gems "omg 1.0", "hi2u 1.0" - end - end - - it "uses a ref if specified" do - build_git "foo" - @revision = revision_for(lib_path("foo-1.0")) - update_git "foo" - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{@revision}" - G - - run <<-RUBY - require 'foo' - puts "WIN" unless defined?(FOO_PREV_REF) - RUBY - - expect(out).to eq("WIN") - end - - it "correctly handles cases with invalid gemspecs" do - build_git "foo" do |s| - s.summary = nil - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - gem "rails", "2.3.2" - G - - expect(the_bundle).to include_gems "foo 1.0" - expect(the_bundle).to include_gems "rails 2.3.2" - end - - it "runs the gemspec in the context of its parent directory" do - build_lib "bar", path: lib_path("foo/bar"), gemspec: false do |s| - s.write lib_path("foo/bar/lib/version.rb"), %(BAR_VERSION = '1.0') - s.write "bar.gemspec", <<-G - $:.unshift Dir.pwd - require 'lib/version' - Gem::Specification.new do |s| - s.name = 'bar' - s.author = 'no one' - s.version = BAR_VERSION - s.summary = 'Bar' - s.files = Dir["lib/**/*.rb"] - end - G - end - - build_git "foo", path: lib_path("foo") do |s| - s.write "bin/foo", "" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "bar", :git => "#{lib_path("foo")}" - gem "rails", "2.3.2" - G - - expect(the_bundle).to include_gems "bar 1.0" - expect(the_bundle).to include_gems "rails 2.3.2" - end - - it "runs the gemspec in the context of its parent directory, when using local overrides" do - build_git "foo", path: lib_path("foo"), gemspec: false do |s| - s.write lib_path("foo/lib/foo/version.rb"), %(FOO_VERSION = '1.0') - s.write "foo.gemspec", <<-G - $:.unshift Dir.pwd - require 'lib/foo/version' - Gem::Specification.new do |s| - s.name = 'foo' - s.author = 'no one' - s.version = FOO_VERSION - s.summary = 'Foo' - s.files = Dir["lib/**/*.rb"] - end - G - end - - gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "https://github.com/gems/foo", branch: "main" - G - - bundle %(config set local.foo #{lib_path("foo")}) - - expect(the_bundle).to include_gems "foo 1.0" - end - - it "installs from git even if a rubygems gem is present" do - build_gem "foo", "1.0", path: lib_path("fake_foo"), to_system: true do |s| - s.write "lib/foo.rb", "raise 'FAIL'" - end - - build_git "foo", "1.0" - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" - G - - expect(the_bundle).to include_gems "foo 1.0" - end - - it "fakes the gem out if there is no gemspec" do - build_git "foo", gemspec: false - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" - gem "rails", "2.3.2" - G - - expect(the_bundle).to include_gems("foo 1.0") - expect(the_bundle).to include_gems("rails 2.3.2") - end - - it "catches git errors and spits out useful output" do - gemfile <<-G - source "https://gem.repo1" - gem "foo", "1.0", :git => "omgomg" - G - - bundle :install, raise_on_error: false - - expect(err).to include("Git error:") - expect(err).to include("fatal") - expect(err).to include("omgomg") - end - - it "works when the gem path has spaces in it" do - build_git "foo", path: lib_path("foo space-1.0") - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo space-1.0")}" - G - - expect(the_bundle).to include_gems "foo 1.0" - end - - it "handles repos that have been force-pushed" do - build_git "forced", "1.0" - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("forced-1.0")}" do - gem 'forced' - end - G - expect(the_bundle).to include_gems "forced 1.0" - - update_git "forced" do |s| - s.write "lib/forced.rb", "FORCED = '1.1'" - end - - bundle "update", all: true - expect(the_bundle).to include_gems "forced 1.1" - - git("reset --hard HEAD^", lib_path("forced-1.0")) - - bundle "update", all: true - expect(the_bundle).to include_gems "forced 1.0" - end - - it "ignores submodules if :submodule is not passed" do - # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ - system(*%W[git config --global protocol.file.allow always]) - - build_git "submodule", "1.0" - build_git "has_submodule", "1.0" do |s| - s.add_dependency "submodule" - end - git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") - git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") - - install_gemfile <<-G, raise_on_error: false - source "https://gem.repo1" - git "#{lib_path("has_submodule-1.0")}" do - gem "has_submodule" - end - G - expect(err).to match(%r{submodule >= 0 could not be found in rubygems repository https://gem.repo1/ or installed locally}) - - expect(the_bundle).not_to include_gems "has_submodule 1.0" - end - - it "handles repos with submodules" do - # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ - system(*%W[git config --global protocol.file.allow always]) - - build_git "submodule", "1.0" - build_git "has_submodule", "1.0" do |s| - s.add_dependency "submodule" - end - git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") - git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("has_submodule-1.0")}", :submodules => true do - gem "has_submodule" - end - G - - expect(the_bundle).to include_gems "has_submodule 1.0" - end - - it "does not warn when deiniting submodules" do - # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ - system(*%W[git config --global protocol.file.allow always]) - - build_git "submodule", "1.0" - build_git "has_submodule", "1.0" - - git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") - git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("has_submodule-1.0")}" do - gem "has_submodule" - end - G - expect(err).to be_empty - - expect(the_bundle).to include_gems "has_submodule 1.0" - expect(the_bundle).to_not include_gems "submodule 1.0" - end - - it "handles implicit updates when modifying the source info" do - git = build_git "foo" - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("foo-1.0")}" do - gem "foo" - end - G - - update_git "foo" - update_git "foo" - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("foo-1.0")}", :ref => "#{git.ref_for("HEAD^")}" do - gem "foo" - end - G - - run <<-RUBY - require 'foo' - puts "WIN" if FOO_PREV_REF == '#{git.ref_for("HEAD^^")}' - RUBY - - expect(out).to eq("WIN") - end - - it "does not do a remote fetch if the revision is cached locally" do - build_git "foo" - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - FileUtils.rm_r(lib_path("foo-1.0")) - - bundle "install" - expect(out).not_to match(/updating/i) - end - - it "doesn't blow up if bundle install is run twice in a row" do - build_git "foo" - - gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - bundle "install" - bundle "install" - end - - it "prints a friendly error if a file blocks the git repo" do - build_git "foo" - - FileUtils.mkdir_p(default_bundle_path) - FileUtils.touch(default_bundle_path("bundler")) - - install_gemfile <<-G, raise_on_error: false - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - expect(last_command).to be_failure - expect(err).to include("Bundler could not install a gem because it " \ - "needs to create a directory, but a file exists " \ - "- #{default_bundle_path("bundler")}") - end - - it "does not duplicate git gem sources" do - build_lib "foo", path: lib_path("nested/foo") - build_lib "bar", path: lib_path("nested/bar") - - build_git "foo", path: lib_path("nested") - build_git "bar", path: lib_path("nested") - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("nested")}" - gem "bar", :git => "#{lib_path("nested")}" - G - - expect(File.read(bundled_app_lock).scan("GIT").size).to eq(1) - end - - describe "switching sources" do - it "doesn't explode when switching Path to Git sources" do - build_gem "foo", "1.0", to_system: true do |s| - s.write "lib/foo.rb", "raise 'fail'" - end - build_lib "foo", "1.0", path: lib_path("bar/foo") - build_git "bar", "1.0", path: lib_path("bar") do |s| - s.add_dependency "foo" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "bar", :path => "#{lib_path("bar")}" - G - - install_gemfile <<-G - source "https://gem.repo1" - gem "bar", :git => "#{lib_path("bar")}" - G - - expect(the_bundle).to include_gems "foo 1.0", "bar 1.0" - end - - it "doesn't explode when switching Gem to Git source" do - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack-obama" - gem "myrack", "1.0.0" - G - - build_git "myrack", "1.0" do |s| - s.write "lib/new_file.rb", "puts 'USING GIT'" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack-obama" - gem "myrack", "1.0.0", :git => "#{lib_path("myrack-1.0")}" - G - - run "require 'new_file'" - expect(out).to eq("USING GIT") - end - - it "doesn't explode when removing an explicit exact version from a git gem with dependencies" do - build_lib "activesupport", "7.1.4", path: lib_path("rails/activesupport") - build_git "rails", "7.1.4", path: lib_path("rails") do |s| - s.add_dependency "activesupport", "= 7.1.4" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "rails", "7.1.4", :git => "#{lib_path("rails")}" - G - - install_gemfile <<-G - source "https://gem.repo1" - gem "rails", :git => "#{lib_path("rails")}" - G - - expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4" - end - - it "doesn't explode when adding an explicit ref to a git gem with dependencies" do - lib_root = lib_path("rails") - - build_lib "activesupport", "7.1.4", path: lib_root.join("activesupport") - build_git "rails", "7.1.4", path: lib_root do |s| - s.add_dependency "activesupport", "= 7.1.4" - end - - old_revision = revision_for(lib_root) - update_git "rails", "7.1.4", path: lib_root - - install_gemfile <<-G - source "https://gem.repo1" - gem "rails", "7.1.4", :git => "#{lib_root}" - G - - install_gemfile <<-G - source "https://gem.repo1" - gem "rails", :git => "#{lib_root}", :ref => "#{old_revision}" - G - - expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4" - end - end - - describe "bundle install after the remote has been updated" do - it "installs" do - build_git "valim" - - install_gemfile <<-G - source "https://gem.repo1" - gem "valim", :git => "#{lib_path("valim-1.0")}" - G - - old_revision = revision_for(lib_path("valim-1.0")) - update_git "valim" - new_revision = revision_for(lib_path("valim-1.0")) - - old_lockfile = File.read(bundled_app_lock) - lockfile(bundled_app_lock, old_lockfile.gsub(/revision: #{old_revision}/, "revision: #{new_revision}")) - - bundle "install" - - run <<-R - require "valim" - puts VALIM_PREV_REF - R - - expect(out).to eq(old_revision) - end - - it "gives a helpful error message when the remote ref no longer exists" do - build_git "foo" - revision = revision_for(lib_path("foo-1.0")) - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{revision}" - G - expect(out).to_not match(/Revision.*does not exist/) - - install_gemfile <<-G, raise_on_error: false - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "deadbeef" - G - expect(err).to include("Revision deadbeef does not exist in the repository") - end - - it "gives a helpful error message when the remote branch no longer exists" do - build_git "foo" - - install_gemfile <<-G, env: { "LANG" => "en" }, raise_on_error: false - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "deadbeef" - G - - expect(err).to include("Revision deadbeef does not exist in the repository") - end - end - - describe "bundle install with deployment mode configured and git sources" do - it "works" do - build_git "valim", path: lib_path("valim") - - install_gemfile <<-G - source "https://gem.repo1" - gem "valim", "= 1.0", :git => "#{lib_path("valim")}" - G - - pristine_system_gems - - bundle_config "deployment true" - bundle :install - end - end - - describe "gem install hooks" do - it "runs pre-install hooks" do - build_git "foo" - gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - File.open(lib_path("install_hooks.rb"), "w") do |h| - h.write <<-H - Gem.pre_install_hooks << lambda do |inst| - STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}" - end - H - end - - bundle :install, - requires: [lib_path("install_hooks.rb")] - expect(err_without_deprecations).to eq("Ran pre-install hook: foo-1.0") - end - - it "runs post-install hooks" do - build_git "foo" - gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - File.open(lib_path("install_hooks.rb"), "w") do |h| - h.write <<-H - Gem.post_install_hooks << lambda do |inst| - STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}" - end - H - end - - bundle :install, - requires: [lib_path("install_hooks.rb")] - expect(err_without_deprecations).to eq("Ran post-install hook: foo-1.0") - end - - it "complains if the install hook fails" do - build_git "foo" - gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - File.open(lib_path("install_hooks.rb"), "w") do |h| - h.write <<-H - Gem.pre_install_hooks << lambda do |inst| - false - end - H - end - - bundle :install, requires: [lib_path("install_hooks.rb")], raise_on_error: false - expect(err).to include("failed for foo-1.0") - end - end - - context "with an extension" do - it "installs the extension" do - build_git "foo" do |s| - s.add_dependency "rake" - s.extensions << "Rakefile" - s.write "Rakefile", <<-RUBY - task :default do - path = File.expand_path("lib", __dir__) - FileUtils.mkdir_p(path) - File.open("\#{path}/foo.rb", "w") do |f| - f.puts "FOO = 'YES'" - end - end - RUBY - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - run <<-R - require 'foo' - puts FOO - R - expect(out).to eq("YES") - - run <<-R - puts $:.grep(/ext/) - R - expect(out).to include(Pathname.glob(default_bundle_path("bundler/gems/extensions/**/foo-1.0-*")).first.to_s) - end - - it "does not use old extension after ref changes" do - git_reader = build_git "foo", no_default: true do |s| - s.extensions = ["ext/extconf.rb"] - s.write "ext/extconf.rb", <<-RUBY - require "mkmf" - create_makefile("foo") - RUBY - s.write "ext/foo.c", "void Init_foo() {}" - end - - 2.times do |i| - File.open(git_reader.path.join("ext/foo.c"), "w") do |file| - file.write <<-C - #include "ruby.h" - VALUE foo(VALUE self) { return INT2FIX(#{i}); } - void Init_foo() { rb_define_global_function("foo", &foo, 0); } - C - end - git("commit -m \"commit for iteration #{i}\" ext/foo.c", git_reader.path) - - git_commit_sha = git_reader.ref_for("HEAD") - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{git_commit_sha}" - G - - run <<-R - require 'foo' - puts foo - R - - expect(out).to eq(i.to_s) - end - end - - it "does not prompt to gem install if extension fails" do - build_git "foo" do |s| - s.add_dependency "rake" - s.extensions << "Rakefile" - s.write "Rakefile", <<-RUBY - task :default do - raise - end - RUBY - end - - install_gemfile <<-G, raise_on_error: false - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - expect(err).to end_with(<<-M.strip) -An error occurred while installing foo (1.0), and Bundler cannot continue. - -In Gemfile: - foo - M - expect(out).not_to include("gem install foo") - end - - it "does not reinstall the extension" do - build_git "foo" do |s| - s.add_dependency "rake" - s.extensions << "Rakefile" - s.write "Rakefile", <<-RUBY - task :default do - path = File.expand_path("lib", __dir__) - FileUtils.mkdir_p(path) - cur_time = Time.now.to_f.to_s - File.open("\#{path}/foo.rb", "w") do |f| - f.puts "FOO = \#{cur_time}" - end - end - RUBY - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - run <<-R - require 'foo' - puts FOO - R - - installed_time = out - expect(installed_time).to match(/\A\d+\.\d+\z/) - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - run <<-R - require 'foo' - puts FOO - R - expect(out).to eq(installed_time) - end - - it "does not reinstall the extension when changing another gem" do - build_git "foo" do |s| - s.add_dependency "rake" - s.extensions << "Rakefile" - s.write "Rakefile", <<-RUBY - task :default do - path = File.expand_path("lib", __dir__) - FileUtils.mkdir_p(path) - cur_time = Time.now.to_f.to_s - File.open("\#{path}/foo.rb", "w") do |f| - f.puts "FOO = \#{cur_time}" - end - end - RUBY - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", "0.9.1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - run <<-R - require 'foo' - puts FOO - R - - installed_time = out - expect(installed_time).to match(/\A\d+\.\d+\z/) - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", "1.0.0" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - run <<-R - require 'foo' - puts FOO - R - expect(out).to eq(installed_time) - end - - it "does reinstall the extension when changing refs" do - build_git "foo" do |s| - s.add_dependency "rake" - s.extensions << "Rakefile" - s.write "Rakefile", <<-RUBY - task :default do - path = File.expand_path("lib", __dir__) - FileUtils.mkdir_p(path) - cur_time = Time.now.to_f.to_s - File.open("\#{path}/foo.rb", "w") do |f| - f.puts "FOO = \#{cur_time}" - end - end - RUBY - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - run <<-R - require 'foo' - puts FOO - R - - installed_time = out - - update_git("foo", branch: "branch2") - - expect(installed_time).to match(/\A\d+\.\d+\z/) - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "branch2" - G - - run <<-R - require 'foo' - puts FOO - R - expect(out).not_to eq(installed_time) - - installed_time = out - - update_git("foo") - bundle "update foo" - - run <<-R - require 'foo' - puts FOO - R - expect(out).not_to eq(installed_time) - end - end - - it "ignores git environment variables" do - build_git "xxxxxx" do |s| - s.executables = "xxxxxxbar" - end - - Bundler::SharedHelpers.with_clean_git_env do - ENV["GIT_DIR"] = "bar" - ENV["GIT_WORK_TREE"] = "bar" - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("xxxxxx-1.0")}" do - gem 'xxxxxx' - end - G - - expect(ENV["GIT_DIR"]).to eq("bar") - expect(ENV["GIT_WORK_TREE"]).to eq("bar") - end - end - - describe "without git installed" do - it "prints a better error message when installing" do - gemfile <<-G - source "https://gem.repo1" - - gem "rake", git: "https://github.com/ruby/rake" - G - - lockfile <<-L - GIT - remote: https://github.com/ruby/rake - revision: 5c60da8644a9e4f655e819252e3b6ca77f42b7af - specs: - rake (13.0.6) - - GEM - remote: https://rubygems.org/ - specs: - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - rake! - - BUNDLED WITH - #{Bundler::VERSION} - L - - with_path_as("") do - bundle "install", raise_on_error: false - end - expect(err). - to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git") - end - - it "prints a better error message when updating" do - build_git "foo" - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("foo-1.0")}" do - gem 'foo' - end - G - - with_path_as("") do - bundle "update", all: true, raise_on_error: false - end - expect(err). - to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git") - end - - it "doesn't need git in the new machine if an installed git gem is copied to another machine" do - build_git "foo" - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("foo-1.0")}" do - gem 'foo' - end - G - bundle_config_global "path vendor/bundle" - bundle :install - pristine_system_gems - - bundle "install", env: { "PATH" => "" } - expect(out).to_not include("You need to install git to be able to use gems from git repositories.") - end - end - - describe "when the git source is overridden with a local git repo" do - before do - bundle_config_global "local.foo #{lib_path("foo")}" - end - - describe "and git output is colorized" do - before do - File.open("#{ENV["HOME"]}/.gitconfig", "w") do |f| - f.write("[color]\n\tui = always\n") - end - end - - it "installs successfully" do - build_git "foo", "1.0", path: lib_path("foo") - - gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo")}", :branch => "main" - G - - bundle :install - expect(the_bundle).to include_gems "foo 1.0" - end - end - end - - context "git sources that include credentials" do - context "that are username and password" do - let(:credentials) { "user1:password1" } - - it "does not display the password" do - install_gemfile <<-G, raise_on_error: false - source "https://gem.repo1" - git "https://#{credentials}@github.com/company/private-repo" do - gem "foo" - end - G - - expect(stdboth).to_not include("password1") - expect(out).to include("Fetching https://user1@github.com/company/private-repo") - end - end - - context "that is an oauth token" do - let(:credentials) { "oauth_token" } - - it "displays the oauth scheme but not the oauth token" do - install_gemfile <<-G, raise_on_error: false - source "https://gem.repo1" - git "https://#{credentials}:x-oauth-basic@github.com/company/private-repo" do - gem "foo" - end - G - - expect(stdboth).to_not include("oauth_token") - expect(out).to include("Fetching https://x-oauth-basic@github.com/company/private-repo") - end - end - end end diff --git a/spec/bundler/support/shards.rb b/spec/bundler/support/shards.rb index 5718e775b23a9b..887f248e2a020c 100644 --- a/spec/bundler/support/shards.rb +++ b/spec/bundler/support/shards.rb @@ -57,6 +57,8 @@ module Shards ], shard_b: [ "spec/install/gemfile/git_spec.rb", + "spec/install/gemfile/git_overrides_spec.rb", + "spec/install/gemfile/git_extensions_spec.rb", "spec/install/gems/standalone_spec.rb", "spec/commands/lock_spec.rb", "spec/cache/gems_spec.rb", From 9ed115039be1265990e0de12386f8cac579d8b53 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 3 Jul 2026 17:16:19 +0900 Subject: [PATCH 24/47] [ruby/rubygems] Assign plugin/unloaded_source_spec.rb to a shard Its before(:context) shard warning fires on every run since the file was added without a shards.rb entry. https://github.com/ruby/rubygems/commit/f9879f9015 Co-Authored-By: Claude Fable 5 --- spec/bundler/support/shards.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/bundler/support/shards.rb b/spec/bundler/support/shards.rb index 887f248e2a020c..f3ef8f142390ca 100644 --- a/spec/bundler/support/shards.rb +++ b/spec/bundler/support/shards.rb @@ -197,6 +197,7 @@ module Shards "spec/install/gemfile/override_spec.rb", "spec/install/path_spec.rb", "spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb", + "spec/bundler/plugin/unloaded_source_spec.rb", ], }.freeze end From 03b091b890db3fb41e1813ef5f2aa9f74901befd Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 3 Jul 2026 17:43:45 +0900 Subject: [PATCH 25/47] [ruby/rubygems] Split remaining heavy spec files for parallel execution Recorded per-file runtimes show outdated_spec.rb, newgem_spec.rb and update_spec.rb each exceed the ideal per-worker load of a parallel run, and setup_spec.rb dominates standalone runs, so file-based distribution cannot go faster without splitting them. Blocks move verbatim and example sets are unchanged. newgem's shared examples move together with their consumers, with its top-level helpers duplicated into both files. https://github.com/ruby/rubygems/commit/5d40766029 Co-Authored-By: Claude Fable 5 --- spec/bundler/commands/newgem_options_spec.rb | 1454 +++++++++++++++++ spec/bundler/commands/newgem_spec.rb | 1389 ---------------- .../bundler/commands/outdated_filters_spec.rb | 629 +++++++ spec/bundler/commands/outdated_spec.rb | 626 ------- .../bundler/commands/update_scenarios_spec.rb | 1100 +++++++++++++ spec/bundler/commands/update_spec.rb | 1099 ------------- spec/bundler/runtime/setup_gems_spec.rb | 825 ++++++++++ spec/bundler/runtime/setup_spec.rb | 820 ---------- spec/bundler/support/shards.rb | 4 + 9 files changed, 4012 insertions(+), 3934 deletions(-) create mode 100644 spec/bundler/commands/newgem_options_spec.rb create mode 100644 spec/bundler/commands/outdated_filters_spec.rb create mode 100644 spec/bundler/commands/update_scenarios_spec.rb create mode 100644 spec/bundler/runtime/setup_gems_spec.rb diff --git a/spec/bundler/commands/newgem_options_spec.rb b/spec/bundler/commands/newgem_options_spec.rb new file mode 100644 index 00000000000000..0b2e6af14325fb --- /dev/null +++ b/spec/bundler/commands/newgem_options_spec.rb @@ -0,0 +1,1454 @@ +# frozen_string_literal: true + +RSpec.describe "bundle gem" do + def gem_skeleton_assertions + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to exist + expect(bundled_app("#{gem_name}/README.md")).to exist + expect(bundled_app("#{gem_name}/Gemfile")).to exist + expect(bundled_app("#{gem_name}/Rakefile")).to exist + expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb")).to exist + expect(bundled_app("#{gem_name}/lib/#{gem_name}/version.rb")).to exist + + expect(ignore_paths).to include("bin/") + expect(ignore_paths).to include("Gemfile") + end + + def bundle_exec_rubocop + prepare_gemspec(bundled_app(gem_name, "#{gem_name}.gemspec")) + bundle "config set path #{rubocop_gem_path}", dir: bundled_app(gem_name) + bundle "exec rubocop --debug --config .rubocop.yml", dir: bundled_app(gem_name) + end + + def bundle_exec_standardrb + prepare_gemspec(bundled_app(gem_name, "#{gem_name}.gemspec")) + bundle "config set path #{standard_gem_path}", dir: bundled_app(gem_name) + bundle "exec standardrb --debug", dir: bundled_app(gem_name) + end + + def ignore_paths + generated = bundled_app("#{gem_name}/#{gem_name}.gemspec").read + matched = generated.match(/^\s+f\.start_with\?\(\*%w\[(?.*)\]\)$/) + matched[:ignored]&.split(" ") + end + + def installed_go? + sys_exec("go version", raise_on_error: true) + true + rescue StandardError + false + end + + let(:generated_gemspec) { Bundler.load_gemspec_uncached(bundled_app(gem_name).join("#{gem_name}.gemspec")) } + + let(:gem_name) { "mygem" } + + before do + # Write the global git config directly instead of shelling out to `git + # config --global` three times per example: this `before` runs for every + # example in the file, and each `git` call is a separate subprocess. + File.write(home(".gitconfig"), <<~GITCONFIG) + [user] + name = Bundler User + email = user@example.com + [github] + user = bundleuser + GITCONFIG + + bundle_config_global "gem.mit false" + bundle_config_global "gem.test false" + bundle_config_global "gem.coc false" + bundle_config_global "gem.linter false" + bundle_config_global "gem.ci false" + bundle_config_global "gem.changelog false" + bundle_config_global "gem.bundle false" + end + + shared_examples_for "--mit flag" do + before do + bundle "gem #{gem_name} --mit" + end + it "generates a gem skeleton with MIT license" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/LICENSE.txt")).to exist + expect(generated_gemspec.license).to eq("MIT") + end + end + + shared_examples_for "--no-mit flag" do + before do + bundle "gem #{gem_name} --no-mit" + end + it "generates a gem skeleton without MIT license" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/LICENSE.txt")).to_not exist + end + end + + shared_examples_for "--coc flag" do + it "generates a gem skeleton with MIT license" do + bundle "gem #{gem_name} --coc" + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/CODE_OF_CONDUCT.md")).to exist + end + + it "generates the README with a section for the Code of Conduct" do + bundle "gem #{gem_name} --coc" + expect(bundled_app("#{gem_name}/README.md").read).to include("## Code of Conduct") + expect(bundled_app("#{gem_name}/README.md").read).to match(%r{https://github\.com/bundleuser/#{gem_name}/blob/.*/CODE_OF_CONDUCT.md}) + end + + it "generates the README with a section for the Code of Conduct, respecting the configured git default branch", git: ">= 2.28.0" do + git("config --global init.defaultBranch main") + bundle "gem #{gem_name} --coc" + + expect(bundled_app("#{gem_name}/README.md").read).to include("## Code of Conduct") + expect(bundled_app("#{gem_name}/README.md").read).to include("https://github.com/bundleuser/#{gem_name}/blob/main/CODE_OF_CONDUCT.md") + end + end + + shared_examples_for "--no-coc flag" do + before do + bundle "gem #{gem_name} --no-coc" + end + it "generates a gem skeleton without Code of Conduct" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/CODE_OF_CONDUCT.md")).to_not exist + end + + it "generates the README without a section for the Code of Conduct" do + expect(bundled_app("#{gem_name}/README.md").read).not_to include("## Code of Conduct") + expect(bundled_app("#{gem_name}/README.md").read).not_to match(%r{https://github\.com/bundleuser/#{gem_name}/blob/.*/CODE_OF_CONDUCT.md}) + end + end + + shared_examples_for "--changelog flag" do + before do + bundle "gem #{gem_name} --changelog" + end + it "generates a gem skeleton with a CHANGELOG" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/CHANGELOG.md")).to exist + end + end + + shared_examples_for "--no-changelog flag" do + before do + bundle "gem #{gem_name} --no-changelog" + end + it "generates a gem skeleton without a CHANGELOG" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/CHANGELOG.md")).to_not exist + end + end + + shared_examples_for "--bundle flag" do + before do + bundle "gem #{gem_name} --bundle" + end + it "generates a gem skeleton with bundle install" do + gem_skeleton_assertions + expect(out).to include("Running bundle install in the new gem directory.") + end + end + + shared_examples_for "--no-bundle flag" do + before do + bundle "gem #{gem_name} --no-bundle" + end + it "generates a gem skeleton without bundle install" do + gem_skeleton_assertions + expect(out).to_not include("Running bundle install in the new gem directory.") + end + end + + shared_examples_for "--linter=rubocop flag" do + before do + bundle "gem #{gem_name} --linter=rubocop" + end + + it "generates a gem skeleton with rubocop" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/Rakefile")).to read_as( + include("# frozen_string_literal: true"). + and(include('require "rubocop/rake_task"'). + and(include("RuboCop::RakeTask.new"). + and(match(/default:.+:rubocop/)))) + ) + end + + it "includes rubocop in generated Gemfile" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + rubocop_dep = builder.dependencies.find {|d| d.name == "rubocop" } + expect(rubocop_dep).not_to be_specific + expect(rubocop_dep.requirement).to eq(Gem::Requirement.new([">= 0"])) + end + + it "generates a default .rubocop.yml" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist + end + + it "includes .rubocop.yml into ignore list" do + expect(ignore_paths).to include(".rubocop.yml") + end + end + + shared_examples_for "--linter=standard flag" do + before do + bundle "gem #{gem_name} --linter=standard" + end + + it "generates a gem skeleton with standard" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/Rakefile")).to read_as( + include('require "standard/rake"'). + and(match(/default:.+:standard/)) + ) + end + + it "includes standard in generated Gemfile" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + standard_dep = builder.dependencies.find {|d| d.name == "standard" } + expect(standard_dep).not_to be_specific + expect(standard_dep.requirement).to eq(Gem::Requirement.new([">= 0"])) + end + + it "generates a default .standard.yml" do + expect(bundled_app("#{gem_name}/.standard.yml")).to exist + end + + it "includes .standard.yml into ignore list" do + expect(ignore_paths).to include(".standard.yml") + end + end + + shared_examples_for "--no-linter flag" do + define_negated_matcher :exclude, :include + + before do + bundle "gem #{gem_name} --no-linter" + end + + it "generates a gem skeleton without rubocop" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/Rakefile")).to read_as(exclude("rubocop")) + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to read_as(exclude("rubocop")) + end + + it "does not include rubocop in generated Gemfile" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + rubocop_dep = builder.dependencies.find {|d| d.name == "rubocop" } + expect(rubocop_dep).to be_nil + end + + it "does not include standard in generated Gemfile" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + standard_dep = builder.dependencies.find {|d| d.name == "standard" } + expect(standard_dep).to be_nil + end + + it "doesn't generate a default .rubocop.yml" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + end + + it "does not add .rubocop.yml into ignore list" do + expect(ignore_paths).not_to include(".rubocop.yml") + end + + it "doesn't generate a default .standard.yml" do + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "does not add .standard.yml into ignore list" do + expect(ignore_paths).not_to include(".standard.yml") + end + end + + shared_examples_for "CI config is absent" do + it "does not create any CI files" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist + end + end + + shared_examples_for "--github-username option" do |github_username| + before do + bundle "gem #{gem_name} --github-username=#{github_username}" + end + + it "generates a gem skeleton" do + gem_skeleton_assertions + end + + it "contribute URL set to given github username" do + expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]") + expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/#{github_username}") + end + end + + shared_examples_for "github_username configuration" do + context "with github_username setting set to some value" do + before do + bundle_config_global "gem.github_username different_username" + bundle "gem #{gem_name}" + end + + it "generates a gem skeleton" do + gem_skeleton_assertions + end + + it "contribute URL set to bundle config setting" do + expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]") + expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/different_username") + end + end + + context "with github_username setting set to false" do + before do + bundle_config_global "gem.github_username false" + bundle "gem #{gem_name}" + end + + it "generates a gem skeleton" do + gem_skeleton_assertions + end + + it "contribute URL set to [USERNAME]" do + expect(bundled_app("#{gem_name}/README.md").read).to include("[USERNAME]") + expect(bundled_app("#{gem_name}/README.md").read).not_to include("github.com/bundleuser") + end + end + end + + context "--ci with no argument" do + before do + bundle "gem #{gem_name}" + end + + it "does not generate any CI config" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist + end + + it "does not add any CI config files into ignore list" do + expect(ignore_paths).not_to include(".github/") + expect(ignore_paths).not_to include(".gitlab-ci.yml") + expect(ignore_paths).not_to include(".circleci/") + end + end + + context "--ci set to github" do + before do + bundle "gem #{gem_name} --ci=github" + end + + it "generates a GitHub Actions config file" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist + end + + it "includes .github/ into ignore list" do + expect(ignore_paths).to include(".github/") + end + end + + context "--ci set to gitlab" do + before do + bundle "gem #{gem_name} --ci=gitlab" + end + + it "generates a GitLab CI config file" do + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist + end + + it "includes .gitlab-ci.yml into ignore list" do + expect(ignore_paths).to include(".gitlab-ci.yml") + end + end + + context "--ci set to circle" do + before do + bundle "gem #{gem_name} --ci=circle" + end + + it "generates a CircleCI config file" do + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist + end + + it "includes .circleci/ into ignore list" do + expect(ignore_paths).to include(".circleci/") + end + end + + context "--ci set to an invalid value" do + before do + bundle "gem #{gem_name} --ci=foo", raise_on_error: false + end + + it "fails loudly" do + expect(last_command).to be_failure + expect(err).to match(/Expected '--ci' to be one of .*; got foo/) + end + end + + context "gem.ci setting set to none" do + it "doesn't generate any CI config" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist + end + end + + context "gem.ci setting set to github" do + it "generates a GitHub Actions config file" do + bundle_config "gem.ci github" + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist + end + end + + context "gem.ci setting set to gitlab" do + it "generates a GitLab CI config file" do + bundle_config "gem.ci gitlab" + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist + end + end + + context "gem.ci setting set to circle" do + it "generates a CircleCI config file" do + bundle_config "gem.ci circle" + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist + end + end + + context "gem.ci set to github and --ci with no arguments" do + before do + bundle_config "gem.ci github" + bundle "gem #{gem_name} --ci" + end + + it "generates a GitHub Actions config file" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist + end + + it "hints that --ci is already configured" do + expect(out).to match("github is already configured, ignoring --ci flag.") + end + end + + context "gem.ci setting set to false and --ci with no arguments", :readline do + before do + bundle_config "gem.ci false" + bundle "gem #{gem_name} --ci" do |input, _, _| + input.puts "github" + end + end + + it "asks to setup CI" do + expect(out).to match("Do you want to set up continuous integration for your gem?") + end + + it "hints that the choice will only be applied to the current gem" do + expect(out).to match("Your choice will only be applied to this gem.") + end + end + + context "gem.ci setting not set and --ci with no arguments", :readline do + before do + bundle_config_global "BUNDLE_GEM__CI" => nil + bundle "gem #{gem_name} --ci" do |input, _, _| + input.puts "github" + end + end + + it "asks to setup CI" do + expect(out).to match("Do you want to set up continuous integration for your gem?") + end + + it "hints that the choice will be applied to future bundle gem calls" do + hint = "Future `bundle gem` calls will use your choice. " \ + "This setting can be changed anytime with `bundle config gem.ci`." + expect(out).to match(hint) + end + end + + context "gem.ci setting set to a CI service and --no-ci" do + before do + bundle_config "gem.ci github" + bundle "gem #{gem_name} --no-ci" + end + + it "does not generate any CI config" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist + end + end + + context "--linter with no argument" do + before do + bundle "gem #{gem_name}" + end + + it "does not generate any linter config" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "does not add any linter config files into ignore list" do + expect(ignore_paths).not_to include(".rubocop.yml") + expect(ignore_paths).not_to include(".standard.yml") + end + end + + context "--linter set to rubocop" do + before do + bundle "gem #{gem_name} --linter=rubocop" + end + + it "generates a RuboCop config" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "includes .rubocop.yml into ignore list" do + expect(ignore_paths).to include(".rubocop.yml") + expect(ignore_paths).not_to include(".standard.yml") + end + end + + context "--linter set to standard" do + before do + bundle "gem #{gem_name} --linter=standard" + end + + it "generates a Standard config" do + expect(bundled_app("#{gem_name}/.standard.yml")).to exist + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + end + + it "includes .standard.yml into ignore list" do + expect(ignore_paths).to include(".standard.yml") + expect(ignore_paths).not_to include(".rubocop.yml") + end + end + + context "--linter set to an invalid value" do + before do + bundle "gem #{gem_name} --linter=foo", raise_on_error: false + end + + it "fails loudly" do + expect(last_command).to be_failure + expect(err).to match(/Expected '--linter' to be one of .*; got foo/) + end + end + + context "gem.linter setting set to none" do + before do + bundle "gem #{gem_name}" + end + + it "doesn't generate any linter config" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "does not add any linter config files into ignore list" do + expect(ignore_paths).not_to include(".rubocop.yml") + expect(ignore_paths).not_to include(".standard.yml") + end + end + + context "gem.linter setting set to rubocop" do + before do + bundle_config "gem.linter rubocop" + bundle "gem #{gem_name}" + end + + it "generates a RuboCop config file" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist + end + + it "includes .rubocop.yml into ignore list" do + expect(ignore_paths).to include(".rubocop.yml") + end + end + + context "gem.linter setting set to standard" do + before do + bundle_config "gem.linter standard" + bundle "gem #{gem_name}" + end + + it "generates a Standard config file" do + expect(bundled_app("#{gem_name}/.standard.yml")).to exist + end + + it "includes .standard.yml into ignore list" do + expect(ignore_paths).to include(".standard.yml") + end + end + + context "gem.linter set to rubocop and --linter with no arguments" do + before do + bundle_config "gem.linter rubocop" + bundle "gem #{gem_name} --linter" + end + + it "generates a RuboCop config file" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist + end + + it "includes .rubocop.yml into ignore list" do + expect(ignore_paths).to include(".rubocop.yml") + end + + it "hints that --linter is already configured" do + expect(out).to match("rubocop is already configured, ignoring --linter flag.") + end + end + + context "gem.linter setting set to false and --linter with no arguments", :readline do + before do + bundle_config "gem.linter false" + bundle "gem #{gem_name} --linter" do |input, _, _| + input.puts "rubocop" + end + end + + it "asks to setup a linter" do + expect(out).to match("Do you want to add a code linter and formatter to your gem?") + end + + it "hints that the choice will only be applied to the current gem" do + expect(out).to match("Your choice will only be applied to this gem.") + end + end + + context "gem.linter setting not set and --linter with no arguments", :readline do + before do + bundle_config_global "BUNDLE_GEM__LINTER" => nil + bundle "gem #{gem_name} --linter" do |input, _, _| + input.puts "rubocop" + end + end + + it "asks to setup a linter" do + expect(out).to match("Do you want to add a code linter and formatter to your gem?") + end + + it "hints that the choice will be applied to future bundle gem calls" do + hint = "Future `bundle gem` calls will use your choice. " \ + "This setting can be changed anytime with `bundle config gem.linter`." + expect(out).to match(hint) + end + end + + context "gem.linter setting set to a linter and --no-linter" do + before do + bundle_config "gem.linter rubocop" + bundle "gem #{gem_name} --no-linter" + end + + it "does not generate any linter config" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "does not add any linter config files into ignore list" do + expect(ignore_paths).not_to include(".rubocop.yml") + expect(ignore_paths).not_to include(".standard.yml") + end + end + + context "--edit option" do + it "opens the generated gemspec in the user's text editor" do + output = bundle "gem #{gem_name} --edit=echo" + gemspec_path = File.join(bundled_app, gem_name, "#{gem_name}.gemspec") + expect(output).to include("echo \"#{gemspec_path}\"") + end + end + + shared_examples_for "paths that depend on gem name" do + it "generates entrypoint, version file and signatures file at the proper path, with the proper content" do + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb")).to exist + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(%r{require_relative "#{require_relative_path}/version"}) + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/class Error < StandardError; end$/) + + expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb")).to exist + expect(bundled_app("#{gem_name}/sig/#{require_path}.rbs")).to exist + end + + context "--exe parameter set" do + before do + bundle "gem #{gem_name} --exe" + end + + it "builds an exe file that requires the proper entrypoint" do + expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist + expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/) + end + end + + context "--bin parameter set" do + before do + bundle "gem #{gem_name} --bin" + end + + it "builds an exe file that requires the proper entrypoint" do + expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist + expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/) + end + end + + context "--test parameter set to rspec" do + before do + bundle "gem #{gem_name} --test=rspec" + end + + it "builds a spec helper that requires the proper entrypoint, and a default test in the proper path which fails" do + expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist + expect(bundled_app("#{gem_name}/spec/spec_helper.rb").read).to include(%(require "#{require_path}")) + expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb")).to exist + expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb").read).to include("expect(false).to eq(true)") + end + end + + context "--test parameter set to minitest" do + before do + bundle "gem #{gem_name} --test=minitest" + end + + it "builds a test helper that requires the proper entrypoint, and default test file in the proper path that defines the proper test class name, requires helper, and fails" do + expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist + expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}")) + + expect(bundled_app("#{gem_name}/#{minitest_test_file_path}")).to exist + expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include(minitest_test_class_name) + expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include(%(require "test_helper")) + expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include("assert false") + end + end + + context "--test parameter set to test-unit" do + before do + bundle "gem #{gem_name} --test=test-unit" + end + + it "builds a test helper that requires the proper entrypoint, and default test file in the proper path which requires helper and fails" do + expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist + expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}")) + expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb")).to exist + expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include(%(require "test_helper")) + expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include("assert_equal(\"expected\", \"actual\")") + end + end + end + + context "with mit option in bundle config settings set to true" do + before do + bundle_config_global "gem.mit true" + end + it_behaves_like "--mit flag" + it_behaves_like "--no-mit flag" + end + + context "with mit option in bundle config settings set to false" do + before do + bundle_config_global "gem.mit false" + end + it_behaves_like "--mit flag" + it_behaves_like "--no-mit flag" + end + + context "with coc option in bundle config settings set to true" do + before do + bundle_config_global "gem.coc true" + end + it_behaves_like "--coc flag" + it_behaves_like "--no-coc flag" + end + + context "with coc option in bundle config settings set to false" do + before do + bundle_config_global "gem.coc false" + end + it_behaves_like "--coc flag" + it_behaves_like "--no-coc flag" + end + + context "with rubocop option in bundle config settings set to true" do + before do + bundle_config_global "gem.rubocop true" + end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end + + context "with rubocop option in bundle config settings set to false" do + before do + bundle_config_global "gem.rubocop false" + end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end + + context "with linter option in bundle config settings set to rubocop" do + before do + bundle_config_global "gem.linter rubocop" + end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end + + context "with linter option in bundle config settings set to standard" do + before do + bundle_config_global "gem.linter standard" + end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end + + context "with linter option in bundle config settings set to false" do + before do + bundle_config_global "gem.linter false" + end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end + + context "with changelog option in bundle config settings set to true" do + before do + bundle_config_global "gem.changelog true" + end + it_behaves_like "--changelog flag" + it_behaves_like "--no-changelog flag" + end + + context "with changelog option in bundle config settings set to false" do + before do + bundle_config_global "gem.changelog false" + end + it_behaves_like "--changelog flag" + it_behaves_like "--no-changelog flag" + end + + context "with bundle option in bundle config settings set to true" do + before do + bundle_config_global "gem.bundle true" + end + it_behaves_like "--bundle flag" + it_behaves_like "--no-bundle flag" + + it "runs bundle install" do + bundle "gem #{gem_name}" + expect(out).to include("Running bundle install in the new gem directory.") + end + end + + context "with bundle option in bundle config settings set to false" do + before do + bundle_config_global "gem.bundle false" + end + it_behaves_like "--bundle flag" + it_behaves_like "--no-bundle flag" + + it "does not run bundle install" do + bundle "gem #{gem_name}" + expect(out).to_not include("Running bundle install in the new gem directory.") + end + end + + context "without git config github.user set" do + before do + git("config --global --unset github.user") + end + context "with github-username option in bundle config settings set to some value" do + before do + bundle_config_global "gem.github_username different_username" + end + it_behaves_like "--github-username option", "gh_user" + end + + it_behaves_like "github_username configuration" + + context "with github-username option in bundle config settings set to false" do + before do + bundle_config_global "gem.github_username false" + end + it_behaves_like "--github-username option", "gh_user" + end + + context "when changelog is enabled" do + it "sets gemspec changelog_uri, homepage, homepage_uri, source_code_uri to TODOs" do + bundle "gem #{gem_name} --changelog" + + expect(generated_gemspec.metadata["changelog_uri"]). + to eq("TODO: Put your gem's CHANGELOG.md URL here.") + expect(generated_gemspec.homepage).to eq("TODO: Put your gem's website or public repo URL here.") + expect(generated_gemspec.metadata["homepage_uri"]).to eq("TODO: Put your gem's website or public repo URL here.") + expect(generated_gemspec.metadata["source_code_uri"]).to eq("TODO: Put your gem's public repo URL here.") + end + end + + context "when changelog is not enabled" do + it "sets gemspec homepage, homepage_uri, source_code_uri to TODOs and changelog_uri to nil" do + bundle "gem #{gem_name}" + + expect(generated_gemspec.metadata["changelog_uri"]).to be_nil + expect(generated_gemspec.homepage).to eq("TODO: Put your gem's website or public repo URL here.") + expect(generated_gemspec.metadata["homepage_uri"]).to eq("TODO: Put your gem's website or public repo URL here.") + expect(generated_gemspec.metadata["source_code_uri"]).to eq("TODO: Put your gem's public repo URL here.") + end + end + end + + context "with git config github.user set" do + context "with github-username option in bundle config settings set to some value" do + before do + bundle_config_global "gem.github_username different_username" + end + it_behaves_like "--github-username option", "gh_user" + end + + it_behaves_like "github_username configuration" + + context "with github-username option in bundle config settings set to false" do + before do + bundle_config_global "gem.github_username false" + end + it_behaves_like "--github-username option", "gh_user" + end + + context "when changelog is enabled" do + it "sets gemspec changelog_uri, homepage, homepage_uri, source_code_uri based on git username" do + bundle "gem #{gem_name} --changelog" + + expect(generated_gemspec.metadata["changelog_uri"]). + to eq("https://github.com/bundleuser/#{gem_name}/blob/main/CHANGELOG.md") + expect(generated_gemspec.homepage).to eq("https://github.com/bundleuser/#{gem_name}") + expect(generated_gemspec.metadata["homepage_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") + expect(generated_gemspec.metadata["source_code_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") + end + end + + context "when changelog is not enabled" do + it "sets gemspec source_code_uri, homepage, homepage_uri but not changelog_uri" do + bundle "gem #{gem_name}" + + expect(generated_gemspec.metadata["changelog_uri"]).to be_nil + expect(generated_gemspec.homepage).to eq("https://github.com/bundleuser/#{gem_name}") + expect(generated_gemspec.metadata["homepage_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") + expect(generated_gemspec.metadata["source_code_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") + end + end + end + + context "standard gem naming" do + let(:require_path) { gem_name } + + let(:require_relative_path) { gem_name } + + let(:minitest_test_file_path) { "test/test_#{gem_name}.rb" } + + let(:minitest_test_class_name) { "class TestMygem < Minitest::Test" } + + include_examples "paths that depend on gem name" + end + + context "gem naming with underscore" do + let(:gem_name) { "test_gem" } + + let(:require_path) { "test_gem" } + + let(:require_relative_path) { "test_gem" } + + let(:minitest_test_file_path) { "test/test_test_gem.rb" } + + let(:minitest_test_class_name) { "class TestTestGem < Minitest::Test" } + + let(:flags) { nil } + + it "does not nest constants" do + bundle ["gem", gem_name, flags].compact.join(" ") + expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/module TestGem/) + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/module TestGem/) + end + + include_examples "paths that depend on gem name" + + context "--ext parameter set with C" do + let(:flags) { "--ext=c" } + + before do + bundle ["gem", gem_name, flags].compact.join(" ") + end + + it "builds ext skeleton" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist + end + + it "generates native extension loading code" do + expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb").read).to include(<<~RUBY) + require_relative "test_gem/version" + require "#{gem_name}/#{gem_name}" + RUBY + end + + it "includes rake-compiler, but no Rust related changes" do + expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"') + + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.add_dependency "rb_sys"') + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.required_rubygems_version = ">= ') + end + + it "depends on compile task for build" do + rakefile = <<~RAKEFILE + # frozen_string_literal: true + + require "bundler/gem_tasks" + require "rake/extensiontask" + + task build: :compile + + GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec") + + Rake::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext| + ext.lib_dir = "lib/#{gem_name}" + end + + task default: %i[clobber compile] + RAKEFILE + + expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) + end + end + + context "--ext parameter set with rust" do + let(:flags) { "--ext=rust" } + + before do + bundle ["gem", gem_name, flags].compact.join(" ") + end + + it "is not deprecated" do + expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated." + end + + it "builds ext skeleton" do + expect(bundled_app("#{gem_name}/Cargo.toml")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/Cargo.toml")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/src/lib.rs")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/build.rs")).to exist + end + + it "includes rake-compiler and rb_sys gems constraint" do + expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"') + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "rb_sys"') + end + + it "depends on compile task for build" do + rakefile = <<~RAKEFILE + # frozen_string_literal: true + + require "bundler/gem_tasks" + require "rb_sys/extensiontask" + + task build: :compile + + GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec") + + RbSys::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext| + ext.lib_dir = "lib/#{gem_name}" + end + + task default: :compile + RAKEFILE + + expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) + end + + it "configures the crate such that `cargo test` works", :ruby_repo, :mri_only do + env = setup_rust_env + gem_path = bundled_app(gem_name) + result = sys_exec("cargo test", env: env, dir: gem_path, timeout: 300) + + expect(result).to include("1 passed") + end + + def setup_rust_env + skip "rust toolchain of mingw is broken" if RUBY_PLATFORM.match?("mingw") + + env = { + "CARGO_HOME" => ENV.fetch("CARGO_HOME", File.join(ENV["HOME"], ".cargo")), + "RUSTUP_HOME" => ENV.fetch("RUSTUP_HOME", File.join(ENV["HOME"], ".rustup")), + "RUSTUP_TOOLCHAIN" => ENV.fetch("RUSTUP_TOOLCHAIN", "stable"), + } + + system(env, "cargo", "-V", out: IO::NULL, err: [:child, :out]) + skip "cargo not present" unless $?.success? + # Hermetic Cargo setup + RbConfig::CONFIG.each {|k, v| env["RBCONFIG_#{k}"] = v } + env + end + end + + context "--ext parameter set with go" do + let(:flags) { "--ext=go" } + + before do + bundle ["gem", gem_name, flags].compact.join(" ") + end + + after do + sys_exec("go clean -modcache", raise_on_error: true) if installed_go? + end + + it "is not deprecated" do + expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated." + end + + it "builds ext skeleton" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod")).to exist + end + + it "includes extconf.rb in gem_name.gemspec" do + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include(%(spec.extensions = ["ext/#{gem_name}/extconf.rb"])) + end + + it "includes go_gem in gem_name.gemspec" do + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "go_gem", ">= 0.2"') + end + + it "includes go_gem extension in extconf.rb" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(<<~RUBY) + require "mkmf" + require "go_gem/mkmf" + RUBY + + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(%(create_go_makefile("#{gem_name}/#{gem_name}"))) + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).not_to include("create_makefile") + end + + it "includes go_gem extension in gem_name.c" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c").read).to eq(<<~C) + #include "#{gem_name}.h" + #include "_cgo_export.h" + C + end + + it "includes skeleton code in gem_name.go" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) + /* + #include "#{gem_name}.h" + + VALUE rb_#{gem_name}_sum(VALUE self, VALUE a, VALUE b); + */ + import "C" + GO + + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) + //export rb_#{gem_name}_sum + func rb_#{gem_name}_sum(_ C.VALUE, a C.VALUE, b C.VALUE) C.VALUE { + GO + + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) + //export Init_#{gem_name} + func Init_#{gem_name}() { + GO + end + + it "includes valid module name in go.mod" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/bundleuser/#{gem_name}") + end + + it "includes go_gem extension in Rakefile" do + expect(bundled_app("#{gem_name}/Rakefile").read).to include(<<~RUBY) + require "go_gem/rake_task" + + GoGem::RakeTask.new("#{gem_name}") + RUBY + end + + context "with --no-ci" do + let(:flags) { "--ext=go --no-ci" } + + it_behaves_like "CI config is absent" + end + + context "--ci set to github" do + let(:flags) { "--ext=go --ci=github" } + + it "generates .github/workflows/main.yml" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist + expect(bundled_app("#{gem_name}/.github/workflows/main.yml").read).to include("go-version-file: ext/#{gem_name}/go.mod") + end + end + + context "--ci set to circle" do + let(:flags) { "--ext=go --ci=circle" } + + it "generates a .circleci/config.yml" do + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist + + expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML.strip) + environment: + GO_VERSION: + YAML + + expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML) + - run: + name: Install Go + command: | + wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz + tar -C /usr/local -xzf /tmp/go.tar.gz + echo 'export PATH=/usr/local/go/bin:"$PATH"' >> "$BASH_ENV" + YAML + end + end + + context "--ci set to gitlab" do + let(:flags) { "--ext=go --ci=gitlab" } + + it "generates a .gitlab-ci.yml" do + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist + + expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML) + - wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz + - tar -C /usr/local -xzf /tmp/go.tar.gz + - export PATH=/usr/local/go/bin:$PATH + YAML + + expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML.strip) + variables: + GO_VERSION: + YAML + end + end + + context "without github.user" do + before do + # FIXME: GitHub Actions Windows Runner hang up here for some reason... + skip "Workaround for hung up" if Gem.win_platform? + + git("config --global --unset github.user") + bundle ["gem", gem_name, flags].compact.join(" ") + end + + it "includes valid module name in go.mod" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/username/#{gem_name}") + end + end + end + end + + context "gem naming with dashed" do + let(:gem_name) { "test-gem" } + + let(:require_path) { "test/gem" } + + let(:require_relative_path) { "gem" } + + let(:minitest_test_file_path) { "test/test/test_gem.rb" } + + let(:minitest_test_class_name) { "class Test::TestGem < Minitest::Test" } + + it "nests constants so they work" do + bundle "gem #{gem_name}" + expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/module Test\n module Gem/) + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/module Test\n module Gem/) + end + + include_examples "paths that depend on gem name" + end + + describe "uncommon gem names" do + it "can deal with two dashes" do + bundle "gem a--a" + + expect(bundled_app("a--a/a--a.gemspec")).to exist + end + + it "fails gracefully with a ." do + bundle "gem foo.gemspec", raise_on_error: false + expect(err).to end_with("Invalid gem name foo.gemspec -- `Foo.gemspec` is an invalid constant name") + end + + it "fails gracefully with a ^" do + bundle "gem ^", raise_on_error: false + expect(err).to end_with("Invalid gem name ^ -- `^` is an invalid constant name") + end + + it "fails gracefully with a space" do + bundle "gem 'foo bar'", raise_on_error: false + expect(err).to end_with("Invalid gem name foo bar -- `Foo bar` is an invalid constant name") + end + + it "fails gracefully when multiple names are passed" do + bundle "gem foo bar baz", raise_on_error: false + expect(err).to eq(<<-E.strip) +ERROR: "bundle gem" was called with arguments ["foo", "bar", "baz"] +Usage: "bundle gem NAME [OPTIONS]" + E + end + end + + describe "#ensure_safe_gem_name" do + before do + bundle "gem #{subject}", raise_on_error: false + end + + context "with an existing const name" do + subject { "gem" } + it { expect(err).to include("Invalid gem name #{subject}") } + end + + context "with an existing hyphenated const name" do + subject { "gem-specification" } + it { expect(err).to include("Invalid gem name #{subject}") } + end + + context "starting with a number" do + subject { "1gem" } + it { expect(err).to include("Invalid gem name #{subject}") } + end + + context "including capital letter" do + subject { "CAPITAL" } + it "should warn but not error" do + expect(err).to include("Gem names with capital letters are not recommended") + expect(bundled_app("#{subject}/#{subject}.gemspec")).to exist + end + end + + context "starting with an existing const name" do + subject { "gem-somenewconstantname" } + it { expect(err).not_to include("Invalid gem name #{subject}") } + end + + context "ending with an existing const name" do + subject { "somenewconstantname-gem" } + it { expect(err).not_to include("Invalid gem name #{subject}") } + end + end + + context "on first run", :readline do + it "asks about test framework" do + bundle_config_global "BUNDLE_GEM__TEST" => nil + + bundle "gem foobar" do |input, _, _| + input.puts "rspec" + end + + expect(bundled_app("foobar/spec/spec_helper.rb")).to exist + rakefile = <<~RAKEFILE + # frozen_string_literal: true + + require "bundler/gem_tasks" + require "rspec/core/rake_task" + + RSpec::Core::RakeTask.new(:spec) + + task default: :spec + RAKEFILE + + expect(bundled_app("foobar/Rakefile").read).to eq(rakefile) + expect(bundled_app("foobar/Gemfile").read).to include('gem "rspec"') + end + + it "asks about CI service" do + bundle_config_global "BUNDLE_GEM__CI" => nil + + bundle "gem foobar" do |input, _, _| + input.puts "github" + end + + expect(bundled_app("foobar/.github/workflows/main.yml")).to exist + end + + it "asks about MIT license just once" do + bundle_config_global "BUNDLE_GEM__MIT" => nil + + bundle "config list" + + bundle "gem foobar" do |input, _, _| + input.puts "yes" + end + + expect(bundled_app("foobar/LICENSE.txt")).to exist + expect(out).to include("Using a MIT license means").once + end + + it "asks about CoC just once" do + bundle_config_global "BUNDLE_GEM__COC" => nil + + bundle "gem foobar" do |input, _, _| + input.puts "yes" + end + + expect(bundled_app("foobar/CODE_OF_CONDUCT.md")).to exist + expect(out).to include("Codes of conduct can increase contributions to your project").once + end + + it "asks about CHANGELOG just once" do + bundle_config_global "BUNDLE_GEM__CHANGELOG" => nil + + bundle "gem foobar" do |input, _, _| + input.puts "yes" + end + + expect(bundled_app("foobar/CHANGELOG.md")).to exist + expect(out).to include("A changelog is a file which contains").once + end + end + + context "on conflicts with a previously created file" do + it "should fail gracefully" do + FileUtils.touch(bundled_app("conflict-foobar")) + bundle "gem conflict-foobar", raise_on_error: false + expect(err).to eq("Couldn't create a new gem named `conflict-foobar` because there's an existing file named `conflict-foobar`.") + expect(exitstatus).to eql(32) + end + end + + context "on conflicts with a previously created directory" do + it "should succeed" do + FileUtils.mkdir_p(bundled_app("conflict-foobar/Gemfile")) + bundle "gem conflict-foobar" + expect(out).to include("file_clash conflict-foobar/Gemfile"). + and include "Initializing git repo in #{bundled_app("conflict-foobar")}" + end + end +end diff --git a/spec/bundler/commands/newgem_spec.rb b/spec/bundler/commands/newgem_spec.rb index 2b630d30b6c5d1..f26468bd512500 100644 --- a/spec/bundler/commands/newgem_spec.rb +++ b/spec/bundler/commands/newgem_spec.rb @@ -94,218 +94,6 @@ def installed_go? end end - shared_examples_for "--mit flag" do - before do - bundle "gem #{gem_name} --mit" - end - it "generates a gem skeleton with MIT license" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/LICENSE.txt")).to exist - expect(generated_gemspec.license).to eq("MIT") - end - end - - shared_examples_for "--no-mit flag" do - before do - bundle "gem #{gem_name} --no-mit" - end - it "generates a gem skeleton without MIT license" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/LICENSE.txt")).to_not exist - end - end - - shared_examples_for "--coc flag" do - it "generates a gem skeleton with MIT license" do - bundle "gem #{gem_name} --coc" - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/CODE_OF_CONDUCT.md")).to exist - end - - it "generates the README with a section for the Code of Conduct" do - bundle "gem #{gem_name} --coc" - expect(bundled_app("#{gem_name}/README.md").read).to include("## Code of Conduct") - expect(bundled_app("#{gem_name}/README.md").read).to match(%r{https://github\.com/bundleuser/#{gem_name}/blob/.*/CODE_OF_CONDUCT.md}) - end - - it "generates the README with a section for the Code of Conduct, respecting the configured git default branch", git: ">= 2.28.0" do - git("config --global init.defaultBranch main") - bundle "gem #{gem_name} --coc" - - expect(bundled_app("#{gem_name}/README.md").read).to include("## Code of Conduct") - expect(bundled_app("#{gem_name}/README.md").read).to include("https://github.com/bundleuser/#{gem_name}/blob/main/CODE_OF_CONDUCT.md") - end - end - - shared_examples_for "--no-coc flag" do - before do - bundle "gem #{gem_name} --no-coc" - end - it "generates a gem skeleton without Code of Conduct" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/CODE_OF_CONDUCT.md")).to_not exist - end - - it "generates the README without a section for the Code of Conduct" do - expect(bundled_app("#{gem_name}/README.md").read).not_to include("## Code of Conduct") - expect(bundled_app("#{gem_name}/README.md").read).not_to match(%r{https://github\.com/bundleuser/#{gem_name}/blob/.*/CODE_OF_CONDUCT.md}) - end - end - - shared_examples_for "--changelog flag" do - before do - bundle "gem #{gem_name} --changelog" - end - it "generates a gem skeleton with a CHANGELOG" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/CHANGELOG.md")).to exist - end - end - - shared_examples_for "--no-changelog flag" do - before do - bundle "gem #{gem_name} --no-changelog" - end - it "generates a gem skeleton without a CHANGELOG" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/CHANGELOG.md")).to_not exist - end - end - - shared_examples_for "--bundle flag" do - before do - bundle "gem #{gem_name} --bundle" - end - it "generates a gem skeleton with bundle install" do - gem_skeleton_assertions - expect(out).to include("Running bundle install in the new gem directory.") - end - end - - shared_examples_for "--no-bundle flag" do - before do - bundle "gem #{gem_name} --no-bundle" - end - it "generates a gem skeleton without bundle install" do - gem_skeleton_assertions - expect(out).to_not include("Running bundle install in the new gem directory.") - end - end - - shared_examples_for "--linter=rubocop flag" do - before do - bundle "gem #{gem_name} --linter=rubocop" - end - - it "generates a gem skeleton with rubocop" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/Rakefile")).to read_as( - include("# frozen_string_literal: true"). - and(include('require "rubocop/rake_task"'). - and(include("RuboCop::RakeTask.new"). - and(match(/default:.+:rubocop/)))) - ) - end - - it "includes rubocop in generated Gemfile" do - allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) - builder = Bundler::Dsl.new - builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) - builder.dependencies - rubocop_dep = builder.dependencies.find {|d| d.name == "rubocop" } - expect(rubocop_dep).not_to be_specific - expect(rubocop_dep.requirement).to eq(Gem::Requirement.new([">= 0"])) - end - - it "generates a default .rubocop.yml" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist - end - - it "includes .rubocop.yml into ignore list" do - expect(ignore_paths).to include(".rubocop.yml") - end - end - - shared_examples_for "--linter=standard flag" do - before do - bundle "gem #{gem_name} --linter=standard" - end - - it "generates a gem skeleton with standard" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/Rakefile")).to read_as( - include('require "standard/rake"'). - and(match(/default:.+:standard/)) - ) - end - - it "includes standard in generated Gemfile" do - allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) - builder = Bundler::Dsl.new - builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) - builder.dependencies - standard_dep = builder.dependencies.find {|d| d.name == "standard" } - expect(standard_dep).not_to be_specific - expect(standard_dep.requirement).to eq(Gem::Requirement.new([">= 0"])) - end - - it "generates a default .standard.yml" do - expect(bundled_app("#{gem_name}/.standard.yml")).to exist - end - - it "includes .standard.yml into ignore list" do - expect(ignore_paths).to include(".standard.yml") - end - end - - shared_examples_for "--no-linter flag" do - define_negated_matcher :exclude, :include - - before do - bundle "gem #{gem_name} --no-linter" - end - - it "generates a gem skeleton without rubocop" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/Rakefile")).to read_as(exclude("rubocop")) - expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to read_as(exclude("rubocop")) - end - - it "does not include rubocop in generated Gemfile" do - allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) - builder = Bundler::Dsl.new - builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) - builder.dependencies - rubocop_dep = builder.dependencies.find {|d| d.name == "rubocop" } - expect(rubocop_dep).to be_nil - end - - it "does not include standard in generated Gemfile" do - allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) - builder = Bundler::Dsl.new - builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) - builder.dependencies - standard_dep = builder.dependencies.find {|d| d.name == "standard" } - expect(standard_dep).to be_nil - end - - it "doesn't generate a default .rubocop.yml" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist - end - - it "does not add .rubocop.yml into ignore list" do - expect(ignore_paths).not_to include(".rubocop.yml") - end - - it "doesn't generate a default .standard.yml" do - expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist - end - - it "does not add .standard.yml into ignore list" do - expect(ignore_paths).not_to include(".standard.yml") - end - end - it "has no rubocop offenses when using --linter=rubocop flag" do skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core? bundle "gem #{gem_name} --linter=rubocop" @@ -380,14 +168,6 @@ def installed_go? expect(last_command).to be_success end - shared_examples_for "CI config is absent" do - it "does not create any CI files" do - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist - expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist - expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist - end - end - shared_examples_for "test framework is absent" do it "does not create any test framework files" do expect(bundled_app("#{gem_name}/.rspec")).to_not exist @@ -518,55 +298,6 @@ def create_temporary_dir(dir) end end - shared_examples_for "--github-username option" do |github_username| - before do - bundle "gem #{gem_name} --github-username=#{github_username}" - end - - it "generates a gem skeleton" do - gem_skeleton_assertions - end - - it "contribute URL set to given github username" do - expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]") - expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/#{github_username}") - end - end - - shared_examples_for "github_username configuration" do - context "with github_username setting set to some value" do - before do - bundle_config_global "gem.github_username different_username" - bundle "gem #{gem_name}" - end - - it "generates a gem skeleton" do - gem_skeleton_assertions - end - - it "contribute URL set to bundle config setting" do - expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]") - expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/different_username") - end - end - - context "with github_username setting set to false" do - before do - bundle_config_global "gem.github_username false" - bundle "gem #{gem_name}" - end - - it "generates a gem skeleton" do - gem_skeleton_assertions - end - - it "contribute URL set to [USERNAME]" do - expect(bundled_app("#{gem_name}/README.md").read).to include("[USERNAME]") - expect(bundled_app("#{gem_name}/README.md").read).not_to include("github.com/bundleuser") - end - end - end - it "generates a gem skeleton" do bundle "gem #{gem_name}" @@ -1021,1124 +752,4 @@ def create_temporary_dir(dir) it_behaves_like "test framework is absent" end - - context "--ci with no argument" do - before do - bundle "gem #{gem_name}" - end - - it "does not generate any CI config" do - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist - expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist - expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist - end - - it "does not add any CI config files into ignore list" do - expect(ignore_paths).not_to include(".github/") - expect(ignore_paths).not_to include(".gitlab-ci.yml") - expect(ignore_paths).not_to include(".circleci/") - end - end - - context "--ci set to github" do - before do - bundle "gem #{gem_name} --ci=github" - end - - it "generates a GitHub Actions config file" do - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist - end - - it "includes .github/ into ignore list" do - expect(ignore_paths).to include(".github/") - end - end - - context "--ci set to gitlab" do - before do - bundle "gem #{gem_name} --ci=gitlab" - end - - it "generates a GitLab CI config file" do - expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist - end - - it "includes .gitlab-ci.yml into ignore list" do - expect(ignore_paths).to include(".gitlab-ci.yml") - end - end - - context "--ci set to circle" do - before do - bundle "gem #{gem_name} --ci=circle" - end - - it "generates a CircleCI config file" do - expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist - end - - it "includes .circleci/ into ignore list" do - expect(ignore_paths).to include(".circleci/") - end - end - - context "--ci set to an invalid value" do - before do - bundle "gem #{gem_name} --ci=foo", raise_on_error: false - end - - it "fails loudly" do - expect(last_command).to be_failure - expect(err).to match(/Expected '--ci' to be one of .*; got foo/) - end - end - - context "gem.ci setting set to none" do - it "doesn't generate any CI config" do - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist - expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist - expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist - end - end - - context "gem.ci setting set to github" do - it "generates a GitHub Actions config file" do - bundle_config "gem.ci github" - bundle "gem #{gem_name}" - - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist - end - end - - context "gem.ci setting set to gitlab" do - it "generates a GitLab CI config file" do - bundle_config "gem.ci gitlab" - bundle "gem #{gem_name}" - - expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist - end - end - - context "gem.ci setting set to circle" do - it "generates a CircleCI config file" do - bundle_config "gem.ci circle" - bundle "gem #{gem_name}" - - expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist - end - end - - context "gem.ci set to github and --ci with no arguments" do - before do - bundle_config "gem.ci github" - bundle "gem #{gem_name} --ci" - end - - it "generates a GitHub Actions config file" do - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist - end - - it "hints that --ci is already configured" do - expect(out).to match("github is already configured, ignoring --ci flag.") - end - end - - context "gem.ci setting set to false and --ci with no arguments", :readline do - before do - bundle_config "gem.ci false" - bundle "gem #{gem_name} --ci" do |input, _, _| - input.puts "github" - end - end - - it "asks to setup CI" do - expect(out).to match("Do you want to set up continuous integration for your gem?") - end - - it "hints that the choice will only be applied to the current gem" do - expect(out).to match("Your choice will only be applied to this gem.") - end - end - - context "gem.ci setting not set and --ci with no arguments", :readline do - before do - bundle_config_global "BUNDLE_GEM__CI" => nil - bundle "gem #{gem_name} --ci" do |input, _, _| - input.puts "github" - end - end - - it "asks to setup CI" do - expect(out).to match("Do you want to set up continuous integration for your gem?") - end - - it "hints that the choice will be applied to future bundle gem calls" do - hint = "Future `bundle gem` calls will use your choice. " \ - "This setting can be changed anytime with `bundle config gem.ci`." - expect(out).to match(hint) - end - end - - context "gem.ci setting set to a CI service and --no-ci" do - before do - bundle_config "gem.ci github" - bundle "gem #{gem_name} --no-ci" - end - - it "does not generate any CI config" do - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist - expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist - expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist - end - end - - context "--linter with no argument" do - before do - bundle "gem #{gem_name}" - end - - it "does not generate any linter config" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist - expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist - end - - it "does not add any linter config files into ignore list" do - expect(ignore_paths).not_to include(".rubocop.yml") - expect(ignore_paths).not_to include(".standard.yml") - end - end - - context "--linter set to rubocop" do - before do - bundle "gem #{gem_name} --linter=rubocop" - end - - it "generates a RuboCop config" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist - expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist - end - - it "includes .rubocop.yml into ignore list" do - expect(ignore_paths).to include(".rubocop.yml") - expect(ignore_paths).not_to include(".standard.yml") - end - end - - context "--linter set to standard" do - before do - bundle "gem #{gem_name} --linter=standard" - end - - it "generates a Standard config" do - expect(bundled_app("#{gem_name}/.standard.yml")).to exist - expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist - end - - it "includes .standard.yml into ignore list" do - expect(ignore_paths).to include(".standard.yml") - expect(ignore_paths).not_to include(".rubocop.yml") - end - end - - context "--linter set to an invalid value" do - before do - bundle "gem #{gem_name} --linter=foo", raise_on_error: false - end - - it "fails loudly" do - expect(last_command).to be_failure - expect(err).to match(/Expected '--linter' to be one of .*; got foo/) - end - end - - context "gem.linter setting set to none" do - before do - bundle "gem #{gem_name}" - end - - it "doesn't generate any linter config" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist - expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist - end - - it "does not add any linter config files into ignore list" do - expect(ignore_paths).not_to include(".rubocop.yml") - expect(ignore_paths).not_to include(".standard.yml") - end - end - - context "gem.linter setting set to rubocop" do - before do - bundle_config "gem.linter rubocop" - bundle "gem #{gem_name}" - end - - it "generates a RuboCop config file" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist - end - - it "includes .rubocop.yml into ignore list" do - expect(ignore_paths).to include(".rubocop.yml") - end - end - - context "gem.linter setting set to standard" do - before do - bundle_config "gem.linter standard" - bundle "gem #{gem_name}" - end - - it "generates a Standard config file" do - expect(bundled_app("#{gem_name}/.standard.yml")).to exist - end - - it "includes .standard.yml into ignore list" do - expect(ignore_paths).to include(".standard.yml") - end - end - - context "gem.linter set to rubocop and --linter with no arguments" do - before do - bundle_config "gem.linter rubocop" - bundle "gem #{gem_name} --linter" - end - - it "generates a RuboCop config file" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist - end - - it "includes .rubocop.yml into ignore list" do - expect(ignore_paths).to include(".rubocop.yml") - end - - it "hints that --linter is already configured" do - expect(out).to match("rubocop is already configured, ignoring --linter flag.") - end - end - - context "gem.linter setting set to false and --linter with no arguments", :readline do - before do - bundle_config "gem.linter false" - bundle "gem #{gem_name} --linter" do |input, _, _| - input.puts "rubocop" - end - end - - it "asks to setup a linter" do - expect(out).to match("Do you want to add a code linter and formatter to your gem?") - end - - it "hints that the choice will only be applied to the current gem" do - expect(out).to match("Your choice will only be applied to this gem.") - end - end - - context "gem.linter setting not set and --linter with no arguments", :readline do - before do - bundle_config_global "BUNDLE_GEM__LINTER" => nil - bundle "gem #{gem_name} --linter" do |input, _, _| - input.puts "rubocop" - end - end - - it "asks to setup a linter" do - expect(out).to match("Do you want to add a code linter and formatter to your gem?") - end - - it "hints that the choice will be applied to future bundle gem calls" do - hint = "Future `bundle gem` calls will use your choice. " \ - "This setting can be changed anytime with `bundle config gem.linter`." - expect(out).to match(hint) - end - end - - context "gem.linter setting set to a linter and --no-linter" do - before do - bundle_config "gem.linter rubocop" - bundle "gem #{gem_name} --no-linter" - end - - it "does not generate any linter config" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist - expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist - end - - it "does not add any linter config files into ignore list" do - expect(ignore_paths).not_to include(".rubocop.yml") - expect(ignore_paths).not_to include(".standard.yml") - end - end - - context "--edit option" do - it "opens the generated gemspec in the user's text editor" do - output = bundle "gem #{gem_name} --edit=echo" - gemspec_path = File.join(bundled_app, gem_name, "#{gem_name}.gemspec") - expect(output).to include("echo \"#{gemspec_path}\"") - end - end - - shared_examples_for "paths that depend on gem name" do - it "generates entrypoint, version file and signatures file at the proper path, with the proper content" do - bundle "gem #{gem_name}" - - expect(bundled_app("#{gem_name}/lib/#{require_path}.rb")).to exist - expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(%r{require_relative "#{require_relative_path}/version"}) - expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/class Error < StandardError; end$/) - - expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb")).to exist - expect(bundled_app("#{gem_name}/sig/#{require_path}.rbs")).to exist - end - - context "--exe parameter set" do - before do - bundle "gem #{gem_name} --exe" - end - - it "builds an exe file that requires the proper entrypoint" do - expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist - expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/) - end - end - - context "--bin parameter set" do - before do - bundle "gem #{gem_name} --bin" - end - - it "builds an exe file that requires the proper entrypoint" do - expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist - expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/) - end - end - - context "--test parameter set to rspec" do - before do - bundle "gem #{gem_name} --test=rspec" - end - - it "builds a spec helper that requires the proper entrypoint, and a default test in the proper path which fails" do - expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist - expect(bundled_app("#{gem_name}/spec/spec_helper.rb").read).to include(%(require "#{require_path}")) - expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb")).to exist - expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb").read).to include("expect(false).to eq(true)") - end - end - - context "--test parameter set to minitest" do - before do - bundle "gem #{gem_name} --test=minitest" - end - - it "builds a test helper that requires the proper entrypoint, and default test file in the proper path that defines the proper test class name, requires helper, and fails" do - expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist - expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}")) - - expect(bundled_app("#{gem_name}/#{minitest_test_file_path}")).to exist - expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include(minitest_test_class_name) - expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include(%(require "test_helper")) - expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include("assert false") - end - end - - context "--test parameter set to test-unit" do - before do - bundle "gem #{gem_name} --test=test-unit" - end - - it "builds a test helper that requires the proper entrypoint, and default test file in the proper path which requires helper and fails" do - expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist - expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}")) - expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb")).to exist - expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include(%(require "test_helper")) - expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include("assert_equal(\"expected\", \"actual\")") - end - end - end - - context "with mit option in bundle config settings set to true" do - before do - bundle_config_global "gem.mit true" - end - it_behaves_like "--mit flag" - it_behaves_like "--no-mit flag" - end - - context "with mit option in bundle config settings set to false" do - before do - bundle_config_global "gem.mit false" - end - it_behaves_like "--mit flag" - it_behaves_like "--no-mit flag" - end - - context "with coc option in bundle config settings set to true" do - before do - bundle_config_global "gem.coc true" - end - it_behaves_like "--coc flag" - it_behaves_like "--no-coc flag" - end - - context "with coc option in bundle config settings set to false" do - before do - bundle_config_global "gem.coc false" - end - it_behaves_like "--coc flag" - it_behaves_like "--no-coc flag" - end - - context "with rubocop option in bundle config settings set to true" do - before do - bundle_config_global "gem.rubocop true" - end - it_behaves_like "--linter=rubocop flag" - it_behaves_like "--linter=standard flag" - it_behaves_like "--no-linter flag" - end - - context "with rubocop option in bundle config settings set to false" do - before do - bundle_config_global "gem.rubocop false" - end - it_behaves_like "--linter=rubocop flag" - it_behaves_like "--linter=standard flag" - it_behaves_like "--no-linter flag" - end - - context "with linter option in bundle config settings set to rubocop" do - before do - bundle_config_global "gem.linter rubocop" - end - it_behaves_like "--linter=rubocop flag" - it_behaves_like "--linter=standard flag" - it_behaves_like "--no-linter flag" - end - - context "with linter option in bundle config settings set to standard" do - before do - bundle_config_global "gem.linter standard" - end - it_behaves_like "--linter=rubocop flag" - it_behaves_like "--linter=standard flag" - it_behaves_like "--no-linter flag" - end - - context "with linter option in bundle config settings set to false" do - before do - bundle_config_global "gem.linter false" - end - it_behaves_like "--linter=rubocop flag" - it_behaves_like "--linter=standard flag" - it_behaves_like "--no-linter flag" - end - - context "with changelog option in bundle config settings set to true" do - before do - bundle_config_global "gem.changelog true" - end - it_behaves_like "--changelog flag" - it_behaves_like "--no-changelog flag" - end - - context "with changelog option in bundle config settings set to false" do - before do - bundle_config_global "gem.changelog false" - end - it_behaves_like "--changelog flag" - it_behaves_like "--no-changelog flag" - end - - context "with bundle option in bundle config settings set to true" do - before do - bundle_config_global "gem.bundle true" - end - it_behaves_like "--bundle flag" - it_behaves_like "--no-bundle flag" - - it "runs bundle install" do - bundle "gem #{gem_name}" - expect(out).to include("Running bundle install in the new gem directory.") - end - end - - context "with bundle option in bundle config settings set to false" do - before do - bundle_config_global "gem.bundle false" - end - it_behaves_like "--bundle flag" - it_behaves_like "--no-bundle flag" - - it "does not run bundle install" do - bundle "gem #{gem_name}" - expect(out).to_not include("Running bundle install in the new gem directory.") - end - end - - context "without git config github.user set" do - before do - git("config --global --unset github.user") - end - context "with github-username option in bundle config settings set to some value" do - before do - bundle_config_global "gem.github_username different_username" - end - it_behaves_like "--github-username option", "gh_user" - end - - it_behaves_like "github_username configuration" - - context "with github-username option in bundle config settings set to false" do - before do - bundle_config_global "gem.github_username false" - end - it_behaves_like "--github-username option", "gh_user" - end - - context "when changelog is enabled" do - it "sets gemspec changelog_uri, homepage, homepage_uri, source_code_uri to TODOs" do - bundle "gem #{gem_name} --changelog" - - expect(generated_gemspec.metadata["changelog_uri"]). - to eq("TODO: Put your gem's CHANGELOG.md URL here.") - expect(generated_gemspec.homepage).to eq("TODO: Put your gem's website or public repo URL here.") - expect(generated_gemspec.metadata["homepage_uri"]).to eq("TODO: Put your gem's website or public repo URL here.") - expect(generated_gemspec.metadata["source_code_uri"]).to eq("TODO: Put your gem's public repo URL here.") - end - end - - context "when changelog is not enabled" do - it "sets gemspec homepage, homepage_uri, source_code_uri to TODOs and changelog_uri to nil" do - bundle "gem #{gem_name}" - - expect(generated_gemspec.metadata["changelog_uri"]).to be_nil - expect(generated_gemspec.homepage).to eq("TODO: Put your gem's website or public repo URL here.") - expect(generated_gemspec.metadata["homepage_uri"]).to eq("TODO: Put your gem's website or public repo URL here.") - expect(generated_gemspec.metadata["source_code_uri"]).to eq("TODO: Put your gem's public repo URL here.") - end - end - end - - context "with git config github.user set" do - context "with github-username option in bundle config settings set to some value" do - before do - bundle_config_global "gem.github_username different_username" - end - it_behaves_like "--github-username option", "gh_user" - end - - it_behaves_like "github_username configuration" - - context "with github-username option in bundle config settings set to false" do - before do - bundle_config_global "gem.github_username false" - end - it_behaves_like "--github-username option", "gh_user" - end - - context "when changelog is enabled" do - it "sets gemspec changelog_uri, homepage, homepage_uri, source_code_uri based on git username" do - bundle "gem #{gem_name} --changelog" - - expect(generated_gemspec.metadata["changelog_uri"]). - to eq("https://github.com/bundleuser/#{gem_name}/blob/main/CHANGELOG.md") - expect(generated_gemspec.homepage).to eq("https://github.com/bundleuser/#{gem_name}") - expect(generated_gemspec.metadata["homepage_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") - expect(generated_gemspec.metadata["source_code_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") - end - end - - context "when changelog is not enabled" do - it "sets gemspec source_code_uri, homepage, homepage_uri but not changelog_uri" do - bundle "gem #{gem_name}" - - expect(generated_gemspec.metadata["changelog_uri"]).to be_nil - expect(generated_gemspec.homepage).to eq("https://github.com/bundleuser/#{gem_name}") - expect(generated_gemspec.metadata["homepage_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") - expect(generated_gemspec.metadata["source_code_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") - end - end - end - - context "standard gem naming" do - let(:require_path) { gem_name } - - let(:require_relative_path) { gem_name } - - let(:minitest_test_file_path) { "test/test_#{gem_name}.rb" } - - let(:minitest_test_class_name) { "class TestMygem < Minitest::Test" } - - include_examples "paths that depend on gem name" - end - - context "gem naming with underscore" do - let(:gem_name) { "test_gem" } - - let(:require_path) { "test_gem" } - - let(:require_relative_path) { "test_gem" } - - let(:minitest_test_file_path) { "test/test_test_gem.rb" } - - let(:minitest_test_class_name) { "class TestTestGem < Minitest::Test" } - - let(:flags) { nil } - - it "does not nest constants" do - bundle ["gem", gem_name, flags].compact.join(" ") - expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/module TestGem/) - expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/module TestGem/) - end - - include_examples "paths that depend on gem name" - - context "--ext parameter set with C" do - let(:flags) { "--ext=c" } - - before do - bundle ["gem", gem_name, flags].compact.join(" ") - end - - it "builds ext skeleton" do - expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist - end - - it "generates native extension loading code" do - expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb").read).to include(<<~RUBY) - require_relative "test_gem/version" - require "#{gem_name}/#{gem_name}" - RUBY - end - - it "includes rake-compiler, but no Rust related changes" do - expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"') - - expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.add_dependency "rb_sys"') - expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.required_rubygems_version = ">= ') - end - - it "depends on compile task for build" do - rakefile = <<~RAKEFILE - # frozen_string_literal: true - - require "bundler/gem_tasks" - require "rake/extensiontask" - - task build: :compile - - GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec") - - Rake::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext| - ext.lib_dir = "lib/#{gem_name}" - end - - task default: %i[clobber compile] - RAKEFILE - - expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) - end - end - - context "--ext parameter set with rust" do - let(:flags) { "--ext=rust" } - - before do - bundle ["gem", gem_name, flags].compact.join(" ") - end - - it "is not deprecated" do - expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated." - end - - it "builds ext skeleton" do - expect(bundled_app("#{gem_name}/Cargo.toml")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/Cargo.toml")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/src/lib.rs")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/build.rs")).to exist - end - - it "includes rake-compiler and rb_sys gems constraint" do - expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"') - expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "rb_sys"') - end - - it "depends on compile task for build" do - rakefile = <<~RAKEFILE - # frozen_string_literal: true - - require "bundler/gem_tasks" - require "rb_sys/extensiontask" - - task build: :compile - - GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec") - - RbSys::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext| - ext.lib_dir = "lib/#{gem_name}" - end - - task default: :compile - RAKEFILE - - expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) - end - - it "configures the crate such that `cargo test` works", :ruby_repo, :mri_only do - env = setup_rust_env - gem_path = bundled_app(gem_name) - result = sys_exec("cargo test", env: env, dir: gem_path, timeout: 300) - - expect(result).to include("1 passed") - end - - def setup_rust_env - skip "rust toolchain of mingw is broken" if RUBY_PLATFORM.match?("mingw") - - env = { - "CARGO_HOME" => ENV.fetch("CARGO_HOME", File.join(ENV["HOME"], ".cargo")), - "RUSTUP_HOME" => ENV.fetch("RUSTUP_HOME", File.join(ENV["HOME"], ".rustup")), - "RUSTUP_TOOLCHAIN" => ENV.fetch("RUSTUP_TOOLCHAIN", "stable"), - } - - system(env, "cargo", "-V", out: IO::NULL, err: [:child, :out]) - skip "cargo not present" unless $?.success? - # Hermetic Cargo setup - RbConfig::CONFIG.each {|k, v| env["RBCONFIG_#{k}"] = v } - env - end - end - - context "--ext parameter set with go" do - let(:flags) { "--ext=go" } - - before do - bundle ["gem", gem_name, flags].compact.join(" ") - end - - after do - sys_exec("go clean -modcache", raise_on_error: true) if installed_go? - end - - it "is not deprecated" do - expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated." - end - - it "builds ext skeleton" do - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod")).to exist - end - - it "includes extconf.rb in gem_name.gemspec" do - expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include(%(spec.extensions = ["ext/#{gem_name}/extconf.rb"])) - end - - it "includes go_gem in gem_name.gemspec" do - expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "go_gem", ">= 0.2"') - end - - it "includes go_gem extension in extconf.rb" do - expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(<<~RUBY) - require "mkmf" - require "go_gem/mkmf" - RUBY - - expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(%(create_go_makefile("#{gem_name}/#{gem_name}"))) - expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).not_to include("create_makefile") - end - - it "includes go_gem extension in gem_name.c" do - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c").read).to eq(<<~C) - #include "#{gem_name}.h" - #include "_cgo_export.h" - C - end - - it "includes skeleton code in gem_name.go" do - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) - /* - #include "#{gem_name}.h" - - VALUE rb_#{gem_name}_sum(VALUE self, VALUE a, VALUE b); - */ - import "C" - GO - - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) - //export rb_#{gem_name}_sum - func rb_#{gem_name}_sum(_ C.VALUE, a C.VALUE, b C.VALUE) C.VALUE { - GO - - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) - //export Init_#{gem_name} - func Init_#{gem_name}() { - GO - end - - it "includes valid module name in go.mod" do - expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/bundleuser/#{gem_name}") - end - - it "includes go_gem extension in Rakefile" do - expect(bundled_app("#{gem_name}/Rakefile").read).to include(<<~RUBY) - require "go_gem/rake_task" - - GoGem::RakeTask.new("#{gem_name}") - RUBY - end - - context "with --no-ci" do - let(:flags) { "--ext=go --no-ci" } - - it_behaves_like "CI config is absent" - end - - context "--ci set to github" do - let(:flags) { "--ext=go --ci=github" } - - it "generates .github/workflows/main.yml" do - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist - expect(bundled_app("#{gem_name}/.github/workflows/main.yml").read).to include("go-version-file: ext/#{gem_name}/go.mod") - end - end - - context "--ci set to circle" do - let(:flags) { "--ext=go --ci=circle" } - - it "generates a .circleci/config.yml" do - expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist - - expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML.strip) - environment: - GO_VERSION: - YAML - - expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML) - - run: - name: Install Go - command: | - wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz - tar -C /usr/local -xzf /tmp/go.tar.gz - echo 'export PATH=/usr/local/go/bin:"$PATH"' >> "$BASH_ENV" - YAML - end - end - - context "--ci set to gitlab" do - let(:flags) { "--ext=go --ci=gitlab" } - - it "generates a .gitlab-ci.yml" do - expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist - - expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML) - - wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz - - tar -C /usr/local -xzf /tmp/go.tar.gz - - export PATH=/usr/local/go/bin:$PATH - YAML - - expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML.strip) - variables: - GO_VERSION: - YAML - end - end - - context "without github.user" do - before do - # FIXME: GitHub Actions Windows Runner hang up here for some reason... - skip "Workaround for hung up" if Gem.win_platform? - - git("config --global --unset github.user") - bundle ["gem", gem_name, flags].compact.join(" ") - end - - it "includes valid module name in go.mod" do - expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/username/#{gem_name}") - end - end - end - end - - context "gem naming with dashed" do - let(:gem_name) { "test-gem" } - - let(:require_path) { "test/gem" } - - let(:require_relative_path) { "gem" } - - let(:minitest_test_file_path) { "test/test/test_gem.rb" } - - let(:minitest_test_class_name) { "class Test::TestGem < Minitest::Test" } - - it "nests constants so they work" do - bundle "gem #{gem_name}" - expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/module Test\n module Gem/) - expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/module Test\n module Gem/) - end - - include_examples "paths that depend on gem name" - end - - describe "uncommon gem names" do - it "can deal with two dashes" do - bundle "gem a--a" - - expect(bundled_app("a--a/a--a.gemspec")).to exist - end - - it "fails gracefully with a ." do - bundle "gem foo.gemspec", raise_on_error: false - expect(err).to end_with("Invalid gem name foo.gemspec -- `Foo.gemspec` is an invalid constant name") - end - - it "fails gracefully with a ^" do - bundle "gem ^", raise_on_error: false - expect(err).to end_with("Invalid gem name ^ -- `^` is an invalid constant name") - end - - it "fails gracefully with a space" do - bundle "gem 'foo bar'", raise_on_error: false - expect(err).to end_with("Invalid gem name foo bar -- `Foo bar` is an invalid constant name") - end - - it "fails gracefully when multiple names are passed" do - bundle "gem foo bar baz", raise_on_error: false - expect(err).to eq(<<-E.strip) -ERROR: "bundle gem" was called with arguments ["foo", "bar", "baz"] -Usage: "bundle gem NAME [OPTIONS]" - E - end - end - - describe "#ensure_safe_gem_name" do - before do - bundle "gem #{subject}", raise_on_error: false - end - - context "with an existing const name" do - subject { "gem" } - it { expect(err).to include("Invalid gem name #{subject}") } - end - - context "with an existing hyphenated const name" do - subject { "gem-specification" } - it { expect(err).to include("Invalid gem name #{subject}") } - end - - context "starting with a number" do - subject { "1gem" } - it { expect(err).to include("Invalid gem name #{subject}") } - end - - context "including capital letter" do - subject { "CAPITAL" } - it "should warn but not error" do - expect(err).to include("Gem names with capital letters are not recommended") - expect(bundled_app("#{subject}/#{subject}.gemspec")).to exist - end - end - - context "starting with an existing const name" do - subject { "gem-somenewconstantname" } - it { expect(err).not_to include("Invalid gem name #{subject}") } - end - - context "ending with an existing const name" do - subject { "somenewconstantname-gem" } - it { expect(err).not_to include("Invalid gem name #{subject}") } - end - end - - context "on first run", :readline do - it "asks about test framework" do - bundle_config_global "BUNDLE_GEM__TEST" => nil - - bundle "gem foobar" do |input, _, _| - input.puts "rspec" - end - - expect(bundled_app("foobar/spec/spec_helper.rb")).to exist - rakefile = <<~RAKEFILE - # frozen_string_literal: true - - require "bundler/gem_tasks" - require "rspec/core/rake_task" - - RSpec::Core::RakeTask.new(:spec) - - task default: :spec - RAKEFILE - - expect(bundled_app("foobar/Rakefile").read).to eq(rakefile) - expect(bundled_app("foobar/Gemfile").read).to include('gem "rspec"') - end - - it "asks about CI service" do - bundle_config_global "BUNDLE_GEM__CI" => nil - - bundle "gem foobar" do |input, _, _| - input.puts "github" - end - - expect(bundled_app("foobar/.github/workflows/main.yml")).to exist - end - - it "asks about MIT license just once" do - bundle_config_global "BUNDLE_GEM__MIT" => nil - - bundle "config list" - - bundle "gem foobar" do |input, _, _| - input.puts "yes" - end - - expect(bundled_app("foobar/LICENSE.txt")).to exist - expect(out).to include("Using a MIT license means").once - end - - it "asks about CoC just once" do - bundle_config_global "BUNDLE_GEM__COC" => nil - - bundle "gem foobar" do |input, _, _| - input.puts "yes" - end - - expect(bundled_app("foobar/CODE_OF_CONDUCT.md")).to exist - expect(out).to include("Codes of conduct can increase contributions to your project").once - end - - it "asks about CHANGELOG just once" do - bundle_config_global "BUNDLE_GEM__CHANGELOG" => nil - - bundle "gem foobar" do |input, _, _| - input.puts "yes" - end - - expect(bundled_app("foobar/CHANGELOG.md")).to exist - expect(out).to include("A changelog is a file which contains").once - end - end - - context "on conflicts with a previously created file" do - it "should fail gracefully" do - FileUtils.touch(bundled_app("conflict-foobar")) - bundle "gem conflict-foobar", raise_on_error: false - expect(err).to eq("Couldn't create a new gem named `conflict-foobar` because there's an existing file named `conflict-foobar`.") - expect(exitstatus).to eql(32) - end - end - - context "on conflicts with a previously created directory" do - it "should succeed" do - FileUtils.mkdir_p(bundled_app("conflict-foobar/Gemfile")) - bundle "gem conflict-foobar" - expect(out).to include("file_clash conflict-foobar/Gemfile"). - and include "Initializing git repo in #{bundled_app("conflict-foobar")}" - end - end end diff --git a/spec/bundler/commands/outdated_filters_spec.rb b/spec/bundler/commands/outdated_filters_spec.rb new file mode 100644 index 00000000000000..44c4b2a8846bd7 --- /dev/null +++ b/spec/bundler/commands/outdated_filters_spec.rb @@ -0,0 +1,629 @@ +# frozen_string_literal: true + +RSpec.describe "bundle outdated" do + it "performs an automatic bundle install" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + gem "foo" + G + + bundle_config "auto_install 1" + bundle :outdated, raise_on_error: false + expect(out).to include("Installing foo 1.0") + end + + context "in deployment mode" do + before do + build_repo2 + + gemfile <<-G + source "https://gem.repo2" + + gem "myrack" + gem "foo" + G + bundle :lock + bundle_config "deployment true" + end + + it "outputs a helpful message about being in deployment mode" do + update_repo2 { build_gem "activesupport", "3.0" } + + bundle "outdated", raise_on_error: false + expect(last_command).to be_failure + expect(err).to include("You are trying to check outdated gems in deployment mode.") + expect(err).to include("Run `bundle outdated` elsewhere.") + expect(err).to include("If this is a development machine, remove the ") + expect(err).to include("Gemfile freeze\nby running `bundle config unset deployment`.") + end + end + + context "after bundle config set --local deployment true" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + + gem "myrack" + gem "foo" + G + bundle_config "deployment true" + end + + it "outputs a helpful message about being in deployment mode" do + update_repo2 { build_gem "activesupport", "3.0" } + + bundle "outdated", raise_on_error: false + expect(last_command).to be_failure + expect(err).to include("You are trying to check outdated gems in deployment mode.") + expect(err).to include("Run `bundle outdated` elsewhere.") + expect(err).to include("If this is a development machine, remove the ") + expect(err).to include("Gemfile freeze\nby running `bundle config unset deployment`.") + end + end + + context "update available for a gem on a different platform" do + before do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "laduradura", '= 5.15.2' + G + end + + it "reports that no updates are available" do + bundle "outdated" + expect(out).to end_with("Bundle up to date!") + end + end + + context "update available for a gem on the same platform while multiple platforms used for gem" do + before do + build_repo2 + end + + it "reports that updates are available if the Ruby platform is used" do + install_gemfile <<-G + source "https://gem.repo2" + gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] + G + + bundle "outdated" + expect(out).to end_with("Bundle up to date!") + end + + it "reports that updates are available if the JRuby platform is used", :jruby_only do + install_gemfile <<-G + source "https://gem.repo2" + gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] + G + + bundle "outdated", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + laduradura 5.15.2 5.15.3 = 5.15.2 default + TABLE + + expect(out).to end_with(expected_output) + end + end + + shared_examples_for "version update is detected" do + it "reports that a gem has a newer version" do + subject + + outdated_gems = out.split("\n").drop_while {|l| !l.start_with?("Gem") }[1..-1] + + expect(outdated_gems.size).to be > 0 + end + end + + shared_examples_for "major version updates are detected" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + + update_repo2 do + build_gem "activesupport", "3.3.5" + build_gem "weakling", "0.8.0" + end + end + + it_behaves_like "version update is detected" + end + + context "when on a new machine" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + + pristine_system_gems + + update_git "foo", path: lib_path("foo") + update_repo2 do + build_gem "activesupport", "3.3.5" + build_gem "weakling", "0.8.0" + end + end + + subject { bundle "outdated", raise_on_error: false } + it_behaves_like "version update is detected" + end + + shared_examples_for "minor version updates are detected" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + + update_repo2 do + build_gem "activesupport", "2.7.5" + build_gem "weakling", "2.0.1" + end + end + + it_behaves_like "version update is detected" + end + + shared_examples_for "patch version updates are detected" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + + update_repo2 do + build_gem "activesupport", "2.3.7" + build_gem "weakling", "0.3.1" + end + end + + it_behaves_like "version update is detected" + end + + shared_examples_for "no version updates are detected" do + it "does not detect any version updates" do + subject + expect(out).to end_with("updates to display.") + end + end + + shared_examples_for "major version is ignored" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + + update_repo2 do + build_gem "activesupport", "3.3.5" + build_gem "weakling", "1.0.1" + end + end + + it_behaves_like "no version updates are detected" + end + + shared_examples_for "minor version is ignored" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + + update_repo2 do + build_gem "activesupport", "2.4.5" + build_gem "weakling", "0.3.1" + end + end + + it_behaves_like "no version updates are detected" + end + + shared_examples_for "patch version is ignored" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + + update_repo2 do + build_gem "activesupport", "2.3.6" + build_gem "weakling", "0.0.4" + end + end + + it_behaves_like "no version updates are detected" + end + + describe "with --filter-major option" do + subject { bundle "outdated --filter-major", raise_on_error: false } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version is ignored" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-minor option" do + subject { bundle "outdated --filter-minor", raise_on_error: false } + + it_behaves_like "minor version updates are detected" + it_behaves_like "major version is ignored" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-patch option" do + subject { bundle "outdated --filter-patch", raise_on_error: false } + + it_behaves_like "patch version updates are detected" + it_behaves_like "major version is ignored" + it_behaves_like "minor version is ignored" + end + + describe "with --filter-minor --filter-patch options" do + subject { bundle "outdated --filter-minor --filter-patch", raise_on_error: false } + + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version updates are detected" + it_behaves_like "major version is ignored" + end + + describe "with --filter-major --filter-minor options" do + subject { bundle "outdated --filter-major --filter-minor", raise_on_error: false } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-major --filter-patch options" do + subject { bundle "outdated --filter-major --filter-patch", raise_on_error: false } + + it_behaves_like "major version updates are detected" + it_behaves_like "patch version updates are detected" + it_behaves_like "minor version is ignored" + end + + describe "with --filter-major --filter-minor --filter-patch options" do + subject { bundle "outdated --filter-major --filter-minor --filter-patch", raise_on_error: false } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version updates are detected" + end + + context "conservative updates" do + before do + build_repo4 do + build_gem "patch", %w[1.0.0 1.0.1] + build_gem "minor", %w[1.0.0 1.0.1 1.1.0] + build_gem "major", %w[1.0.0 1.0.1 1.1.0 2.0.0] + end + + # establish a lockfile set to 1.0.0 + install_gemfile <<-G + source "https://gem.repo4" + gem 'patch', '1.0.0' + gem 'minor', '1.0.0' + gem 'major', '1.0.0' + G + + # remove all version requirements + gemfile <<-G + source "https://gem.repo4" + gem 'patch' + gem 'minor' + gem 'major' + G + end + + it "shows nothing when patching and filtering to minor" do + bundle "outdated --patch --filter-minor" + + expect(out).to end_with("No minor updates to display.") + end + + it "shows all gems when patching and filtering to patch" do + bundle "outdated --patch --filter-patch", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + major 1.0.0 1.0.1 >= 0 default + minor 1.0.0 1.0.1 >= 0 default + patch 1.0.0 1.0.1 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + + it "shows minor and major when updating to minor and filtering to patch and minor" do + bundle "outdated --minor --filter-minor", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + major 1.0.0 1.1.0 >= 0 default + minor 1.0.0 1.1.0 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + + it "shows minor when updating to major and filtering to minor with parseable" do + bundle "outdated --major --filter-minor --parseable", raise_on_error: false + + expect(out).not_to include("patch (newest") + expect(out).to include("minor (newest") + expect(out).not_to include("major (newest") + end + end + + context "tricky conservative updates" do + before do + build_repo4 do + build_gem "foo", %w[1.4.3 1.4.4] do |s| + s.add_dependency "bar", "~> 2.0" + end + build_gem "foo", %w[1.4.5 1.5.0] do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "foo", %w[1.5.1] do |s| + s.add_dependency "bar", "~> 3.0" + end + build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0] + build_gem "qux", %w[1.0.0 1.1.0 2.0.0] + end + + # establish a lockfile set to 1.4.3 + install_gemfile <<-G + source "https://gem.repo4" + gem 'foo', '1.4.3' + gem 'bar', '2.0.3' + gem 'qux', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "https://gem.repo4" + gem 'foo' + gem 'qux' + G + end + + it "shows gems updating to patch and filtering to patch" do + bundle "outdated --patch --filter-patch", raise_on_error: false, env: { "DEBUG_RESOLVER" => "1" } + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + bar 2.0.3 2.0.5 + foo 1.4.3 1.4.4 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + + it "shows gems updating to patch and filtering to patch, in debug mode" do + bundle "outdated --patch --filter-patch", raise_on_error: false, env: { "DEBUG" => "1" } + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date Path + bar 2.0.3 2.0.5 + foo 1.4.3 1.4.4 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + end + + describe "with --only-explicit" do + it "does not report outdated dependent gems" do + build_repo4 do + build_gem "weakling", %w[0.2 0.3] do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "bar", %w[2.1 2.2] + end + + install_gemfile <<-G + source "https://gem.repo4" + gem 'weakling', '0.2' + gem 'bar', '2.1' + G + + gemfile <<-G + source "https://gem.repo4" + gem 'weakling' + G + + bundle "outdated --only-explicit", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + weakling 0.2 0.3 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + end + + describe "with a multiplatform lockfile" do + before do + build_repo4 do + build_gem "nokogiri", "1.11.1" + build_gem "nokogiri", "1.11.1" do |s| + s.platform = Bundler.local_platform + end + + build_gem "nokogiri", "1.11.2" + build_gem "nokogiri", "1.11.2" do |s| + s.platform = Bundler.local_platform + end + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.11.1) + nokogiri (1.11.1-#{Bundler.local_platform}) + + PLATFORMS + ruby + #{Bundler.local_platform} + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<-G + source "https://gem.repo4" + gem "nokogiri" + G + end + + it "reports a single entry per gem" do + bundle "outdated", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + nokogiri 1.11.1 1.11.2 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + end + + context "when a gem is no longer a dependency after a full update" do + before do + build_repo4 do + build_gem "mini_portile2", "2.5.2" do |s| + s.add_dependency "net-ftp", "~> 0.1" + end + + build_gem "mini_portile2", "2.5.3" + + build_gem "net-ftp", "0.1.2" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "mini_portile2" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + mini_portile2 (2.5.2) + net-ftp (~> 0.1) + net-ftp (0.1.2) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + mini_portile2 + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "works" do + bundle "outdated", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + mini_portile2 2.5.2 2.5.3 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + end +end diff --git a/spec/bundler/commands/outdated_spec.rb b/spec/bundler/commands/outdated_spec.rb index dea78ba83f0c5d..e6a276cceafde4 100644 --- a/spec/bundler/commands/outdated_spec.rb +++ b/spec/bundler/commands/outdated_spec.rb @@ -744,630 +744,4 @@ def test_group_option(group) expect(exitstatus).to_not be_zero end end - - it "performs an automatic bundle install" do - gemfile <<-G - source "https://gem.repo1" - gem "myrack", "0.9.1" - gem "foo" - G - - bundle_config "auto_install 1" - bundle :outdated, raise_on_error: false - expect(out).to include("Installing foo 1.0") - end - - context "in deployment mode" do - before do - build_repo2 - - gemfile <<-G - source "https://gem.repo2" - - gem "myrack" - gem "foo" - G - bundle :lock - bundle_config "deployment true" - end - - it "outputs a helpful message about being in deployment mode" do - update_repo2 { build_gem "activesupport", "3.0" } - - bundle "outdated", raise_on_error: false - expect(last_command).to be_failure - expect(err).to include("You are trying to check outdated gems in deployment mode.") - expect(err).to include("Run `bundle outdated` elsewhere.") - expect(err).to include("If this is a development machine, remove the ") - expect(err).to include("Gemfile freeze\nby running `bundle config unset deployment`.") - end - end - - context "after bundle config set --local deployment true" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - - gem "myrack" - gem "foo" - G - bundle_config "deployment true" - end - - it "outputs a helpful message about being in deployment mode" do - update_repo2 { build_gem "activesupport", "3.0" } - - bundle "outdated", raise_on_error: false - expect(last_command).to be_failure - expect(err).to include("You are trying to check outdated gems in deployment mode.") - expect(err).to include("Run `bundle outdated` elsewhere.") - expect(err).to include("If this is a development machine, remove the ") - expect(err).to include("Gemfile freeze\nby running `bundle config unset deployment`.") - end - end - - context "update available for a gem on a different platform" do - before do - build_repo2 - - install_gemfile <<-G - source "https://gem.repo2" - gem "laduradura", '= 5.15.2' - G - end - - it "reports that no updates are available" do - bundle "outdated" - expect(out).to end_with("Bundle up to date!") - end - end - - context "update available for a gem on the same platform while multiple platforms used for gem" do - before do - build_repo2 - end - - it "reports that updates are available if the Ruby platform is used" do - install_gemfile <<-G - source "https://gem.repo2" - gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] - G - - bundle "outdated" - expect(out).to end_with("Bundle up to date!") - end - - it "reports that updates are available if the JRuby platform is used", :jruby_only do - install_gemfile <<-G - source "https://gem.repo2" - gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] - G - - bundle "outdated", raise_on_error: false - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date - laduradura 5.15.2 5.15.3 = 5.15.2 default - TABLE - - expect(out).to end_with(expected_output) - end - end - - shared_examples_for "version update is detected" do - it "reports that a gem has a newer version" do - subject - - outdated_gems = out.split("\n").drop_while {|l| !l.start_with?("Gem") }[1..-1] - - expect(outdated_gems.size).to be > 0 - end - end - - shared_examples_for "major version updates are detected" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - - update_repo2 do - build_gem "activesupport", "3.3.5" - build_gem "weakling", "0.8.0" - end - end - - it_behaves_like "version update is detected" - end - - context "when on a new machine" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - - pristine_system_gems - - update_git "foo", path: lib_path("foo") - update_repo2 do - build_gem "activesupport", "3.3.5" - build_gem "weakling", "0.8.0" - end - end - - subject { bundle "outdated", raise_on_error: false } - it_behaves_like "version update is detected" - end - - shared_examples_for "minor version updates are detected" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - - update_repo2 do - build_gem "activesupport", "2.7.5" - build_gem "weakling", "2.0.1" - end - end - - it_behaves_like "version update is detected" - end - - shared_examples_for "patch version updates are detected" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - - update_repo2 do - build_gem "activesupport", "2.3.7" - build_gem "weakling", "0.3.1" - end - end - - it_behaves_like "version update is detected" - end - - shared_examples_for "no version updates are detected" do - it "does not detect any version updates" do - subject - expect(out).to end_with("updates to display.") - end - end - - shared_examples_for "major version is ignored" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - - update_repo2 do - build_gem "activesupport", "3.3.5" - build_gem "weakling", "1.0.1" - end - end - - it_behaves_like "no version updates are detected" - end - - shared_examples_for "minor version is ignored" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - - update_repo2 do - build_gem "activesupport", "2.4.5" - build_gem "weakling", "0.3.1" - end - end - - it_behaves_like "no version updates are detected" - end - - shared_examples_for "patch version is ignored" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - - update_repo2 do - build_gem "activesupport", "2.3.6" - build_gem "weakling", "0.0.4" - end - end - - it_behaves_like "no version updates are detected" - end - - describe "with --filter-major option" do - subject { bundle "outdated --filter-major", raise_on_error: false } - - it_behaves_like "major version updates are detected" - it_behaves_like "minor version is ignored" - it_behaves_like "patch version is ignored" - end - - describe "with --filter-minor option" do - subject { bundle "outdated --filter-minor", raise_on_error: false } - - it_behaves_like "minor version updates are detected" - it_behaves_like "major version is ignored" - it_behaves_like "patch version is ignored" - end - - describe "with --filter-patch option" do - subject { bundle "outdated --filter-patch", raise_on_error: false } - - it_behaves_like "patch version updates are detected" - it_behaves_like "major version is ignored" - it_behaves_like "minor version is ignored" - end - - describe "with --filter-minor --filter-patch options" do - subject { bundle "outdated --filter-minor --filter-patch", raise_on_error: false } - - it_behaves_like "minor version updates are detected" - it_behaves_like "patch version updates are detected" - it_behaves_like "major version is ignored" - end - - describe "with --filter-major --filter-minor options" do - subject { bundle "outdated --filter-major --filter-minor", raise_on_error: false } - - it_behaves_like "major version updates are detected" - it_behaves_like "minor version updates are detected" - it_behaves_like "patch version is ignored" - end - - describe "with --filter-major --filter-patch options" do - subject { bundle "outdated --filter-major --filter-patch", raise_on_error: false } - - it_behaves_like "major version updates are detected" - it_behaves_like "patch version updates are detected" - it_behaves_like "minor version is ignored" - end - - describe "with --filter-major --filter-minor --filter-patch options" do - subject { bundle "outdated --filter-major --filter-minor --filter-patch", raise_on_error: false } - - it_behaves_like "major version updates are detected" - it_behaves_like "minor version updates are detected" - it_behaves_like "patch version updates are detected" - end - - context "conservative updates" do - before do - build_repo4 do - build_gem "patch", %w[1.0.0 1.0.1] - build_gem "minor", %w[1.0.0 1.0.1 1.1.0] - build_gem "major", %w[1.0.0 1.0.1 1.1.0 2.0.0] - end - - # establish a lockfile set to 1.0.0 - install_gemfile <<-G - source "https://gem.repo4" - gem 'patch', '1.0.0' - gem 'minor', '1.0.0' - gem 'major', '1.0.0' - G - - # remove all version requirements - gemfile <<-G - source "https://gem.repo4" - gem 'patch' - gem 'minor' - gem 'major' - G - end - - it "shows nothing when patching and filtering to minor" do - bundle "outdated --patch --filter-minor" - - expect(out).to end_with("No minor updates to display.") - end - - it "shows all gems when patching and filtering to patch" do - bundle "outdated --patch --filter-patch", raise_on_error: false - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date - major 1.0.0 1.0.1 >= 0 default - minor 1.0.0 1.0.1 >= 0 default - patch 1.0.0 1.0.1 >= 0 default - TABLE - - expect(out).to end_with(expected_output) - end - - it "shows minor and major when updating to minor and filtering to patch and minor" do - bundle "outdated --minor --filter-minor", raise_on_error: false - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date - major 1.0.0 1.1.0 >= 0 default - minor 1.0.0 1.1.0 >= 0 default - TABLE - - expect(out).to end_with(expected_output) - end - - it "shows minor when updating to major and filtering to minor with parseable" do - bundle "outdated --major --filter-minor --parseable", raise_on_error: false - - expect(out).not_to include("patch (newest") - expect(out).to include("minor (newest") - expect(out).not_to include("major (newest") - end - end - - context "tricky conservative updates" do - before do - build_repo4 do - build_gem "foo", %w[1.4.3 1.4.4] do |s| - s.add_dependency "bar", "~> 2.0" - end - build_gem "foo", %w[1.4.5 1.5.0] do |s| - s.add_dependency "bar", "~> 2.1" - end - build_gem "foo", %w[1.5.1] do |s| - s.add_dependency "bar", "~> 3.0" - end - build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0] - build_gem "qux", %w[1.0.0 1.1.0 2.0.0] - end - - # establish a lockfile set to 1.4.3 - install_gemfile <<-G - source "https://gem.repo4" - gem 'foo', '1.4.3' - gem 'bar', '2.0.3' - gem 'qux', '1.0.0' - G - - # remove 1.4.3 requirement and bar altogether - # to setup update specs below - gemfile <<-G - source "https://gem.repo4" - gem 'foo' - gem 'qux' - G - end - - it "shows gems updating to patch and filtering to patch" do - bundle "outdated --patch --filter-patch", raise_on_error: false, env: { "DEBUG_RESOLVER" => "1" } - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date - bar 2.0.3 2.0.5 - foo 1.4.3 1.4.4 >= 0 default - TABLE - - expect(out).to end_with(expected_output) - end - - it "shows gems updating to patch and filtering to patch, in debug mode" do - bundle "outdated --patch --filter-patch", raise_on_error: false, env: { "DEBUG" => "1" } - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date Path - bar 2.0.3 2.0.5 - foo 1.4.3 1.4.4 >= 0 default - TABLE - - expect(out).to end_with(expected_output) - end - end - - describe "with --only-explicit" do - it "does not report outdated dependent gems" do - build_repo4 do - build_gem "weakling", %w[0.2 0.3] do |s| - s.add_dependency "bar", "~> 2.1" - end - build_gem "bar", %w[2.1 2.2] - end - - install_gemfile <<-G - source "https://gem.repo4" - gem 'weakling', '0.2' - gem 'bar', '2.1' - G - - gemfile <<-G - source "https://gem.repo4" - gem 'weakling' - G - - bundle "outdated --only-explicit", raise_on_error: false - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date - weakling 0.2 0.3 >= 0 default - TABLE - - expect(out).to end_with(expected_output) - end - end - - describe "with a multiplatform lockfile" do - before do - build_repo4 do - build_gem "nokogiri", "1.11.1" - build_gem "nokogiri", "1.11.1" do |s| - s.platform = Bundler.local_platform - end - - build_gem "nokogiri", "1.11.2" - build_gem "nokogiri", "1.11.2" do |s| - s.platform = Bundler.local_platform - end - end - - lockfile <<~L - GEM - remote: https://gem.repo4/ - specs: - nokogiri (1.11.1) - nokogiri (1.11.1-#{Bundler.local_platform}) - - PLATFORMS - ruby - #{Bundler.local_platform} - - DEPENDENCIES - nokogiri - - BUNDLED WITH - #{Bundler::VERSION} - L - - gemfile <<-G - source "https://gem.repo4" - gem "nokogiri" - G - end - - it "reports a single entry per gem" do - bundle "outdated", raise_on_error: false - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date - nokogiri 1.11.1 1.11.2 >= 0 default - TABLE - - expect(out).to end_with(expected_output) - end - end - - context "when a gem is no longer a dependency after a full update" do - before do - build_repo4 do - build_gem "mini_portile2", "2.5.2" do |s| - s.add_dependency "net-ftp", "~> 0.1" - end - - build_gem "mini_portile2", "2.5.3" - - build_gem "net-ftp", "0.1.2" - end - - gemfile <<~G - source "https://gem.repo4" - - gem "mini_portile2" - G - - lockfile <<~L - GEM - remote: https://gem.repo4/ - specs: - mini_portile2 (2.5.2) - net-ftp (~> 0.1) - net-ftp (0.1.2) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - mini_portile2 - - BUNDLED WITH - #{Bundler::VERSION} - L - end - - it "works" do - bundle "outdated", raise_on_error: false - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date - mini_portile2 2.5.2 2.5.3 >= 0 default - TABLE - - expect(out).to end_with(expected_output) - end - end end diff --git a/spec/bundler/commands/update_scenarios_spec.rb b/spec/bundler/commands/update_scenarios_spec.rb new file mode 100644 index 00000000000000..f1d9635bb22b48 --- /dev/null +++ b/spec/bundler/commands/update_scenarios_spec.rb @@ -0,0 +1,1100 @@ +# frozen_string_literal: true + +RSpec.describe "bundle update in more complicated situations" do + before do + build_repo2 + end + + it "will eagerly unlock dependencies of a specified gem" do + install_gemfile <<-G + source "https://gem.repo2" + + gem "thin" + gem "myrack-obama" + G + + update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + + build_gem "thin", "2.0" do |s| + s.add_dependency "myrack" + end + end + + bundle "update thin" + expect(the_bundle).to include_gems "thin 2.0", "myrack 1.2", "myrack-obama 1.0" + end + + it "will warn when some explicitly updated gems are not updated" do + install_gemfile <<-G + source "https://gem.repo2" + + gem "thin" + gem "myrack-obama" + G + + update_repo2 do + build_gem("thin", "2.0") {|s| s.add_dependency "myrack" } + build_gem "myrack", "10.0" + end + + bundle "update thin myrack-obama" + expect(stdboth).to include "Bundler attempted to update myrack-obama but its version stayed the same" + expect(the_bundle).to include_gems "thin 2.0", "myrack 10.0", "myrack-obama 1.0" + end + + it "will not warn when an explicitly updated git gem changes sha but not version" do + build_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + update_git "foo" do |s| + s.write "lib/foo2.rb", "puts :foo2" + end + + bundle "update foo" + + expect(stdboth).not_to include "attempted to update" + end + + it "will not warn when changing gem sources but not versions" do + build_git "myrack" + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack", :git => '#{lib_path("myrack-1.0")}' + G + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "update myrack" + + expect(stdboth).not_to include "attempted to update" + end + + it "will update only from pinned source" do + install_gemfile <<-G + source "https://gem.repo2" + + source "https://gem.repo1" do + gem "thin" + end + G + + update_repo2 do + build_gem "thin", "2.0" + end + + bundle "update", artifice: "compact_index" + expect(the_bundle).to include_gems "thin 1.0" + end + + context "when the lockfile is for a different platform" do + around do |example| + build_repo4 do + build_gem("a", "0.9") + build_gem("a", "0.9") {|s| s.platform = "java" } + build_gem("a", "1.1") + build_gem("a", "1.1") {|s| s.platform = "java" } + end + + gemfile <<-G + source "https://gem.repo4" + gem "a" + G + + lockfile <<-L + GEM + remote: https://gem.repo4 + specs: + a (0.9-java) + + PLATFORMS + java + + DEPENDENCIES + a + L + + simulate_platform "x86_64-linux", &example + end + + it "allows updating" do + bundle :update, all: true + expect(the_bundle).to include_gem "a 1.1" + end + + it "allows updating a specific gem" do + bundle "update a" + expect(the_bundle).to include_gem "a 1.1" + end + end + + context "when the dependency is for a different platform" do + before do + build_repo4 do + build_gem("a", "0.9") {|s| s.platform = "java" } + build_gem("a", "1.1") {|s| s.platform = "java" } + end + + gemfile <<-G + source "https://gem.repo4" + gem "a", platform: :jruby + G + + lockfile <<-L + GEM + remote: https://gem.repo4 + specs: + a (0.9-java) + + PLATFORMS + java + + DEPENDENCIES + a + L + end + + it "is not updated because it is not actually included in the bundle" do + simulate_platform "x86_64-linux" do + bundle "update a" + expect(stdboth).to include "Bundler attempted to update a but it was not considered because it is for a different platform from the current one" + expect(the_bundle).to_not include_gem "a" + end + end + end +end + +RSpec.describe "bundle update without a Gemfile.lock" do + it "should not explode" do + build_repo2 + + gemfile <<-G + source "https://gem.repo2" + + gem "myrack", "1.0" + G + + bundle "update", all: true + + expect(the_bundle).to include_gems "myrack 1.0.0" + end +end + +RSpec.describe "bundle update when a gem depends on a newer version of bundler" do + before do + build_repo2 do + build_gem "rails", "3.0.1" do |s| + s.add_dependency "bundler", "9.9.9" + end + + build_gem "bundler", "9.9.9" + end + + gemfile <<-G + source "https://gem.repo2" + gem "rails", "3.0.1" + G + end + + it "should explain that bundler conflicted and how to resolve the conflict" do + bundle "update", all: true, raise_on_error: false + expect(stdboth).not_to match(/in snapshot/i) + expect(err).to match(/current Bundler version/i). + and match(/Install the necessary version with `gem install bundler:9\.9\.9`/i) + end +end + +RSpec.describe "bundle update --ruby" do + context "when the Gemfile removes the ruby" do + before do + install_gemfile <<-G + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + + gemfile <<-G + source "https://gem.repo1" + G + end + + it "removes the Ruby from the Gemfile.lock" do + bundle "update --ruby" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + #{checksums_section_when_enabled} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when the Gemfile specified an updated Ruby version" do + before do + install_gemfile <<-G + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + + gemfile <<-G + ruby '~> #{current_ruby_minor}' + source "https://gem.repo1" + G + end + + it "updates the Gemfile.lock with the latest version" do + bundle "update --ruby" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + #{checksums_section_when_enabled} + RUBY VERSION + #{Bundler::RubyVersion.system} + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when a different Ruby is being used than has been versioned" do + before do + install_gemfile <<-G + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + + gemfile <<-G + ruby '~> 2.1.0' + source "https://gem.repo1" + G + end + it "shows a helpful error message" do + bundle "update --ruby", raise_on_error: false + + expect(err).to include("Your Ruby version is #{Bundler::RubyVersion.system.gem_version}, but your Gemfile specified ~> 2.1.0") + end + end + + context "when updating Ruby version and Gemfile `ruby`" do + before do + lockfile <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + + CHECKSUMS + + RUBY VERSION + ruby 2.1.4p222 + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<-G + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + end + + it "updates the Gemfile.lock with the latest version" do + bundle "update --ruby" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + #{checksums_section_when_enabled} + RUBY VERSION + #{Bundler::RubyVersion.system} + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end +end + +RSpec.describe "bundle update --bundler" do + it "updates the bundler version in the lockfile" do + build_repo4 do + build_gem "bundler", "2.5.9" + build_gem "myrack", "1.0" + end + + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo4, "myrack", "1.0") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2') + + bundle :update, bundler: true, verbose: true + expect(out).to include("Using bundler #{Bundler::VERSION}") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + expect(the_bundle).to include_gem "myrack 1.0" + end + + it "updates the bundler version in the lockfile without re-resolving if the highest version is already installed" do + build_repo4 do + build_gem "bundler", "2.3.9" + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, "2.3.9") + + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo4, "myrack", "1.0") + end + + bundle :update, bundler: true, verbose: true + expect(out).to include("Using bundler #{Bundler::VERSION}") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + expect(the_bundle).to include_gem "myrack 1.0" + end + + it "updates the bundler version in the lockfile even if the latest version is not installed", :ruby_repo do + bundle_config "path.system true" + + pristine_system_gems "bundler-9.0.0" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "999.0.0" + end + + checksums = checksums_section do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "999.0.0") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: true, verbose: true + + expect(out).to include("Updating bundler to 999.0.0") + expect(out).to include("Running `bundle update --bundler \"> 0.a\" --verbose` with bundler 999.0.0") + expect(out).not_to include("Installing Bundler 2.99.9 and restarting using that version.") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 999.0.0 + L + + bundle "--version" + expect(out).to include("999.0.0") + + bundle "list" + expect(out).to include("myrack (1.0)") + end + + it "does not claim to update to Bundler version to a wrong version when cached gems are present" do + pristine_system_gems "bundler-4.99.0" + + build_repo4 do + build_gem "myrack", "3.0.9.1" + + build_bundler "4.99.0" + end + + gemfile <<~G + source "https://gem.repo4" + gem "myrack" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (3.0.9.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + + BUNDLED WITH + 2.99.0 + L + + bundle :cache, verbose: true + + bundle :update, bundler: true, verbose: true + + expect(out).not_to include("Updating bundler to") + end + + it "does not update the bundler version in the lockfile if the latest version is not compatible with current ruby", :ruby_repo do + pristine_system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "9.9.9" + build_bundler "999.0.0" do |s| + s.required_ruby_version = "> #{Gem.ruby_version}" + end + end + + checksums = checksums_section do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "9.9.9") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: true, verbose: true + + expect(out).to include("Using bundler 9.9.9") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 9.9.9 + L + + bundle "--version" + expect(out).to include("9.9.9") + + bundle "list" + expect(out).to include("myrack (1.0)") + end + + it "errors if the explicit target version does not exist" do + pristine_system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: "999.999.999", raise_on_error: false + + expect(last_command).to be_failure + expect(err).to eq("The `bundle update --bundler` target version (999.999.999) does not exist") + end + + it "errors if the explicit target version does not exist, even if auto switching is disabled" do + pristine_system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: "999.999.999", raise_on_error: false, env: { "BUNDLER_VERSION" => "9.9.9" } + + expect(last_command).to be_failure + expect(err).to eq("The `bundle update --bundler` target version (999.999.999) does not exist") + end + + it "allows updating to development versions if already installed locally" do + system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + system_gems "bundler-9.0.0.dev", path: local_gem_path + bundle :update, bundler: "9.0.0.dev", verbose: "true" + + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo4, "myrack", "1.0") + end + checksums.delete("bundler") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 9.0.0.dev + L + + expect(out).to include("Using bundler 9.0.0.dev") + end + + it "does not touch the network if not necessary" do + system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + system_gems "bundler-9.0.0", path: local_gem_path + bundle :update, bundler: "9.0.0", verbose: true + + expect(out).not_to include("Fetching gem metadata from https://rubygems.org/") + + # Only updates properly on modern RubyGems. + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(local_gem_path, "bundler", "9.0.0", Gem::Platform::RUBY, "cache") + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 9.0.0 + L + + expect(out).to include("Using bundler 9.0.0") + end + + it "preserves the locked bundler checksum when re-locking without the bundler gem cached" do + system_gems "bundler-9.0.0" + + build_repo4 do + build_gem "myrack", "1.0" + build_gem "weakling", "0.0.3" + + build_bundler "9.0.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + system_gems "bundler-9.0.0", path: local_gem_path + bundle :update, bundler: "9.0.0", verbose: true + + # Sanity check: the lockfile now records the bundler checksum. + expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) + + # Simulate a machine where the bundler gem is not present in the cache + # (e.g. a fresh CI checkout that never downloaded bundler-9.0.0.gem). + FileUtils.rm_f Dir[local_gem_path("cache", "bundler-9.0.0.gem")] + FileUtils.rm_f Dir[system_gem_path("cache", "bundler-9.0.0.gem")] + + # Force a re-resolution / lockfile rewrite. + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + gem "weakling" + G + + # The bundler checksum must survive the rewrite, since it was already locked. + expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) + end + + it "drops the locked bundler checksum when the bundler version changes and the gem isn't cached" do + system_gems "bundler-9.0.0" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "9.0.0" + build_bundler "9.9.9" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + system_gems "bundler-9.0.0", path: local_gem_path + bundle :update, bundler: "9.0.0", verbose: true + + # Sanity check: the lockfile records the bundler 9.0.0 checksum and is + # locked to bundler 9.0.0. + expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) + expect(lockfile).to match(/BUNDLED WITH\n\s+9\.0\.0\n/) + + # Simulate a machine where the bundler gem is not present in the cache + # (e.g. a fresh CI checkout), so a fresh checksum can't be computed. + FileUtils.rm_f Dir[local_gem_path("cache", "bundler-9.0.0.gem")] + FileUtils.rm_f Dir[system_gem_path("cache", "bundler-9.0.0.gem")] + + # Change the locked bundler version. `bundle lock --update --bundler` rewrites + # the BUNDLED WITH section without switching the running bundler, so the gem + # whose checksum is locked (9.0.0) is no longer the version being locked. + bundle "lock --update --bundler 9.9.9", verbose: true + + # The BUNDLED WITH version was bumped... + expect(lockfile).to match(/BUNDLED WITH\n\s+9\.9\.9\n/) + + # ...so the stale `bundler (9.0.0)` checksum must be dropped rather than kept, + # otherwise we'd lock a checksum that no longer matches the BUNDLED WITH + # version (and that we can't recompute since the gem isn't cached). + expect(lockfile).not_to match(/^ bundler \(/) + end + + it "prints an error when trying to update bundler in frozen mode" do + system_gems "bundler-9.0.0" + + gemfile <<~G + source "https://gem.repo2" + G + + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + + PLATFORMS + ruby + + DEPENDENCIES + + BUNDLED WITH + 9.0.0 + L + + system_gems "bundler-9.9.9", path: local_gem_path + + bundle "update --bundler=9.9.9", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false + expect(err).to include("An update to the version of Bundler itself was requested, but the lockfile can't be updated because frozen mode is set") + end +end + +# these specs are slow and focus on integration and therefore are not exhaustive. unit specs elsewhere handle that. +RSpec.describe "bundle update conservative" do + context "patch and minor options" do + before do + build_repo4 do + build_gem "foo", %w[1.4.3 1.4.4] do |s| + s.add_dependency "bar", "~> 2.0" + end + build_gem "foo", %w[1.4.5 1.5.0] do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "foo", %w[1.5.1] do |s| + s.add_dependency "bar", "~> 3.0" + end + build_gem "foo", %w[2.0.0.pre] do |s| + s.add_dependency "bar" + end + build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 2.1.2.pre 3.0.0 3.1.0.pre 4.0.0.pre] + build_gem "qux", %w[1.0.0 1.0.1 1.1.0 2.0.0] + end + + # establish a lockfile set to 1.4.3 + install_gemfile <<-G + source "https://gem.repo4" + gem 'foo', '1.4.3' + gem 'bar', '2.0.3' + gem 'qux', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "https://gem.repo4" + gem 'foo' + gem 'qux' + G + end + + context "with patch set as default update level in config" do + it "should do a patch level update" do + bundle_config "prefer_patch true" + bundle "update foo" + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0" + end + end + + context "patch preferred" do + it "single gem updates dependent gem to minor" do + bundle "update --patch foo" + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0" + end + + it "update all" do + bundle "update --patch", all: true + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.1" + end + end + + context "minor preferred" do + it "single gem updates dependent gem to major" do + bundle "update --minor foo" + + expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.0.0", "qux 1.0.0" + end + end + + context "strict" do + it "patch preferred" do + bundle "update --patch foo bar --strict" + + expect(the_bundle).to include_gems "foo 1.4.4", "bar 2.0.5", "qux 1.0.0" + end + + it "minor preferred" do + bundle "update --minor --strict", all: true + + expect(the_bundle).to include_gems "foo 1.5.0", "bar 2.1.1", "qux 1.1.0" + end + end + + context "pre" do + it "defaults to major" do + bundle "update --pre foo bar" + + expect(the_bundle).to include_gems "foo 2.0.0.pre", "bar 4.0.0.pre", "qux 1.0.0" + end + + it "patch preferred" do + bundle "update --patch --pre foo bar" + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.2.pre", "qux 1.0.0" + end + + it "minor preferred" do + bundle "update --minor --pre foo bar" + + expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.1.0.pre", "qux 1.0.0" + end + + it "major preferred" do + bundle "update --major --pre foo bar" + + expect(the_bundle).to include_gems "foo 2.0.0.pre", "bar 4.0.0.pre", "qux 1.0.0" + end + end + end + + context "eager unlocking" do + before do + build_repo4 do + build_gem "isolated_owner", %w[1.0.1 1.0.2] do |s| + s.add_dependency "isolated_dep", "~> 2.0" + end + build_gem "isolated_dep", %w[2.0.1 2.0.2] + + build_gem "shared_owner_a", %w[3.0.1 3.0.2] do |s| + s.add_dependency "shared_dep", "~> 5.0" + end + build_gem "shared_owner_b", %w[4.0.1 4.0.2] do |s| + s.add_dependency "shared_dep", "~> 5.0" + end + build_gem "shared_dep", %w[5.0.1 5.0.2] + end + + gemfile <<-G + source "https://gem.repo4" + gem 'isolated_owner' + + gem 'shared_owner_a' + gem 'shared_owner_b' + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + isolated_dep (2.0.1) + isolated_owner (1.0.1) + isolated_dep (~> 2.0) + shared_dep (5.0.1) + shared_owner_a (3.0.1) + shared_dep (~> 5.0) + shared_owner_b (4.0.1) + shared_dep (~> 5.0) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + isolated_owner + shared_owner_a + shared_owner_b + + CHECKSUMS + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "should eagerly unlock isolated dependency" do + bundle "update isolated_owner" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.1", "shared_owner_b 4.0.1" + end + + it "should eagerly unlock shared dependency" do + bundle "update shared_owner_a" + + expect(the_bundle).to include_gems "isolated_owner 1.0.1", "isolated_dep 2.0.1", "shared_dep 5.0.2", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + + it "should not eagerly unlock with --conservative" do + bundle "update --conservative shared_owner_a isolated_owner" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + + it "should only update direct dependencies when fully updating with --conservative" do + bundle "update --conservative" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.2" + end + + it "should only change direct dependencies when updating the lockfile with --conservative" do + bundle "lock --update --conservative" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "isolated_dep", "2.0.1" + c.checksum gem_repo4, "isolated_owner", "1.0.2" + c.checksum gem_repo4, "shared_dep", "5.0.1" + c.checksum gem_repo4, "shared_owner_a", "3.0.2" + c.checksum gem_repo4, "shared_owner_b", "4.0.2" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + isolated_dep (2.0.1) + isolated_owner (1.0.2) + isolated_dep (~> 2.0) + shared_dep (5.0.1) + shared_owner_a (3.0.2) + shared_dep (~> 5.0) + shared_owner_b (4.0.2) + shared_dep (~> 5.0) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + isolated_owner + shared_owner_a + shared_owner_b + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "should match bundle install conservative update behavior when not eagerly unlocking" do + gemfile <<-G + source "https://gem.repo4" + gem 'isolated_owner', '1.0.2' + + gem 'shared_owner_a', '3.0.2' + gem 'shared_owner_b' + G + + bundle "install" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + end + + context "when Gemfile dependencies have changed" do + before do + build_repo4 do + build_gem "nokogiri", "1.16.4" do |s| + s.platform = "arm64-darwin" + end + + build_gem "nokogiri", "1.16.4" do |s| + s.platform = "x86_64-linux" + end + + build_gem "prism", "0.25.0" + end + + gemfile <<~G + source "https://gem.repo4" + gem "nokogiri", ">=1.16.4" + gem "prism", ">=0.25.0" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.16.4-arm64-darwin) + nokogiri (1.16.4-x86_64-linux) + + PLATFORMS + arm64-darwin + x86_64-linux + + DEPENDENCIES + nokogiri (>= 1.16.4) + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "still works" do + simulate_platform "arm64-darwin-23" do + bundle "update" + end + end + end + + context "error handling" do + before do + gemfile "source 'https://gem.repo1'" + end + + it "raises if too many flags are provided" do + bundle "update --patch --minor", all: true, raise_on_error: false + + expect(err).to eq "Provide only one of the following options: minor, patch" + end + end +end diff --git a/spec/bundler/commands/update_spec.rb b/spec/bundler/commands/update_spec.rb index e838468a2771eb..fcba43bbea05c6 100644 --- a/spec/bundler/commands/update_spec.rb +++ b/spec/bundler/commands/update_spec.rb @@ -1096,1102 +1096,3 @@ end end end - -RSpec.describe "bundle update in more complicated situations" do - before do - build_repo2 - end - - it "will eagerly unlock dependencies of a specified gem" do - install_gemfile <<-G - source "https://gem.repo2" - - gem "thin" - gem "myrack-obama" - G - - update_repo2 do - build_gem "myrack", "1.2" do |s| - s.executables = "myrackup" - end - - build_gem "thin", "2.0" do |s| - s.add_dependency "myrack" - end - end - - bundle "update thin" - expect(the_bundle).to include_gems "thin 2.0", "myrack 1.2", "myrack-obama 1.0" - end - - it "will warn when some explicitly updated gems are not updated" do - install_gemfile <<-G - source "https://gem.repo2" - - gem "thin" - gem "myrack-obama" - G - - update_repo2 do - build_gem("thin", "2.0") {|s| s.add_dependency "myrack" } - build_gem "myrack", "10.0" - end - - bundle "update thin myrack-obama" - expect(stdboth).to include "Bundler attempted to update myrack-obama but its version stayed the same" - expect(the_bundle).to include_gems "thin 2.0", "myrack 10.0", "myrack-obama 1.0" - end - - it "will not warn when an explicitly updated git gem changes sha but not version" do - build_git "foo" - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => '#{lib_path("foo-1.0")}' - G - - update_git "foo" do |s| - s.write "lib/foo2.rb", "puts :foo2" - end - - bundle "update foo" - - expect(stdboth).not_to include "attempted to update" - end - - it "will not warn when changing gem sources but not versions" do - build_git "myrack" - - install_gemfile <<-G - source "https://gem.repo2" - gem "myrack", :git => '#{lib_path("myrack-1.0")}' - G - - gemfile <<-G - source "https://gem.repo1" - gem "myrack" - G - - bundle "update myrack" - - expect(stdboth).not_to include "attempted to update" - end - - it "will update only from pinned source" do - install_gemfile <<-G - source "https://gem.repo2" - - source "https://gem.repo1" do - gem "thin" - end - G - - update_repo2 do - build_gem "thin", "2.0" - end - - bundle "update", artifice: "compact_index" - expect(the_bundle).to include_gems "thin 1.0" - end - - context "when the lockfile is for a different platform" do - around do |example| - build_repo4 do - build_gem("a", "0.9") - build_gem("a", "0.9") {|s| s.platform = "java" } - build_gem("a", "1.1") - build_gem("a", "1.1") {|s| s.platform = "java" } - end - - gemfile <<-G - source "https://gem.repo4" - gem "a" - G - - lockfile <<-L - GEM - remote: https://gem.repo4 - specs: - a (0.9-java) - - PLATFORMS - java - - DEPENDENCIES - a - L - - simulate_platform "x86_64-linux", &example - end - - it "allows updating" do - bundle :update, all: true - expect(the_bundle).to include_gem "a 1.1" - end - - it "allows updating a specific gem" do - bundle "update a" - expect(the_bundle).to include_gem "a 1.1" - end - end - - context "when the dependency is for a different platform" do - before do - build_repo4 do - build_gem("a", "0.9") {|s| s.platform = "java" } - build_gem("a", "1.1") {|s| s.platform = "java" } - end - - gemfile <<-G - source "https://gem.repo4" - gem "a", platform: :jruby - G - - lockfile <<-L - GEM - remote: https://gem.repo4 - specs: - a (0.9-java) - - PLATFORMS - java - - DEPENDENCIES - a - L - end - - it "is not updated because it is not actually included in the bundle" do - simulate_platform "x86_64-linux" do - bundle "update a" - expect(stdboth).to include "Bundler attempted to update a but it was not considered because it is for a different platform from the current one" - expect(the_bundle).to_not include_gem "a" - end - end - end -end - -RSpec.describe "bundle update without a Gemfile.lock" do - it "should not explode" do - build_repo2 - - gemfile <<-G - source "https://gem.repo2" - - gem "myrack", "1.0" - G - - bundle "update", all: true - - expect(the_bundle).to include_gems "myrack 1.0.0" - end -end - -RSpec.describe "bundle update when a gem depends on a newer version of bundler" do - before do - build_repo2 do - build_gem "rails", "3.0.1" do |s| - s.add_dependency "bundler", "9.9.9" - end - - build_gem "bundler", "9.9.9" - end - - gemfile <<-G - source "https://gem.repo2" - gem "rails", "3.0.1" - G - end - - it "should explain that bundler conflicted and how to resolve the conflict" do - bundle "update", all: true, raise_on_error: false - expect(stdboth).not_to match(/in snapshot/i) - expect(err).to match(/current Bundler version/i). - and match(/Install the necessary version with `gem install bundler:9\.9\.9`/i) - end -end - -RSpec.describe "bundle update --ruby" do - context "when the Gemfile removes the ruby" do - before do - install_gemfile <<-G - ruby '~> #{Gem.ruby_version}' - source "https://gem.repo1" - G - - gemfile <<-G - source "https://gem.repo1" - G - end - - it "removes the Ruby from the Gemfile.lock" do - bundle "update --ruby" - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo1/ - specs: - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - #{checksums_section_when_enabled} - BUNDLED WITH - #{Bundler::VERSION} - L - end - end - - context "when the Gemfile specified an updated Ruby version" do - before do - install_gemfile <<-G - ruby '~> #{Gem.ruby_version}' - source "https://gem.repo1" - G - - gemfile <<-G - ruby '~> #{current_ruby_minor}' - source "https://gem.repo1" - G - end - - it "updates the Gemfile.lock with the latest version" do - bundle "update --ruby" - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo1/ - specs: - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - #{checksums_section_when_enabled} - RUBY VERSION - #{Bundler::RubyVersion.system} - - BUNDLED WITH - #{Bundler::VERSION} - L - end - end - - context "when a different Ruby is being used than has been versioned" do - before do - install_gemfile <<-G - ruby '~> #{Gem.ruby_version}' - source "https://gem.repo1" - G - - gemfile <<-G - ruby '~> 2.1.0' - source "https://gem.repo1" - G - end - it "shows a helpful error message" do - bundle "update --ruby", raise_on_error: false - - expect(err).to include("Your Ruby version is #{Bundler::RubyVersion.system.gem_version}, but your Gemfile specified ~> 2.1.0") - end - end - - context "when updating Ruby version and Gemfile `ruby`" do - before do - lockfile <<~L - GEM - remote: https://gem.repo1/ - specs: - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - - CHECKSUMS - - RUBY VERSION - ruby 2.1.4p222 - - BUNDLED WITH - #{Bundler::VERSION} - L - - gemfile <<-G - ruby '~> #{Gem.ruby_version}' - source "https://gem.repo1" - G - end - - it "updates the Gemfile.lock with the latest version" do - bundle "update --ruby" - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo1/ - specs: - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - #{checksums_section_when_enabled} - RUBY VERSION - #{Bundler::RubyVersion.system} - - BUNDLED WITH - #{Bundler::VERSION} - L - end - end -end - -RSpec.describe "bundle update --bundler" do - it "updates the bundler version in the lockfile" do - build_repo4 do - build_gem "bundler", "2.5.9" - build_gem "myrack", "1.0" - end - - checksums = checksums_section_when_enabled do |c| - c.checksum(gem_repo4, "myrack", "1.0") - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (1.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - BUNDLED WITH - #{Bundler::VERSION} - L - lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2') - - bundle :update, bundler: true, verbose: true - expect(out).to include("Using bundler #{Bundler::VERSION}") - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (1.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - BUNDLED WITH - #{Bundler::VERSION} - L - - expect(the_bundle).to include_gem "myrack 1.0" - end - - it "updates the bundler version in the lockfile without re-resolving if the highest version is already installed" do - build_repo4 do - build_gem "bundler", "2.3.9" - build_gem "myrack", "1.0" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, "2.3.9") - - checksums = checksums_section_when_enabled do |c| - c.checksum(gem_repo4, "myrack", "1.0") - end - - bundle :update, bundler: true, verbose: true - expect(out).to include("Using bundler #{Bundler::VERSION}") - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (1.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - BUNDLED WITH - #{Bundler::VERSION} - L - - expect(the_bundle).to include_gem "myrack 1.0" - end - - it "updates the bundler version in the lockfile even if the latest version is not installed", :ruby_repo do - bundle_config "path.system true" - - pristine_system_gems "bundler-9.0.0" - - build_repo4 do - build_gem "myrack", "1.0" - - build_bundler "999.0.0" - end - - checksums = checksums_section do |c| - c.checksum(gem_repo4, "myrack", "1.0") - c.checksum(gem_repo4, "bundler", "999.0.0") - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - - bundle :update, bundler: true, verbose: true - - expect(out).to include("Updating bundler to 999.0.0") - expect(out).to include("Running `bundle update --bundler \"> 0.a\" --verbose` with bundler 999.0.0") - expect(out).not_to include("Installing Bundler 2.99.9 and restarting using that version.") - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (1.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - BUNDLED WITH - 999.0.0 - L - - bundle "--version" - expect(out).to include("999.0.0") - - bundle "list" - expect(out).to include("myrack (1.0)") - end - - it "does not claim to update to Bundler version to a wrong version when cached gems are present" do - pristine_system_gems "bundler-4.99.0" - - build_repo4 do - build_gem "myrack", "3.0.9.1" - - build_bundler "4.99.0" - end - - gemfile <<~G - source "https://gem.repo4" - gem "myrack" - G - - lockfile <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (3.0.9.1) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - - BUNDLED WITH - 2.99.0 - L - - bundle :cache, verbose: true - - bundle :update, bundler: true, verbose: true - - expect(out).not_to include("Updating bundler to") - end - - it "does not update the bundler version in the lockfile if the latest version is not compatible with current ruby", :ruby_repo do - pristine_system_gems "bundler-9.9.9" - - build_repo4 do - build_gem "myrack", "1.0" - - build_bundler "9.9.9" - build_bundler "999.0.0" do |s| - s.required_ruby_version = "> #{Gem.ruby_version}" - end - end - - checksums = checksums_section do |c| - c.checksum(gem_repo4, "myrack", "1.0") - c.checksum(gem_repo4, "bundler", "9.9.9") - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - - bundle :update, bundler: true, verbose: true - - expect(out).to include("Using bundler 9.9.9") - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (1.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - BUNDLED WITH - 9.9.9 - L - - bundle "--version" - expect(out).to include("9.9.9") - - bundle "list" - expect(out).to include("myrack (1.0)") - end - - it "errors if the explicit target version does not exist" do - pristine_system_gems "bundler-9.9.9" - - build_repo4 do - build_gem "myrack", "1.0" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - - bundle :update, bundler: "999.999.999", raise_on_error: false - - expect(last_command).to be_failure - expect(err).to eq("The `bundle update --bundler` target version (999.999.999) does not exist") - end - - it "errors if the explicit target version does not exist, even if auto switching is disabled" do - pristine_system_gems "bundler-9.9.9" - - build_repo4 do - build_gem "myrack", "1.0" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - - bundle :update, bundler: "999.999.999", raise_on_error: false, env: { "BUNDLER_VERSION" => "9.9.9" } - - expect(last_command).to be_failure - expect(err).to eq("The `bundle update --bundler` target version (999.999.999) does not exist") - end - - it "allows updating to development versions if already installed locally" do - system_gems "bundler-9.9.9" - - build_repo4 do - build_gem "myrack", "1.0" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - - system_gems "bundler-9.0.0.dev", path: local_gem_path - bundle :update, bundler: "9.0.0.dev", verbose: "true" - - checksums = checksums_section_when_enabled do |c| - c.checksum(gem_repo4, "myrack", "1.0") - end - checksums.delete("bundler") - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (1.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - BUNDLED WITH - 9.0.0.dev - L - - expect(out).to include("Using bundler 9.0.0.dev") - end - - it "does not touch the network if not necessary" do - system_gems "bundler-9.9.9" - - build_repo4 do - build_gem "myrack", "1.0" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - system_gems "bundler-9.0.0", path: local_gem_path - bundle :update, bundler: "9.0.0", verbose: true - - expect(out).not_to include("Fetching gem metadata from https://rubygems.org/") - - # Only updates properly on modern RubyGems. - checksums = checksums_section_when_enabled do |c| - c.checksum(gem_repo4, "myrack", "1.0") - c.checksum(local_gem_path, "bundler", "9.0.0", Gem::Platform::RUBY, "cache") - end - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (1.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - BUNDLED WITH - 9.0.0 - L - - expect(out).to include("Using bundler 9.0.0") - end - - it "preserves the locked bundler checksum when re-locking without the bundler gem cached" do - system_gems "bundler-9.0.0" - - build_repo4 do - build_gem "myrack", "1.0" - build_gem "weakling", "0.0.3" - - build_bundler "9.0.0" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - - system_gems "bundler-9.0.0", path: local_gem_path - bundle :update, bundler: "9.0.0", verbose: true - - # Sanity check: the lockfile now records the bundler checksum. - expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) - - # Simulate a machine where the bundler gem is not present in the cache - # (e.g. a fresh CI checkout that never downloaded bundler-9.0.0.gem). - FileUtils.rm_f Dir[local_gem_path("cache", "bundler-9.0.0.gem")] - FileUtils.rm_f Dir[system_gem_path("cache", "bundler-9.0.0.gem")] - - # Force a re-resolution / lockfile rewrite. - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - gem "weakling" - G - - # The bundler checksum must survive the rewrite, since it was already locked. - expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) - end - - it "drops the locked bundler checksum when the bundler version changes and the gem isn't cached" do - system_gems "bundler-9.0.0" - - build_repo4 do - build_gem "myrack", "1.0" - - build_bundler "9.0.0" - build_bundler "9.9.9" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - - system_gems "bundler-9.0.0", path: local_gem_path - bundle :update, bundler: "9.0.0", verbose: true - - # Sanity check: the lockfile records the bundler 9.0.0 checksum and is - # locked to bundler 9.0.0. - expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) - expect(lockfile).to match(/BUNDLED WITH\n\s+9\.0\.0\n/) - - # Simulate a machine where the bundler gem is not present in the cache - # (e.g. a fresh CI checkout), so a fresh checksum can't be computed. - FileUtils.rm_f Dir[local_gem_path("cache", "bundler-9.0.0.gem")] - FileUtils.rm_f Dir[system_gem_path("cache", "bundler-9.0.0.gem")] - - # Change the locked bundler version. `bundle lock --update --bundler` rewrites - # the BUNDLED WITH section without switching the running bundler, so the gem - # whose checksum is locked (9.0.0) is no longer the version being locked. - bundle "lock --update --bundler 9.9.9", verbose: true - - # The BUNDLED WITH version was bumped... - expect(lockfile).to match(/BUNDLED WITH\n\s+9\.9\.9\n/) - - # ...so the stale `bundler (9.0.0)` checksum must be dropped rather than kept, - # otherwise we'd lock a checksum that no longer matches the BUNDLED WITH - # version (and that we can't recompute since the gem isn't cached). - expect(lockfile).not_to match(/^ bundler \(/) - end - - it "prints an error when trying to update bundler in frozen mode" do - system_gems "bundler-9.0.0" - - gemfile <<~G - source "https://gem.repo2" - G - - lockfile <<-L - GEM - remote: https://gem.repo2/ - specs: - - PLATFORMS - ruby - - DEPENDENCIES - - BUNDLED WITH - 9.0.0 - L - - system_gems "bundler-9.9.9", path: local_gem_path - - bundle "update --bundler=9.9.9", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false - expect(err).to include("An update to the version of Bundler itself was requested, but the lockfile can't be updated because frozen mode is set") - end -end - -# these specs are slow and focus on integration and therefore are not exhaustive. unit specs elsewhere handle that. -RSpec.describe "bundle update conservative" do - context "patch and minor options" do - before do - build_repo4 do - build_gem "foo", %w[1.4.3 1.4.4] do |s| - s.add_dependency "bar", "~> 2.0" - end - build_gem "foo", %w[1.4.5 1.5.0] do |s| - s.add_dependency "bar", "~> 2.1" - end - build_gem "foo", %w[1.5.1] do |s| - s.add_dependency "bar", "~> 3.0" - end - build_gem "foo", %w[2.0.0.pre] do |s| - s.add_dependency "bar" - end - build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 2.1.2.pre 3.0.0 3.1.0.pre 4.0.0.pre] - build_gem "qux", %w[1.0.0 1.0.1 1.1.0 2.0.0] - end - - # establish a lockfile set to 1.4.3 - install_gemfile <<-G - source "https://gem.repo4" - gem 'foo', '1.4.3' - gem 'bar', '2.0.3' - gem 'qux', '1.0.0' - G - - # remove 1.4.3 requirement and bar altogether - # to setup update specs below - gemfile <<-G - source "https://gem.repo4" - gem 'foo' - gem 'qux' - G - end - - context "with patch set as default update level in config" do - it "should do a patch level update" do - bundle_config "prefer_patch true" - bundle "update foo" - - expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0" - end - end - - context "patch preferred" do - it "single gem updates dependent gem to minor" do - bundle "update --patch foo" - - expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0" - end - - it "update all" do - bundle "update --patch", all: true - - expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.1" - end - end - - context "minor preferred" do - it "single gem updates dependent gem to major" do - bundle "update --minor foo" - - expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.0.0", "qux 1.0.0" - end - end - - context "strict" do - it "patch preferred" do - bundle "update --patch foo bar --strict" - - expect(the_bundle).to include_gems "foo 1.4.4", "bar 2.0.5", "qux 1.0.0" - end - - it "minor preferred" do - bundle "update --minor --strict", all: true - - expect(the_bundle).to include_gems "foo 1.5.0", "bar 2.1.1", "qux 1.1.0" - end - end - - context "pre" do - it "defaults to major" do - bundle "update --pre foo bar" - - expect(the_bundle).to include_gems "foo 2.0.0.pre", "bar 4.0.0.pre", "qux 1.0.0" - end - - it "patch preferred" do - bundle "update --patch --pre foo bar" - - expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.2.pre", "qux 1.0.0" - end - - it "minor preferred" do - bundle "update --minor --pre foo bar" - - expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.1.0.pre", "qux 1.0.0" - end - - it "major preferred" do - bundle "update --major --pre foo bar" - - expect(the_bundle).to include_gems "foo 2.0.0.pre", "bar 4.0.0.pre", "qux 1.0.0" - end - end - end - - context "eager unlocking" do - before do - build_repo4 do - build_gem "isolated_owner", %w[1.0.1 1.0.2] do |s| - s.add_dependency "isolated_dep", "~> 2.0" - end - build_gem "isolated_dep", %w[2.0.1 2.0.2] - - build_gem "shared_owner_a", %w[3.0.1 3.0.2] do |s| - s.add_dependency "shared_dep", "~> 5.0" - end - build_gem "shared_owner_b", %w[4.0.1 4.0.2] do |s| - s.add_dependency "shared_dep", "~> 5.0" - end - build_gem "shared_dep", %w[5.0.1 5.0.2] - end - - gemfile <<-G - source "https://gem.repo4" - gem 'isolated_owner' - - gem 'shared_owner_a' - gem 'shared_owner_b' - G - - lockfile <<~L - GEM - remote: https://gem.repo4/ - specs: - isolated_dep (2.0.1) - isolated_owner (1.0.1) - isolated_dep (~> 2.0) - shared_dep (5.0.1) - shared_owner_a (3.0.1) - shared_dep (~> 5.0) - shared_owner_b (4.0.1) - shared_dep (~> 5.0) - - PLATFORMS - #{local_platform} - - DEPENDENCIES - isolated_owner - shared_owner_a - shared_owner_b - - CHECKSUMS - - BUNDLED WITH - #{Bundler::VERSION} - L - end - - it "should eagerly unlock isolated dependency" do - bundle "update isolated_owner" - - expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.1", "shared_owner_b 4.0.1" - end - - it "should eagerly unlock shared dependency" do - bundle "update shared_owner_a" - - expect(the_bundle).to include_gems "isolated_owner 1.0.1", "isolated_dep 2.0.1", "shared_dep 5.0.2", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" - end - - it "should not eagerly unlock with --conservative" do - bundle "update --conservative shared_owner_a isolated_owner" - - expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" - end - - it "should only update direct dependencies when fully updating with --conservative" do - bundle "update --conservative" - - expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.2" - end - - it "should only change direct dependencies when updating the lockfile with --conservative" do - bundle "lock --update --conservative" - - checksums = checksums_section_when_enabled do |c| - c.checksum gem_repo4, "isolated_dep", "2.0.1" - c.checksum gem_repo4, "isolated_owner", "1.0.2" - c.checksum gem_repo4, "shared_dep", "5.0.1" - c.checksum gem_repo4, "shared_owner_a", "3.0.2" - c.checksum gem_repo4, "shared_owner_b", "4.0.2" - end - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - isolated_dep (2.0.1) - isolated_owner (1.0.2) - isolated_dep (~> 2.0) - shared_dep (5.0.1) - shared_owner_a (3.0.2) - shared_dep (~> 5.0) - shared_owner_b (4.0.2) - shared_dep (~> 5.0) - - PLATFORMS - #{local_platform} - - DEPENDENCIES - isolated_owner - shared_owner_a - shared_owner_b - #{checksums} - BUNDLED WITH - #{Bundler::VERSION} - L - end - - it "should match bundle install conservative update behavior when not eagerly unlocking" do - gemfile <<-G - source "https://gem.repo4" - gem 'isolated_owner', '1.0.2' - - gem 'shared_owner_a', '3.0.2' - gem 'shared_owner_b' - G - - bundle "install" - - expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" - end - end - - context "when Gemfile dependencies have changed" do - before do - build_repo4 do - build_gem "nokogiri", "1.16.4" do |s| - s.platform = "arm64-darwin" - end - - build_gem "nokogiri", "1.16.4" do |s| - s.platform = "x86_64-linux" - end - - build_gem "prism", "0.25.0" - end - - gemfile <<~G - source "https://gem.repo4" - gem "nokogiri", ">=1.16.4" - gem "prism", ">=0.25.0" - G - - lockfile <<~L - GEM - remote: https://gem.repo4/ - specs: - nokogiri (1.16.4-arm64-darwin) - nokogiri (1.16.4-x86_64-linux) - - PLATFORMS - arm64-darwin - x86_64-linux - - DEPENDENCIES - nokogiri (>= 1.16.4) - - BUNDLED WITH - #{Bundler::VERSION} - L - end - - it "still works" do - simulate_platform "arm64-darwin-23" do - bundle "update" - end - end - end - - context "error handling" do - before do - gemfile "source 'https://gem.repo1'" - end - - it "raises if too many flags are provided" do - bundle "update --patch --minor", all: true, raise_on_error: false - - expect(err).to eq "Provide only one of the following options: minor, patch" - end - end -end diff --git a/spec/bundler/runtime/setup_gems_spec.rb b/spec/bundler/runtime/setup_gems_spec.rb new file mode 100644 index 00000000000000..1a8df525d3cbf2 --- /dev/null +++ b/spec/bundler/runtime/setup_gems_spec.rb @@ -0,0 +1,825 @@ +# frozen_string_literal: true + +require "tmpdir" + +RSpec.describe "Bundler.setup" do + it "should prepend gemspec require paths to $LOAD_PATH in order" do + update_repo2 do + build_gem("requirepaths") do |s| + s.write("lib/rq.rb", "puts 'yay'") + s.write("src/rq.rb", "puts 'nooo'") + s.require_paths = %w[lib src] + end + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "requirepaths", :require => nil + G + + run "require 'rq'" + expect(out).to eq("yay") + end + + it "should clean $LOAD_PATH properly" do + gem_name = "very_simple_binary" + full_gem_name = gem_name + "-1.0" + + system_gems full_gem_name + + install_gemfile <<-G + source "https://gem.repo1" + G + + ruby <<-R + require 'bundler' + gem '#{gem_name}' + + puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} >= 2 + + Bundler.setup + + puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} == 0 + R + + expect(out).to eq("true\ntrue") + end + + context "with bundler is located in symlinked GEM_HOME" do + let(:gem_home) { Dir.mktmpdir } + let(:symlinked_gem_home) { tmp("gem_home-symlink").to_s } + let(:full_name) { "bundler-#{Bundler::VERSION}" } + + before do + File.symlink(gem_home, symlinked_gem_home) + gems_dir = File.join(gem_home, "gems") + specifications_dir = File.join(gem_home, "specifications") + Dir.mkdir(gems_dir) + Dir.mkdir(specifications_dir) + + File.symlink(source_root, File.join(gems_dir, full_name)) + + gemspec_content = File.binread(gemspec). + sub("Bundler::VERSION", %("#{Bundler::VERSION}")). + lines.reject {|line| line.include?("lib/bundler/version") }.join + + File.open(File.join(specifications_dir, "#{full_name}.gemspec"), "wb") do |f| + f.write(gemspec_content) + end + end + + it "should not remove itself from the LOAD_PATH and require a different copy of 'bundler/setup'" do + install_gemfile "source 'https://gem.repo1'" + + ruby <<-R, env: { "GEM_PATH" => symlinked_gem_home } + TracePoint.trace(:class) do |tp| + if tp.path.include?("bundler") && !tp.path.start_with?("#{source_root}") + puts "OMG. Defining a class from another bundler at \#{tp.path}:\#{tp.lineno}" + end + end + gem 'bundler', '#{Bundler::VERSION}' + require 'bundler/setup' + R + + expect(out).to be_empty + end + end + + it "does not reveal system gems even when Gem.refresh is called" do + system_gems "myrack-1.0.0" + + install_gemfile <<-G + source "https://gem.repo1" + gem "activesupport" + G + + run <<-R + puts Bundler.rubygems.installed_specs.map(&:name) + Gem.refresh + puts Bundler.rubygems.installed_specs.map(&:name) + R + + expect(out).to include("activesupport") + expect(out).not_to include("myrack") + end + + describe "when a vendored gem specification uses the :path option" do + let(:filesystem_root) do + current = Pathname.new(Dir.pwd) + current = current.parent until current == current.parent + current + end + + it "should resolve paths relative to the Gemfile" do + path = bundled_app(File.join("vendor", "foo")) + build_lib "foo", path: path + + # If the .gemspec exists, then Bundler handles the path differently. + # See Source::Path.load_spec_files for details. + FileUtils.rm(File.join(path, "foo.gemspec")) + + install_gemfile <<-G + source "https://gem.repo1" + gem 'foo', '1.2.3', :path => 'vendor/foo' + G + + run <<-R, env: { "BUNDLE_GEMFILE" => bundled_app_gemfile.to_s }, dir: bundled_app.parent + require 'foo' + R + expect(err).to be_empty + end + + it "should make sure the Bundler.root is really included in the path relative to the Gemfile" do + relative_path = File.join("vendor", Dir.pwd.gsub(/^#{filesystem_root}/, "")) + absolute_path = bundled_app(relative_path) + FileUtils.mkdir_p(absolute_path) + build_lib "foo", path: absolute_path + + # If the .gemspec exists, then Bundler handles the path differently. + # See Source::Path.load_spec_files for details. + FileUtils.rm(File.join(absolute_path, "foo.gemspec")) + + gemfile <<-G + source "https://gem.repo1" + gem 'foo', '1.2.3', :path => '#{relative_path}' + G + + bundle :install + + run <<-R, env: { "BUNDLE_GEMFILE" => bundled_app_gemfile.to_s }, dir: bundled_app.parent + require 'foo' + R + + expect(err).to be_empty + end + end + + describe "with git gems that don't have gemspecs" do + before :each do + build_git "no_gemspec", gemspec: false + + install_gemfile <<-G + source "https://gem.repo1" + gem "no_gemspec", "1.0", :git => "#{lib_path("no_gemspec-1.0")}" + G + end + + it "loads the library via a virtual spec" do + run <<-R + require 'no_gemspec' + puts NO_GEMSPEC + R + + expect(out).to eq("1.0") + end + end + + describe "with bundled and system gems" do + before :each do + system_gems "myrack-1.0.0" + + install_gemfile <<-G + source "https://gem.repo1" + + gem "activesupport", "2.3.5" + G + end + + it "does not pull in system gems" do + run <<-R + begin; + require 'myrack' + rescue LoadError + puts 'WIN' + end + R + + expect(out).to eq("WIN") + end + + it "provides a gem method" do + run <<-R + gem 'activesupport' + require 'activesupport' + puts ACTIVESUPPORT + R + + expect(out).to eq("2.3.5") + end + + it "raises an exception if gem is used to invoke a system gem not in the bundle" do + run <<-R + begin + gem 'myrack' + rescue LoadError => e + puts e.message + end + R + + expect(out).to eq("myrack is not part of the bundle. Add it to your Gemfile.") + end + + it "sets GEM_HOME appropriately" do + run "puts ENV['GEM_HOME']" + expect(out).to eq(default_bundle_path.to_s) + end + end + + describe "with system gems in the bundle" do + before :each do + bundle_config "path.system true" + system_gems "myrack-1.0.0" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0" + gem "activesupport", "2.3.5" + G + end + + it "sets GEM_PATH appropriately" do + run "puts Gem.path" + paths = out.split("\n") + expect(paths).to include(system_gem_path.to_s) + end + end + + describe "with a gemspec that requires other files" do + before :each do + build_git "bar", gemspec: false do |s| + s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + require_relative 'lib/bar/version' + + Gem::Specification.new do |s| + s.name = 'bar' + s.version = BAR_VERSION + s.summary = 'Bar' + s.files = Dir["lib/**/*.rb"] + s.author = 'no one' + end + G + end + + gemfile <<-G + source "https://gem.repo1" + gem "bar", :git => "#{lib_path("bar-1.0")}" + G + end + + it "evals each gemspec in the context of its parent directory" do + bundle :install + run "require 'bar'; puts BAR" + expect(out).to eq("1.0") + end + + it "error intelligently if the gemspec has a LoadError" do + ref = update_git "bar", gemspec: false do |s| + s.write "bar.gemspec", "require 'foobarbaz'" + end.ref_for("HEAD") + bundle :install, raise_on_error: false + + expect(err.lines.map(&:chomp)).to include( + a_string_starting_with("[!] There was an error while loading `bar.gemspec`:"), + " # from #{default_bundle_path "bundler", "gems", "bar-1.0-#{ref[0, 12]}", "bar.gemspec"}:1", + " > require 'foobarbaz'" + ) + end + + it "evals each gemspec with a binding from the top level" do + bundle "install" + + ruby <<-RUBY + require 'bundler' + bundler_module = class << Bundler; self; end + bundler_module.send(:remove_method, :require) + def Bundler.require(path) + raise StandardError, "didn't use binding from top level" + end + Bundler.load + RUBY + + expect(err).to be_empty + expect(out).to be_empty + end + end + + describe "when Bundler is bundled" do + it "doesn't blow up" do + install_gemfile <<-G + source "https://gem.repo1" + gem "bundler", :path => "#{root}" + G + + bundle %(exec ruby -e "require 'bundler'; Bundler.setup") + expect(err).to be_empty + end + end + + describe "when BUNDLED WITH" do + def lock_with(bundler_version = nil) + lock = <<~L + GEM + remote: https://gem.repo1/ + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + L + + if bundler_version + lock += "\nBUNDLED WITH\n #{bundler_version}\n" + end + + lock + end + + before do + bundle_config "path.system true" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + end + + context "is not present" do + it "does not change the lock" do + lockfile lock_with(nil) + ruby "require 'bundler/setup'" + expect(lockfile).to eq lock_with(nil) + end + end + + context "is newer" do + it "does not change the lock or warn" do + lockfile lock_with(Bundler::VERSION.succ) + ruby "require 'bundler/setup'" + expect(out).to be_empty + expect(err).to be_empty + expect(lockfile).to eq lock_with(Bundler::VERSION.succ) + end + end + + context "is older" do + it "does not change the lock" do + system_gems "bundler-1.10.1" + lockfile lock_with("1.10.1") + ruby "require 'bundler/setup'" + expect(lockfile).to eq lock_with("1.10.1") + end + end + end + + describe "when RUBY VERSION" do + let(:ruby_version) { nil } + + def lock_with(ruby_version = nil) + checksums = checksums_section do |c| + c.checksum gem_repo1, "myrack", "1.0.0" + end + + lock = <<~L + GEM + remote: https://gem.repo1/ + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + L + + if ruby_version + lock += "\nRUBY VERSION\n ruby #{ruby_version}\n" + end + + lock += <<~L + + BUNDLED WITH + #{Bundler::VERSION} + L + + lock + end + + before do + install_gemfile <<-G + ruby ">= 0" + source "https://gem.repo1" + gem "myrack" + G + lockfile lock_with(ruby_version) + end + + context "is not present" do + # Skipped on ruby-core, and on the release-version CI variant, because + # `ruby "require 'bundler/setup'"` does not activate bundler as a gem + # there, so Source::Metadata falls back to a synthetic spec whose + # cache_file does not exist on disk and LockfileGenerator#bundler_checksum + # drops the bundler checksum, while the on-disk lockfile still has it. + # In-development (.dev) versions never write a bundler checksum, so the + # regular suite stays unaffected. + it "does not change the lock", :ruby_repo do + skip "bundler is loaded from the source tree, not installed as a gem" unless Bundler::VERSION.end_with?(".dev") + + expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } + end + end + + context "is newer" do + let(:ruby_version) { "5.5.5" } + it "does not change the lock or warn" do + expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } + expect(out).to be_empty + expect(err).to be_empty + end + end + + context "is older" do + let(:ruby_version) { "1.0.0" } + it "does not change the lock" do + expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } + end + end + end + + describe "with gemified standard libraries" do + it "does not load Digest", :ruby_repo do + build_git "bar", gemspec: false do |s| + s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + require_relative 'lib/bar/version' + + Gem::Specification.new do |s| + s.name = 'bar' + s.version = BAR_VERSION + s.summary = 'Bar' + s.files = Dir["lib/**/*.rb"] + s.author = 'no one' + + s.add_dependency 'digest' + end + G + end + + gemfile <<-G + source "https://gem.repo1" + gem "bar", :git => "#{lib_path("bar-1.0")}" + G + + bundle :install, env: { "BUNDLE_LOCKFILE_CHECKSUMS" => "false" } + + ruby <<-RUBY, artifice: nil + require 'bundler/setup' + puts defined?(::Digest) ? "Digest defined" : "Digest undefined" + require 'digest' + RUBY + expect(out).to eq("Digest undefined") + end + + it "does not load Psych" do + gemfile "source 'https://gem.repo1'" + ruby <<-RUBY + require 'bundler/setup' + puts defined?(Psych::VERSION) ? Psych::VERSION : "undefined" + require 'psych' + puts Psych::VERSION + RUBY + pre_bundler, post_bundler = out.split("\n") + expect(pre_bundler).to eq("undefined") + expect(post_bundler).to match(/\d+\.\d+\.\d+/) + end + + it "does not load openssl" do + install_gemfile "source 'https://gem.repo1'" + ruby <<-RUBY, artifice: nil + require "bundler/setup" + puts defined?(OpenSSL) || "undefined" + require "openssl" + puts defined?(OpenSSL) || "undefined" + RUBY + expect(out).to eq("undefined\nconstant") + end + + it "does not load uri while reading gemspecs", rubygems: ">= 3.6.0.dev" do + Dir.mkdir bundled_app("test") + + create_file(bundled_app("test/test.gemspec"), <<-G) + Gem::Specification.new do |s| + s.name = "test" + s.version = "1.0.0" + s.summary = "test" + s.authors = ['John Doe'] + s.homepage = 'https://example.com' + end + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "test", path: "#{bundled_app("test")}" + G + + ruby <<-RUBY, artifice: nil + require "bundler/setup" + puts defined?(URI) || "undefined" + require "uri" + puts defined?(URI) || "undefined" + RUBY + expect(out).to eq("undefined\nconstant") + end + + it "activates default gems when they are part of the bundle, but not installed explicitly", :ruby_repo do + default_delegate_version = ruby "gem 'delegate'; require 'delegate'; puts Delegator::VERSION" + + build_repo2 do + build_gem "delegate", default_delegate_version + end + + gemfile "source \"https://gem.repo2\"; gem 'delegate'" + + ruby <<-RUBY + require "bundler/setup" + require "delegate" + puts defined?(::Delegator) ? "Delegator defined" : "Delegator undefined" + RUBY + + expect(out).to eq("Delegator defined") + expect(err).to be_empty + end + + describe "default gem activation" do + let(:exemptions) do + exempts = %w[did_you_mean bundler uri pathname] + exempts << "error_highlight" # added in Ruby 3.1 as a default gem + exempts << "ruby2_keywords" # added in Ruby 3.1 as a default gem + exempts << "syntax_suggest" # added in Ruby 3.2 as a default gem + exempts + end + + let(:activation_warning_hack) { <<~RUBY } + require #{spec_dir.join("support/hax").to_s.dump} + + Gem::Specification.send(:alias_method, :bundler_spec_activate, :activate) + Gem::Specification.send(:define_method, :activate) do + unless #{exemptions.inspect}.include?(name) + warn '-' * 80 + warn "activating \#{full_name}" + warn(*caller) + warn '*' * 80 + end + bundler_spec_activate + end + RUBY + + let(:activation_warning_hack_rubyopt) do + create_file("activation_warning_hack.rb", activation_warning_hack) + "-r#{bundled_app("activation_warning_hack.rb")} #{ENV["RUBYOPT"]}" + end + + let(:code) { <<~RUBY } + require "pp" + loaded_specs = Gem.loaded_specs.dup + #{exemptions.inspect}.each {|s| loaded_specs.delete(s) } + pp loaded_specs + + # not a default gem, but harmful to have loaded + open_uri = $LOADED_FEATURES.grep(/open.uri/) + unless open_uri.empty? + warn "open_uri: \#{open_uri}" + end + RUBY + + it "activates no gems with -rbundler/setup" do + install_gemfile "source 'https://gem.repo1'" + ruby code, env: { "RUBYOPT" => activation_warning_hack_rubyopt + " -rbundler/setup" }, artifice: nil + expect(out).to eq("{}") + end + + it "activates no gems with bundle exec" do + install_gemfile "source 'https://gem.repo1'" + create_file("script.rb", code) + bundle "exec ruby ./script.rb", env: { "RUBYOPT" => activation_warning_hack_rubyopt } + expect(out).to eq("{}") + end + + it "activates no gems with bundle exec that is loaded" do + skip "not executable" if Gem.win_platform? + + install_gemfile "source 'https://gem.repo1'" + create_file("script.rb", "#!/usr/bin/env ruby\n\n#{code}") + FileUtils.chmod(0o777, bundled_app("script.rb")) + bundle "exec ./script.rb", env: { "RUBYOPT" => activation_warning_hack_rubyopt } + expect(out).to eq("{}") + end + + it "does not load net-http-pipeline too early" do + build_repo4 do + build_gem "net-http-pipeline", "1.0.1" + end + + system_gems "net-http-pipeline-1.0.1", gem_repo: gem_repo4 + + gemfile <<-G + source "https://gem.repo4" + gem "net-http-pipeline", "1.0.1" + G + + bundle_config "path vendor/bundle" + + bundle :install + + bundle :check + + expect(out).to eq("The Gemfile's dependencies are satisfied") + end + + Gem::Specification.select(&:default_gem?).map(&:name).each do |g| + it "activates newer versions of #{g}", :ruby_repo do + skip if exemptions.include?(g) + + build_repo4 do + build_gem g, "999999" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "#{g}", "999999" + G + + expect(the_bundle).to include_gem("#{g} 999999", env: { "RUBYOPT" => activation_warning_hack_rubyopt }, artifice: nil) + end + + it "activates older versions of #{g}", :ruby_repo do + skip if exemptions.include?(g) + + build_repo4 do + build_gem g, "0.0.0.a" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "#{g}", "0.0.0.a" + G + + expect(the_bundle).to include_gem("#{g} 0.0.0.a", env: { "RUBYOPT" => activation_warning_hack_rubyopt }, artifice: nil) + end + end + end + end + + describe "after setup" do + it "keeps Kernel#gem private" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + ruby <<-RUBY, raise_on_error: false + require "bundler/setup" + Object.new.gem "myrack" + puts "FAIL" + RUBY + + expect(stdboth).not_to include "FAIL" + expect(err).to match(/private method [`']gem'/) + end + + it "keeps Kernel#require private" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + ruby <<-RUBY, raise_on_error: false + require "bundler/setup" + Object.new.require "myrack" + puts "FAIL" + RUBY + + expect(stdboth).not_to include "FAIL" + expect(err).to match(/private method [`']require'/) + end + + it "memoizes initial set of specs when requiring bundler/setup, so that even if further code mutates dependencies, Bundler.definition.specs is not affected" do + install_gemfile <<~G + source "https://gem.repo1" + gem "yard" + gem "myrack", :group => :test + G + + ruby <<-RUBY, raise_on_error: false + require "bundler/setup" + Bundler.require(:test).select! {|d| (d.groups & [:test]).any? } + puts Bundler.definition.specs.map(&:name).join(", ") + RUBY + + expect(out).to include("myrack, yard") + end + + it "does not cause double loads when higher versions of default gems are activated before bundler" do + build_repo2 do + build_gem "json", "999.999.999" do |s| + s.write "lib/json.rb", <<~RUBY + module JSON + VERSION = "999.999.999" + end + RUBY + end + end + + system_gems "json-999.999.999", gem_repo: gem_repo2 + + install_gemfile "source 'https://gem.repo1'" + ruby <<-RUBY + require "json" + require "bundler/setup" + require "json" + RUBY + + expect(err).to be_empty + end + end + + it "does not undo the Kernel.require decorations", rubygems: ">= 3.4.6" do + install_gemfile "source 'https://gem.repo1'" + script = bundled_app("bin/script") + create_file(script, <<~RUBY) + module Kernel + module_function + + alias_method :require_before_extra_monkeypatches, :require + + def require(path) + puts "requiring \#{path} used the monkeypatch" + + require_before_extra_monkeypatches(path) + end + end + + require "bundler/setup" + + require "foo" + RUBY + + sys_exec "#{Gem.ruby} #{script}", raise_on_error: false + expect(out).to include("requiring foo used the monkeypatch") + end + + it "performs an automatic bundle install" do + build_repo4 do + build_gem "myrack", "1.0.0" + end + + gemfile <<-G + source "https://gem.repo1" + gem "myrack", :group => :test + G + + bundle_config "auto_install 1" + + ruby <<-RUBY, artifice: "compact_index" + require 'bundler/setup' + RUBY + expect(err).to be_empty + expect(out).to include("Installing myrack 1.0.0") + end + + context "in a read-only filesystem" do + before do + gemfile <<-G + source "https://gem.repo4" + G + + lockfile <<-L + GEM + remote: https://gem.repo4/ + + PLATFORMS + x86_64-darwin-19 + + DEPENDENCIES + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "should fail loudly if the lockfile platforms don't include the current platform" do + simulate_platform "x86_64-linux" do + ruby <<-RUBY, raise_on_error: false, env: { "BUNDLER_SPEC_READ_ONLY" => "true", "BUNDLER_FORCE_TTY" => "true" } + require "bundler/setup" + RUBY + end + + expect(err).to include("Your lockfile is missing the current platform, but can't be updated because file system is read-only") + end + end +end diff --git a/spec/bundler/runtime/setup_spec.rb b/spec/bundler/runtime/setup_spec.rb index fc841f1de155ef..46b88513d47fd6 100644 --- a/spec/bundler/runtime/setup_spec.rb +++ b/spec/bundler/runtime/setup_spec.rb @@ -890,824 +890,4 @@ def clean_load_path(lp) expect(lines).to include("LS MANPAGE") end end - - it "should prepend gemspec require paths to $LOAD_PATH in order" do - update_repo2 do - build_gem("requirepaths") do |s| - s.write("lib/rq.rb", "puts 'yay'") - s.write("src/rq.rb", "puts 'nooo'") - s.require_paths = %w[lib src] - end - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "requirepaths", :require => nil - G - - run "require 'rq'" - expect(out).to eq("yay") - end - - it "should clean $LOAD_PATH properly" do - gem_name = "very_simple_binary" - full_gem_name = gem_name + "-1.0" - - system_gems full_gem_name - - install_gemfile <<-G - source "https://gem.repo1" - G - - ruby <<-R - require 'bundler' - gem '#{gem_name}' - - puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} >= 2 - - Bundler.setup - - puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} == 0 - R - - expect(out).to eq("true\ntrue") - end - - context "with bundler is located in symlinked GEM_HOME" do - let(:gem_home) { Dir.mktmpdir } - let(:symlinked_gem_home) { tmp("gem_home-symlink").to_s } - let(:full_name) { "bundler-#{Bundler::VERSION}" } - - before do - File.symlink(gem_home, symlinked_gem_home) - gems_dir = File.join(gem_home, "gems") - specifications_dir = File.join(gem_home, "specifications") - Dir.mkdir(gems_dir) - Dir.mkdir(specifications_dir) - - File.symlink(source_root, File.join(gems_dir, full_name)) - - gemspec_content = File.binread(gemspec). - sub("Bundler::VERSION", %("#{Bundler::VERSION}")). - lines.reject {|line| line.include?("lib/bundler/version") }.join - - File.open(File.join(specifications_dir, "#{full_name}.gemspec"), "wb") do |f| - f.write(gemspec_content) - end - end - - it "should not remove itself from the LOAD_PATH and require a different copy of 'bundler/setup'" do - install_gemfile "source 'https://gem.repo1'" - - ruby <<-R, env: { "GEM_PATH" => symlinked_gem_home } - TracePoint.trace(:class) do |tp| - if tp.path.include?("bundler") && !tp.path.start_with?("#{source_root}") - puts "OMG. Defining a class from another bundler at \#{tp.path}:\#{tp.lineno}" - end - end - gem 'bundler', '#{Bundler::VERSION}' - require 'bundler/setup' - R - - expect(out).to be_empty - end - end - - it "does not reveal system gems even when Gem.refresh is called" do - system_gems "myrack-1.0.0" - - install_gemfile <<-G - source "https://gem.repo1" - gem "activesupport" - G - - run <<-R - puts Bundler.rubygems.installed_specs.map(&:name) - Gem.refresh - puts Bundler.rubygems.installed_specs.map(&:name) - R - - expect(out).to include("activesupport") - expect(out).not_to include("myrack") - end - - describe "when a vendored gem specification uses the :path option" do - let(:filesystem_root) do - current = Pathname.new(Dir.pwd) - current = current.parent until current == current.parent - current - end - - it "should resolve paths relative to the Gemfile" do - path = bundled_app(File.join("vendor", "foo")) - build_lib "foo", path: path - - # If the .gemspec exists, then Bundler handles the path differently. - # See Source::Path.load_spec_files for details. - FileUtils.rm(File.join(path, "foo.gemspec")) - - install_gemfile <<-G - source "https://gem.repo1" - gem 'foo', '1.2.3', :path => 'vendor/foo' - G - - run <<-R, env: { "BUNDLE_GEMFILE" => bundled_app_gemfile.to_s }, dir: bundled_app.parent - require 'foo' - R - expect(err).to be_empty - end - - it "should make sure the Bundler.root is really included in the path relative to the Gemfile" do - relative_path = File.join("vendor", Dir.pwd.gsub(/^#{filesystem_root}/, "")) - absolute_path = bundled_app(relative_path) - FileUtils.mkdir_p(absolute_path) - build_lib "foo", path: absolute_path - - # If the .gemspec exists, then Bundler handles the path differently. - # See Source::Path.load_spec_files for details. - FileUtils.rm(File.join(absolute_path, "foo.gemspec")) - - gemfile <<-G - source "https://gem.repo1" - gem 'foo', '1.2.3', :path => '#{relative_path}' - G - - bundle :install - - run <<-R, env: { "BUNDLE_GEMFILE" => bundled_app_gemfile.to_s }, dir: bundled_app.parent - require 'foo' - R - - expect(err).to be_empty - end - end - - describe "with git gems that don't have gemspecs" do - before :each do - build_git "no_gemspec", gemspec: false - - install_gemfile <<-G - source "https://gem.repo1" - gem "no_gemspec", "1.0", :git => "#{lib_path("no_gemspec-1.0")}" - G - end - - it "loads the library via a virtual spec" do - run <<-R - require 'no_gemspec' - puts NO_GEMSPEC - R - - expect(out).to eq("1.0") - end - end - - describe "with bundled and system gems" do - before :each do - system_gems "myrack-1.0.0" - - install_gemfile <<-G - source "https://gem.repo1" - - gem "activesupport", "2.3.5" - G - end - - it "does not pull in system gems" do - run <<-R - begin; - require 'myrack' - rescue LoadError - puts 'WIN' - end - R - - expect(out).to eq("WIN") - end - - it "provides a gem method" do - run <<-R - gem 'activesupport' - require 'activesupport' - puts ACTIVESUPPORT - R - - expect(out).to eq("2.3.5") - end - - it "raises an exception if gem is used to invoke a system gem not in the bundle" do - run <<-R - begin - gem 'myrack' - rescue LoadError => e - puts e.message - end - R - - expect(out).to eq("myrack is not part of the bundle. Add it to your Gemfile.") - end - - it "sets GEM_HOME appropriately" do - run "puts ENV['GEM_HOME']" - expect(out).to eq(default_bundle_path.to_s) - end - end - - describe "with system gems in the bundle" do - before :each do - bundle_config "path.system true" - system_gems "myrack-1.0.0" - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", "1.0.0" - gem "activesupport", "2.3.5" - G - end - - it "sets GEM_PATH appropriately" do - run "puts Gem.path" - paths = out.split("\n") - expect(paths).to include(system_gem_path.to_s) - end - end - - describe "with a gemspec that requires other files" do - before :each do - build_git "bar", gemspec: false do |s| - s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') - s.write "bar.gemspec", <<-G - require_relative 'lib/bar/version' - - Gem::Specification.new do |s| - s.name = 'bar' - s.version = BAR_VERSION - s.summary = 'Bar' - s.files = Dir["lib/**/*.rb"] - s.author = 'no one' - end - G - end - - gemfile <<-G - source "https://gem.repo1" - gem "bar", :git => "#{lib_path("bar-1.0")}" - G - end - - it "evals each gemspec in the context of its parent directory" do - bundle :install - run "require 'bar'; puts BAR" - expect(out).to eq("1.0") - end - - it "error intelligently if the gemspec has a LoadError" do - ref = update_git "bar", gemspec: false do |s| - s.write "bar.gemspec", "require 'foobarbaz'" - end.ref_for("HEAD") - bundle :install, raise_on_error: false - - expect(err.lines.map(&:chomp)).to include( - a_string_starting_with("[!] There was an error while loading `bar.gemspec`:"), - " # from #{default_bundle_path "bundler", "gems", "bar-1.0-#{ref[0, 12]}", "bar.gemspec"}:1", - " > require 'foobarbaz'" - ) - end - - it "evals each gemspec with a binding from the top level" do - bundle "install" - - ruby <<-RUBY - require 'bundler' - bundler_module = class << Bundler; self; end - bundler_module.send(:remove_method, :require) - def Bundler.require(path) - raise StandardError, "didn't use binding from top level" - end - Bundler.load - RUBY - - expect(err).to be_empty - expect(out).to be_empty - end - end - - describe "when Bundler is bundled" do - it "doesn't blow up" do - install_gemfile <<-G - source "https://gem.repo1" - gem "bundler", :path => "#{root}" - G - - bundle %(exec ruby -e "require 'bundler'; Bundler.setup") - expect(err).to be_empty - end - end - - describe "when BUNDLED WITH" do - def lock_with(bundler_version = nil) - lock = <<~L - GEM - remote: https://gem.repo1/ - specs: - myrack (1.0.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - L - - if bundler_version - lock += "\nBUNDLED WITH\n #{bundler_version}\n" - end - - lock - end - - before do - bundle_config "path.system true" - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack" - G - end - - context "is not present" do - it "does not change the lock" do - lockfile lock_with(nil) - ruby "require 'bundler/setup'" - expect(lockfile).to eq lock_with(nil) - end - end - - context "is newer" do - it "does not change the lock or warn" do - lockfile lock_with(Bundler::VERSION.succ) - ruby "require 'bundler/setup'" - expect(out).to be_empty - expect(err).to be_empty - expect(lockfile).to eq lock_with(Bundler::VERSION.succ) - end - end - - context "is older" do - it "does not change the lock" do - system_gems "bundler-1.10.1" - lockfile lock_with("1.10.1") - ruby "require 'bundler/setup'" - expect(lockfile).to eq lock_with("1.10.1") - end - end - end - - describe "when RUBY VERSION" do - let(:ruby_version) { nil } - - def lock_with(ruby_version = nil) - checksums = checksums_section do |c| - c.checksum gem_repo1, "myrack", "1.0.0" - end - - lock = <<~L - GEM - remote: https://gem.repo1/ - specs: - myrack (1.0.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - L - - if ruby_version - lock += "\nRUBY VERSION\n ruby #{ruby_version}\n" - end - - lock += <<~L - - BUNDLED WITH - #{Bundler::VERSION} - L - - lock - end - - before do - install_gemfile <<-G - ruby ">= 0" - source "https://gem.repo1" - gem "myrack" - G - lockfile lock_with(ruby_version) - end - - context "is not present" do - # Skipped on ruby-core, and on the release-version CI variant, because - # `ruby "require 'bundler/setup'"` does not activate bundler as a gem - # there, so Source::Metadata falls back to a synthetic spec whose - # cache_file does not exist on disk and LockfileGenerator#bundler_checksum - # drops the bundler checksum, while the on-disk lockfile still has it. - # In-development (.dev) versions never write a bundler checksum, so the - # regular suite stays unaffected. - it "does not change the lock", :ruby_repo do - skip "bundler is loaded from the source tree, not installed as a gem" unless Bundler::VERSION.end_with?(".dev") - - expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } - end - end - - context "is newer" do - let(:ruby_version) { "5.5.5" } - it "does not change the lock or warn" do - expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } - expect(out).to be_empty - expect(err).to be_empty - end - end - - context "is older" do - let(:ruby_version) { "1.0.0" } - it "does not change the lock" do - expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } - end - end - end - - describe "with gemified standard libraries" do - it "does not load Digest", :ruby_repo do - build_git "bar", gemspec: false do |s| - s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') - s.write "bar.gemspec", <<-G - require_relative 'lib/bar/version' - - Gem::Specification.new do |s| - s.name = 'bar' - s.version = BAR_VERSION - s.summary = 'Bar' - s.files = Dir["lib/**/*.rb"] - s.author = 'no one' - - s.add_dependency 'digest' - end - G - end - - gemfile <<-G - source "https://gem.repo1" - gem "bar", :git => "#{lib_path("bar-1.0")}" - G - - bundle :install, env: { "BUNDLE_LOCKFILE_CHECKSUMS" => "false" } - - ruby <<-RUBY, artifice: nil - require 'bundler/setup' - puts defined?(::Digest) ? "Digest defined" : "Digest undefined" - require 'digest' - RUBY - expect(out).to eq("Digest undefined") - end - - it "does not load Psych" do - gemfile "source 'https://gem.repo1'" - ruby <<-RUBY - require 'bundler/setup' - puts defined?(Psych::VERSION) ? Psych::VERSION : "undefined" - require 'psych' - puts Psych::VERSION - RUBY - pre_bundler, post_bundler = out.split("\n") - expect(pre_bundler).to eq("undefined") - expect(post_bundler).to match(/\d+\.\d+\.\d+/) - end - - it "does not load openssl" do - install_gemfile "source 'https://gem.repo1'" - ruby <<-RUBY, artifice: nil - require "bundler/setup" - puts defined?(OpenSSL) || "undefined" - require "openssl" - puts defined?(OpenSSL) || "undefined" - RUBY - expect(out).to eq("undefined\nconstant") - end - - it "does not load uri while reading gemspecs", rubygems: ">= 3.6.0.dev" do - Dir.mkdir bundled_app("test") - - create_file(bundled_app("test/test.gemspec"), <<-G) - Gem::Specification.new do |s| - s.name = "test" - s.version = "1.0.0" - s.summary = "test" - s.authors = ['John Doe'] - s.homepage = 'https://example.com' - end - G - - install_gemfile <<-G - source "https://gem.repo1" - gem "test", path: "#{bundled_app("test")}" - G - - ruby <<-RUBY, artifice: nil - require "bundler/setup" - puts defined?(URI) || "undefined" - require "uri" - puts defined?(URI) || "undefined" - RUBY - expect(out).to eq("undefined\nconstant") - end - - it "activates default gems when they are part of the bundle, but not installed explicitly", :ruby_repo do - default_delegate_version = ruby "gem 'delegate'; require 'delegate'; puts Delegator::VERSION" - - build_repo2 do - build_gem "delegate", default_delegate_version - end - - gemfile "source \"https://gem.repo2\"; gem 'delegate'" - - ruby <<-RUBY - require "bundler/setup" - require "delegate" - puts defined?(::Delegator) ? "Delegator defined" : "Delegator undefined" - RUBY - - expect(out).to eq("Delegator defined") - expect(err).to be_empty - end - - describe "default gem activation" do - let(:exemptions) do - exempts = %w[did_you_mean bundler uri pathname] - exempts << "error_highlight" # added in Ruby 3.1 as a default gem - exempts << "ruby2_keywords" # added in Ruby 3.1 as a default gem - exempts << "syntax_suggest" # added in Ruby 3.2 as a default gem - exempts - end - - let(:activation_warning_hack) { <<~RUBY } - require #{spec_dir.join("support/hax").to_s.dump} - - Gem::Specification.send(:alias_method, :bundler_spec_activate, :activate) - Gem::Specification.send(:define_method, :activate) do - unless #{exemptions.inspect}.include?(name) - warn '-' * 80 - warn "activating \#{full_name}" - warn(*caller) - warn '*' * 80 - end - bundler_spec_activate - end - RUBY - - let(:activation_warning_hack_rubyopt) do - create_file("activation_warning_hack.rb", activation_warning_hack) - "-r#{bundled_app("activation_warning_hack.rb")} #{ENV["RUBYOPT"]}" - end - - let(:code) { <<~RUBY } - require "pp" - loaded_specs = Gem.loaded_specs.dup - #{exemptions.inspect}.each {|s| loaded_specs.delete(s) } - pp loaded_specs - - # not a default gem, but harmful to have loaded - open_uri = $LOADED_FEATURES.grep(/open.uri/) - unless open_uri.empty? - warn "open_uri: \#{open_uri}" - end - RUBY - - it "activates no gems with -rbundler/setup" do - install_gemfile "source 'https://gem.repo1'" - ruby code, env: { "RUBYOPT" => activation_warning_hack_rubyopt + " -rbundler/setup" }, artifice: nil - expect(out).to eq("{}") - end - - it "activates no gems with bundle exec" do - install_gemfile "source 'https://gem.repo1'" - create_file("script.rb", code) - bundle "exec ruby ./script.rb", env: { "RUBYOPT" => activation_warning_hack_rubyopt } - expect(out).to eq("{}") - end - - it "activates no gems with bundle exec that is loaded" do - skip "not executable" if Gem.win_platform? - - install_gemfile "source 'https://gem.repo1'" - create_file("script.rb", "#!/usr/bin/env ruby\n\n#{code}") - FileUtils.chmod(0o777, bundled_app("script.rb")) - bundle "exec ./script.rb", env: { "RUBYOPT" => activation_warning_hack_rubyopt } - expect(out).to eq("{}") - end - - it "does not load net-http-pipeline too early" do - build_repo4 do - build_gem "net-http-pipeline", "1.0.1" - end - - system_gems "net-http-pipeline-1.0.1", gem_repo: gem_repo4 - - gemfile <<-G - source "https://gem.repo4" - gem "net-http-pipeline", "1.0.1" - G - - bundle_config "path vendor/bundle" - - bundle :install - - bundle :check - - expect(out).to eq("The Gemfile's dependencies are satisfied") - end - - Gem::Specification.select(&:default_gem?).map(&:name).each do |g| - it "activates newer versions of #{g}", :ruby_repo do - skip if exemptions.include?(g) - - build_repo4 do - build_gem g, "999999" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "#{g}", "999999" - G - - expect(the_bundle).to include_gem("#{g} 999999", env: { "RUBYOPT" => activation_warning_hack_rubyopt }, artifice: nil) - end - - it "activates older versions of #{g}", :ruby_repo do - skip if exemptions.include?(g) - - build_repo4 do - build_gem g, "0.0.0.a" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "#{g}", "0.0.0.a" - G - - expect(the_bundle).to include_gem("#{g} 0.0.0.a", env: { "RUBYOPT" => activation_warning_hack_rubyopt }, artifice: nil) - end - end - end - end - - describe "after setup" do - it "keeps Kernel#gem private" do - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack" - G - - ruby <<-RUBY, raise_on_error: false - require "bundler/setup" - Object.new.gem "myrack" - puts "FAIL" - RUBY - - expect(stdboth).not_to include "FAIL" - expect(err).to match(/private method [`']gem'/) - end - - it "keeps Kernel#require private" do - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack" - G - - ruby <<-RUBY, raise_on_error: false - require "bundler/setup" - Object.new.require "myrack" - puts "FAIL" - RUBY - - expect(stdboth).not_to include "FAIL" - expect(err).to match(/private method [`']require'/) - end - - it "memoizes initial set of specs when requiring bundler/setup, so that even if further code mutates dependencies, Bundler.definition.specs is not affected" do - install_gemfile <<~G - source "https://gem.repo1" - gem "yard" - gem "myrack", :group => :test - G - - ruby <<-RUBY, raise_on_error: false - require "bundler/setup" - Bundler.require(:test).select! {|d| (d.groups & [:test]).any? } - puts Bundler.definition.specs.map(&:name).join(", ") - RUBY - - expect(out).to include("myrack, yard") - end - - it "does not cause double loads when higher versions of default gems are activated before bundler" do - build_repo2 do - build_gem "json", "999.999.999" do |s| - s.write "lib/json.rb", <<~RUBY - module JSON - VERSION = "999.999.999" - end - RUBY - end - end - - system_gems "json-999.999.999", gem_repo: gem_repo2 - - install_gemfile "source 'https://gem.repo1'" - ruby <<-RUBY - require "json" - require "bundler/setup" - require "json" - RUBY - - expect(err).to be_empty - end - end - - it "does not undo the Kernel.require decorations", rubygems: ">= 3.4.6" do - install_gemfile "source 'https://gem.repo1'" - script = bundled_app("bin/script") - create_file(script, <<~RUBY) - module Kernel - module_function - - alias_method :require_before_extra_monkeypatches, :require - - def require(path) - puts "requiring \#{path} used the monkeypatch" - - require_before_extra_monkeypatches(path) - end - end - - require "bundler/setup" - - require "foo" - RUBY - - sys_exec "#{Gem.ruby} #{script}", raise_on_error: false - expect(out).to include("requiring foo used the monkeypatch") - end - - it "performs an automatic bundle install" do - build_repo4 do - build_gem "myrack", "1.0.0" - end - - gemfile <<-G - source "https://gem.repo1" - gem "myrack", :group => :test - G - - bundle_config "auto_install 1" - - ruby <<-RUBY, artifice: "compact_index" - require 'bundler/setup' - RUBY - expect(err).to be_empty - expect(out).to include("Installing myrack 1.0.0") - end - - context "in a read-only filesystem" do - before do - gemfile <<-G - source "https://gem.repo4" - G - - lockfile <<-L - GEM - remote: https://gem.repo4/ - - PLATFORMS - x86_64-darwin-19 - - DEPENDENCIES - - BUNDLED WITH - #{Bundler::VERSION} - L - end - - it "should fail loudly if the lockfile platforms don't include the current platform" do - simulate_platform "x86_64-linux" do - ruby <<-RUBY, raise_on_error: false, env: { "BUNDLER_SPEC_READ_ONLY" => "true", "BUNDLER_FORCE_TTY" => "true" } - require "bundler/setup" - RUBY - end - - expect(err).to include("Your lockfile is missing the current platform, but can't be updated because file system is read-only") - end - end end diff --git a/spec/bundler/support/shards.rb b/spec/bundler/support/shards.rb index f3ef8f142390ca..396ee8049d6b66 100644 --- a/spec/bundler/support/shards.rb +++ b/spec/bundler/support/shards.rb @@ -10,6 +10,7 @@ module Shards EXAMPLE_MAPPINGS = { shard_a: [ "spec/runtime/setup_spec.rb", + "spec/runtime/setup_gems_spec.rb", "spec/commands/install_spec.rb", "spec/commands/add_spec.rb", "spec/install/gems/compact_index_spec.rb", @@ -102,6 +103,7 @@ module Shards ], shard_c: [ "spec/commands/newgem_spec.rb", + "spec/commands/newgem_options_spec.rb", "spec/commands/exec_spec.rb", "spec/commands/clean_spec.rb", "spec/commands/platform_spec.rb", @@ -149,7 +151,9 @@ module Shards "spec/bundler/resolver/cooldown_spec.rb", "spec/install/cooldown_spec.rb", "spec/commands/outdated_spec.rb", + "spec/commands/outdated_filters_spec.rb", "spec/commands/update_spec.rb", + "spec/commands/update_scenarios_spec.rb", "spec/install/deploy_spec.rb", "spec/install/gemfile/sources_spec.rb", "spec/runtime/self_management_spec.rb", From 4659696b1918a72bfca1939f97ebaa367187dd3a Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 15 Jul 2026 08:51:14 +0900 Subject: [PATCH 26/47] Remove BSD/OS (BSDi) support BSD/OS was discontinued in 2003 and the bsdi* target has been unbuildable for decades. This also removes the BROKEN_SETREUID and BROKEN_SETREGID fallbacks in process.c, which no other platform ever defined. Co-Authored-By: Claude Opus 4.8 --- configure.ac | 13 +++---------- process.c | 45 +++------------------------------------------ 2 files changed, 6 insertions(+), 52 deletions(-) diff --git a/configure.ac b/configure.ac index a88cd219b9fffc..8977a46b3c38ef 100644 --- a/configure.ac +++ b/configure.ac @@ -1344,10 +1344,6 @@ main() AC_CHECK_FUNCS(_wfreopen_s) AC_LIBOBJ([langinfo]) ], -[bsdi*], [ LIBS="-lm $LIBS" - AC_DEFINE(BROKEN_SETREUID, 1) - AC_DEFINE(BROKEN_SETREGID, 1) - ac_cv_sizeof_rlim_t=8], [freebsd*], [ LIBS="-lm $LIBS" ac_cv_func_getpeername=no ac_cv_func_getsockname=no @@ -3112,7 +3108,7 @@ AS_IF([test "$ac_cv_header_mach_o_loader_h" = yes], [ ]) AS_CASE(["$target_os"], -[linux* | gnu* | k*bsd*-gnu | bsdi* | kopensolaris*-gnu], [ +[linux* | gnu* | k*bsd*-gnu | kopensolaris*-gnu], [ AS_IF([test "$rb_cv_binary_elf" = no], [ AC_MSG_ERROR(Not ELF) ], [ @@ -3147,7 +3143,7 @@ STATIC= # mkmf.rb's have_header() to fail if the desired resource happens to be # installed in the /usr/local tree. RUBY_APPEND_OPTION(CCDLFLAGS, -fno-common)], - [bsdi*|cygwin*|msys*|mingw*|aix*|interix*], [ ], + [cygwin*|msys*|mingw*|aix*|interix*], [ ], [ RUBY_APPEND_OPTION(CCDLFLAGS, -fPIC)]) ], [ @@ -3206,10 +3202,7 @@ AC_SUBST(EXTOBJS) rb_cv_dlopen=yes], [osf*], [ : ${LDSHARED='$(LD) -shared -expect_unresolved "*"'} rb_cv_dlopen=yes], - [bsdi3*], [ AS_CASE(["$CC"], - [*shlicc*], [ : ${LDSHARED='$(CC) -r'} - rb_cv_dlopen=yes])], - [linux* | gnu* | k*bsd*-gnu | netbsd* | bsdi* | kopensolaris*-gnu | haiku*], [ + [linux* | gnu* | k*bsd*-gnu | netbsd* | kopensolaris*-gnu | haiku*], [ : ${LDSHARED='$(CC) -shared'} AS_IF([test "$rb_cv_binary_elf" = yes], [ LDFLAGS="$LDFLAGS -Wl,-export-dynamic" diff --git a/process.c b/process.c index 8bb631fe5d46c6..87c1c39709246c 100644 --- a/process.c +++ b/process.c @@ -154,7 +154,7 @@ static VALUE rb_cProcessTms; #define WSTOPSIG WEXITSTATUS #endif -#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__bsdi__) +#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) #define HAVE_44BSD_SETUID 1 #define HAVE_44BSD_SETGID 1 #endif @@ -164,20 +164,11 @@ static VALUE rb_cProcessTms; #undef HAVE_SETRGID #endif -#ifdef BROKEN_SETREUID -#define setreuid ruby_setreuid -int setreuid(rb_uid_t ruid, rb_uid_t euid); -#endif -#ifdef BROKEN_SETREGID -#define setregid ruby_setregid -int setregid(rb_gid_t rgid, rb_gid_t egid); -#endif - #if defined(HAVE_44BSD_SETUID) || defined(__APPLE__) -#if !defined(USE_SETREUID) && !defined(BROKEN_SETREUID) +#if !defined(USE_SETREUID) #define OBSOLETE_SETREUID 1 #endif -#if !defined(USE_SETREGID) && !defined(BROKEN_SETREGID) +#if !defined(USE_SETREGID) #define OBSOLETE_SETREGID 1 #endif #endif @@ -6293,21 +6284,6 @@ proc_setuid(VALUE obj, VALUE id) static rb_uid_t SAVED_USER_ID = -1; -#ifdef BROKEN_SETREUID -int -setreuid(rb_uid_t ruid, rb_uid_t euid) -{ - if (ruid != (rb_uid_t)-1 && ruid != getuid()) { - if (euid == (rb_uid_t)-1) euid = geteuid(); - if (setuid(ruid) < 0) return -1; - } - if (euid != (rb_uid_t)-1 && euid != geteuid()) { - if (seteuid(euid) < 0) return -1; - } - return 0; -} -#endif - /* * call-seq: * Process::UID.change_privilege(user) -> integer @@ -7007,21 +6983,6 @@ rb_daemon(int nochdir, int noclose) static rb_gid_t SAVED_GROUP_ID = -1; -#ifdef BROKEN_SETREGID -int -setregid(rb_gid_t rgid, rb_gid_t egid) -{ - if (rgid != (rb_gid_t)-1 && rgid != getgid()) { - if (egid == (rb_gid_t)-1) egid = getegid(); - if (setgid(rgid) < 0) return -1; - } - if (egid != (rb_gid_t)-1 && egid != getegid()) { - if (setegid(egid) < 0) return -1; - } - return 0; -} -#endif - /* * call-seq: * Process::GID.change_privilege(group) -> integer From 849d9d6e9758004b6ed28954dd06b0490a51c7e2 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Tue, 14 Jul 2026 20:33:11 +0000 Subject: [PATCH 27/47] Relax vm_cc_cme/vm_cc_call assertions under multi-ractor, extension-safely Same relaxation as PR #17873: another ractor can invalidate a cc (klass = Qundef) while a call using it is in flight; cme_/call_ stay intact, so the call proceeds with the method resolved before the redefinition (same treatment as vm_cc_invalidate). Route the check through VM_CC_RACED_P(), which extensions see as a constant: ext/objspace/objspace_dump.c includes vm_callinfo.h and calls vm_cc_cme, and with assertions enabled at -O0 (--enable-yjit/zjit=dev on macOS) a direct rb_multi_ractor_p() there materializes references to the core-internal globals ruby_current_vm_ptr / ruby_single_main_ractor, which are hidden from extensions -- objspace.bundle then fails to load ("symbol not found in flat namespace '_ruby_current_vm_ptr'", the five JIT-dev CI failures on #17873). At -O1+ the compiler proves the first disjunct from the caller's Qundef guard and dead-codes the call, which is why only the -O0 dev jobs saw it. Verified both ways on the objspace_dump.c TU: PR state at -O0+RUBY_DEBUG has both unresolved globals; with VM_CC_RACED_P() it has none. Co-Authored-By: Claude Fable 5 --- vm_callinfo.h | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/vm_callinfo.h b/vm_callinfo.h index 3ce2f4a3916d29..5c7cc6757e9a03 100644 --- a/vm_callinfo.h +++ b/vm_callinfo.h @@ -439,11 +439,26 @@ vm_cc_valid(const struct rb_callcache *cc) return !UNDEF_P(cc->klass); } +/* Whether another ractor may have invalidated this cc mid-use (see + * vm_cc_invalidate()). Extensions cannot use rb_multi_ractor_p() here: with + * assertions enabled at -O0 (e.g. --enable-yjit=dev on macOS) the inline + * materializes references to core-internal globals (ruby_current_vm_ptr, + * ruby_single_main_ractor) that are not exported to them, and objspace.bundle + * fails to load. RUBY_EXPORT marks a core TU. */ +#ifdef RUBY_EXPORT +# define VM_CC_RACED_P() rb_multi_ractor_p() +#else +# define VM_CC_RACED_P() true +#endif + static inline const struct rb_callable_method_entry_struct * vm_cc_cme(const struct rb_callcache *cc) { VM_ASSERT(IMEMO_TYPE_P(cc, imemo_callcache)); - VM_ASSERT(cc->klass != Qundef || !vm_cc_markable(cc) || vm_cc_invalid_super(cc)); + // VM_CC_RACED_P(): another ractor can invalidate this cc (klass = Qundef) + // while a call using it is in flight; cme_/call_ stay intact, so the call + // proceeds with the method resolved before the redefinition. + VM_ASSERT(cc->klass != Qundef || !vm_cc_markable(cc) || vm_cc_invalid_super(cc) || VM_CC_RACED_P()); VM_ASSERT(cc_check_class(cc->klass)); VM_ASSERT(cc->call_ == NULL || // not initialized yet !vm_cc_markable(cc) || @@ -458,7 +473,7 @@ vm_cc_call(const struct rb_callcache *cc) { VM_ASSERT(IMEMO_TYPE_P(cc, imemo_callcache)); VM_ASSERT(cc->call_ != NULL); - VM_ASSERT(cc->klass != Qundef || !vm_cc_markable(cc) || vm_cc_invalid_super(cc)); + VM_ASSERT(cc->klass != Qundef || !vm_cc_markable(cc) || vm_cc_invalid_super(cc) || VM_CC_RACED_P()); VM_ASSERT(cc_check_class(cc->klass)); return cc->call_; } From 6a952923e3b2f59a5408ddf0a02137901d33e47b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 02:08:40 +0000 Subject: [PATCH 28/47] Bump ruby/setup-ruby in the github-actions group across 1 directory Bumps the github-actions group with 1 update in the / directory: [ruby/setup-ruby](https://github.com/ruby/setup-ruby). Updates `ruby/setup-ruby` from 1.316.0 to 1.318.0 - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/d45b1a4e94b71acab930e56e79c6aa188764e7f9...8e41b362d2589a22a44c1cfa214b3c83052c195b) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.318.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/annocheck.yml | 2 +- .github/workflows/auto_review_pr.yml | 2 +- .github/workflows/baseruby.yml | 2 +- .github/workflows/bundled_gems.yml | 2 +- .github/workflows/check_dependencies.yml | 2 +- .github/workflows/check_misc.yml | 2 +- .github/workflows/modgc.yml | 2 +- .github/workflows/parse_y.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/spec_guards.yml | 2 +- .github/workflows/sync_default_gems.yml | 2 +- .github/workflows/tarball-ubuntu.yml | 2 +- .github/workflows/tarball-windows.yml | 2 +- .github/workflows/ubuntu.yml | 2 +- .github/workflows/wasm.yml | 2 +- .github/workflows/windows.yml | 2 +- .github/workflows/yjit-ubuntu.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/annocheck.yml b/.github/workflows/annocheck.yml index 2c1d35899fb722..e839cddc179b0b 100644 --- a/.github/workflows/annocheck.yml +++ b/.github/workflows/annocheck.yml @@ -73,7 +73,7 @@ jobs: builddir: build makeup: true - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/auto_review_pr.yml b/.github/workflows/auto_review_pr.yml index 72611269a40eca..93ef85f4963d73 100644 --- a/.github/workflows/auto_review_pr.yml +++ b/.github/workflows/auto_review_pr.yml @@ -29,7 +29,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.4' bundler: none diff --git a/.github/workflows/baseruby.yml b/.github/workflows/baseruby.yml index 695c468153be36..413e5d7ee7ec17 100644 --- a/.github/workflows/baseruby.yml +++ b/.github/workflows/baseruby.yml @@ -48,7 +48,7 @@ jobs: - ruby-3.3 steps: - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: ${{ matrix.ruby }} bundler: none diff --git a/.github/workflows/bundled_gems.yml b/.github/workflows/bundled_gems.yml index 464a44dfceb9dd..5621e5bc7a8a1f 100644 --- a/.github/workflows/bundled_gems.yml +++ b/.github/workflows/bundled_gems.yml @@ -38,7 +38,7 @@ jobs: with: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: 4.0 diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml index 56eea4001fd274..327734c9e21363 100644 --- a/.github/workflows/check_dependencies.yml +++ b/.github/workflows/check_dependencies.yml @@ -42,7 +42,7 @@ jobs: - uses: ./.github/actions/setup/directories - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 55238ffba504d9..e50ebb3f9961d6 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -23,7 +23,7 @@ jobs: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} persist-credentials: false - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: head diff --git a/.github/workflows/modgc.yml b/.github/workflows/modgc.yml index b0fe1c827f51a8..2cc77eefed2f6f 100644 --- a/.github/workflows/modgc.yml +++ b/.github/workflows/modgc.yml @@ -67,7 +67,7 @@ jobs: uses: ./.github/actions/setup/ubuntu if: ${{ contains(matrix.os, 'ubuntu') }} - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/parse_y.yml b/.github/workflows/parse_y.yml index 0e51c852335355..7f355d3d0cc0e7 100644 --- a/.github/workflows/parse_y.yml +++ b/.github/workflows/parse_y.yml @@ -59,7 +59,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bea2aba1012685..4ce3fd0476ad73 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,7 +22,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: 3.3.4 diff --git a/.github/workflows/spec_guards.yml b/.github/workflows/spec_guards.yml index bf6b6f1d0155ec..d3a497ea8cbacd 100644 --- a/.github/workflows/spec_guards.yml +++ b/.github/workflows/spec_guards.yml @@ -49,7 +49,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: ${{ matrix.ruby }} bundler: none diff --git a/.github/workflows/sync_default_gems.yml b/.github/workflows/sync_default_gems.yml index 98957ab01acb29..3b1e5e191ed11a 100644 --- a/.github/workflows/sync_default_gems.yml +++ b/.github/workflows/sync_default_gems.yml @@ -39,7 +39,7 @@ jobs: with: token: ${{ github.repository == 'ruby/ruby' && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.4' bundler: none diff --git a/.github/workflows/tarball-ubuntu.yml b/.github/workflows/tarball-ubuntu.yml index d0d4e5ba575d66..88782a801c7c4a 100644 --- a/.github/workflows/tarball-ubuntu.yml +++ b/.github/workflows/tarball-ubuntu.yml @@ -43,7 +43,7 @@ jobs: set -x sudo apt-get update -q sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev zlib1g-dev libffi-dev libgmp-dev bison- autoconf- - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.2' # test-bundled-gems requires executable host ruby diff --git a/.github/workflows/tarball-windows.yml b/.github/workflows/tarball-windows.yml index 2f591341d45d0a..24bfe8dde1bd00 100644 --- a/.github/workflows/tarball-windows.yml +++ b/.github/workflows/tarball-windows.yml @@ -48,7 +48,7 @@ jobs: - run: md build working-directory: - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.2' bundler: none diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 6518e77d72b07b..19f89eae6f5c86 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -79,7 +79,7 @@ jobs: with: arch: ${{ matrix.arch }} - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index 75344395bb4155..cba593d1c0d6e0 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -65,7 +65,7 @@ jobs: sparse-checkout: /.github persist-credentials: false - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index a48b0f6471073f..d83fa0144ebc54 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -63,7 +63,7 @@ jobs: - run: md build working-directory: - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: # windows-11-arm has only 3.4.1, 3.4.2, 3.4.3, head ruby-version: ${{ !endsWith(matrix.os, 'arm') && '3.1' || '3.4' }} diff --git a/.github/workflows/yjit-ubuntu.yml b/.github/workflows/yjit-ubuntu.yml index 793f93d6d10f44..8385cbd9b112f7 100644 --- a/.github/workflows/yjit-ubuntu.yml +++ b/.github/workflows/yjit-ubuntu.yml @@ -138,7 +138,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 80c7a0cb7986bf..25bde6b8b2a16c 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -136,7 +136,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none From 57b8f1857f1403e84ad401a99735a8f8aa774062 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 14 Jul 2026 17:27:53 +0900 Subject: [PATCH 29/47] [ruby/rubygems] Validate spec name before writing to the spec cache Gem::Source#fetch_spec built the local spec cache path directly from the name tuple returned by a remote index, which is never validated for use as a path component. A crafted gem name containing path separators or `..` could therefore make fetch_spec write the downloaded gemspec bytes outside Gem.spec_cache_dir. Reject any spec name that is not a plain basename before constructing the cache path. https://github.com/ruby/rubygems/commit/56ed326cb2 Co-Authored-By: Claude --- lib/rubygems/source.rb | 7 +++++++ test/rubygems/test_gem_source.rb | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb index 348bf6a24bc08f..b01619371efc4e 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -109,6 +109,13 @@ def fetch_spec(name_tuple) spec_file_name = name_tuple.spec_name + # The name tuple comes from a remote index and is not otherwise + # validated, so refuse anything that would escape the spec cache + # directory when used as a path component. + if File.basename(spec_file_name) != spec_file_name + raise Gem::Exception, "malformed spec name: #{spec_file_name.inspect}" + end + source_uri = enforce_trailing_slash(uri) + "#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}" cache_dir = cache_dir source_uri diff --git a/test/rubygems/test_gem_source.rb b/test/rubygems/test_gem_source.rb index a0bc10560df5ec..a030b58851c796 100644 --- a/test/rubygems/test_gem_source.rb +++ b/test/rubygems/test_gem_source.rb @@ -121,6 +121,19 @@ def test_fetch_spec_platform_ruby assert_equal @specs["a-1"].full_name, spec.full_name end + def test_fetch_spec_path_traversal + escape = File.expand_path(File.join(Gem.spec_cache_dir, "..", "owned.gemspec")) + + name_tuple = tuple("../owned", Gem::Version.new(1), "ruby") + + e = assert_raise Gem::Exception do + @source.fetch_spec name_tuple + end + + assert_includes e.message, "malformed spec name" + refute File.exist?(escape), "spec must not be written outside the spec cache" + end + def test_load_specs released = @source.load_specs(:released).map(&:full_name) assert_equal %W[a-2 a-1 b-2], released From 878d20ef1bdabe150e0a8b6f2dde237c292cbc4c Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 14 Jul 2026 22:01:49 +0900 Subject: [PATCH 30/47] Use bounded strings in process and cwd handling Track explicit lengths while parsing command strings and comparing the current directory. This avoids relying on Ruby string terminators and makes separators in the generated argument buffer explicit. --- dir.c | 11 +++++++---- process.c | 33 +++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/dir.c b/dir.c index b1e4f1edb01d16..c24030b59f9f0e 100644 --- a/dir.c +++ b/dir.c @@ -1616,19 +1616,22 @@ rb_dir_getwd_ospath_slowpath(void) VALUE rb_dir_getwd_ospath(void) { - char buf[PATH_MAX]; + char buf[PATH_MAX < LONG_MAX ? PATH_MAX : -1]; char *path = getcwd(buf, PATH_MAX); if (!path) { return rb_dir_getwd_ospath_slowpath(); } + size_t len = strlen(path); + RBIMPL_ASSERT_OR_ASSUME(len < PATH_MAX); VALUE cached_cwd = RUBY_ATOMIC_VALUE_LOAD(last_cwd); - if (!cached_cwd || strcmp(RSTRING_PTR(cached_cwd), path) != 0) { + if (!cached_cwd || (size_t)RSTRING_LEN(cached_cwd) != len || + memcmp(RSTRING_PTR(cached_cwd), path, len) != 0) { #ifdef __APPLE__ - cached_cwd = rb_str_normalize_ospath(path, strlen(path)); + cached_cwd = rb_str_normalize_ospath(path, (long)len); #else - cached_cwd = rb_str_new2(path); + cached_cwd = rb_str_new(path, (long)len); #endif rb_str_freeze(cached_cwd); RUBY_ATOMIC_VALUE_SET(last_cwd, cached_cwd); diff --git a/process.c b/process.c index 87c1c39709246c..82e3e376fe6f1e 100644 --- a/process.c +++ b/process.c @@ -2436,6 +2436,8 @@ compare_posix_sh(const void *key, const void *el) } #endif +#define append_terminator(buf) rb_str_buf_cat(buf, "", 1) /* append '\0' */ + static void rb_exec_fillarg(VALUE prog, int argc, VALUE *argv, VALUE env, VALUE opthash, VALUE execarg_obj) { @@ -2493,7 +2495,10 @@ rb_exec_fillarg(VALUE prog, int argc, VALUE *argv, VALUE env, VALUE opthash, VAL "while", /* reserved */ }; const char *p; + const char *const s = rb_str_null_check(prog); + const char *const e = RSTRING_END(prog); struct string_part first = {0, 0}; + int has_slash = 0; int has_meta = 0; /* * meta characters: @@ -2519,7 +2524,7 @@ rb_exec_fillarg(VALUE prog, int argc, VALUE *argv, VALUE env, VALUE opthash, VAL * = Assignment preceding command name * % (used in Parameter Expansion) */ - for (p = RSTRING_PTR(prog); *p; p++) { + for (p = s; p < e; p++) { if (*p == ' ' || *p == '\t') { if (first.ptr && !first.len) first.len = p - first.ptr; } @@ -2533,15 +2538,17 @@ rb_exec_fillarg(VALUE prog, int argc, VALUE *argv, VALUE env, VALUE opthash, VAL has_meta = 1; } else if (*p == '/') { - first.len = 0x100; /* longer than any posix_sh_cmds */ + has_slash = 1; } } if (has_meta) break; } - if (!has_meta && first.ptr) { + if (!has_meta) { + if (!first.ptr) first.ptr = e; if (!first.len) first.len = p - first.ptr; if (first.len > 0 && first.len <= sizeof(posix_sh_cmds[0]) && + !has_slash && bsearch(&first, posix_sh_cmds, numberof(posix_sh_cmds), sizeof(posix_sh_cmds[0]), compare_posix_sh)) has_meta = 1; } @@ -2552,21 +2559,22 @@ rb_exec_fillarg(VALUE prog, int argc, VALUE *argv, VALUE env, VALUE opthash, VAL if (!eargp->use_shell) { VALUE argv_buf; argv_buf = hide_obj(rb_str_buf_new(0)); - p = RSTRING_PTR(prog); - while (*p) { - while (*p == ' ' || *p == '\t') + rb_str_buf_cat(argv_buf, first.ptr, first.len); + append_terminator(argv_buf); + for (p = first.ptr + first.len; p < e;) { + while (p < e && (*p == ' ' || *p == '\t')) p++; - if (*p) { + if (p < e) { const char *w = p; - while (*p && *p != ' ' && *p != '\t') + while (p < e && *p != ' ' && *p != '\t') p++; rb_str_buf_cat(argv_buf, w, p-w); - rb_str_buf_cat(argv_buf, "", 1); /* append '\0' */ + append_terminator(argv_buf); } } eargp->invoke.cmd.argv_buf = argv_buf; eargp->invoke.cmd.command_name = - hide_obj(rb_str_subseq(argv_buf, 0, strlen(RSTRING_PTR(argv_buf)))); + hide_obj(rb_str_subseq(argv_buf, 0, first.len)); rb_enc_copy(eargp->invoke.cmd.command_name, prog); } } @@ -2596,7 +2604,8 @@ rb_exec_fillarg(VALUE prog, int argc, VALUE *argv, VALUE env, VALUE opthash, VAL arg = EXPORT_STR(arg); s = RSTRING_PTR(arg); #endif - rb_str_buf_cat(argv_buf, s, RSTRING_LEN(arg) + 1); /* include '\0' */ + rb_str_buf_cat(argv_buf, s, RSTRING_LEN(arg)); + append_terminator(argv_buf); } eargp->invoke.cmd.argv_buf = argv_buf; } @@ -2676,7 +2685,7 @@ fill_envp_buf_i(st_data_t st_key, st_data_t st_val, st_data_t arg) rb_str_buf_cat2(envp_buf, StringValueCStr(key)); rb_str_buf_cat2(envp_buf, "="); rb_str_buf_cat2(envp_buf, StringValueCStr(val)); - rb_str_buf_cat(envp_buf, "", 1); /* append '\0' */ + append_terminator(envp_buf); return ST_CONTINUE; } From 21b7c7257f35ee14ddce163404e5c92c8e363c89 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 15 Jul 2026 03:34:40 +0900 Subject: [PATCH 31/47] Use bounded comparisons for compiler attributes Compare compiler attribute names using Ruby string lengths. Share attribute handling between the compiler paths to keep their behavior aligned. --- compile.c | 51 ++++++++++++++++++++++++++--------------------- internal/string.h | 8 ++++++++ iseq.c | 2 +- prism_compile.c | 22 ++------------------ 4 files changed, 39 insertions(+), 44 deletions(-) diff --git a/compile.c b/compile.c index 1d762c485ef17b..8f10ce0cbeb482 100644 --- a/compile.c +++ b/compile.c @@ -9273,12 +9273,37 @@ delegate_call_p(const rb_iseq_t *iseq, unsigned int argc, const LINK_ANCHOR *arg } } +static int +compile_builtin_attr_symbol(rb_iseq_t *iseq, VALUE symbol) +{ + VALUE string = rb_sym2str(symbol); + if (rb_streql_lit(string, "leaf")) { + ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_LEAF; + } + else if (rb_streql_lit(string, "inline_block")) { + ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_INLINE_BLOCK; + } + else if (rb_streql_lit(string, "use_block")) { + iseq_set_use_block(iseq); + } + else if (rb_streql_lit(string, "c_trace")) { + // Let the iseq act like a C method in backtraces + ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_C_TRACE; + } + else if (rb_streql_lit(string, "without_interrupts")) { + ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_WITHOUT_INTERRUPTS; + } + else { + return COMPILE_NG; + } + return COMPILE_OK; +} + // Compile Primitive.attr! :leaf, ... static int compile_builtin_attr(rb_iseq_t *iseq, const NODE *node) { VALUE symbol; - VALUE string; if (!node) goto no_arg; while (node) { if (!nd_type_p(node, NODE_LIST)) goto bad_arg; @@ -9295,27 +9320,7 @@ compile_builtin_attr(rb_iseq_t *iseq, const NODE *node) } if (!SYMBOL_P(symbol)) goto non_symbol_arg; - - string = rb_sym2str(symbol); - if (strcmp(RSTRING_PTR(string), "leaf") == 0) { - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_LEAF; - } - else if (strcmp(RSTRING_PTR(string), "inline_block") == 0) { - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_INLINE_BLOCK; - } - else if (strcmp(RSTRING_PTR(string), "use_block") == 0) { - iseq_set_use_block(iseq); - } - else if (strcmp(RSTRING_PTR(string), "c_trace") == 0) { - // Let the iseq act like a C method in backtraces - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_C_TRACE; - } - else if (strcmp(RSTRING_PTR(string), "without_interrupts") == 0) { - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_WITHOUT_INTERRUPTS; - } - else { - goto unknown_arg; - } + if (compile_builtin_attr_symbol(iseq, symbol) != COMPILE_OK) goto unknown_arg; node = next; } return COMPILE_OK; @@ -9326,7 +9331,7 @@ compile_builtin_attr(rb_iseq_t *iseq, const NODE *node) COMPILE_ERROR(ERROR_ARGS "non symbol argument to attr!: %s", rb_builtin_class_name(symbol)); return COMPILE_NG; unknown_arg: - COMPILE_ERROR(ERROR_ARGS "unknown argument to attr!: %s", RSTRING_PTR(string)); + COMPILE_ERROR(ERROR_ARGS "unknown argument to attr!: %" PRIsVALUE, symbol); return COMPILE_NG; bad_arg: UNKNOWN_NODE("attr!", node, COMPILE_NG); diff --git a/internal/string.h b/internal/string.h index 060703e2856639..6dd6c2e4dd231e 100644 --- a/internal/string.h +++ b/internal/string.h @@ -220,6 +220,14 @@ rb_str_eql_internal(const VALUE str1, const VALUE str2) return Qfalse; } +static inline bool +rb_streql_cstr(VALUE str, const char *lit, size_t len) +{ + if ((size_t)RSTRING_LEN(str) != len) return false; + return memcmp(RSTRING_PTR(str), lit, len) == 0; +} +#define rb_streql_lit(str, lit) rb_streql_cstr(str, lit, rb_strlen_lit(lit)) + #if __has_builtin(__builtin_constant_p) # define rb_fstring_cstr(str) \ (__builtin_constant_p(str) ? \ diff --git a/iseq.c b/iseq.c index 720c8f976b5ad5..be10f5d90c3286 100644 --- a/iseq.c +++ b/iseq.c @@ -616,7 +616,7 @@ iseq_location_setup(rb_iseq_t *iseq, VALUE name, VALUE path, VALUE realpath, int RB_OBJ_WRITE(iseq, &loc->base_label, name); loc->first_lineno = first_lineno; - if (ISEQ_BODY(iseq)->local_iseq == iseq && strcmp(RSTRING_PTR(name), "initialize") == 0) { + if (ISEQ_BODY(iseq)->local_iseq == iseq && rb_streql_lit(name, "initialize")) { ISEQ_BODY(iseq)->param.flags.use_block = 1; } diff --git a/prism_compile.c b/prism_compile.c index 6a0422c4f4daab..bba486c97201cd 100644 --- a/prism_compile.c +++ b/prism_compile.c @@ -3534,26 +3534,8 @@ pm_compile_builtin_attr(rb_iseq_t *iseq, pm_scope_node_t *scope_node, const pm_a } VALUE symbol = pm_static_literal_value(iseq, argument, scope_node); - VALUE string = rb_sym2str(symbol); - - if (strcmp(RSTRING_PTR(string), "leaf") == 0) { - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_LEAF; - } - else if (strcmp(RSTRING_PTR(string), "inline_block") == 0) { - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_INLINE_BLOCK; - } - else if (strcmp(RSTRING_PTR(string), "use_block") == 0) { - iseq_set_use_block(iseq); - } - else if (strcmp(RSTRING_PTR(string), "c_trace") == 0) { - // Let the iseq act like a C method in backtraces - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_C_TRACE; - } - else if (strcmp(RSTRING_PTR(string), "without_interrupts") == 0) { - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_WITHOUT_INTERRUPTS; - } - else { - COMPILE_ERROR(iseq, node_location->line, "unknown argument to attr!: %s", RSTRING_PTR(string)); + if (compile_builtin_attr_symbol(iseq, symbol) != COMPILE_OK) { + COMPILE_ERROR(iseq, node_location->line, "unknown argument to attr!: %" PRIsVALUE, symbol); return COMPILE_NG; } } From 4cc24ebb69e56fc9b3d62f5a676389d70f3919e5 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 15 Jul 2026 03:48:05 +0900 Subject: [PATCH 32/47] Resurrect hidden compiler operands Restore hidden operand strings and arrays with their dedicated resurrection functions so inspection preserves all contents. --- compile.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compile.c b/compile.c index 8f10ce0cbeb482..0dba8d86666049 100644 --- a/compile.c +++ b/compile.c @@ -11735,10 +11735,10 @@ opobj_inspect(VALUE obj) if (!SPECIAL_CONST_P(obj) && !RBASIC_CLASS(obj)) { switch (BUILTIN_TYPE(obj)) { case T_STRING: - obj = rb_str_new_cstr(RSTRING_PTR(obj)); + obj = rb_str_resurrect(obj); break; case T_ARRAY: - obj = rb_ary_dup(obj); + obj = rb_ary_resurrect(obj); break; default: break; From 37d2bb3c8c2480a02d6f1d404b62659a2d3d26b1 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 15 Jul 2026 03:53:16 +0900 Subject: [PATCH 33/47] Search NameError messages within string bounds Use the message length when looking for the receiver placeholder instead of requiring a NUL-terminated string. --- error.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/error.c b/error.c index 4515590845fc83..751300565e2aa8 100644 --- a/error.c +++ b/error.c @@ -2624,6 +2624,8 @@ name_err_mesg_to_str(VALUE obj) int state = 0; rb_encoding *usascii = rb_usascii_encoding(); +#define rb_memsearch_lit(str, v) \ + rb_memsearch((str), rb_strlen_lit(str), RSTRING_PTR(v), RSTRING_LEN(v), rb_enc_get(v)) #define FAKE_CSTR(v, str) rb_setup_fake_str((v), (str), rb_strlen_lit(str), usascii) c = s = FAKE_CSTR(&s_str, ""); obj = ptr->recv; @@ -2638,7 +2640,7 @@ name_err_mesg_to_str(VALUE obj) c = d = FAKE_CSTR(&d_str, "false"); break; default: - if (strstr(RSTRING_PTR(mesg), "%2$s")) { + if (rb_memsearch_lit("%2$s", mesg) >= 0) { d = rb_protect(name_err_mesg_receiver_name, obj, &state); if (state || NIL_OR_UNDEF_P(d)) d = rb_protect(rb_inspect, obj, &state); From b9a50a0b81a1befe0ab02bd93f01a9e684c55149 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 15 Jul 2026 03:53:22 +0900 Subject: [PATCH 34/47] Bound the hostname terminator search Search only the gethostname buffer for its terminator so resizing the Ruby string does not read beyond the returned data. --- ext/socket/socket.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/socket/socket.c b/ext/socket/socket.c index 7902d3b606a8b1..64f5822ae53fd3 100644 --- a/ext/socket/socket.c +++ b/ext/socket/socket.c @@ -873,7 +873,8 @@ sock_gethostname(VALUE obj) rb_str_modify_expand(name, len); len += len; } - rb_str_resize(name, strlen(RSTRING_PTR(name))); + char *s = RSTRING_PTR(name), *p = memchr(s, 0, len); + if (p) rb_str_resize(name, (long)(p - s)); return name; } #else From 4b638eecd159f363a25f14bb5db4a31b82b3b84c Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 15 Jul 2026 09:06:11 +0900 Subject: [PATCH 35/47] Provide a fallback implementation of memrchr Move the local fallback out of string.c so other source files can use memrchr on platforms that do not provide it. --- configure.ac | 2 +- include/ruby/missing.h | 4 ++++ missing/memrchr.c | 12 ++++++++++++ string.c | 13 ------------- win32/Makefile.sub | 2 +- 5 files changed, 18 insertions(+), 15 deletions(-) create mode 100644 missing/memrchr.c diff --git a/configure.ac b/configure.ac index 8977a46b3c38ef..a461adc697cc80 100644 --- a/configure.ac +++ b/configure.ac @@ -2131,6 +2131,7 @@ AC_REPLACE_FUNCS(flock) AC_REPLACE_FUNCS(hypot) AC_REPLACE_FUNCS(lgamma_r) AC_REPLACE_FUNCS(memmove) +AC_REPLACE_FUNCS(memrchr) AC_REPLACE_FUNCS(nan) AC_REPLACE_FUNCS(nextafter) AC_REPLACE_FUNCS(setproctitle) @@ -2246,7 +2247,6 @@ AC_CHECK_FUNCS(mblen) AC_CHECK_FUNCS(memalign) AC_CHECK_FUNCS(memset_s) AC_CHECK_FUNCS(writev) -AC_CHECK_FUNCS(memrchr) AC_CHECK_FUNCS(memmem) AC_CHECK_FUNCS(mkfifo) AC_CHECK_FUNCS(mknod) diff --git a/include/ruby/missing.h b/include/ruby/missing.h index aea6c9088d9d22..8e2e0f8f601fb0 100644 --- a/include/ruby/missing.h +++ b/include/ruby/missing.h @@ -183,6 +183,10 @@ RUBY_EXTERN int memcmp(const void *, const void *, size_t); RUBY_EXTERN void *memmove(void *, const void *, size_t); #endif +#ifndef HAVE_MEMRCHR +RUBY_EXTERN void *memrchr(const void *, int, size_t); +#endif + /* #ifndef HAVE_MODF RUBY_EXTERN double modf(double, double *); diff --git a/missing/memrchr.c b/missing/memrchr.c new file mode 100644 index 00000000000000..89147aad22b1af --- /dev/null +++ b/missing/memrchr.c @@ -0,0 +1,12 @@ +#include "ruby/missing.h" + +void * +memrchr(const void *ptr, int ch, size_t len) +{ + const unsigned char *p = (const unsigned char *)ptr + len; + + while (p > (const unsigned char *)ptr) { + if (*--p == (unsigned char)ch) return (void *)p; + } + return NULL; +} diff --git a/string.c b/string.c index 322e43eb2fd64a..a503a2d505e436 100644 --- a/string.c +++ b/string.c @@ -4763,19 +4763,6 @@ rb_str_byteindex_m(int argc, VALUE *argv, VALUE str) return Qnil; } -#ifndef HAVE_MEMRCHR -static void* -memrchr(const char *search_str, int chr, long search_len) -{ - const char *ptr = search_str + search_len; - while (ptr > search_str) { - if ((unsigned char)*(--ptr) == chr) return (void *)ptr; - } - - return ((void *)0); -} -#endif - static long str_rindex(VALUE str, VALUE sub, const char *s, rb_encoding *enc) { diff --git a/win32/Makefile.sub b/win32/Makefile.sub index 619faf6663cae4..e5c099972d2860 100644 --- a/win32/Makefile.sub +++ b/win32/Makefile.sub @@ -320,7 +320,7 @@ LIBS = $(LIBS) imagehlp.lib shlwapi.lib bcrypt.lib $(EXTLIBS) !endif !if !defined(MISSING) MISSING = crypt.obj ffs.obj langinfo.obj lgamma_r.obj strlcat.obj strlcpy.obj win32/win32.obj win32/file.obj setproctitle.obj -MISSING = $(MISSING) explicit_bzero.obj +MISSING = $(MISSING) explicit_bzero.obj memrchr.obj !endif DLNOBJ = dln.obj From c2883ddf97358a3bf4fbcfa71c4965b571de4354 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 15 Jul 2026 09:06:25 +0900 Subject: [PATCH 36/47] Avoid NUL-terminated extension checks in require Search and compare feature extensions within the Ruby string length so require does not rely on a terminator while classifying paths. --- load.c | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/load.c b/load.c index 933e3047ae8aac..069617397affee 100644 --- a/load.c +++ b/load.c @@ -1077,6 +1077,26 @@ rb_f_require_relative(VALUE obj, VALUE fname) return rb_require_relative_entrypoint(fname); } +static char * +find_ext(VALUE str, char **ptr, const char **end) +{ + long len = RSTRING_LEN(str); + *ptr = RSTRING_PTR(str); + *end = *ptr + len; + return memrchr(*ptr, '.', len); +} + +static bool +ext_equal(const char *ext, const char *end, const char *suffix, size_t len) +{ + return (size_t)(end - ext) == len && memcmp(ext, suffix, len) == 0; +} + +#define EXT_RB_P(ext, end) ext_equal(ext, end, ".rb", rb_strlen_lit(".rb")) +#define EXT_SO_P(ext, end) (ext_equal(ext, end, ".so", rb_strlen_lit(".so")) || \ + ext_equal(ext, end, ".o", rb_strlen_lit(".o"))) +#define EXT_DLEXT_P(ext, end) ext_equal(ext, end, DLEXT, rb_strlen_lit(DLEXT)) + typedef int (*feature_func)(const rb_box_t *box, const char *feature, const char *ext, int rb, int expanded, const char **fn); static int @@ -1085,25 +1105,25 @@ search_required(const rb_box_t *box, VALUE fname, volatile VALUE *path, feature_ VALUE tmp; char *ext, *ftptr; int ft = 0; - const char *loading; + const char *ftend, *loading; *path = 0; - ext = strrchr(ftptr = RSTRING_PTR(fname), '.'); - if (ext && !strchr(ext, '/')) { - if (IS_RBEXT(ext)) { + ext = find_ext(fname, &ftptr, &ftend); + if (ext && !memchr(ext, '/', ftend - ext)) { + if (EXT_RB_P(ext, ftend)) { if (rb_feature_p(box, ftptr, ext, TRUE, FALSE, &loading)) { if (loading) *path = rb_filesystem_str_new_cstr(loading); return 'r'; } if ((tmp = rb_find_file(fname)) != 0) { - ext = strrchr(ftptr = RSTRING_PTR(tmp), '.'); + ext = find_ext(tmp, &ftptr, &ftend); if (!rb_feature_p(box, ftptr, ext, TRUE, TRUE, &loading) || loading) *path = tmp; return 'r'; } return 0; } - else if (IS_SOEXT(ext)) { + else if (EXT_SO_P(ext, ftend)) { if (rb_feature_p(box, ftptr, ext, FALSE, FALSE, &loading)) { if (loading) *path = rb_filesystem_str_new_cstr(loading); return 's'; @@ -1112,19 +1132,19 @@ search_required(const rb_box_t *box, VALUE fname, volatile VALUE *path, feature_ rb_str_cat2(tmp, DLEXT); OBJ_FREEZE(tmp); if ((tmp = rb_find_file(tmp)) != 0) { - ext = strrchr(ftptr = RSTRING_PTR(tmp), '.'); + ext = find_ext(tmp, &ftptr, &ftend); if (!rb_feature_p(box, ftptr, ext, FALSE, TRUE, &loading) || loading) *path = tmp; return 's'; } } - else if (IS_DLEXT(ext)) { + else if (EXT_DLEXT_P(ext, ftend)) { if (rb_feature_p(box, ftptr, ext, FALSE, FALSE, &loading)) { if (loading) *path = rb_filesystem_str_new_cstr(loading); return 's'; } if ((tmp = rb_find_file(fname)) != 0) { - ext = strrchr(ftptr = RSTRING_PTR(tmp), '.'); + ext = find_ext(tmp, &ftptr, &ftend); if (!rb_feature_p(box, ftptr, ext, FALSE, TRUE, &loading) || loading) *path = tmp; return 's'; @@ -1173,7 +1193,7 @@ search_required(const rb_box_t *box, VALUE fname, volatile VALUE *path, feature_ } /* fall through */ case loadable_ext_rb: - ext = strrchr(ftptr = RSTRING_PTR(tmp), '.'); + ext = find_ext(tmp, &ftptr, &ftend); if (rb_feature_p(box, ftptr, ext, type == loadable_ext_rb, TRUE, &loading) && !loading) break; *path = tmp; From c67dc0dce78384dfdc436fec88c804aebc9bd382 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 15 Jul 2026 09:40:07 +0900 Subject: [PATCH 37/47] Pass Prism file paths as Ruby strings Create system errors from the filepath VALUE so the full path is retained without relying on a NUL terminator or a separate GC guard. --- prism_compile.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/prism_compile.c b/prism_compile.c index bba486c97201cd..d1ed2cbb339cb6 100644 --- a/prism_compile.c +++ b/prism_compile.c @@ -11540,8 +11540,7 @@ pm_load_file(pm_parse_result_t *result, VALUE filepath, bool load_error) error = rb_exc_new3(rb_eLoadError, message); rb_ivar_set(error, rb_intern_const("@path"), filepath); } else { - error = rb_syserr_new(err, RSTRING_PTR(filepath)); - RB_GC_GUARD(filepath); + error = rb_syserr_new_str(err, filepath); } return error; From b9ad19da120f3bfcec1e09c1e21e1e3c97908a69 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 15 Jul 2026 10:46:11 +0900 Subject: [PATCH 38/47] Format backtrace locations as Ruby strings --- vm_backtrace.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vm_backtrace.c b/vm_backtrace.c index a865b6e71f3f6a..2e862de8383a2b 100644 --- a/vm_backtrace.c +++ b/vm_backtrace.c @@ -452,7 +452,7 @@ location_absolute_path_m(VALUE self) static VALUE location_format(VALUE file, int lineno, VALUE name) { - VALUE s = rb_enc_sprintf(rb_enc_compatible(file, name), "%s", RSTRING_PTR(file)); + VALUE s = rb_enc_sprintf(rb_enc_compatible(file, name), "%"PRIsVALUE, file); if (lineno != 0) { rb_str_catf(s, ":%d", lineno); } @@ -461,7 +461,7 @@ location_format(VALUE file, int lineno, VALUE name) rb_str_cat_cstr(s, "unknown method"); } else { - rb_str_catf(s, "'%s'", RSTRING_PTR(name)); + rb_str_catf(s, "'%"PRIsVALUE"'", name); } RB_GC_GUARD(name); return s; From 529d86a2c32728db3f96797691270fa1aeeb0d08 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 15 Jul 2026 10:46:35 +0900 Subject: [PATCH 39/47] Format unknown struct keywords as a Ruby string --- struct.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/struct.c b/struct.c index 606208e87268e5..32da26860b993a 100644 --- a/struct.c +++ b/struct.c @@ -785,8 +785,8 @@ rb_struct_initialize_m(int argc, const VALUE *argv, VALUE self) rb_mem_clear((VALUE *)RSTRUCT_CONST_PTR(self), n); rb_hash_foreach(argv[0], struct_hash_set_i, (VALUE)&arg); if (UNLIKELY(!NIL_P(arg.unknown_keywords))) { - rb_raise(rb_eArgError, "unknown keywords: %s", - RSTRING_PTR(rb_ary_join(arg.unknown_keywords, rb_str_new2(", ")))); + rb_raise(rb_eArgError, "unknown keywords: %"PRIsVALUE, + rb_ary_join(arg.unknown_keywords, rb_str_new2(", "))); } } else { From 043f949813b888d76e5c0c3097e56dc5db152f52 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 15 Jul 2026 10:47:15 +0900 Subject: [PATCH 40/47] Bound backtrace output strings --- vm_backtrace.c | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/vm_backtrace.c b/vm_backtrace.c index 2e862de8383a2b..5af6cc341a8237 100644 --- a/vm_backtrace.c +++ b/vm_backtrace.c @@ -1114,12 +1114,13 @@ oldbt_print(void *data, VALUE file, int lineno, VALUE name) FILE *fp = (FILE *)data; if (NIL_P(name)) { - fprintf(fp, "\tfrom %s:%d:in unknown method\n", - RSTRING_PTR(file), lineno); + fprintf(fp, "\tfrom %.*s:%d:in unknown method\n", + RSTRING_LENINT(file), RSTRING_PTR(file), lineno); } else { - fprintf(fp, "\tfrom %s:%d:in '%s'\n", - RSTRING_PTR(file), lineno, RSTRING_PTR(name)); + fprintf(fp, "\tfrom %.*s:%d:in '%.*s'\n", + RSTRING_LENINT(file), RSTRING_PTR(file), lineno, + RSTRING_LENINT(name), RSTRING_PTR(name)); } } @@ -1148,16 +1149,20 @@ oldbt_bugreport(void *arg, VALUE file, int line, VALUE method) struct oldbt_bugreport_arg *p = arg; FILE *fp = p->fp; const char *filename = NIL_P(file) ? "ruby" : RSTRING_PTR(file); + int filename_len = NIL_P(file) ? 4 : RSTRING_LENINT(file); if (!p->count) { fprintf(fp, "-- Ruby level backtrace information " "----------------------------------------\n"); p->count = 1; } if (NIL_P(method)) { - fprintf(fp, "%s:%d:in unknown method\n", filename, line); + fprintf(fp, "%.*s:%d:in unknown method\n", + filename_len, filename, line); } else { - fprintf(fp, "%s:%d:in '%s'\n", filename, line, RSTRING_PTR(method)); + fprintf(fp, "%.*s:%d:in '%.*s'\n", + filename_len, filename, line, + RSTRING_LENINT(method), RSTRING_PTR(method)); } } From ec002ff3619531e4789ddd8010d69084a433f5eb Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 15 Jul 2026 10:48:43 +0900 Subject: [PATCH 41/47] Bound VM dump strings --- vm_dump.c | 52 +++++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/vm_dump.c b/vm_dump.c index 22ed7cfee58505..6c8342c88eef8e 100644 --- a/vm_dump.c +++ b/vm_dump.c @@ -59,6 +59,7 @@ control_frame_dump(const rb_execution_context_t *ec, const rb_control_frame_t *c char ep_in_heap = ' '; char posbuf[MAX_POSBUF+1]; int line = 0; + int iseq_name_len = -1; const char *magic, *iseq_name = "-", *selfstr = "-", *biseq_name = "-"; VALUE tmp; const rb_iseq_t *iseq = NULL; @@ -131,18 +132,22 @@ control_frame_dump(const rb_execution_context_t *ec, const rb_control_frame_t *c else if (SYMBOL_P((VALUE)iseq)) { tmp = rb_sym2str((VALUE)iseq); iseq_name = RSTRING_PTR(tmp); - snprintf(posbuf, MAX_POSBUF, ":%s", iseq_name); + iseq_name_len = RSTRING_LENINT(tmp); + snprintf(posbuf, MAX_POSBUF, ":%.*s", iseq_name_len, iseq_name); line = -1; } else { if (CFP_PC(cfp)) { pc = CFP_PC(cfp) - ISEQ_BODY(iseq)->iseq_encoded; iseq_name = RSTRING_PTR(ISEQ_BODY(iseq)->location.label); + iseq_name_len = RSTRING_LENINT(ISEQ_BODY(iseq)->location.label); if (pc >= 0 && (size_t)pc <= ISEQ_BODY(iseq)->iseq_size) { line = rb_vm_get_sourceline(cfp); } if (line) { - snprintf(posbuf, MAX_POSBUF, "%s:%d", RSTRING_PTR(rb_iseq_path(iseq)), line); + VALUE path = rb_iseq_path(iseq); + snprintf(posbuf, MAX_POSBUF, "%.*s:%d", + RSTRING_LENINT(path), RSTRING_PTR(path), line); } } else { @@ -183,7 +188,12 @@ control_frame_dump(const rb_execution_context_t *ec, const rb_control_frame_t *c } if (0) { kprintf(" \t"); - kprintf("iseq: %-24s ", iseq_name); + if (iseq_name_len < 0) { + kprintf("iseq: %-24s ", iseq_name); + } + else { + kprintf("iseq: %-24.*s ", iseq_name_len, iseq_name); + } kprintf("self: %-24s ", selfstr); kprintf("%-1s ", biseq_name); } @@ -286,7 +296,7 @@ box_env_dump(const rb_execution_context_t *ec, const VALUE *env, const rb_contro char ep_in_heap = ' '; char posbuf[MAX_POSBUF+1]; int line = 0; - const char *magic, *iseq_name = "-"; + const char *magic; VALUE tmp; const rb_iseq_t *iseq = NULL; const rb_box_t *box = NULL; @@ -347,35 +357,27 @@ box_env_dump(const rb_execution_context_t *ec, const VALUE *env, const rb_contro if (cfp && CFP_ISEQ(cfp)) { #define RUBY_VM_IFUNC_P(ptr) IMEMO_TYPE_P(ptr, imemo_ifunc) const rb_iseq_t *resolved_iseq = CFP_ISEQ(cfp); - if (RUBY_VM_IFUNC_P(resolved_iseq)) { - iseq_name = ""; - } - else if (SYMBOL_P((VALUE)resolved_iseq)) { + if (SYMBOL_P((VALUE)resolved_iseq)) { tmp = rb_sym2str((VALUE)resolved_iseq); - iseq_name = RSTRING_PTR(tmp); - snprintf(posbuf, MAX_POSBUF, ":%s", iseq_name); + snprintf(posbuf, MAX_POSBUF, ":%.*s", + RSTRING_LENINT(tmp), RSTRING_PTR(tmp)); line = -1; } - else { - if (CFP_PC(cfp)) { - iseq = resolved_iseq; - pc = CFP_PC(cfp) - ISEQ_BODY(iseq)->iseq_encoded; - iseq_name = RSTRING_PTR(ISEQ_BODY(iseq)->location.label); - if (pc >= 0 && (size_t)pc <= ISEQ_BODY(iseq)->iseq_size) { - line = rb_vm_get_sourceline(cfp); - } - if (line) { - snprintf(posbuf, MAX_POSBUF, "%s:%d", RSTRING_PTR(rb_iseq_path(iseq)), line); - } + else if (!RUBY_VM_IFUNC_P(resolved_iseq) && CFP_PC(cfp)) { + iseq = resolved_iseq; + pc = CFP_PC(cfp) - ISEQ_BODY(iseq)->iseq_encoded; + if (pc >= 0 && (size_t)pc <= ISEQ_BODY(iseq)->iseq_size) { + line = rb_vm_get_sourceline(cfp); } - else { - iseq_name = ""; + if (line) { + VALUE path = rb_iseq_path(iseq); + snprintf(posbuf, MAX_POSBUF, "%.*s:%d", + RSTRING_LENINT(path), RSTRING_PTR(path), line); } } } else if (me != NULL && IMEMO_TYPE_P(me, imemo_ment)) { - iseq_name = rb_id2name(me->def->original_id); - snprintf(posbuf, MAX_POSBUF, ":%s", iseq_name); + snprintf(posbuf, MAX_POSBUF, ":%s", rb_id2name(me->def->original_id)); line = -1; } From 92c6f4331b1a78f40b2e5b7beaccd626147d2c3d Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 15 Jul 2026 10:51:13 +0900 Subject: [PATCH 42/47] Bound GC and VM diagnostic strings --- compile.c | 20 ++++++++++++-------- ext/objspace/object_tracing.c | 2 +- gc.c | 30 +++++++++++++++++++++--------- iseq.c | 6 +++++- vm.c | 4 +++- vm_exec.h | 5 +++-- vm_insnhelper.c | 8 +++++--- vm_method.c | 8 ++++---- 8 files changed, 54 insertions(+), 29 deletions(-) diff --git a/compile.c b/compile.c index 0dba8d86666049..2de1d57641cb3e 100644 --- a/compile.c +++ b/compile.c @@ -2481,8 +2481,9 @@ fix_sp_depth(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) lobj->sp = sp; } else if (lobj->sp != sp) { - debugs("%s:%d: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", - RSTRING_PTR(rb_iseq_path(iseq)), line, + VALUE path = rb_iseq_path(iseq); + debugs("%.*s:%d: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", + RSTRING_LENINT(path), RSTRING_PTR(path), line, lobj->label_no, lobj->sp, sp); } } @@ -2497,8 +2498,9 @@ fix_sp_depth(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) } else { if (lobj->sp != sp) { - debugs("%s:%d: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", - RSTRING_PTR(rb_iseq_path(iseq)), line, + VALUE path = rb_iseq_path(iseq); + debugs("%.*s:%d: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", + RSTRING_LENINT(path), RSTRING_PTR(path), line, lobj->label_no, lobj->sp, sp); } sp = lobj->sp; @@ -2645,8 +2647,9 @@ iseq_set_sequence(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) LABEL *lobj = (LABEL *)list; lobj->position = code_index; if (lobj->sp != sp) { - debugs("%s: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", - RSTRING_PTR(rb_iseq_path(iseq)), + VALUE path = rb_iseq_path(iseq); + debugs("%.*s: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", + RSTRING_LENINT(path), RSTRING_PTR(path), lobj->label_no, lobj->sp, sp); } sp = lobj->sp; @@ -2870,8 +2873,9 @@ iseq_set_sequence(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) { LABEL *lobj = (LABEL *)list; if (lobj->sp != sp) { - debugs("%s: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", - RSTRING_PTR(rb_iseq_path(iseq)), + VALUE path = rb_iseq_path(iseq); + debugs("%.*s: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", + RSTRING_LENINT(path), RSTRING_PTR(path), lobj->label_no, lobj->sp, sp); } sp = lobj->sp; diff --git a/ext/objspace/object_tracing.c b/ext/objspace/object_tracing.c index 74d793a6e232c1..0497c89210e6f5 100644 --- a/ext/objspace/object_tracing.c +++ b/ext/objspace/object_tracing.c @@ -390,7 +390,7 @@ object_allocations_reporter_i(st_data_t key, st_data_t val, st_data_t ptr) fprintf(out, "@%s:%lu", info->path ? info->path : "", info->line); if (!NIL_P(info->mid)) { VALUE m = rb_sym2str(info->mid); - fprintf(out, " (%s)", RSTRING_PTR(m)); + fprintf(out, " (%.*s)", RSTRING_LENINT(m), RSTRING_PTR(m)); } fprintf(out, ")\n"); diff --git a/gc.c b/gc.c index 29fe600f27c797..4930ba63521911 100644 --- a/gc.c +++ b/gc.c @@ -4659,9 +4659,10 @@ rb_raw_iseq_info(char *const buff, const size_t buff_size, const rb_iseq_t *iseq if (buff_size > 0 && ISEQ_BODY(iseq) && ISEQ_BODY(iseq)->location.label && !RB_TYPE_P(ISEQ_BODY(iseq)->location.pathobj, T_MOVED)) { VALUE path = rb_iseq_path(iseq); int n = ISEQ_BODY(iseq)->location.first_lineno; - snprintf(buff, buff_size, " %s@%s:%d", - RSTRING_PTR(ISEQ_BODY(iseq)->location.label), - RSTRING_PTR(path), n); + VALUE label = ISEQ_BODY(iseq)->location.label; + snprintf(buff, buff_size, " %.*s@%.*s:%d", + RSTRING_LENINT(label), RSTRING_PTR(label), + RSTRING_LENINT(path), RSTRING_PTR(path), n); } } @@ -4733,7 +4734,7 @@ rb_raw_obj_info_common(char *const buff, const size_t buff_size, const VALUE obj else if (RTEST(RBASIC(obj)->klass)) { VALUE class_path = rb_mod_name(RBASIC(obj)->klass); if (!NIL_P(class_path)) { - APPEND_F("%s ", RSTRING_PTR(class_path)); + APPEND_F("%.*s ", str_len_no_raise(class_path), RSTRING_PTR(class_path)); } } } @@ -4797,7 +4798,7 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU VALUE fstr = RSYMBOL(obj)->fstr; ID id = RSYMBOL(obj)->id; if (RB_TYPE_P(fstr, T_STRING)) { - APPEND_F(":%s id:%d", RSTRING_PTR(fstr), (unsigned int)id); + APPEND_F(":%.*s id:%d", str_len_no_raise(fstr), RSTRING_PTR(fstr), (unsigned int)id); } else { APPEND_F("(%p) id:%d", (void *)fstr, (unsigned int)id); @@ -4819,7 +4820,7 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU { VALUE class_path = rb_mod_name(obj); if (!NIL_P(class_path)) { - APPEND_F("%s", RSTRING_PTR(class_path)); + APPEND_F("%.*s", str_len_no_raise(class_path), RSTRING_PTR(class_path)); } else { APPEND_S("(anon)"); @@ -4830,7 +4831,7 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU { VALUE class_path = rb_mod_name(RBASIC_CLASS(obj)); if (!NIL_P(class_path)) { - APPEND_F("src:%s", RSTRING_PTR(class_path)); + APPEND_F("src:%.*s", str_len_no_raise(class_path), RSTRING_PTR(class_path)); } break; } @@ -4936,9 +4937,20 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU const struct rb_callcache *cc = (const struct rb_callcache *)obj; VALUE class_path = vm_cc_valid(cc) ? rb_mod_name(cc->klass) : Qnil; const rb_callable_method_entry_t *cme = vm_cc_cme(cc); + const char *class_name; + int class_name_len; - APPEND_F("(klass:%s cme:%s%s (%p) call:%p", - NIL_P(class_path) ? (vm_cc_valid(cc) ? "??" : "") : RSTRING_PTR(class_path), + if (NIL_P(class_path)) { + class_name = vm_cc_valid(cc) ? "??" : ""; + class_name_len = vm_cc_valid(cc) ? 2 : 6; + } + else { + class_name = RSTRING_PTR(class_path); + class_name_len = str_len_no_raise(class_path); + } + + APPEND_F("(klass:%.*s cme:%s%s (%p) call:%p", + class_name_len, class_name, cme ? rb_id2name(cme->called_id) : "", cme ? (METHOD_ENTRY_INVALIDATED(cme) ? " [inv]" : "") : "", (void *)cme, diff --git a/iseq.c b/iseq.c index be10f5d90c3286..93e58db2e65287 100644 --- a/iseq.c +++ b/iseq.c @@ -4112,7 +4112,11 @@ rb_iseq_add_local_tracepoint_recursively(const rb_iseq_t *iseq, rb_event_flag_t data.r = GET_RACTOR(); iseq_add_local_tracepoint_i(iseq, (void *)&data); - if (0) fprintf(stderr, "Iseq disasm:\n:%s", RSTRING_PTR(rb_iseq_disasm(iseq))); /* for debug */ + if (0) { + VALUE disasm = rb_iseq_disasm(iseq); + fprintf(stderr, "Iseq disasm:\n:%.*s", + RSTRING_LENINT(disasm), RSTRING_PTR(disasm)); + } return data.n; } diff --git a/vm.c b/vm.c index 07d03c864ae6f8..65edfce546c4a8 100644 --- a/vm.c +++ b/vm.c @@ -424,7 +424,9 @@ vm_cref_dump(const char *mesg, const rb_cref_t *cref) ruby_debug_printf("vm_cref_dump: %s (%p)\n", mesg, (void *)cref); while (cref) { - ruby_debug_printf("= cref| klass: %s\n", RSTRING_PTR(rb_class_path(CREF_CLASS(cref)))); + VALUE path = rb_class_path(CREF_CLASS(cref)); + ruby_debug_printf("= cref| klass: %.*s\n", + RSTRING_LENINT(path), RSTRING_PTR(path)); cref = CREF_NEXT(cref); } } diff --git a/vm_exec.h b/vm_exec.h index 0db016ec164f07..b234345d2e73c5 100644 --- a/vm_exec.h +++ b/vm_exec.h @@ -67,10 +67,11 @@ error ! #define INSN_ENTRY_SIG(insn) \ if (0) { \ - ruby_debug_printf("exec: %s@(%"PRIdPTRDIFF", %"PRIdPTRDIFF")@%s:%u\n", #insn, \ + VALUE path = rb_iseq_path(CFP_ISEQ(reg_cfp)); \ + ruby_debug_printf("exec: %s@(%"PRIdPTRDIFF", %"PRIdPTRDIFF")@%.*s:%u\n", #insn, \ (reg_pc - ISEQ_BODY(CFP_ISEQ(reg_cfp))->iseq_encoded), \ (reg_cfp->pc - ISEQ_BODY(CFP_ISEQ(reg_cfp))->iseq_encoded), \ - RSTRING_PTR(rb_iseq_path(CFP_ISEQ(reg_cfp))), \ + RSTRING_LENINT(path), RSTRING_PTR(path), \ rb_iseq_line_no(CFP_ISEQ(reg_cfp), reg_pc - ISEQ_BODY(CFP_ISEQ(reg_cfp))->iseq_encoded)); \ } diff --git a/vm_insnhelper.c b/vm_insnhelper.c index f7b007d814e47a..fde6ac3245cb36 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -7258,12 +7258,14 @@ vm_trace(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp) rb_event_flag_t bmethod_events = ractor_events | bmethod_local_events; if (0) { - ruby_debug_printf("vm_trace>>%4d (%4x) - %s:%d %s\n", + VALUE path = rb_iseq_path(iseq); + VALUE label = rb_iseq_label(iseq); + ruby_debug_printf("vm_trace>>%4d (%4x) - %.*s:%d %.*s\n", (int)pos, (int)pc_events, - RSTRING_PTR(rb_iseq_path(iseq)), + RSTRING_LENINT(path), RSTRING_PTR(path), (int)rb_iseq_line_no(iseq, pos), - RSTRING_PTR(rb_iseq_label(iseq))); + RSTRING_LENINT(label), RSTRING_PTR(label)); } VM_ASSERT(reg_cfp->pc == pc); VM_ASSERT(pc_events != 0); diff --git a/vm_method.c b/vm_method.c index 43ca4313bca487..a51a6155506ebf 100644 --- a/vm_method.c +++ b/vm_method.c @@ -1513,9 +1513,9 @@ rb_method_entry_make(VALUE klass, ID mid, VALUE defined_class, rb_method_visibil } if (iseq) { rb_warning( - "method redefined; discarding old %"PRIsVALUE"\n%s:%d: warning: previous definition of %"PRIsVALUE" was here", + "method redefined; discarding old %"PRIsVALUE"\n%"PRIsVALUE":%d: warning: previous definition of %"PRIsVALUE" was here", rb_id2str(mid), - RSTRING_PTR(rb_iseq_path(iseq)), + rb_iseq_path(iseq), ISEQ_BODY(iseq)->location.first_lineno, rb_id2str(old_def->original_id) ); @@ -1832,9 +1832,9 @@ search_method0(VALUE klass, ID id, VALUE *defined_class_ptr, bool skip_refined) if (me == NULL) RB_DEBUG_COUNTER_INC(mc_search_notfound); VM_ASSERT(me == NULL || !METHOD_ENTRY_INVALIDATED(me), - "invalid me, mid:%s, klass:%s(%s)", + "invalid me, mid:%s, klass:%"PRIsVALUE"(%s)", rb_id2name(id), - RTEST(rb_mod_name(klass)) ? RSTRING_PTR(rb_mod_name(klass)) : "anonymous", + RTEST(rb_mod_name(klass)) ? rb_mod_name(klass) : rb_str_new_lit("anonymous"), rb_obj_info(klass)); return me; } From f636ac3825b79cb479f50064bb568d2c9fb0a578 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 15 Jul 2026 13:41:30 +0900 Subject: [PATCH 43/47] Fix broken RDoc examples that raise or fail to run The Hash#[] and Array#product examples referenced undefined variables, the StringIO doc named a nonexistent File::RDRW constant, and Pathname#mkpath's call-seq advertised a positional permissions argument that the keyword-only implementation does not accept. Co-Authored-By: Claude Opus 4.8 --- array.c | 2 +- doc/stringio/stringio.md | 2 +- hash.c | 2 +- pathname_builtin.rb | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/array.c b/array.c index 58f41287bdb3cd..35892be43e5ecf 100644 --- a/array.c +++ b/array.c @@ -7723,7 +7723,7 @@ rb_ary_repeated_combination(VALUE ary, VALUE num) * If no argument is given, returns an array of 1-element arrays, * each containing an element of +self+: * - * a.product # => [[0], [1], [2]] + * [0, 1, 2].product # => [[0], [1], [2]] * * With a block given, calls the block with each combination; returns +self+: * diff --git a/doc/stringio/stringio.md b/doc/stringio/stringio.md index f81f79cfea728a..4a0550e10a9fbd 100644 --- a/doc/stringio/stringio.md +++ b/doc/stringio/stringio.md @@ -173,7 +173,7 @@ strio.gets # Raises IOError: not opened for reading Mode specified as one of: - String: `'r+'`. -- Constant: `File::RDRW`. +- Constant: `File::RDWR`. Initial state: diff --git a/hash.c b/hash.c index b863f217bcae99..64c343b15f0338 100644 --- a/hash.c +++ b/hash.c @@ -2097,7 +2097,7 @@ rb_hash_stlike_lookup(VALUE hash, st_data_t key, st_data_t *pval) * * If the key is found, returns its value: * - * {foo: 0, bar: 1, baz: 2} + * h = {foo: 0, bar: 1, baz: 2} * h[:bar] # => 1 * * Otherwise, returns a default value (see {Hash Default}[rdoc-ref:Hash@Hash+Default]). diff --git a/pathname_builtin.rb b/pathname_builtin.rb index dc9f81a3b369d8..da9806650d3685 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -305,7 +305,7 @@ def inspect # :nodoc: # :markup: markdown # # call-seq: - # mkpath(permissions = 0775) -> self + # mkpath(mode: nil) -> self # # Creates a directory at the path in `self`; # creates intermediate directories as needed: @@ -318,7 +318,7 @@ def inspect # :nodoc: # pn.rmtree # Clean up. # ``` # - # Directories are created with the given permissions; + # When `mode` is given, directories are created with those permissions; # see {File Permissions}[rdoc-ref:File@File+Permissions]. # The permissions for already-existing directories are not changed. def mkpath(mode: nil) From 6f8d0dd25682b647f1f4eca7cf92890c72cd267f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 15 Jul 2026 13:41:36 +0900 Subject: [PATCH 44/47] Fix Set RDoc examples to use Set and proper output comments Set#include? built its receiver from an Array literal, so the `hash`/`eql?` semantics the surrounding text describes did not apply. Set#each wrote `sum => 6`, which is a rightward-assignment pattern match rather than an output comment. Co-Authored-By: Claude Opus 4.8 --- set.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/set.c b/set.c index 4e76750a9f0400..2efdab74173dae 100644 --- a/set.c +++ b/set.c @@ -1198,7 +1198,7 @@ set_i_intersection(VALUE set, VALUE other) * * Returns whether the given +object+ is an element of +self+: * - * set = [0, :zero, '0'] + * set = Set[0, :zero, '0'] * set.include?('0') # => true * set.include?('zero') # => false * @@ -1585,7 +1585,7 @@ set_each_i(st_data_t key, st_data_t dummy) * * sum = 0 * Set[1, 2, 3].each {|i| sum += i } - * sum => 6 + * sum # => 6 * * With no block given, returns an Enumerator. */ From cce7041630642c56dd4e32eb669d9fd8bffa4040 Mon Sep 17 00:00:00 2001 From: Shugo Maeda Date: Wed, 15 Jul 2026 13:10:35 +0900 Subject: [PATCH 45/47] [Feature #22097] Add Proc#refined Add Proc#refined(mod, ...), which returns a new Proc that behaves like the receiver but with the refinements activated by the given modules in effect inside its body, without affecting the original Proc. This is an alternative to the previously proposed Proc#using (https://bugs.ruby-lang.org/issues/16461) that does not modify the existing block and needs no `using Proc::Refinements` declaration. Refinements are resolved at run time through the cref, so a refined proc gets two pieces of its own: * A cref carrying the additional refinements, built by activating the modules (rb_using_module_recursive, under the VM lock) on a duplicate of the block's captured cref. It is stored in a hidden ivar on the Proc and injected into the block frame on every proc-invocation path (Proc#call, yield from C methods, invokeblock/opt_call, and instance_eval/instance_exec/module_eval/class_eval via yield_under). The captured environment stays shared, so closure variables are still shared with the original Proc. `using` inside the body is rejected (CREF_REFINED_PROC), so the cref's refinements never change after publication. * A recursive deep copy of the block's instruction sequences (rb_iseq_dup_with_independent_caches) so the new Proc has its own inline method caches. This is required for correctness: the VM caches refinement method resolution per call site assuming a single lexical cref per iseq, so sharing the iseq would leak the refined methods into the original Proc. Coverage keeps measuring the copied iseq. The copy and the cref are memoized in a hidden identity Hash keyed by the source iseq (one slot per iseq, matched by the captured cref and the module arguments, accessed under the VM lock), so repeated calls at the same site reuse one copy and procs refined with the same modules share the iseq and the cref. A differing module set or a ruby2_keywords flag mismatch rebuilds the entry (with a performance warning). The refinements table of the memoized cref is frozen and marked shareable so the memo also serves other Ractors. Procs without an iseq block (Symbol#to_proc, C-function procs), procs created from methods, and procs that already have refinements are rejected with ArgumentError. The YJIT/ZJIT Rust bindings are regenerated for the new rb_proc_t field. Co-Authored-By: Claude Opus 4.8 Co-Authored-By: Claude Fable 5 --- compile.c | 131 ++++- cont.c | 3 +- eval.c | 32 +- eval_intern.h | 13 + internal/eval.h | 1 + internal/vm.h | 2 + iseq.h | 1 + jit.c | 3 +- proc.c | 244 +++++++++- spec/ruby/core/proc/fixtures/refined.rb | 24 + spec/ruby/core/proc/refined_spec.rb | 115 +++++ test/ruby/test_proc.rb | 609 ++++++++++++++++++++++++ thread.c | 10 +- vm.c | 79 ++- vm_args.c | 5 +- vm_core.h | 9 +- vm_eval.c | 21 +- vm_insnhelper.c | 61 ++- yjit/src/cruby_bindings.inc.rs | 16 + zjit/src/cruby_bindings.inc.rs | 38 ++ 20 files changed, 1349 insertions(+), 68 deletions(-) create mode 100644 spec/ruby/core/proc/fixtures/refined.rb create mode 100644 spec/ruby/core/proc/refined_spec.rb diff --git a/compile.c b/compile.c index 2de1d57641cb3e..3888134c7c8402 100644 --- a/compile.c +++ b/compile.c @@ -13722,8 +13722,9 @@ ibf_dump_iseq_each(struct ibf_dump *dump, const rb_iseq_t *iseq) const ibf_offset_t lvar_states_offset = ibf_dump_lvar_states(dump, iseq); const unsigned int catch_table_size = body->catch_table ? body->catch_table->size : 0; const ibf_offset_t catch_table_offset = ibf_dump_catch_table(dump, iseq); - const int parent_iseq_index = ibf_dump_iseq(dump, ISEQ_BODY(iseq)->parent_iseq); - const int local_iseq_index = ibf_dump_iseq(dump, ISEQ_BODY(iseq)->local_iseq); + /* a NULL parent (persisted root) and references outside a Proc#refined subtree dump come out absent (-1) */ + const int parent_iseq_index = ibf_table_lookup(dump->iseq_table, (st_data_t)ISEQ_BODY(iseq)->parent_iseq); + const int local_iseq_index = ibf_table_lookup(dump->iseq_table, (st_data_t)ISEQ_BODY(iseq)->local_iseq); const int mandatory_only_iseq_index = ibf_dump_iseq(dump, ISEQ_BODY(iseq)->mandatory_only_iseq); const ibf_offset_t ci_entries_offset = ibf_dump_ci_entries(dump, iseq); const ibf_offset_t outer_variables_offset = ibf_dump_outer_variables(dump, iseq); @@ -14884,24 +14885,10 @@ ibf_dump_setup(struct ibf_dump *dump, VALUE dumper_obj) dump->current_buffer = &dump->global_buffer; } -VALUE -rb_iseq_ibf_dump(const rb_iseq_t *iseq, VALUE opt) +static VALUE +iseq_ibf_dump_0(struct ibf_dump *dump, const rb_iseq_t *iseq, VALUE opt) { - struct ibf_dump *dump; struct ibf_header header = {{0}}; - VALUE dump_obj; - VALUE str; - - if (ISEQ_BODY(iseq)->parent_iseq != NULL || - ISEQ_BODY(iseq)->local_iseq != iseq) { - rb_raise(rb_eRuntimeError, "should be top of iseq"); - } - if (RTEST(ISEQ_COVERAGE(iseq))) { - rb_raise(rb_eRuntimeError, "should not compile with coverage"); - } - - dump_obj = TypedData_Make_Struct(0, struct ibf_dump, &ibf_dump_type, dump); - ibf_dump_setup(dump, dump_obj); ibf_dump_write(dump, &header, sizeof(header)); ibf_dump_iseq(dump, iseq); @@ -14930,7 +14917,28 @@ rb_iseq_ibf_dump(const rb_iseq_t *iseq, VALUE opt) ibf_dump_overwrite(dump, &header, sizeof(header), 0); - str = dump->global_buffer.str; + return dump->global_buffer.str; +} + +VALUE +rb_iseq_ibf_dump(const rb_iseq_t *iseq, VALUE opt) +{ + struct ibf_dump *dump; + VALUE dump_obj; + VALUE str; + + if (ISEQ_BODY(iseq)->parent_iseq != NULL || + ISEQ_BODY(iseq)->local_iseq != iseq) { + rb_raise(rb_eRuntimeError, "should be top of iseq"); + } + if (RTEST(ISEQ_COVERAGE(iseq))) { + rb_raise(rb_eRuntimeError, "should not compile with coverage"); + } + + dump_obj = TypedData_Make_Struct(0, struct ibf_dump, &ibf_dump_type, dump); + ibf_dump_setup(dump, dump_obj); + + str = iseq_ibf_dump_0(dump, iseq, opt); RB_GC_GUARD(dump_obj); return str; } @@ -15136,6 +15144,91 @@ rb_iseq_ibf_load_bytes(const char *bytes, size_t size) return iseq; } +/* collect the iseq table into an array indexed by iseq-list index */ +static int +ibf_dump_iseq_table_collect_i(st_data_t key, st_data_t val, st_data_t arg) +{ + const rb_iseq_t **srcs = (const rb_iseq_t **)arg; + srcs[(int)val] = (const rb_iseq_t *)key; + return ST_CONTINUE; +} + +/* pc2branchindex (branch coverage) is per-iseq, so pair each copy with its + * source by list index */ +static void +iseq_subtree_copy_pc2branchindex(const struct ibf_dump *dump, const struct ibf_load *load) +{ + unsigned int iseq_count = (unsigned int)dump->iseq_table->num_entries; + VALUE srcs_buf; + const rb_iseq_t **srcs = ALLOCV_N(const rb_iseq_t *, srcs_buf, iseq_count); + st_foreach(dump->iseq_table, ibf_dump_iseq_table_collect_i, (st_data_t)srcs); + + for (unsigned int i = 0; i < iseq_count; i++) { + rb_iseq_t *copy = ibf_load_iseq(load, (const rb_iseq_t *)(VALUE)i); + VALUE pc2branchindex = ISEQ_PC2BRANCHINDEX(srcs[i]); + if (pc2branchindex != Qnil) { + ISEQ_PC2BRANCHINDEX_SET(copy, pc2branchindex); + } + } + + ALLOCV_END(srcs_buf); +} + +/* Deep-copy an iseq subtree with independent inline caches for Proc#refined */ +const rb_iseq_t * +rb_iseq_dup_with_independent_caches(const rb_iseq_t *src_root) +{ + struct ibf_dump *dump; + VALUE dump_obj; + VALUE str; + + /* --- dump the subtree --- */ + dump_obj = TypedData_Make_Struct(0, struct ibf_dump, &ibf_dump_type, dump); + ibf_dump_setup(dump, dump_obj); + + str = iseq_ibf_dump_0(dump, src_root, Qnil); + + /* --- load it back --- */ + struct ibf_load *load; + VALUE loader_obj = TypedData_Make_Struct(0, struct ibf_load, &ibf_load_type, load); + ibf_load_setup(load, loader_obj, str); + + /* What the loader could not reconstruct is the same for every iseq in + * the subtree: the only absent parent is the top's, every absent + * local_iseq is the source's local root (runtime walks compare it + * against the original frames, so a copy must not stand in), and + * pathobj/script_lines/coverage are per-file values. */ + const struct rb_iseq_constant_body *sb = ISEQ_BODY(src_root); + unsigned int iseq_count = (unsigned int)dump->iseq_table->num_entries; + const rb_iseq_t *result = NULL; + for (unsigned int i = 0; i < iseq_count; i++) { + rb_iseq_t *copy = ibf_load_iseq(load, (const rb_iseq_t *)(VALUE)i); + /* force completion even under USE_LAZY_LOAD */ + if (FL_TEST((VALUE)copy, ISEQ_NOT_LOADED_YET)) { + rb_ibf_load_iseq_complete(copy); + } + + struct rb_iseq_constant_body *cb = ISEQ_BODY(copy); + if (!cb->local_iseq) RB_OBJ_WRITE(copy, &cb->local_iseq, sb->local_iseq); + RB_OBJ_WRITE(copy, &cb->location.pathobj, sb->location.pathobj); + RB_OBJ_WRITE(copy, &cb->variable.script_lines, sb->variable.script_lines); + ISEQ_COVERAGE_SET(copy, ISEQ_COVERAGE(src_root)); + + if (i == 0) { + RB_OBJ_WRITE(copy, &cb->parent_iseq, sb->parent_iseq); + result = copy; + } + } + + if (ISEQ_PC2BRANCHINDEX(src_root) != Qnil) { + iseq_subtree_copy_pc2branchindex(dump, load); + } + + RB_GC_GUARD(dump_obj); + RB_GC_GUARD(loader_obj); + return result; +} + VALUE rb_iseq_ibf_load_extra_data(VALUE str) { diff --git a/cont.c b/cont.c index 77ed342e2438f9..2c4fafac8e4c75 100644 --- a/cont.c +++ b/cont.c @@ -2659,7 +2659,8 @@ rb_fiber_start(rb_fiber_t *fiber) th->ec->root_svar = Qfalse; EXEC_EVENT_HOOK(th->ec, RUBY_EVENT_FIBER_SWITCH, th->self, 0, 0, 0, Qnil); - cont->value = rb_vm_invoke_proc(th->ec, proc, argc, argv, cont->kw_splat, VM_BLOCK_HANDLER_NONE); + cont->value = rb_vm_invoke_proc(th->ec, proc, argc, argv, cont->kw_splat, VM_BLOCK_HANDLER_NONE, + rb_proc_refinements_cref(fiber->first_proc)); } EC_POP_TAG(); diff --git a/eval.c b/eval.c index 0d2d2c4089ec09..307762d29229ef 100644 --- a/eval.c +++ b/eval.c @@ -1462,15 +1462,20 @@ using_refinement(VALUE klass, VALUE module, VALUE arg) return ST_CONTINUE; } -static void -using_module_recursive(const rb_cref_t *cref, VALUE klass) +/*! + * \private + * rb_using_module without the refinement method cache flush, for a fresh + * cref that no call site references yet (Proc#refined). + */ +void +rb_using_module_recursive(rb_cref_t *cref, VALUE klass) { ID id_refinements; VALUE super, module, refinements; super = RCLASS_SUPER(klass); if (super) { - using_module_recursive(cref, super); + rb_using_module_recursive(cref, super); } switch (BUILTIN_TYPE(klass)) { case T_MODULE: @@ -1496,10 +1501,10 @@ using_module_recursive(const rb_cref_t *cref, VALUE klass) * \private */ static void -rb_using_module(const rb_cref_t *cref, VALUE module) +rb_using_module(rb_cref_t *cref, VALUE module) { Check_Type(module, T_MODULE); - using_module_recursive(cref, module); + rb_using_module_recursive(cref, module); rb_clear_all_refinement_method_cache(); } @@ -1638,6 +1643,21 @@ ignored_block(VALUE module, const char *klass) rb_warn("%s""using doesn't call the given block""%s.", klass, anon); } +/* Reject `using` anywhere inside a refined proc's body: the procs sharing + * the memoized iseq copy (and its call caches) must all run under the same + * refinement set. */ +static void +check_not_refined_proc_scope(const char *using_name) +{ + const rb_cref_t *cref; + for (cref = rb_vm_cref(); cref; cref = CREF_NEXT(cref)) { + if (CREF_REFINED_PROC(cref)) { + rb_raise(rb_eRuntimeError, + "%s is not permitted in a proc with refinements", using_name); + } + } +} + /* * call-seq: * using(module) -> self @@ -1661,6 +1681,7 @@ mod_using(VALUE self, VALUE module) if (rb_block_given_p()) { ignored_block(module, "Module#"); } + check_not_refined_proc_scope("Module#using"); rb_using_module(rb_vm_cref_replace_with_duplicated_cref(), module); return self; } @@ -2004,6 +2025,7 @@ top_using(VALUE self, VALUE module) if (rb_block_given_p()) { ignored_block(module, "main."); } + check_not_refined_proc_scope("main.using"); rb_using_module(rb_vm_cref_replace_with_duplicated_cref(), module); return self; } diff --git a/eval_intern.h b/eval_intern.h index 5e4249e0b87d6c..d323165107f119 100644 --- a/eval_intern.h +++ b/eval_intern.h @@ -189,6 +189,7 @@ rb_ec_tag_jump(const rb_execution_context_t *ec, enum ruby_tag_type st) #define CREF_FL_OMOD_SHARED IMEMO_FL_USER2 #define CREF_FL_SINGLETON IMEMO_FL_USER3 #define CREF_FL_DYNAMIC_CREF IMEMO_FL_USER4 +#define CREF_FL_REFINED_PROC IMEMO_FL_USER5 static inline int CREF_SINGLETON(const rb_cref_t *cref); @@ -292,6 +293,18 @@ CREF_OMOD_SHARED_UNSET(rb_cref_t *cref) cref->flags &= ~CREF_FL_OMOD_SHARED; } +static inline int +CREF_REFINED_PROC(const rb_cref_t *cref) +{ + return cref->flags & CREF_FL_REFINED_PROC; +} + +static inline void +CREF_REFINED_PROC_SET(rb_cref_t *cref) +{ + cref->flags |= CREF_FL_REFINED_PROC; +} + enum { RAISED_EXCEPTION = 1, RAISED_STACKOVERFLOW = 2, diff --git a/internal/eval.h b/internal/eval.h index 3f6b62d8fe5341..d7f55bffee80de 100644 --- a/internal/eval.h +++ b/internal/eval.h @@ -29,6 +29,7 @@ void rb_class_modify_check(VALUE); NORETURN(VALUE rb_f_raise(int argc, VALUE *argv)); VALUE rb_exception_setup(int argc, VALUE *argv); void rb_refinement_setup(struct rb_refinements_data *data, VALUE module, VALUE klass); +void rb_using_module_recursive(rb_cref_t *cref, VALUE module); VALUE rb_top_main_class(const char *method); VALUE rb_ec_ensure(rb_execution_context_t *ec, VALUE (*b_proc)(VALUE), VALUE data1, VALUE (*e_proc)(VALUE), VALUE data2); diff --git a/internal/vm.h b/internal/vm.h index 4e50ec27106f91..99e956bdc143f2 100644 --- a/internal/vm.h +++ b/internal/vm.h @@ -24,6 +24,7 @@ struct rb_callable_method_entry_struct; /* in method.h */ struct rb_method_definition_struct; /* in method.h */ +struct rb_cref_struct; /* in method.h */ struct rb_execution_context_struct; /* in vm_core.h */ struct rb_control_frame_struct; /* in vm_core.h */ struct rb_callinfo; /* in vm_core.h */ @@ -55,6 +56,7 @@ void rb_vm_pop_cfunc_frame(void); void rb_vm_check_redefinition_by_prepend(VALUE klass); int rb_vm_check_optimizable_mid(VALUE mid); VALUE rb_yield_refine_block(VALUE refinement, VALUE refinements); +struct rb_cref_struct *rb_vm_cref_dup(const struct rb_cref_struct *cref); VALUE ruby_vm_special_exception_copy(VALUE); void rb_lastline_set_up(VALUE val, unsigned int up); diff --git a/iseq.h b/iseq.h index b9bc03c23c80fd..df74ec86e3fdf4 100644 --- a/iseq.h +++ b/iseq.h @@ -189,6 +189,7 @@ void rb_ibf_load_iseq_complete(rb_iseq_t *iseq); const rb_iseq_t *rb_iseq_ibf_load(VALUE str); const rb_iseq_t *rb_iseq_ibf_load_bytes(const char *cstr, size_t); VALUE rb_iseq_ibf_load_extra_data(VALUE str); +const rb_iseq_t *rb_iseq_dup_with_independent_caches(const rb_iseq_t *iseq); void rb_iseq_init_trace(rb_iseq_t *iseq); int rb_iseq_add_local_tracepoint_recursively(const rb_iseq_t *iseq, rb_event_flag_t turnon_events, VALUE tpval, unsigned int target_line, bool target_bmethod); int rb_iseq_remove_local_tracepoint_recursively(const rb_iseq_t *iseq, VALUE tpval, rb_ractor_t *r); diff --git a/jit.c b/jit.c index 377ab3fe16177f..e142ab44c4e45a 100644 --- a/jit.c +++ b/jit.c @@ -234,7 +234,8 @@ rb_optimized_call(VALUE recv, rb_execution_context_t *ec, int argc, VALUE *argv, { rb_proc_t *proc; GetProcPtr(recv, proc); - return rb_vm_invoke_proc(ec, proc, argc, argv, kw_splat, block_handler); + return rb_vm_invoke_proc(ec, proc, argc, argv, kw_splat, block_handler, + rb_proc_refinements_cref(recv)); } unsigned int diff --git a/proc.c b/proc.c index ec08f8ad52de0c..716e4586a2a6b3 100644 --- a/proc.c +++ b/proc.c @@ -19,6 +19,7 @@ #include "internal/object.h" #include "internal/proc.h" #include "internal/symbol.h" +#include "internal/vm.h" #include "method.h" #include "iseq.h" #include "vm_core.h" @@ -267,6 +268,9 @@ block_mark_and_move(struct rb_block *block) } } +/* hidden ivar holding a refined proc's cref; see Proc#refined */ +static ID id_refinements_cref; + static void proc_mark_and_move(void *ptr) { @@ -274,6 +278,24 @@ proc_mark_and_move(void *ptr) block_mark_and_move((struct rb_block *)&proc->block); } +const rb_cref_t * +rb_proc_refinements_cref(VALUE procval) +{ + rb_proc_t *proc; + GetProcPtr(procval, proc); + if (!proc->is_refined) return NULL; + return (const rb_cref_t *)rb_ivar_get(procval, id_refinements_cref); +} + +void +rb_proc_set_refinements_cref(VALUE procval, const rb_cref_t *cref) +{ + rb_proc_t *proc; + GetProcPtr(procval, proc); + rb_ivar_set(procval, id_refinements_cref, (VALUE)cref); + proc->is_refined = 1; +} + typedef struct { rb_proc_t basic; VALUE env[VM_ENV_DATA_SIZE + 1]; /* ..., envval */ @@ -318,7 +340,7 @@ rb_obj_is_proc(VALUE proc) static VALUE proc_clone(VALUE self) { - VALUE procval = rb_proc_dup(self); + VALUE procval = rb_proc_dup_0(self); return rb_obj_clone_setup(self, procval, Qnil); } @@ -326,10 +348,211 @@ proc_clone(VALUE self) static VALUE proc_dup(VALUE self) { - VALUE procval = rb_proc_dup(self); + VALUE procval = rb_proc_dup_0(self); return rb_obj_dup_setup(self, procval); } +rb_cref_t *rb_vm_get_cref(const VALUE *ep); +VALUE rb_proc_dup_with_iseq_and_cref(VALUE self, const rb_iseq_t *iseq, const rb_cref_t *cref); + +/* Proc#refined memoizes the most recent {copied iseq, cref} pair per + * source iseq, since rb_iseq_dup_with_independent_caches is expensive. + * The memo lives in a hidden identity Hash (source iseq -> frozen Array): + * + * [base_cref, copied_iseq, cref, mod1, mod2, ...] + * + * keyed by (base_cref, modules). + * An entry is retained for the VM's lifetime */ + +enum refinement_memo_index { + REFINEMENT_MEMO_BASE_CREF, /* key: captured cref of the source proc */ + REFINEMENT_MEMO_COPIED_ISEQ, /* value: copied iseq with independent caches */ + REFINEMENT_MEMO_CREF, /* value: cref with refinements activated */ + REFINEMENT_MEMO_MODS /* key: modules, in argument order */ +}; + +static VALUE refinement_memo_map; /* set once under the VM lock */ + +static bool +refinement_memo_key_match(VALUE memo, VALUE base_cref, long argc, const VALUE *mods) +{ + const VALUE *p = RARRAY_CONST_PTR(memo); + if (p[REFINEMENT_MEMO_BASE_CREF] != base_cref) return false; + if (RARRAY_LEN(memo) - REFINEMENT_MEMO_MODS != argc) return false; + for (long i = 0; i < argc; i++) { + if (p[REFINEMENT_MEMO_MODS + i] != mods[i]) return false; + } + return true; +} + +static bool +refinement_memo_lookup(const rb_iseq_t *src_iseq, VALUE base_cref, + long argc, const VALUE *mods, + const rb_iseq_t **iseq_out, const rb_cref_t **cref_out) +{ + VM_ASSERT(ISEQ_BODY(src_iseq)->type == ISEQ_TYPE_BLOCK); + VALUE memo = Qnil; + RB_VM_LOCKING() { + if (refinement_memo_map) { + memo = rb_hash_lookup(refinement_memo_map, (VALUE)src_iseq); + } + } + if (!NIL_P(memo)) { + const VALUE *p = RARRAY_CONST_PTR(memo); + if (refinement_memo_key_match(memo, base_cref, argc, mods)) { + const rb_iseq_t *copied_iseq = (const rb_iseq_t *)p[REFINEMENT_MEMO_COPIED_ISEQ]; + if (ISEQ_BODY(copied_iseq)->param.flags.ruby2_keywords == + ISEQ_BODY(src_iseq)->param.flags.ruby2_keywords) { + *iseq_out = copied_iseq; + *cref_out = (const rb_cref_t *)p[REFINEMENT_MEMO_CREF]; + return true; + } + rb_category_warn( + RB_WARN_CATEGORY_PERFORMANCE, + "Proc#refined re-copies the block because the ruby2_keywords flag changed after the copy was memoized" + ); + return false; + } + rb_category_warn( + RB_WARN_CATEGORY_PERFORMANCE, + "Proc#refined called with different modules for the same block disables memoization" + ); + } + return false; +} + +static void +refinement_memo_store(const rb_iseq_t *src_iseq, VALUE base_cref, + long argc, const VALUE *mods, + const rb_iseq_t *copied_iseq, const rb_cref_t *cref) +{ + VM_ASSERT(ISEQ_BODY(src_iseq)->type == ISEQ_TYPE_BLOCK); + + VALUE memo = rb_ary_hidden_new(REFINEMENT_MEMO_MODS + argc); + rb_ary_push(memo, base_cref); + rb_ary_push(memo, (VALUE)copied_iseq); + rb_ary_push(memo, (VALUE)cref); + for (long i = 0; i < argc; i++) { + rb_ary_push(memo, mods[i]); + } + OBJ_FREEZE(memo); + + /* create the map outside the lock; losing the race just discards it */ + VALUE new_map = 0; + if (!refinement_memo_map) { + new_map = rb_obj_hide(rb_ident_hash_new()); + } + + RB_VM_LOCKING() { + if (!refinement_memo_map) { + rb_vm_register_global_object(new_map); + refinement_memo_map = new_map; + } + rb_hash_aset(refinement_memo_map, (VALUE)src_iseq, memo); + } +} + +/* + * call-seq: + * prc.refined(mod, ...) -> a_proc + * + * Returns a new Proc that behaves like the receiver but with the refinements + * activated by the given modules in effect inside its body. The receiver is + * left unchanged. + * + * module StringRefinement + * refine String do + * def shout = upcase + "!" + * end + * end + * + * original = ->(s) { s.shout } + * refined_proc = original.refined(StringRefinement) + * refined_proc.call("hi") #=> "HI!" + * original.call("hi") #=> NoMethodError + * + * Only Procs created from a Ruby block are supported; calling this on a Proc + * backed by a C function, a Symbol, or a method raises ArgumentError. + * + * Calling this method on a Proc that already has refinements applied by this + * method also raises ArgumentError. To activate the refinements of multiple + * modules, pass them all in a single call: + * + * refined_proc = original.refined(StringRefinement, OtherRefinement) + * + * The refinement set of the returned Proc is fixed when it is created: + * calling +using+ inside its body raises RuntimeError. + * + * The refinements are in effect throughout the body, including nested blocks + * and methods defined with +def+ inside it. As with a +def+ inside a +using+ + * scope, such a method keeps the refinements even when it is called later: + * + * refined_proc = ->(s) { + * -> { s.shout }.call # nested block: "HI!" + * }.refined(StringRefinement) + * + * refined_proc = -> { + * obj = Object.new + * def obj.shout_hi = "hi".shout # the method sees the refinement + * obj.shout_hi #=> "HI!" + * }.refined(StringRefinement) + * + * This method copies the instruction sequence of the block and of all of its + * nested blocks so that the copy can resolve methods through the refinements + * without affecting the original Proc. Applying refinements therefore + * increases memory use roughly in proportion to the size of the block. The + * copy is cached and reused for the same receiver and the same modules. + */ +static VALUE +proc_refined(int argc, VALUE *argv, VALUE self) +{ + rb_proc_t *src; + GetProcPtr(self, src); + + rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS); + + if (vm_block_type(&src->block) != block_type_iseq || src->is_from_method) { + rb_raise(rb_eArgError, "can't apply refinements to a Proc without a Ruby block"); + } + + if (src->is_refined) { + rb_raise(rb_eArgError, "can't apply refinements to a Proc that already has refinements"); + } + + for (int i = 0; i < argc; i++) { + Check_Type(argv[i], T_MODULE); + } + + VALUE base_cref = (VALUE)rb_vm_get_cref(src->block.as.captured.ep); + const rb_iseq_t *src_iseq = src->block.as.captured.code.iseq; + + const rb_iseq_t *new_iseq; + const rb_cref_t *new_cref; + if (!refinement_memo_lookup(src_iseq, base_cref, argc, argv, &new_iseq, &new_cref)) { + new_iseq = rb_iseq_dup_with_independent_caches(src_iseq); + rb_cref_t *cref = rb_vm_cref_dup((const rb_cref_t *)base_cref); + /* rb_using_module_recursive modifies shared subclass lists */ + RB_VM_LOCKING() { + for (int i = 0; i < argc; i++) { + rb_using_module_recursive(cref, argv[i]); + } + } + /* Freeze the refinements table and mark it shareable so the memoized + * cref can be reused from any Ractor. */ + VALUE refs = CREF_REFINEMENTS(cref); + if (!NIL_P(refs)) { + OBJ_FREEZE(refs); + RB_OBJ_SET_SHAREABLE(refs); + } + CREF_OMOD_SHARED_SET(cref); + CREF_REFINED_PROC_SET(cref); + new_cref = cref; + refinement_memo_store(src_iseq, base_cref, argc, argv, new_iseq, new_cref); + } + + return rb_proc_dup_with_iseq_and_cref(self, new_iseq, new_cref); +} + /* * call-seq: * prc.lambda? -> true or false @@ -1342,7 +1565,8 @@ rb_proc_call_kw(VALUE self, VALUE args, int kw_splat) VALUE *argv = RARRAY_PTR(args); GetProcPtr(self, proc); vret = rb_vm_invoke_proc(GET_EC(), proc, argc, argv, - kw_splat, VM_BLOCK_HANDLER_NONE); + kw_splat, VM_BLOCK_HANDLER_NONE, + rb_proc_refinements_cref(self)); RB_GC_GUARD(self); RB_GC_GUARD(args); return vret; @@ -1367,7 +1591,8 @@ rb_proc_call_with_block_kw(VALUE self, int argc, const VALUE *argv, VALUE passed VALUE vret; rb_proc_t *proc; GetProcPtr(self, proc); - vret = rb_vm_invoke_proc(ec, proc, argc, argv, kw_splat, proc_to_block_handler(passed_procval)); + vret = rb_vm_invoke_proc(ec, proc, argc, argv, kw_splat, proc_to_block_handler(passed_procval), + rb_proc_refinements_cref(self)); RB_GC_GUARD(self); return vret; } @@ -2695,6 +2920,14 @@ rb_mod_define_method_with_visibility(int argc, VALUE *argv, VALUE mod, const str RB_GC_GUARD(body); } else { + rb_proc_t *body_proc; + GetProcPtr(body, body_proc); + /* A bmethod never reads the refinement cref carried on the proc; + * reject rather than silently drop the refinements. */ + if (body_proc->is_refined) { + rb_raise(rb_eArgError, + "can't define a method from a Proc with refinements"); + } VALUE procval = rb_proc_dup(body); if (vm_proc_iseq(procval) != NULL) { rb_proc_t *proc; @@ -4901,6 +5134,8 @@ void Init_Proc(void) { #undef rb_intern + id_refinements_cref = rb_make_internal_id(); + VALUE mRuby = rb_define_module("Ruby"); /* Ruby::SourceRange */ @@ -4936,6 +5171,7 @@ Init_Proc(void) rb_define_method(rb_cProc, "arity", proc_arity, 0); rb_define_method(rb_cProc, "clone", proc_clone, 0); rb_define_method(rb_cProc, "dup", proc_dup, 0); + rb_define_method(rb_cProc, "refined", proc_refined, -1); rb_define_method(rb_cProc, "hash", proc_hash, 0); rb_define_method(rb_cProc, "to_s", proc_to_s, 0); rb_define_alias(rb_cProc, "inspect", "to_s"); diff --git a/spec/ruby/core/proc/fixtures/refined.rb b/spec/ruby/core/proc/fixtures/refined.rb new file mode 100644 index 00000000000000..014e8695fdd633 --- /dev/null +++ b/spec/ruby/core/proc/fixtures/refined.rb @@ -0,0 +1,24 @@ +module ProcRefinedSpecs + module StringShout + refine String do + def shout + upcase + "!" + end + end + end + + # Refines the same String#shout as StringShout, plus its own #quiet, so + # specs can observe both which module wins for a conflicting method and + # that all given modules are activated. + module StringQuiet + refine String do + def shout + downcase + end + + def quiet + "..." + end + end + end +end diff --git a/spec/ruby/core/proc/refined_spec.rb b/spec/ruby/core/proc/refined_spec.rb new file mode 100644 index 00000000000000..8e232e52b1c3c7 --- /dev/null +++ b/spec/ruby/core/proc/refined_spec.rb @@ -0,0 +1,115 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/refined' + +ruby_version_is "4.1" do + describe "Proc#refined" do + it "returns a new Proc with the refinements of the given module activated inside its body" do + pr = -> s { s.shout } + refined = pr.refined(ProcRefinedSpecs::StringShout) + refined.should be_an_instance_of(Proc) + refined.should_not equal(pr) + refined.call("hi").should == "HI!" + end + + it "does not change the receiver" do + pr = -> s { s.shout } + pr.refined(ProcRefinedSpecs::StringShout) + -> { pr.call("hi") }.should.raise(NoMethodError) + end + + it "activates the refinements of all the given modules" do + pr = -> s { [s.shout, s.quiet] } + refined = pr.refined(ProcRefinedSpecs::StringShout, ProcRefinedSpecs::StringQuiet) + refined.call("Hi").should == ["hi", "..."] + end + + it "gives precedence to the module applied last for the same refined method, as with nested using" do + pr = -> s { s.shout } + pr.refined(ProcRefinedSpecs::StringShout, ProcRefinedSpecs::StringQuiet).call("Hi").should == "hi" + pr.refined(ProcRefinedSpecs::StringQuiet, ProcRefinedSpecs::StringShout).call("Hi").should == "HI!" + end + + it "shares the closure environment with the receiver" do + counter = 0 + pr = -> { counter += 1 } + refined = pr.refined(ProcRefinedSpecs::StringShout) + refined.call + pr.call + counter.should == 2 + end + + it "preserves lambda-ness and arity" do + l = -> s { s }.refined(ProcRefinedSpecs::StringShout) + l.should.lambda? + l.arity.should == 1 + + pr = proc { |s| s }.refined(ProcRefinedSpecs::StringShout) + pr.should_not.lambda? + end + + it "keeps the refinements active in blocks nested inside the body" do + pr = -> a { a.map { |s| s.shout } } + pr.refined(ProcRefinedSpecs::StringShout).call(%w[a b]).should == ["A!", "B!"] + end + + it "keeps the refinements active in methods defined inside the body" do + pr = -> { + obj = Object.new + def obj.shout_hi + "hi".shout + end + obj.shout_hi + } + pr.refined(ProcRefinedSpecs::StringShout).call.should == "HI!" + end + + it "keeps the refinements active when called via instance_eval, instance_exec and class_eval" do + pr = proc { "hi".shout } + refined = pr.refined(ProcRefinedSpecs::StringShout) + Object.new.instance_eval(&refined).should == "HI!" + Object.new.instance_exec(&refined).should == "HI!" + Class.new.class_eval(&refined).should == "HI!" + end + + it "raises ArgumentError when called with no modules" do + -> { -> {}.refined }.should.raise(ArgumentError) + end + + it "raises TypeError when called with a non-Module argument" do + -> { -> {}.refined(42) }.should.raise(TypeError) + -> { -> {}.refined(String) }.should.raise(TypeError) + end + + it "raises ArgumentError for a Proc not created from a Ruby block" do + -> { :upcase.to_proc.refined(ProcRefinedSpecs::StringShout) }.should.raise(ArgumentError) + method_proc = "hi".method(:upcase).to_proc + -> { method_proc.refined(ProcRefinedSpecs::StringShout) }.should.raise(ArgumentError) + end + + it "raises ArgumentError for a Proc that already has refinements applied" do + refined = -> s { s.shout }.refined(ProcRefinedSpecs::StringShout) + -> { refined.refined(ProcRefinedSpecs::StringQuiet) }.should.raise(ArgumentError) + end + + it "keeps the refinements on dup and clone" do + refined = -> s { s.shout }.refined(ProcRefinedSpecs::StringShout) + refined.dup.call("hi").should == "HI!" + refined.clone.call("hi").should == "HI!" + -> { refined.dup.refined(ProcRefinedSpecs::StringQuiet) }.should.raise(ArgumentError) + -> { refined.clone.refined(ProcRefinedSpecs::StringQuiet) }.should.raise(ArgumentError) + end + + it "raises ArgumentError when the result is passed to define_method" do + refined = -> s { s.shout }.refined(ProcRefinedSpecs::StringShout) + -> { Class.new { define_method(:m, refined) } }.should.raise(ArgumentError) + -> { Object.new.define_singleton_method(:m, refined) }.should.raise(ArgumentError) + end + + it "raises RuntimeError when the body calls using" do + mod = Module.new { refine(String) { def whisper; downcase; end } } + pr = proc { using mod } + refined = pr.refined(ProcRefinedSpecs::StringShout) + -> { Module.new.module_eval(&refined) }.should.raise(RuntimeError) + end + end +end diff --git a/test/ruby/test_proc.rb b/test/ruby/test_proc.rb index eefd5b189b8943..1caf8a41032775 100644 --- a/test/ruby/test_proc.rb +++ b/test/ruby/test_proc.rb @@ -500,6 +500,615 @@ def test_dup_ifunc_proc_bug_20950 RUBY end + module RefinementsModule + refine String do + def shout = upcase + "!" + end + refine Integer do + def tripled = self * 3 + end + end + + def test_refined + orig = ->(s) { s.shout } + refined = orig.refined(RefinementsModule) + assert_equal("HI!", refined.call("hi")) + # the original Proc is unaffected + assert_raise(NoMethodError) { orig.call("hi") } + # idempotent: calling repeatedly keeps using the refinement + assert_equal("HI!", refined.call("hi")) + end + + def test_refined_nested_block + orig = ->(a) { a.map { |s| s.shout } } + refined = orig.refined(RefinementsModule) + assert_equal(["A!", "B!"], refined.call(["a", "b"])) + assert_raise(NoMethodError) { orig.call(["a"]) } + end + + def test_refined_multiple_modules + refined = ->(s, n) { "#{s.shout}#{n.tripled}" }.refined(RefinementsModule) + assert_equal("A!6", refined.call("a", 2)) + end + + def test_refined_shares_environment + counter = 0 + inc = -> { counter += 1 } + refined = inc.refined(RefinementsModule) + refined.call + inc.call + # the closure environment is shared with the original Proc + assert_equal(2, counter) + end + + def test_refined_via_yield + refined = ->(s) { s.shout }.refined(RefinementsModule) + result = [].tap {|a| [1, 2].each {|i| a << refined.call("x#{i}") } } + assert_equal(["X1!", "X2!"], result) + + forwarded = [] + rr = ->(s) { forwarded << s.shout }.refined(RefinementsModule) + %w[p q].each(&rr) + assert_equal(["P!", "Q!"], forwarded) + end + + def test_refined_via_c_call_paths + refined = ->(s) { s.shout }.refined(RefinementsModule) + # These reach the proc via the C invocation path (rb_vm_invoke_proc) rather + # than the optimized opt_call, so the refinement cref must be carried there. + assert_equal("A!", refined.send(:call, "a")) + assert_equal("B!", refined.method(:call).call("b")) + assert_equal("C!", Fiber.new(&refined).resume("c")) + assert_equal("D!", Thread.new("d", &refined).value) + end + + def test_refined_instance_eval + refined = proc { self.shout }.refined(RefinementsModule) + assert_equal("HI!", "hi".instance_eval(&refined)) + assert_equal("HI!", "hi".instance_exec(&refined)) + # the original Proc is still unaffected via instance_eval + orig = proc { self.shout } + assert_raise(NoMethodError) { "hi".instance_eval(&orig) } + end + + def test_refined_module_eval + refined = proc { "ok".shout }.refined(RefinementsModule) + klass = Class.new + assert_equal("OK!", klass.class_eval(&refined)) + end + + def test_refined_instance_eval_does_not_leak_refinements + # instance_eval/class_eval gets its own copy of the refinements hash, so a + # `using` inside must not mutate the cref shared by every derived proc. + refined = proc { "ok".shout }.refined(RefinementsModule) + Class.new.class_eval(&refined) + # a second proc derived from the same source iseq still sees the refinement + again = proc { "ok".shout }.refined(RefinementsModule) + assert_equal("OK!", Class.new.class_eval(&again)) + end + + def test_refined_once_regexp + # A /o (once) regexp literal interpolating a refined-method call must be + # built under the refinement, and the copy's once cache is independent of + # the source iseq's (which may be mid-flight or already completed). + refined = ->(s) { /\A#{s.shout}\z/o }.refined(RefinementsModule) + r1 = refined.call("ab") + assert_equal('\AAB!\z', r1.source) + # /o caches the first regexp on the copy's own once entry + assert_same(r1, refined.call("zz")) + # the original proc has no refinement, so building the regexp raises + assert_raise(NoMethodError) { ->(s) { /\A#{s.shout}\z/o }.call("ab") } + end + + def test_refined_preserved_by_dup + refined = ->(s) { s.shout }.refined(RefinementsModule) + # dup/clone copy the refinement cref (a hidden ivar) with the other ivars + assert_equal("Z!", refined.dup.call("z")) + assert_equal("Z!", refined.clone.call("z")) + end + + def test_refined_errors + assert_raise(ArgumentError) { ->(s) { s }.refined } + assert_raise(TypeError) { ->(s) { s }.refined(42) } + # non-iseq Procs are not supported + assert_raise(ArgumentError) { :upcase.to_proc.refined(RefinementsModule) } + assert_raise(ArgumentError) { method(:p).to_proc.refined(RefinementsModule) } + end + + def test_refined_non_main_ractor + assert_separately([], <<~'RUBY') + Warning[:experimental] = false + module M1; refine(String) { def shout = upcase + "!" }; end + module M2; refine(String) { def shout = downcase }; end + ractors = 10.times.map { |i| + Ractor.new(i) { |i| + ->(s) { s.shout }.refined(i.even? ? M1 : M2).call("Hi") + } + } + values = ractors.map(&:value) + assert_equal(["HI!", "hi"] * 5, values) + RUBY + end + + def test_refined_shareable_proc_across_ractors + assert_separately([], <<~'RUBY') + Warning[:experimental] = false + module RefMod; refine(String) { def shout = upcase + "!" }; end + module RefHolder + # module body: self is shareable, as make_shareable requires + PROC = Ractor.make_shareable(->(s) { s.shout }) + end + orig = RefHolder::PROC + # Store a memo in the main Ractor first, then refine the same shareable + # proc in another Ractor: cross-Ractor reuse of the memo is legal + # because the memoized cref's refinements table is frozen and shareable. + assert_equal("HI!", orig.refined(RefMod).call("hi")) + r = Ractor.new(orig) { |pr| pr.refined(RefMod).call("hi") } + assert_equal("HI!", r.value) + assert_equal("HI!", orig.refined(RefMod).call("hi")) + RUBY + end + + def test_refined_coverage + assert_separately(%w[-rcoverage -rtempfile], <<~'RUBY') + f = Tempfile.open(["refined_coverage", ".rb"]) + f.write(<<~'FIXTURE') + module RefinedCoverageExt + refine String do + def shout = upcase + "!" + end + end + REFINED_COVERAGE_PROC = ->(s) { + a = s.length + s.shout * a + } + FIXTURE + f.close + Coverage.start(lines: true) + load f.path + refined = REFINED_COVERAGE_PROC.refined(RefinedCoverageExt) + assert_equal("HI!HI!", refined.call("hi")) + lines = Coverage.result[f.path][:lines] + assert_equal(1, lines[6], "line 7 (a = s.length) must be counted when run through the refined copy") + assert_equal(1, lines[7], "line 8 (s.shout * a) must be counted when run through the refined copy") + RUBY + end + + def test_refined_chain_rejected + # Chaining would need merge-or-replace semantics for the refinement sets; + # both are confusing, so a refined proc rejects further refined. + # Multiple modules can be activated by passing them in a single call. + refined = ->(s) { s.shout }.refined(RefinementsModule) + assert_raise(ArgumentError) { refined.refined(RefinementsModule2) } + # the refinement state survives dup, so the dup is rejected too + assert_raise(ArgumentError) { refined.dup.refined(RefinementsModule2) } + # the receiver remains usable + assert_equal("HI!", refined.call("hi")) + end + + def test_refined_using_in_body_rejected + # The refinement set of a refined proc is fixed at refined() time: procs + # derived from the same source and modules share the copied iseq (and its + # refined call caches), so `using` inside the body would diverge one + # proc's refinement set and poison the caches its siblings use. + assert_separately([], <<~'RUBY') + module M1; refine(String) { def shout = upcase + "!" }; end + module M2; refine(String) { def whisper = downcase + "..." }; end + msg = /using is not permitted in a proc with refinements/ + r = proc { using M2 }.refined(M1) + assert_raise_with_message(RuntimeError, msg) { r.call } + # through module_eval (Module#using) as well + e = proc { using M2 }.refined(M1) + assert_raise_with_message(RuntimeError, msg) { Module.new.module_eval(&e) } + # in a class body or a nested block inside the refined proc + c = proc { class ::RefinedUsingTmp; using M2; end }.refined(M1) + assert_raise_with_message(RuntimeError, msg) { c.call } + n = proc { -> { using M2 }.call }.refined(M1) + assert_raise_with_message(RuntimeError, msg) { n.call } + # plain procs are unaffected + assert_equal("ok...", Module.new.module_eval(&proc { using M2; "ok".whisper })) + # and the refined proc itself still works + assert_equal("OK!", proc { "ok".shout }.refined(M1).call) + RUBY + end + + def test_refined_rejected_by_define_method + # A bmethod is invoked against its method entry, not the proc's refinement + # cref, so defining a method from a refined proc would silently drop + # the refinements; it is rejected instead. + refined = ->(s) { s.shout }.refined(RefinementsModule) + assert_raise(ArgumentError) { Class.new { define_method(:m, refined) } } + assert_raise(ArgumentError) { Class.new { define_method(:m, &refined) } } + assert_raise(ArgumentError) { Object.new.define_singleton_method(:m, refined) } + end + + # Each refinement module refines only one class so the nested test below can + # tell apart the refinement added on the inner Proc (String) from the one + # inherited from the enclosing refined Proc (Integer). + module RefinementsStringOnly + refine String do + def shout = upcase + "!" + end + end + + module RefinementsIntegerOnly + refine Integer do + def doubled = self * 2 + end + end + + def test_refined_nested_proc_is_not_a_chain + # A Proc created lexically INSIDE a refined Proc is not itself "a + # Proc that already has refinements": it only inherits the enclosing + # refinements lexically. refined (and define_method) must therefore + # be accepted on it, and the inner Proc must see both the enclosing + # refinement and the one it adds. Only the Proc returned by refined + # is rejected for chaining. + result = -> { + inner = ->(s, n) { [s.shout, n.doubled] } + inner.refined(RefinementsStringOnly).call("hi", 3) + }.refined(RefinementsIntegerOnly).call + # RefinementsStringOnly is added on the inner copy; RefinementsIntegerOnly + # is inherited from the enclosing refined Proc. + assert_equal(["HI!", 6], result) + + # define_method from such a nested Proc is allowed (it is not the + # refined result itself). + assert_nothing_raised do + -> { Class.new { define_method(:m, ->(s) { s }) } }.refined(RefinementsIntegerOnly).call + end + end + + def test_refined_gc + assert_normal_exit(<<~RUBY) + module M + refine(String) { def shout = upcase + "!" } + end + procs = 100.times.map { |i| ->(s) { s.shout } .refined(M) } + GC.start + GC.compact rescue nil + procs.each { |pr| raise "bad" unless pr.call("a") == "A!" } + RUBY + end + + def test_refined_gc_stress + # Under GC.stress, the memo store allocates its module array while the memo + # is reachable from the iseq; a GC during that allocation must not observe a + # half-initialized memo (argc set but mods not yet allocated). Alternating + # module sets keeps missing the memo so each call re-runs the store; one + # small iseq keeps it cheap enough for slow CI runners. + assert_normal_exit(<<~RUBY) + module M1; refine(String) { def shout = upcase + "!" }; end + module M2; refine(String) { def shout = downcase }; end + orig = ->(s) { s.shout } + r = nil + GC.stress = true + 6.times { |i| r = orig.refined(i.even? ? M1 : M2) } + GC.stress = false + raise "bad" unless r.call("Hi") == "hi" + RUBY + end + + def test_refined_iseq_to_binary + # A block iseq that has memoized copies must still serialize cleanly + # (the memo lives outside the iseq). + assert_separately([], <<~'RUBY') + module M + refine(String) { def shout = upcase + "!" } + end + src = 'x = ->(s) { s.shout }; x.refined(M).call("hi")' + iseq = RubyVM::InstructionSequence.compile(src) + eval(src) # runs refined, setting the memo on the block iseq + bin = iseq.to_binary + loaded = RubyVM::InstructionSequence.load_from_binary(bin) + assert_equal("HI!", loaded.eval) + RUBY + end + + module RefinementsModule2 + refine String do + def shout = "?" + end + end + + def test_refined_memoized + orig = ->(s) { s.shout } + # Repeating the same (proc, modules) reuses the cached copy but stays correct. + assert_equal("HI!", orig.refined(RefinementsModule).call("hi")) + assert_equal("HI!", orig.refined(RefinementsModule).call("hi")) + # A different module set must not return the previously cached copy. + assert_equal("?", orig.refined(RefinementsModule2).call("hi")) + # ...and switching back is still correct. + assert_equal("HI!", orig.refined(RefinementsModule).call("hi")) + end + + def test_refined_ruby2_keywords_memo + # Proc#ruby2_keywords marks the shared block iseq, possibly after a copy + # was memoized. The stale memo is rebuilt (with a warning naming the + # cause) rather than reused or mutated: the new proc delegates keywords + # like its source, while procs built before the mark keep their + # creation-time behavior. + assert_separately([], <<~'RUBY') + module M; refine(String) { def shout = upcase + "!" }; end + Warning[:performance] = true + $warned = [] + def Warning.warn(msg, category: nil) = $warned << msg + target = ->(a, k: nil) { [a, k] } + pr = proc { |*args| target.call(*args) } + q1 = pr.refined(M) # memoize a copy before the mark + pr.ruby2_keywords + assert_equal([1, 2], pr.call(1, k: 2)) + q2 = pr.refined(M) + assert_equal([1, 2], q2.call(1, k: 2)) + assert_equal(1, $warned.grep(/ruby2_keywords/).size) + # the copy made before the mark is not retroactively changed + assert_raise(ArgumentError) { q1.call(1, k: 2) } + # the rebuilt memo is hit from now on; no further warnings + assert_equal([1, 2], pr.refined(M).call(1, k: 2)) + assert_equal(1, $warned.grep(/ruby2_keywords/).size) + RUBY + end + + def test_refined_memo_distinct_environments + # Procs sharing the same block iseq but capturing different closure + # environments hit the same memo entry (env is not part of the key), yet each + # result must keep its own environment. + factory = ->(tag) { ->(s) { "#{tag}:#{s.shout}" } } + p1 = factory.call("A") + p2 = factory.call("B") + r1 = p1.refined(RefinementsModule) + r2 = p2.refined(RefinementsModule) + assert_equal("A:X!", r1.call("x")) + assert_equal("B:Y!", r2.call("y")) + # the original closures still see their own captured tag too + assert_equal("A:X!", r1.call("x")) + end + + def test_refined_memo_avoids_recopy + orig = ->(s) { s.shout } + orig.refined(RefinementsModule) # warm the memo + GC.disable + begin + before = GC.stat(:total_allocated_objects) + 100.times { orig.refined(RefinementsModule) } + hits = GC.stat(:total_allocated_objects) - before + + before = GC.stat(:total_allocated_objects) + 100.times do |i| + orig.refined(i.even? ? RefinementsModule : RefinementsModule2) + end + misses = GC.stat(:total_allocated_objects) - before + ensure + GC.enable + end + # Memo hits must allocate far less than recomputing the iseq copy each time. + assert_operator(misses, :>, hits * 2, + "expected memo hits (#{hits}) to allocate much less than misses (#{misses})") + end + + def test_refined_memo_different_modules_warning + assert_separately([], <<~'RUBY') + module M1; refine(String) { def shout = "1" }; end + module M2; refine(String) { def shout = "2" }; end + Warning[:performance] = true + $warned = [] + def Warning.warn(msg, category: nil) = $warned << msg + pr = ->(s) { s.shout } + pr.refined(M1) + pr.refined(M2) # evicts the M1 entry + assert_equal(1, $warned.grep(/different modules/).size) + RUBY + end + + def test_refined_memo_survives_gc + assert_separately([], <<~'RUBY') + module M; refine(String) { def shout = upcase + "!" }; end + pr = ->(s) { [1].each { }; s.shout } + # the memo lives as long as its source iseq, so the copy stays + # memoized even when no refined proc survives the GC + pr.refined(M) + GC.start + assert_equal("HI!", pr.refined(M).call("hi")) + rp = pr.refined(M) + GC.start + assert_equal("HI!", rp.call("hi")) + assert_equal("HI!", pr.refined(M).call("hi")) + RUBY + end + + def test_refined_memo_gc_compact + assert_separately([], <<~'RUBY') + module M; refine(String) { def shout = upcase + "!" }; end + pr = ->(s) { [1].each { }; s.shout } + rp = pr.refined(M) + begin + GC.compact + rescue NotImplementedError + omit "GC compaction not supported on this platform" + end + assert_equal("HI!", rp.call("hi")) + assert_equal("HI!", pr.refined(M).call("hi")) + RUBY + end + + def test_refined_memo_survives_compaction + # the memo is a hidden identity Hash: its keys (source iseqs) are pinned, + # while its values (copied iseqs, crefs) move and must be updated correctly + assert_separately([], <<~'RUBY') + omit "compaction unsupported" unless GC.respond_to?(:verify_compaction_references) + module M; refine(String) { def shout = upcase + "!" }; end + blocks = eval("[" + (["->(s) { s.shout }"] * 500).join(",\n") + "]") + kept = blocks.map { |b| b.refined(M) } + begin + GC.verify_compaction_references(expand_heap: true, toward: :empty) + rescue NotImplementedError + omit "compaction unsupported on this platform" + end + kept.each { |r| assert_equal("HI!", r.call("hi")) } + blocks.each { |b| assert_equal("HI!", b.refined(M).call("hi")) } + GC.compact + blocks.each { |b| assert_equal("HI!", b.refined(M).call("hi")) } + RUBY + end + + def test_refined_rescue_ensure + # exercises the copied catch table and shared rescue local table + refined = ->(s) { + r = nil + begin + r = s.shout + rescue NoMethodError + r = "rescued" + ensure + r = "#{r}." + end + r + }.refined(RefinementsModule) + assert_equal("YO!.", refined.call("yo")) + end + + def test_refined_def_in_block + # a literal def inside the block creates a nested method iseq whose + # local_iseq is itself (in-subtree); the copy must rebuild it. + # Specified behavior: like a def inside a `using` scope, a method defined + # inside the block sees the refinements (the def captures the block's + # cref), so the refinement also applies when the method is called later. + refined = ->(s) { + o = Object.new + def o.m = "hi".shout + [s.shout, o.m] + }.refined(RefinementsModule) + assert_equal(["YO!", "HI!"], refined.call("yo")) + end + + def test_refined_keyword_and_optional_args + refined = ->(a, b = "z", c:, d: "w") { + [a, b, c, d].map(&:shout).join + }.refined(RefinementsModule) + assert_equal("A!Z!C!W!", refined.call("a", c: "c")) + assert_equal("A!B!C!D!", refined.call("a", "b", c: "c", d: "d")) + end + + def test_refined_case_when_literal + # literal when-clauses compile to a CDHASH operand that must round-trip + refined = ->(s) { + case s.shout + when "A!" then 1 + when "B!" then 2 + else 0 + end + }.refined(RefinementsModule) + assert_equal(1, refined.call("a")) + assert_equal(2, refined.call("b")) + assert_equal(0, refined.call("c")) + end + + def test_refined_flip_flop + # flip-flop allocates a special-variable slot keyed off the local iseq; + # the copy must run its own flip state independent of the original + body = ->(arr) { + out = [] + arr.each { |i| out << i if (i == 2)..(i == 4) } + out + } + refined = body.refined(RefinementsModule) + assert_equal([2, 3, 4], refined.call([1, 2, 3, 4, 5])) + # a second call starts from a fresh flip state + assert_equal([2, 3, 4], refined.call([1, 2, 3, 4, 5])) + end + + def test_refined_preserves_location_and_parameters + orig = ->(a, b = 1, *c, d:, **e, &f) { a } + refined = orig.refined(RefinementsModule) + assert_equal(orig.source_location, refined.source_location) + assert_equal(orig.parameters, refined.parameters) + assert_equal(orig.arity, refined.arity) + end + + module RefinementsOperators + refine Integer do + def +(other) = "plus(#{self},#{other})" + def <(other) = "lt" + end + refine Array do + def [](i) = "at#{i}" + end + end + + def test_refined_operators + # Specialized instructions (opt_plus, opt_lt, opt_aref, ...) must honor the + # refinement on the copy without leaking it into the original Proc. + refined = ->(a, b) { [a + b, a < b] }.refined(RefinementsOperators) + assert_equal(["plus(1,2)", "lt"], refined.call(1, 2)) + aref = ->(a) { a[0] }.refined(RefinementsOperators) + assert_equal("at0", aref.call([9])) + # the original Procs keep using the builtin operators + assert_equal(3, ->(a, b) { a + b }.call(1, 2)) + end + + def test_refined_preserves_lambda + # The copy must keep the receiver's lambda/proc nature, including a lambda's + # strict argument checking. + lam = ->(a, b) { [a, b] }.refined(RefinementsModule) + assert_equal(true, lam.lambda?) + assert_raise(ArgumentError) { lam.call(1) } + pr = proc { |a, b| [a, b] }.refined(RefinementsModule) + assert_equal(false, pr.lambda?) + # proc argument handling fills missing parameters with nil instead of raising + assert_equal([1, nil], pr.call(1)) + end + + def test_refined_preserved_by_clone + refined = ->(s) { s.shout }.refined(RefinementsModule) + assert_equal("Z!", refined.clone.call("z")) + # the refinement state survives clone, so chaining on the clone is rejected too + assert_raise(ArgumentError) { refined.clone.refined(RefinementsModule2) } + end + + def test_refined_module_precedence + # When several modules refine the same method, the last one wins, matching + # the precedence of nested `using`. + body = ->(s) { s.shout } + assert_equal("?", body.refined(RefinementsModule, RefinementsModule2).call("Hi")) + assert_equal("HI!", body.refined(RefinementsModule2, RefinementsModule).call("Hi")) + end + + class RefinementsSuperBase + def greet = "base" + end + + module RefinementsSuperModule + refine RefinementsSuperBase do + def greet = "ref-" + super + end + end + + def test_refined_super + # A refined method may call super to reach the method it refines. + refined = ->(o) { o.greet }.refined(RefinementsSuperModule) + assert_equal("ref-base", refined.call(RefinementsSuperBase.new)) + end + + def test_refined_tracepoint + # Line events fire on the copied iseq just like on the original. + src = "->(s) {\n x = s.shout\n x\n}" + refined = eval(src, binding, "wr_trace_eval").refined(RefinementsModule) + lines = [] + tp = TracePoint.new(:line) { |t| lines << t.lineno if t.path == "wr_trace_eval" } + result = tp.enable { refined.call("hi") } + assert_equal("HI!", result) + assert_equal([2, 3], lines) + end + + def test_refined_recursion_sees_refinements + # The copy shares the source Proc's environment, so a recursive call through + # the captured local reaches the refined Proc and keeps the refinements. + fact = nil + fact = ->(s) { s.empty? ? "" : s[0].shout + fact.call(s[1..]) } + .refined(RefinementsModule) + assert_equal("A!B!C!", fact.call("abc")) + end + def test_clone_subclass c1 = Class.new(Proc) assert_equal c1, c1.new{}.clone.class, '[Bug #17545]' diff --git a/thread.c b/thread.c index 3607b6c4e333df..e9c67aa7793449 100644 --- a/thread.c +++ b/thread.c @@ -589,7 +589,8 @@ rb_vm_proc_local_ep(VALUE proc) // for ractor, defined in vm.c VALUE rb_vm_invoke_proc_with_self(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, - int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler); + int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler, + const rb_cref_t *cref); static VALUE thread_do_start_proc(rb_thread_t *th) @@ -600,6 +601,7 @@ thread_do_start_proc(rb_thread_t *th) VALUE procval = th->invoke_arg.proc.proc; rb_proc_t *proc; GetProcPtr(procval, proc); + const rb_cref_t *cref = rb_proc_refinements_cref(procval); th->ec->errinfo = Qnil; th->ec->root_lep = rb_vm_proc_local_ep(procval); @@ -621,7 +623,8 @@ thread_do_start_proc(rb_thread_t *th) th->ec, proc, self, args_len, args_ptr, th->invoke_arg.proc.kw_splat, - VM_BLOCK_HANDLER_NONE + VM_BLOCK_HANDLER_NONE, + cref ); } else { @@ -642,7 +645,8 @@ thread_do_start_proc(rb_thread_t *th) th->ec, proc, args_len, args_ptr, th->invoke_arg.proc.kw_splat, - VM_BLOCK_HANDLER_NONE + VM_BLOCK_HANDLER_NONE, + cref ); } } diff --git a/vm.c b/vm.c index 65edfce546c4a8..df24fa3c300dc8 100644 --- a/vm.c +++ b/vm.c @@ -360,8 +360,8 @@ ref_delete_symkey(VALUE key, VALUE value, VALUE unused) return SYMBOL_P(key) ? ST_DELETE : ST_CONTINUE; } -static rb_cref_t * -vm_cref_dup(const rb_cref_t *cref) +rb_cref_t * +rb_vm_cref_dup(const rb_cref_t *cref) { const rb_scope_visibility_t *visi = CREF_SCOPE_VISI(cref); rb_cref_t *next_cref = CREF_NEXT(cref), *new_cref; @@ -380,7 +380,6 @@ vm_cref_dup(const rb_cref_t *cref) return new_cref; } - rb_cref_t * rb_vm_cref_dup_without_refinements(const rb_cref_t *cref) { @@ -456,7 +455,7 @@ static VALUE vm_make_env_object(const rb_execution_context_t *ec, rb_control_fra static VALUE vm_invoke_bmethod(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE block_handler, const rb_callable_method_entry_t *me); -static VALUE vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE block_handler); +static VALUE vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE block_handler, const rb_cref_t *cref); #if USE_YJIT // Counter to serve as a proxy for execution time, total number of calls @@ -1341,7 +1340,7 @@ proc_create(VALUE klass, const struct rb_block *block, int8_t is_from_method, in } VALUE -rb_proc_dup(VALUE self) +rb_proc_dup_0(VALUE self) { VALUE procval; rb_proc_t *src; @@ -1357,11 +1356,45 @@ rb_proc_dup(VALUE self) break; } + if (src->is_refined) { + rb_proc_t *dst; + GetProcPtr(procval, dst); + dst->is_refined = 1; + } + if (RB_OBJ_SHAREABLE_P(self)) RB_OBJ_SET_SHAREABLE(procval); RB_GC_GUARD(self); /* for: body = rb_proc_dup(body) */ return procval; } +VALUE +rb_proc_dup(VALUE self) +{ + VALUE procval = rb_proc_dup_0(self); + const rb_cref_t *cref = rb_proc_refinements_cref(self); + if (cref) rb_proc_set_refinements_cref(procval, cref); + return procval; +} + +/* Proc#refined: build a Proc that runs `iseq` (a copy of self's block iseq) + * with `cref` as its refinement cref, sharing self's environment. */ +VALUE +rb_proc_dup_with_iseq_and_cref(VALUE self, const rb_iseq_t *iseq, const rb_cref_t *cref) +{ + rb_proc_t *src; + GetProcPtr(self, src); + VM_ASSERT(vm_block_type(&src->block) == block_type_iseq); + + struct rb_block block = src->block; + block.as.captured.code.iseq = iseq; + + VALUE procval = proc_create(rb_obj_class(self), &block, src->is_from_method, src->is_lambda); + rb_proc_set_refinements_cref(procval, cref); + + RB_GC_GUARD(self); + return procval; +} + struct collect_outer_variable_name_data { VALUE ary; VALUE read_only; @@ -1844,11 +1877,17 @@ invoke_block_from_c_bh(rb_execution_context_t *ec, VALUE block_handler, return vm_yield_with_symbol(ec, VM_BH_TO_SYMBOL(block_handler), argc, argv, kw_splat, passed_block_handler); case block_handler_type_proc: - if (force_blockarg == FALSE) { - is_lambda = block_proc_is_lambda(VM_BH_TO_PROC(block_handler)); + { + VALUE procval = VM_BH_TO_PROC(block_handler); + rb_proc_t *po; + GetProcPtr(procval, po); + if (po->is_refined) cref = rb_proc_refinements_cref(procval); + if (force_blockarg == FALSE) { + is_lambda = po->is_lambda; + } + block_handler = vm_block_to_block_handler(&po->block); + goto again; } - block_handler = vm_proc_to_block_handler(VM_BH_TO_PROC(block_handler)); - goto again; } VM_UNREACHABLE(invoke_block_from_c_splattable); return Qundef; @@ -1899,12 +1938,14 @@ ALWAYS_INLINE(static VALUE invoke_block_from_c_proc(rb_execution_context_t *ec, const rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler, int is_lambda, + const rb_cref_t *cref, const rb_callable_method_entry_t *me)); static inline VALUE invoke_block_from_c_proc(rb_execution_context_t *ec, const rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler, int is_lambda, + const rb_cref_t *cref, const rb_callable_method_entry_t *me) { const struct rb_block *block = &proc->block; @@ -1912,7 +1953,7 @@ invoke_block_from_c_proc(rb_execution_context_t *ec, const rb_proc_t *proc, again: switch (vm_block_type(block)) { case block_type_iseq: - return invoke_iseq_block_from_c(ec, &block->as.captured, self, argc, argv, kw_splat, passed_block_handler, NULL, is_lambda, me); + return invoke_iseq_block_from_c(ec, &block->as.captured, self, argc, argv, kw_splat, passed_block_handler, cref, is_lambda, me); case block_type_ifunc: if (kw_splat == 1) { VALUE keyword_hash = argv[argc-1]; @@ -1940,21 +1981,24 @@ invoke_block_from_c_proc(rb_execution_context_t *ec, const rb_proc_t *proc, static VALUE vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, - int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler) + int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler, + const rb_cref_t *cref) { - return invoke_block_from_c_proc(ec, proc, self, argc, argv, kw_splat, passed_block_handler, proc->is_lambda, NULL); + return invoke_block_from_c_proc(ec, proc, self, argc, argv, kw_splat, passed_block_handler, proc->is_lambda, cref, NULL); } static VALUE vm_invoke_bmethod(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE block_handler, const rb_callable_method_entry_t *me) { - return invoke_block_from_c_proc(ec, proc, self, argc, argv, kw_splat, block_handler, TRUE, me); + /* bmethod procs never carry a refinement cref (Proc#refined rejects them) */ + return invoke_block_from_c_proc(ec, proc, self, argc, argv, kw_splat, block_handler, TRUE, NULL, me); } VALUE rb_vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, - int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler) + int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler, + const rb_cref_t *cref) { VALUE self = vm_block_self(&proc->block); vm_block_handler_verify(passed_block_handler); @@ -1963,13 +2007,14 @@ rb_vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, return vm_invoke_bmethod(ec, proc, self, argc, argv, kw_splat, passed_block_handler, NULL); } else { - return vm_invoke_proc(ec, proc, self, argc, argv, kw_splat, passed_block_handler); + return vm_invoke_proc(ec, proc, self, argc, argv, kw_splat, passed_block_handler, cref); } } VALUE rb_vm_invoke_proc_with_self(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, - int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler) + int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler, + const rb_cref_t *cref) { vm_block_handler_verify(passed_block_handler); @@ -1977,7 +2022,7 @@ rb_vm_invoke_proc_with_self(rb_execution_context_t *ec, rb_proc_t *proc, VALUE s return vm_invoke_bmethod(ec, proc, self, argc, argv, kw_splat, passed_block_handler, NULL); } else { - return vm_invoke_proc(ec, proc, self, argc, argv, kw_splat, passed_block_handler); + return vm_invoke_proc(ec, proc, self, argc, argv, kw_splat, passed_block_handler, cref); } } diff --git a/vm_args.c b/vm_args.c index bf5dc3cadb80e2..1b8d10a159e858 100644 --- a/vm_args.c +++ b/vm_args.c @@ -1149,7 +1149,10 @@ vm_caller_setup_arg_block(const rb_execution_context_t *ec, rb_control_frame_t * rb_ary_push(callback_arg, ref); OBJ_FREEZE(callback_arg); func = rb_func_lambda_new(refine_sym_proc_call, callback_arg, 1, UNLIMITED_ARGUMENTS); - rb_hash_aset(ref, block_code, func); + /* the table is frozen when it belongs to a Proc#refined memo; skip the cache then */ + if (!OBJ_FROZEN(ref)) { + rb_hash_aset(ref, block_code, func); + } } block_code = func; } diff --git a/vm_core.h b/vm_core.h index 320d4cc7eb3f1d..b189023c4c63d5 100644 --- a/vm_core.h +++ b/vm_core.h @@ -1318,8 +1318,14 @@ typedef struct { unsigned int is_from_method: 1; /* bool */ unsigned int is_lambda: 1; /* bool */ unsigned int is_isolated: 1; /* bool */ + unsigned int is_refined: 1; /* bool: Proc#refined */ } rb_proc_t; +/* A refined proc's cref lives in a hidden ivar on the proc object; + * rb_proc_refinements_cref returns NULL unless is_refined is set. */ +const rb_cref_t *rb_proc_refinements_cref(VALUE procval); +void rb_proc_set_refinements_cref(VALUE procval, const rb_cref_t *cref); + RUBY_SYMBOL_EXPORT_BEGIN VALUE rb_proc_isolate(VALUE self); VALUE rb_proc_isolate_bang(VALUE self, VALUE replace_self); @@ -1954,6 +1960,7 @@ VALUE rb_thread_alloc(VALUE klass); VALUE rb_binding_alloc(VALUE klass); VALUE rb_proc_alloc(VALUE klass); VALUE rb_proc_dup(VALUE self); +VALUE rb_proc_dup_0(VALUE self); /* for debug */ extern bool rb_vmdebug_stack_dump_raw(const rb_execution_context_t *ec, const rb_control_frame_t *cfp, FILE *); @@ -1981,7 +1988,7 @@ void rb_iseq_pathobj_set(const rb_iseq_t *iseq, VALUE path, VALUE realpath); int rb_ec_frame_method_id_and_class(const rb_execution_context_t *ec, ID *idp, ID *called_idp, VALUE *klassp); void rb_ec_setup_exception(const rb_execution_context_t *ec, VALUE mesg, VALUE cause); -VALUE rb_vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, int argc, const VALUE *argv, int kw_splat, VALUE block_handler); +VALUE rb_vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, int argc, const VALUE *argv, int kw_splat, VALUE block_handler, const rb_cref_t *cref); VALUE rb_vm_make_proc_lambda(const rb_execution_context_t *ec, const struct rb_captured_block *captured, VALUE klass, int8_t is_lambda); static inline VALUE diff --git a/vm_eval.c b/vm_eval.c index 049131b0914d7b..5bd4be41d12089 100644 --- a/vm_eval.c +++ b/vm_eval.c @@ -290,7 +290,8 @@ vm_call0_body(rb_execution_context_t *ec, struct rb_calling_info *calling, const { rb_proc_t *proc; GetProcPtr(calling->recv, proc); - ret = rb_vm_invoke_proc(ec, proc, calling->argc, argv, calling->kw_splat, calling->block_handler); + ret = rb_vm_invoke_proc(ec, proc, calling->argc, argv, calling->kw_splat, calling->block_handler, + rb_proc_refinements_cref(calling->recv)); goto success; } case OPTIMIZED_METHOD_TYPE_STRUCT_AREF: @@ -2005,7 +2006,7 @@ eval_string_with_cref(VALUE self, VALUE src, rb_cref_t *cref, VALUE file, int li /* TODO: what the code checking? */ if (!cref && block.as.captured.code.val) { rb_cref_t *orig_cref = vm_get_cref(vm_block_ep(&block)); - cref = vm_cref_dup(orig_cref); + cref = rb_vm_cref_dup(orig_cref); } vm_set_eval_stack(ec, iseq, cref, &block); @@ -2217,6 +2218,7 @@ yield_under(VALUE self, int singleton, int argc, const VALUE *argv, int kw_splat const VALUE *ep = NULL; rb_cref_t *cref; int is_lambda = FALSE; + const rb_cref_t *proc_cref = NULL; if (block_handler != VM_BLOCK_HANDLER_NONE) { again: @@ -2232,8 +2234,14 @@ yield_under(VALUE self, int singleton, int argc, const VALUE *argv, int kw_splat new_block_handler = VM_BH_FROM_IFUNC_BLOCK(&new_captured); break; case block_handler_type_proc: - is_lambda = rb_proc_lambda_p(block_handler) != Qfalse; - block_handler = vm_proc_to_block_handler(VM_BH_TO_PROC(block_handler)); + { + VALUE procval = VM_BH_TO_PROC(block_handler); + rb_proc_t *po; + GetProcPtr(procval, po); + is_lambda = po->is_lambda; + if (po->is_refined) proc_cref = rb_proc_refinements_cref(procval); + block_handler = vm_block_to_block_handler(&po->block); + } goto again; case block_handler_type_symbol: return rb_sym_proc_call(SYM2ID(VM_BH_TO_SYMBOL(block_handler)), @@ -2250,6 +2258,11 @@ yield_under(VALUE self, int singleton, int argc, const VALUE *argv, int kw_splat VM_ASSERT(singleton || RB_TYPE_P(self, T_MODULE) || RB_TYPE_P(self, T_CLASS)); cref = vm_cref_push(ec, self, ep, TRUE, singleton); + if (proc_cref && !NIL_P(CREF_REFINEMENTS(proc_cref))) { + CREF_REFINEMENTS_SET(cref, rb_hash_dup(CREF_REFINEMENTS(proc_cref))); + CREF_REFINED_PROC_SET(cref); + } + return vm_yield_with_cref(ec, argc, argv, kw_splat, cref, is_lambda); } diff --git a/vm_insnhelper.c b/vm_insnhelper.c index fde6ac3245cb36..aff6ac1970abd9 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -908,7 +908,7 @@ cref_replace_with_duplicated_cref_each_frame(const VALUE *vptr, int can_be_svar, switch (imemo_type(v)) { case imemo_cref: cref = (rb_cref_t *)v; - new_cref = vm_cref_dup(cref); + new_cref = rb_vm_cref_dup(cref); if (parent) { RB_OBJ_WRITE(parent, vptr, new_cref); } @@ -5298,9 +5298,9 @@ vm_yield_setup_args(rb_execution_context_t *ec, const rb_iseq_t *iseq, const int /* ruby iseq -> ruby block */ static VALUE -vm_invoke_iseq_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, - struct rb_calling_info *calling, const struct rb_callinfo *ci, - bool is_lambda, VALUE block_handler) +vm_invoke_iseq_block_with_cref(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, + struct rb_calling_info *calling, const struct rb_callinfo *ci, + bool is_lambda, VALUE block_handler, const rb_cref_t *cref) { const struct rb_captured_block *captured = VM_BH_TO_ISEQ_BLOCK(block_handler); const rb_iseq_t *iseq = rb_iseq_check(captured->code.iseq); @@ -5312,10 +5312,11 @@ vm_invoke_iseq_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, SET_SP(rsp); + /* cref is normally 0; Proc#refined supplies a refinement cref */ vm_push_frame(ec, iseq, frame_flag, captured->self, - VM_GUARDED_PREV_EP(captured->ep), 0, + VM_GUARDED_PREV_EP(captured->ep), (VALUE)cref, ISEQ_BODY(iseq)->iseq_encoded + opt_pc, rsp + arg_size, ISEQ_BODY(iseq)->local_table_size - arg_size, ISEQ_BODY(iseq)->stack_max); @@ -5323,6 +5324,14 @@ vm_invoke_iseq_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, return Qundef; } +static VALUE +vm_invoke_iseq_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, + struct rb_calling_info *calling, const struct rb_callinfo *ci, + bool is_lambda, VALUE block_handler) +{ + return vm_invoke_iseq_block_with_cref(ec, reg_cfp, calling, ci, is_lambda, block_handler, NULL); +} + static VALUE vm_invoke_symbol_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_callinfo *ci, @@ -5386,11 +5395,9 @@ vm_invoke_ifunc_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, return val; } -static VALUE -vm_proc_to_block_handler(VALUE procval) +static inline VALUE +vm_block_to_block_handler(const struct rb_block *block) { - const struct rb_block *block = vm_proc_block(procval); - switch (vm_block_type(block)) { case block_type_iseq: return VM_BH_FROM_ISEQ_BLOCK(&block->as.captured); @@ -5405,15 +5412,45 @@ vm_proc_to_block_handler(VALUE procval) return Qundef; } +static VALUE +vm_proc_to_block_handler(VALUE procval) +{ + return vm_block_to_block_handler(vm_proc_block(procval)); +} + +NOINLINE(static VALUE +vm_invoke_proc_block_with_cref(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, + struct rb_calling_info *calling, const struct rb_callinfo *ci, + bool is_lambda, VALUE block_handler, VALUE refined_procval)); +static VALUE +vm_invoke_proc_block_with_cref(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, + struct rb_calling_info *calling, const struct rb_callinfo *ci, + bool is_lambda, VALUE block_handler, VALUE refined_procval) +{ + const rb_cref_t *cref = rb_proc_refinements_cref(refined_procval); + return vm_invoke_iseq_block_with_cref(ec, reg_cfp, calling, ci, is_lambda, block_handler, cref); +} + static VALUE vm_invoke_proc_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_callinfo *ci, bool is_lambda, VALUE block_handler) { + VALUE refined_procval = 0; + while (vm_block_handler_type(block_handler) == block_handler_type_proc) { - VALUE proc = VM_BH_TO_PROC(block_handler); - is_lambda = block_proc_is_lambda(proc); - block_handler = vm_proc_to_block_handler(proc); + VALUE procval = VM_BH_TO_PROC(block_handler); + rb_proc_t *po; + GetProcPtr(procval, po); + if (po->is_refined) refined_procval = procval; + is_lambda = po->is_lambda; + block_handler = vm_block_to_block_handler(&po->block); + } + + if (UNLIKELY(refined_procval) && vm_block_handler_type(block_handler) == block_handler_type_iseq) { + /* should not be inlined so that vm_invoke_proc_block stays leaf/frameless. */ + return vm_invoke_proc_block_with_cref(ec, reg_cfp, calling, ci, is_lambda, block_handler, + refined_procval); } return vm_invoke_block(ec, reg_cfp, calling, ci, is_lambda, block_handler); diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 47c62c4a7f3f63..48569908056e46 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -569,10 +569,22 @@ impl rb_proc_t { } } #[inline] + pub fn is_refined(&self) -> ::std::os::raw::c_uint { + unsafe { ::std::mem::transmute(self._bitfield_1.get(3usize, 1u8) as u32) } + } + #[inline] + pub fn set_is_refined(&mut self, val: ::std::os::raw::c_uint) { + unsafe { + let val: u32 = ::std::mem::transmute(val); + self._bitfield_1.set(3usize, 1u8, val as u64) + } + } + #[inline] pub fn new_bitfield_1( is_from_method: ::std::os::raw::c_uint, is_lambda: ::std::os::raw::c_uint, is_isolated: ::std::os::raw::c_uint, + is_refined: ::std::os::raw::c_uint, ) -> __BindgenBitfieldUnit<[u8; 1usize]> { let mut __bindgen_bitfield_unit: __BindgenBitfieldUnit<[u8; 1usize]> = Default::default(); __bindgen_bitfield_unit.set(0usize, 1u8, { @@ -587,6 +599,10 @@ impl rb_proc_t { let is_isolated: u32 = unsafe { ::std::mem::transmute(is_isolated) }; is_isolated as u64 }); + __bindgen_bitfield_unit.set(3usize, 1u8, { + let is_refined: u32 = unsafe { ::std::mem::transmute(is_refined) }; + is_refined as u64 + }); __bindgen_bitfield_unit } } diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index b552e8323b3ec5..6ebcbd5335d5a6 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1443,10 +1443,44 @@ impl rb_proc_t { } } #[inline] + pub fn is_refined(&self) -> ::std::os::raw::c_uint { + unsafe { ::std::mem::transmute(self._bitfield_1.get(3usize, 1u8) as u32) } + } + #[inline] + pub fn set_is_refined(&mut self, val: ::std::os::raw::c_uint) { + unsafe { + let val: u32 = ::std::mem::transmute(val); + self._bitfield_1.set(3usize, 1u8, val as u64) + } + } + #[inline] + pub unsafe fn is_refined_raw(this: *const Self) -> ::std::os::raw::c_uint { + unsafe { + ::std::mem::transmute(<__BindgenBitfieldUnit<[u8; 1usize]>>::raw_get( + ::std::ptr::addr_of!((*this)._bitfield_1), + 3usize, + 1u8, + ) as u32) + } + } + #[inline] + pub unsafe fn set_is_refined_raw(this: *mut Self, val: ::std::os::raw::c_uint) { + unsafe { + let val: u32 = ::std::mem::transmute(val); + <__BindgenBitfieldUnit<[u8; 1usize]>>::raw_set( + ::std::ptr::addr_of_mut!((*this)._bitfield_1), + 3usize, + 1u8, + val as u64, + ) + } + } + #[inline] pub fn new_bitfield_1( is_from_method: ::std::os::raw::c_uint, is_lambda: ::std::os::raw::c_uint, is_isolated: ::std::os::raw::c_uint, + is_refined: ::std::os::raw::c_uint, ) -> __BindgenBitfieldUnit<[u8; 1usize]> { let mut __bindgen_bitfield_unit: __BindgenBitfieldUnit<[u8; 1usize]> = Default::default(); __bindgen_bitfield_unit.set(0usize, 1u8, { @@ -1461,6 +1495,10 @@ impl rb_proc_t { let is_isolated: u32 = unsafe { ::std::mem::transmute(is_isolated) }; is_isolated as u64 }); + __bindgen_bitfield_unit.set(3usize, 1u8, { + let is_refined: u32 = unsafe { ::std::mem::transmute(is_refined) }; + is_refined as u64 + }); __bindgen_bitfield_unit } } From e34c45efbb0e2eb8af0d7478b6ad311604d621f5 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 15 Jul 2026 11:12:37 +0900 Subject: [PATCH 46/47] [ruby/rubygems] Normalize CRLF line breaks when parsing YAML in Gem::YAMLSerializer Splitting the source on LF alone left a trailing CR on every line, so the empty-value check saw a non-empty scalar and folded nested mappings and sequences into a single plain scalar. YAML normalizes CRLF to LF during parsing, so the CR belongs to the line break, not the content. The existing CRLF test only covered a flat mapping, which happens to survive the stray CR. https://github.com/ruby/rubygems/commit/067e0aa4bf Co-Authored-By: Claude Fable 5 --- lib/rubygems/yaml_serializer.rb | 2 +- test/rubygems/test_gem_safe_yaml.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index 5a46b10a1c038d..e9d077bb1f95fb 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -52,7 +52,7 @@ class Parser }.freeze def initialize(source) - @lines = source.split("\n") + @lines = source.split(/\r?\n/) @anchors = {} @depth = 0 strip_document_prefix diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index 5bbfa57a95e446..64c3c75eee6c1e 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -1100,6 +1100,18 @@ def test_load_crlf_line_endings assert_equal "data", result["other"] end + def test_load_crlf_with_sequence + yaml = <<~YAML.gsub("\n", "\r\n") + --- + nested_hash: + contains_array: + - "quoted item" + - plain item + YAML + + assert_equal({ "nested_hash" => { "contains_array" => ["quoted item", "plain item"] } }, yaml_load(yaml)) + end + def test_load_version_requirement_old_tag yaml = <<~YAML !ruby/object:Gem::Version::Requirement From b1c35b998f70167523a5964788693c79aa7d5895 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 15 Jul 2026 13:37:35 +0900 Subject: [PATCH 47/47] [ruby/rubygems] Quote strings containing CR instead of emitting a block scalar A literal block scalar cannot represent a CR because parsing normalizes CRLF line breaks to LF, so a string like a CRLF PEM certificate lost its CRs on roundtrip. Emit such strings as double-quoted scalars with escapes, matching Psych. This fixes TestGemPackage signing tests on Windows JRuby where to_pem returns CRLF line endings. https://github.com/ruby/rubygems/commit/afc73d34d8 Co-Authored-By: Claude Fable 5 --- lib/rubygems/yaml_serializer.rb | 4 +++- test/rubygems/test_gem_safe_yaml.rb | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index e9d077bb1f95fb..cf88a942c7e492 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -815,7 +815,9 @@ def emit_time(time) end def emit_string(str, indent, quote: false) - if str.include?("\n") + # A CR cannot be represented in a block scalar because parsing + # normalizes CRLF line breaks to LF, so quote and escape instead. + if str.include?("\n") && !str.include?("\r") emit_block_scalar(str, indent) elsif needs_quoting?(str, quote) " #{quote_string(str)}\n" diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index 64c3c75eee6c1e..15c8f84d21be38 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -1100,6 +1100,11 @@ def test_load_crlf_line_endings assert_equal "data", result["other"] end + def test_dump_crlf_string_roundtrip + obj = { "pem" => "line one\r\nline two\r\n" } + assert_equal obj, yaml_load(yaml_dump(obj)) + end + def test_load_crlf_with_sequence yaml = <<~YAML.gsub("\n", "\r\n") ---