require-hooks 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +5 -2
- data/lib/require-hooks/api.rb +88 -28
- data/lib/require-hooks/iseq.rb +33 -0
- data/lib/require-hooks/mode/bootsnap.rb +79 -1
- data/lib/require-hooks/mode/kernel_patch.rb +25 -44
- data/lib/require-hooks/mode/load_iseq.rb +11 -13
- data/lib/require-hooks/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 046c644cbb446d1abd3263c3c1ee0c578cddac17693d102172e123f1a880966a
|
|
4
|
+
data.tar.gz: a102736a0665a9de69c9aba242033a479af9b090bf135fa729e74b9bd8f6e006
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 35e088739e021cc4e83827122ce2daa63934439f6e4d0c9597ccafdaf6f6a08b50eba9302cfed1ced10538a62b317406ded4da1882b06f4d04b527a5e4902b47
|
|
7
|
+
data.tar.gz: 801d491df8a08926067f47b44ee2372d73a53fada8cdfe36b62c0d66191f52f6900499121fc87a326125f00e443848734e5f7756df541d7e92f688ae293e5026
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## master
|
|
4
4
|
|
|
5
|
+
## 0.4.0 (2026-04-29)
|
|
6
|
+
|
|
7
|
+
- Improved Bootsnap cache invalidation logic on hooks configuration changes.
|
|
8
|
+
|
|
9
|
+
- Latest Bootsnap compatibility
|
|
10
|
+
|
|
11
|
+
- Coverage compatibility (w/ some limitations)
|
|
12
|
+
|
|
13
|
+
## 0.3.0 (2026-04-22)
|
|
14
|
+
|
|
15
|
+
- Fix the order of around hooks execution (after part) when using `#load_iseq` driven hooks.
|
|
16
|
+
|
|
17
|
+
- Improve `Kernel#require` patch performance.
|
|
18
|
+
|
|
19
|
+
- Reduce context object creation and use a single object when only one context defined.
|
|
20
|
+
|
|
5
21
|
## 0.2.3 (2026-01-13)
|
|
6
22
|
|
|
7
23
|
- Gem metadata fixes.
|
data/README.md
CHANGED
|
@@ -9,6 +9,8 @@ Require Hooks is a library providing universal interface for injecting custom co
|
|
|
9
9
|
|
|
10
10
|
Require hooks allows you to interfere with `Kernel#require` (incl. `Kernel#require_relative`) and `Kernel#load`.
|
|
11
11
|
|
|
12
|
+
> Check the ["Require Hooks: Filling the Gap in Ruby's Extensibility"](https://evilmartians.com/events/require-hooks-rubykaigi) talk from RubyKaigi 2026 to learn more.
|
|
13
|
+
|
|
12
14
|
<a href="https://evilmartians.com/">
|
|
13
15
|
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
|
|
14
16
|
|
|
@@ -126,6 +128,7 @@ Thus, if you introduce new source transformers or hijackers, you must invalidate
|
|
|
126
128
|
|
|
127
129
|
## Limitations
|
|
128
130
|
|
|
131
|
+
- Coverage tracking is only supported if `eval` coverage tracking is enabled (`Coverage.start(eval: true, ...)` or `Simplecov.start { enable_coverage_for_eval; ... }`). Currently requires **Ruby 3.4+**.
|
|
129
132
|
- `Kernel#load` with a wrap argument (e.g., `load "some_path", true` or `load "some_path", MyModule)`) is not supported (fallbacked to the original implementation). The biggest challenge here is to support constants nesting.
|
|
130
133
|
- Some very edgy symlinking scenarios are not supported (unlikely to affect real-world projects).
|
|
131
134
|
|
|
@@ -175,9 +178,9 @@ Test script: `time bundle exec rails runner 'puts "done"'`.
|
|
|
175
178
|
| rhooks (patch) | **8m** |
|
|
176
179
|
| rhooks (bootsnap) | 12s |
|
|
177
180
|
|
|
178
|
-
You can see that requiring tons of files with Require Hooks in patch mode is very slow for now. Why?
|
|
181
|
+
You can see that requiring tons of files with Require Hooks in patch mode is very slow for now. Why? Manipulating the `$LOADED_FEATURES` index from Ruby triggers costly invalidation at the VM side when a regular `#require` occurs. We recommend activating Require Hooks after loading all the dependencies and limiting the scope of affected files (via the `patterns` option) on non-MRI platforms to avoid this overhead.
|
|
179
182
|
|
|
180
|
-
**NOTE:**
|
|
183
|
+
**NOTE:** This could be improved in the future if we get [optimized API](https://github.com/palkan/ruby/pull/1) for managing `$LOADED_FEATURES` from Ruby code.
|
|
181
184
|
|
|
182
185
|
Here are the numbers for the same project with scoped hooks (only some folders) activated after `Bundler.require(*)`:
|
|
183
186
|
|
data/lib/require-hooks/api.rb
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RequireHooks
|
|
4
|
-
@@around_load = []
|
|
5
|
-
@@source_transform = []
|
|
6
|
-
@@hijack_load = []
|
|
7
|
-
|
|
8
4
|
class Context
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
attr_reader :around_load, :source_transform, :hijack_load,
|
|
6
|
+
:patterns, :exclude_patterns
|
|
7
|
+
|
|
8
|
+
def initialize(patterns: nil, exclude_patterns: nil)
|
|
9
|
+
@patterns = patterns.freeze
|
|
10
|
+
@exclude_patterns = exclude_patterns.freeze
|
|
11
|
+
|
|
12
|
+
@around_load = []
|
|
13
|
+
@source_transform = []
|
|
14
|
+
@hijack_load = []
|
|
15
|
+
|
|
16
|
+
@empty = nil
|
|
17
|
+
@readonly = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_key
|
|
21
|
+
[patterns, exclude_patterns]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def match?(path)
|
|
25
|
+
return false unless !patterns || patterns.any? { |pattern| File.fnmatch?(pattern, path) }
|
|
26
|
+
return false if exclude_patterns&.any? { |pattern| File.fnmatch?(pattern, path) }
|
|
27
|
+
true
|
|
13
28
|
end
|
|
14
29
|
|
|
15
30
|
def empty?
|
|
16
|
-
@
|
|
31
|
+
return @empty unless @empty.nil?
|
|
32
|
+
@empty = @around_load.empty? && @source_transform.empty? && @hijack_load.empty?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def readonly?
|
|
36
|
+
return @readonly unless @readonly.nil?
|
|
37
|
+
|
|
38
|
+
@readonly = @source_transform.empty? && @hijack_load.empty?
|
|
17
39
|
end
|
|
18
40
|
|
|
19
41
|
def source_transform?
|
|
@@ -55,8 +77,18 @@ module RequireHooks
|
|
|
55
77
|
end
|
|
56
78
|
nil
|
|
57
79
|
end
|
|
80
|
+
|
|
81
|
+
def merge!(another_ctx)
|
|
82
|
+
around_load.concat(another_ctx.around_load)
|
|
83
|
+
source_transform.concat(another_ctx.source_transform)
|
|
84
|
+
hijack_load.concat(another_ctx.hijack_load)
|
|
85
|
+
end
|
|
58
86
|
end
|
|
59
87
|
|
|
88
|
+
@@default_context = Context.new
|
|
89
|
+
@@noop_context = Context.new
|
|
90
|
+
@@contexts = {}
|
|
91
|
+
|
|
60
92
|
class << self
|
|
61
93
|
attr_accessor :print_warnings
|
|
62
94
|
|
|
@@ -69,7 +101,7 @@ module RequireHooks
|
|
|
69
101
|
# block.call.tap { puts "Loaded #{path}" }
|
|
70
102
|
# end
|
|
71
103
|
def around_load(patterns: nil, exclude_patterns: nil, &block)
|
|
72
|
-
|
|
104
|
+
register_hook(:around_load, block, patterns: patterns, exclude_patterns: exclude_patterns)
|
|
73
105
|
end
|
|
74
106
|
|
|
75
107
|
# Define hooks to perform source-to-source transformations.
|
|
@@ -83,7 +115,7 @@ module RequireHooks
|
|
|
83
115
|
# "# frozen_string_literal: true\n#{source}"
|
|
84
116
|
# end
|
|
85
117
|
def source_transform(patterns: nil, exclude_patterns: nil, &block)
|
|
86
|
-
|
|
118
|
+
register_hook(:source_transform, block, patterns: patterns, exclude_patterns: exclude_patterns)
|
|
87
119
|
end
|
|
88
120
|
|
|
89
121
|
# This hook should be used to manually compile byte code to be loaded by the VM.
|
|
@@ -101,32 +133,60 @@ module RequireHooks
|
|
|
101
133
|
# end
|
|
102
134
|
# end
|
|
103
135
|
def hijack_load(patterns: nil, exclude_patterns: nil, &block)
|
|
104
|
-
|
|
136
|
+
register_hook(:hijack_load, block, patterns: patterns, exclude_patterns: exclude_patterns)
|
|
105
137
|
end
|
|
106
138
|
|
|
107
139
|
def context_for(path)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
140
|
+
# Fast-track in case we have just a single non-global context defined
|
|
141
|
+
if @@default_context
|
|
142
|
+
return @@default_context if @@default_context.match?(path)
|
|
111
143
|
|
|
112
|
-
|
|
113
|
-
end
|
|
144
|
+
return @@noop_context
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
matching = @@contexts.values.select { |ctx| ctx.match?(path) }
|
|
148
|
+
|
|
149
|
+
return matching[0] || @@noop_context if matching.size < 2
|
|
150
|
+
|
|
151
|
+
ctx = Context.new
|
|
152
|
+
matching.each { |mctx| ctx.merge!(mctx) }
|
|
153
|
+
|
|
154
|
+
ctx
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Hack to enable coverage for hooked files.
|
|
158
|
+
# Requires eval coverage to be on.
|
|
159
|
+
# See https://bugs.ruby-lang.org/issues/22018 (https://github.com/ruby/ruby/pull/16805)
|
|
160
|
+
def setup_path_coverage(path, contents = nil)
|
|
161
|
+
return unless defined?(Coverage) && Coverage.running?
|
|
114
162
|
|
|
115
|
-
|
|
116
|
-
next unless !patterns || patterns.any? { |pattern| File.fnmatch?(pattern, path) }
|
|
117
|
-
next if exclude_patterns&.any? { |pattern| File.fnmatch?(pattern, path) }
|
|
163
|
+
return unless eval_coverage_enabled?
|
|
118
164
|
|
|
119
|
-
|
|
120
|
-
|
|
165
|
+
Kernel.eval("\n" * (contents || File.read(path)).lines.size, TOPLEVEL_BINDING, path, 1) # rubocop:disable Style/EvalWithLocation,Security/Eval
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def contexts
|
|
169
|
+
@@contexts
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
121
173
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
174
|
+
def register_hook(type, block, patterns: nil, exclude_patterns: nil)
|
|
175
|
+
@@default_context = nil
|
|
176
|
+
ctx = Context.new(patterns: patterns, exclude_patterns: exclude_patterns)
|
|
177
|
+
|
|
178
|
+
@@contexts[ctx.to_key] ||= ctx
|
|
179
|
+
@@contexts[ctx.to_key].public_send(type) << block
|
|
180
|
+
|
|
181
|
+
@@default_context = @@contexts.values.first if @@contexts.size == 1
|
|
182
|
+
end
|
|
125
183
|
|
|
126
|
-
|
|
127
|
-
|
|
184
|
+
def eval_coverage_enabled?
|
|
185
|
+
return @eval_coverage_enabled if defined?(@eval_coverage_enabled)
|
|
186
|
+
probe_path = File.join(__dir__, "coverage_probe.rb")
|
|
187
|
+
Kernel.eval("proc { |val| val }", TOPLEVEL_BINDING, probe_path, 1) # rubocop:disable Style/EvalWithLocation
|
|
128
188
|
|
|
129
|
-
|
|
189
|
+
@eval_coverage_enabled = Coverage.peek_result.key?(probe_path)
|
|
130
190
|
end
|
|
131
191
|
end
|
|
132
192
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RequireHooks
|
|
4
|
+
module Iseq
|
|
5
|
+
class << self
|
|
6
|
+
def compile_with_coverage(ctx, path)
|
|
7
|
+
iseq =
|
|
8
|
+
if ctx.source_transform? || ctx.hijack?
|
|
9
|
+
new_contents = ctx.perform_source_transform(path)
|
|
10
|
+
|
|
11
|
+
RequireHooks.setup_path_coverage(path, new_contents)
|
|
12
|
+
|
|
13
|
+
hijacked = ctx.try_hijack_load(path, new_contents)
|
|
14
|
+
|
|
15
|
+
if hijacked
|
|
16
|
+
raise TypeError, "Unsupported bytecode format for #{path}: #{hijack.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence)
|
|
17
|
+
hijacked
|
|
18
|
+
elsif new_contents
|
|
19
|
+
RubyVM::InstructionSequence.compile(new_contents, path, path, 1)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
RequireHooks.setup_path_coverage(path, new_contents)
|
|
24
|
+
|
|
25
|
+
iseq ||= yield if block_given?
|
|
26
|
+
|
|
27
|
+
iseq ||= RubyVM::InstructionSequence.compile_file(path)
|
|
28
|
+
|
|
29
|
+
iseq
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "require-hooks/iseq"
|
|
4
|
+
|
|
3
5
|
module RequireHooks
|
|
4
6
|
module Bootsnap
|
|
7
|
+
EMPTY_ISEQ = RubyVM::InstructionSequence.compile("").freeze
|
|
8
|
+
|
|
9
|
+
# For older Bootsnap
|
|
5
10
|
module CompileCacheExt
|
|
6
11
|
def input_to_storage(source, path, *)
|
|
7
12
|
ctx = RequireHooks.context_for(path)
|
|
13
|
+
return super if ctx.empty?
|
|
8
14
|
|
|
9
15
|
new_contents = ctx.perform_source_transform(path)
|
|
10
16
|
hijacked = ctx.try_hijack_load(path, new_contents)
|
|
@@ -22,15 +28,87 @@ module RequireHooks
|
|
|
22
28
|
end
|
|
23
29
|
end
|
|
24
30
|
|
|
31
|
+
# For new Bootsnap
|
|
32
|
+
module CompilerExt
|
|
33
|
+
def input_to_storage(source, path, *)
|
|
34
|
+
ctx = RequireHooks.context_for(path)
|
|
35
|
+
return super if ctx.empty?
|
|
36
|
+
|
|
37
|
+
new_contents = ctx.perform_source_transform(path)
|
|
38
|
+
hijacked = ctx.try_hijack_load(path, new_contents)
|
|
39
|
+
|
|
40
|
+
if hijacked
|
|
41
|
+
raise TypeError, "Unsupported bytecode format for #{path}: #{hijack.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence)
|
|
42
|
+
return hijacked.to_binary
|
|
43
|
+
elsif new_contents
|
|
44
|
+
return RubyVM::InstructionSequence.compile(new_contents, path, path, 1, @compile_options).to_binary
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
super
|
|
48
|
+
rescue SyntaxError, TypeError
|
|
49
|
+
::Bootsnap::CompileCache::UNCOMPILABLE
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
25
53
|
module LoadIseqExt
|
|
54
|
+
class << self
|
|
55
|
+
attr_accessor :orig_cache_dir
|
|
56
|
+
end
|
|
57
|
+
|
|
26
58
|
# Around hooks must be performed every time we trigger a file load, even if
|
|
27
59
|
# the file is already cached.
|
|
28
60
|
def load_iseq(path)
|
|
29
|
-
RequireHooks.context_for(path)
|
|
61
|
+
ctx = RequireHooks.context_for(path)
|
|
62
|
+
# Early-return for non-trackable paths
|
|
63
|
+
if ctx.empty?
|
|
64
|
+
::Bootsnap::CompileCache::ISeq.cache_dir = LoadIseqExt.orig_cache_dir if LoadIseqExt.orig_cache_dir
|
|
65
|
+
return super
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
LoadIseqExt.orig_cache_dir ||= ::Bootsnap::CompileCache::ISeq.cache_dir
|
|
69
|
+
::Bootsnap::CompileCache::ISeq.cache_dir = File.join(LoadIseqExt.orig_cache_dir, RequireHooks::Bootsnap.version_hash)
|
|
70
|
+
|
|
71
|
+
ctx.run_around_load_callbacks(path) do
|
|
72
|
+
iseq = super
|
|
73
|
+
|
|
74
|
+
::Bootsnap::CompileCache::ISeq.cache_dir = LoadIseqExt.orig_cache_dir
|
|
75
|
+
|
|
76
|
+
# Bootsnap returns nil when the coverage is on,
|
|
77
|
+
# we fallback to our custom #compile_with_coverage
|
|
78
|
+
unless iseq
|
|
79
|
+
next unless defined?(Coverage) && Coverage.running?
|
|
80
|
+
|
|
81
|
+
iseq = RequireHooks::Iseq.compile_with_coverage(ctx, path)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
iseq.eval
|
|
85
|
+
EMPTY_ISEQ
|
|
86
|
+
ensure
|
|
87
|
+
::Bootsnap::CompileCache::ISeq.cache_dir = LoadIseqExt.orig_cache_dir
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class << self
|
|
93
|
+
def version_hash
|
|
94
|
+
@version_key ||= RequireHooks.contexts.values.map(&:to_cache_key).join("-")
|
|
30
95
|
end
|
|
96
|
+
|
|
97
|
+
attr_writer :version_hash
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class Context
|
|
102
|
+
def to_cache_key
|
|
103
|
+
Zlib.crc32(
|
|
104
|
+
(around_load + source_transform + hijack_load).map do |pr|
|
|
105
|
+
RubyVM::InstructionSequence.disasm(pr)
|
|
106
|
+
end.join("\n")
|
|
107
|
+
).to_s
|
|
31
108
|
end
|
|
32
109
|
end
|
|
33
110
|
end
|
|
34
111
|
|
|
35
112
|
Bootsnap::CompileCache::ISeq.singleton_class.prepend(RequireHooks::Bootsnap::CompileCacheExt)
|
|
113
|
+
Bootsnap::CompileCache::ISeq::Compiler.prepend(RequireHooks::Bootsnap::CompilerExt) if defined?(Bootsnap::CompileCache::ISeq::Compiler)
|
|
36
114
|
RubyVM::InstructionSequence.singleton_class.prepend(RequireHooks::Bootsnap::LoadIseqExt)
|
|
@@ -5,8 +5,8 @@ require "pathname"
|
|
|
5
5
|
module RequireHooks
|
|
6
6
|
module KernelPatch
|
|
7
7
|
class << self
|
|
8
|
-
def load(path)
|
|
9
|
-
ctx
|
|
8
|
+
def load(path, ctx: nil)
|
|
9
|
+
ctx ||= RequireHooks.context_for(path)
|
|
10
10
|
|
|
11
11
|
ctx.run_around_load_callbacks(path) do
|
|
12
12
|
next load_without_require_hooks(path) unless ctx.source_transform? || ctx.hijack?
|
|
@@ -14,7 +14,9 @@ module RequireHooks
|
|
|
14
14
|
new_contents = ctx.perform_source_transform(path)
|
|
15
15
|
hijacked = ctx.try_hijack_load(path, new_contents)
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
if hijacked
|
|
18
|
+
return try_evaluate(path, hijacked)
|
|
19
|
+
end
|
|
18
20
|
|
|
19
21
|
if new_contents
|
|
20
22
|
evaluate(new_contents, path)
|
|
@@ -29,6 +31,7 @@ module RequireHooks
|
|
|
29
31
|
|
|
30
32
|
def try_evaluate(path, bytecode)
|
|
31
33
|
if defined?(::RubyVM::InstructionSequence) && bytecode.is_a?(::RubyVM::InstructionSequence)
|
|
34
|
+
RequireHooks.setup_path_coverage(path)
|
|
32
35
|
bytecode.eval
|
|
33
36
|
else
|
|
34
37
|
raise TypeError, "Unknown bytecode format for #{path}: #{bytecode.inspect}"
|
|
@@ -49,6 +52,8 @@ module RequireHooks
|
|
|
49
52
|
end
|
|
50
53
|
else
|
|
51
54
|
def evaluate(code, filepath)
|
|
55
|
+
RequireHooks.setup_path_coverage(filepath, code)
|
|
56
|
+
|
|
52
57
|
# This is workaround to solve the "leaking refinements" problem in MRI
|
|
53
58
|
RubyVM::InstructionSequence.compile(code, filepath).tap do |iseq|
|
|
54
59
|
iseq.eval
|
|
@@ -140,32 +145,6 @@ module RequireHooks
|
|
|
140
145
|
path
|
|
141
146
|
end
|
|
142
147
|
|
|
143
|
-
# Based on https://github.com/ruby/ruby/blob/b588fd552390c55809719100d803c36bc7430f2f/load.c#L403-L415
|
|
144
|
-
def feature_loaded?(feature)
|
|
145
|
-
return true if $LOADED_FEATURES.include?(feature) && !LOCK.locked_feature?(feature)
|
|
146
|
-
|
|
147
|
-
feature = Pathname.new(feature).cleanpath.to_s
|
|
148
|
-
efeature = File.expand_path(feature)
|
|
149
|
-
|
|
150
|
-
# Check absoulute and relative paths
|
|
151
|
-
return true if $LOADED_FEATURES.include?(efeature) && !LOCK.locked_feature?(efeature)
|
|
152
|
-
|
|
153
|
-
candidates = []
|
|
154
|
-
|
|
155
|
-
$LOADED_FEATURES.each do |lf|
|
|
156
|
-
candidates << lf if lf.end_with?("/#{feature}")
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
return false if candidates.empty?
|
|
160
|
-
|
|
161
|
-
$LOAD_PATH.each do |lp|
|
|
162
|
-
lp_feature = File.join(lp, feature)
|
|
163
|
-
return true if candidates.include?(lp_feature) && !LOCK.locked_feature?(lp_feature)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
false
|
|
167
|
-
end
|
|
168
|
-
|
|
169
148
|
private
|
|
170
149
|
|
|
171
150
|
def lookup_feature_path(path, implitic_ext: true)
|
|
@@ -250,24 +229,26 @@ module Kernel
|
|
|
250
229
|
|
|
251
230
|
return require_without_require_hooks(path) if ctx.empty?
|
|
252
231
|
|
|
253
|
-
return false if
|
|
232
|
+
return false if $LOADED_FEATURES.include?(realpath)
|
|
254
233
|
|
|
255
|
-
|
|
256
|
-
|
|
234
|
+
begin
|
|
235
|
+
RequireHooks::KernelPatch::Features::LOCK.lock_feature(feature) do |loaded|
|
|
236
|
+
return false if loaded
|
|
257
237
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
238
|
+
$LOADED_FEATURES << realpath
|
|
239
|
+
RequireHooks::KernelPatch.load(realpath, ctx: ctx)
|
|
240
|
+
true
|
|
241
|
+
end
|
|
242
|
+
rescue LoadError => e
|
|
243
|
+
$LOADED_FEATURES.delete(realpath) if realpath
|
|
244
|
+
warn "RequireHooks failed to require '#{path}' from '#{realpath}': #{e.message}" if RequireHooks.print_warnings
|
|
245
|
+
require_without_require_hooks(path)
|
|
246
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
247
|
+
raise LoadError, "cannot load such file -- #{path}"
|
|
248
|
+
rescue
|
|
249
|
+
$LOADED_FEATURES.delete(realpath) if realpath
|
|
250
|
+
raise
|
|
261
251
|
end
|
|
262
|
-
rescue LoadError => e
|
|
263
|
-
$LOADED_FEATURES.delete(realpath) if realpath
|
|
264
|
-
warn "RequireHooks failed to require '#{path}': #{e.message}" if RequireHooks.print_warnings
|
|
265
|
-
require_without_require_hooks(path)
|
|
266
|
-
rescue Errno::ENOENT, Errno::EACCES
|
|
267
|
-
raise LoadError, "cannot load such file -- #{path}"
|
|
268
|
-
rescue
|
|
269
|
-
$LOADED_FEATURES.delete(realpath) if realpath
|
|
270
|
-
raise
|
|
271
252
|
end
|
|
272
253
|
|
|
273
254
|
alias_method :require_relative_without_require_hooks, :require_relative
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "require-hooks/iseq"
|
|
4
|
+
|
|
3
5
|
module RequireHooks
|
|
6
|
+
EMPTY_ISEQ = RubyVM::InstructionSequence.compile("").freeze
|
|
7
|
+
|
|
4
8
|
module LoadIseq
|
|
5
9
|
def load_iseq(path)
|
|
6
10
|
ctx = RequireHooks.context_for(path)
|
|
7
11
|
|
|
12
|
+
# Early-return for non-trackable paths
|
|
13
|
+
return if ctx.empty?
|
|
14
|
+
|
|
8
15
|
ctx.run_around_load_callbacks(path) do
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if hijacked
|
|
14
|
-
raise TypeError, "Unsupported bytecode format for #{path}: #{hijack.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence)
|
|
15
|
-
return hijacked
|
|
16
|
-
elsif new_contents
|
|
17
|
-
return RubyVM::InstructionSequence.compile(new_contents, path, path, 1)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
defined?(super) ? super : RubyVM::InstructionSequence.compile_file(path)
|
|
16
|
+
iseq = RequireHooks::Iseq.compile_with_coverage(ctx, path) { defined?(super) && super }
|
|
17
|
+
|
|
18
|
+
iseq.eval
|
|
19
|
+
EMPTY_ISEQ
|
|
22
20
|
end
|
|
23
21
|
end
|
|
24
22
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: require-hooks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vladimir Dementyev
|
|
@@ -22,6 +22,7 @@ files:
|
|
|
22
22
|
- README.md
|
|
23
23
|
- lib/require-hooks.rb
|
|
24
24
|
- lib/require-hooks/api.rb
|
|
25
|
+
- lib/require-hooks/iseq.rb
|
|
25
26
|
- lib/require-hooks/mode/bootsnap.rb
|
|
26
27
|
- lib/require-hooks/mode/kernel_patch.rb
|
|
27
28
|
- lib/require-hooks/mode/load_iseq.rb
|
|
@@ -37,6 +38,7 @@ metadata:
|
|
|
37
38
|
homepage_uri: https://github.com/ruby-next/require-hooks
|
|
38
39
|
source_code_uri: https://github.com/ruby-next/require-hooks
|
|
39
40
|
funding_uri: https://github.com/sponsors/palkan
|
|
41
|
+
rubygems_mfa_required: 'true'
|
|
40
42
|
rdoc_options: []
|
|
41
43
|
require_paths:
|
|
42
44
|
- lib
|