require-hooks 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 25df07edf75eef2e55e17548e0e02d54ba54e542e4ee3ebe20ec54958d3e2385
4
+ data.tar.gz: 8337f6fc90e9ee9e917ce30c3f1eb7536309ca590b0882798e70969d519cf626
5
+ SHA512:
6
+ metadata.gz: 55979ba3219aacfc8a1626e4c198518f29e527a1bb547e39d86f4fd170d1562c71166df87e50c5d4ad5a7deb58adfa5248dfeac85baa21a41839cecac4441460
7
+ data.tar.gz: 33c22e744609eccfd80eb46949e7e5740a563cabbbfb9e0a6d68e6fede8a377ea10cfb47998670ae67003e7723e6b2f9c58e3a7cce832488409f254624a4f2f0
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Change log
2
+
3
+ ## master
4
+
5
+ ## 0.1.0 (2023-07-14)
6
+
7
+ - Extracted from Ruby Next. ([@palkan][]])
8
+
9
+ [@palkan]: https://github.com/palkan
data/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2023 Vladimir Dementyev
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
data/README.md ADDED
@@ -0,0 +1,130 @@
1
+ [![Gem Version](https://badge.fury.io/rb/require-hooks.svg)](https://rubygems.org/gems/require-hooks)
2
+ [![Build](https://github.com/ruby-next/require-hooks/workflows/Build/badge.svg)](https://github.com/palkan/require-hooks/actions)
3
+ [![JRuby Build](https://github.com/ruby-next/require-hooks/workflows/JRuby%20Build/badge.svg)](https://github.com/ruby-next/require-hooks/actions)
4
+ [![TruffleRuby Build](https://github.com/ruby-next/require-hooks/workflows/TruffleRuby%20Build/badge.svg)](https://github.com/ruby-next/require-hooks/actions)
5
+
6
+ # Require Hooks
7
+
8
+ Require Hooks is a library providing universal interface for injecting custom code into the Ruby's loading mechanism. It works on MRI, JRuby, and TruffleRuby.
9
+
10
+ Require hooks allows you to interfere with `Kernel#require` (incl. `Kernel#require_relative`) and `Kernel#load`.
11
+
12
+ <a href="https://evilmartians.com/">
13
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
14
+
15
+ ## Examples
16
+
17
+ - [Ruby Next][ruby-next]
18
+ - [Freezolite](https://github.com/ruby-next/freezolite)
19
+
20
+ ## Installation
21
+
22
+ Add to your Gemfile:
23
+
24
+ ```ruby
25
+ gem "require-hooks"
26
+ ```
27
+
28
+ or gemspec:
29
+
30
+ ```ruby
31
+ spec.add_dependency "require-hooks"
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ To enable hooks, you need to load `require-hooks/setup` as early as possible. For example, in your gem's entrypoint:
37
+
38
+ ```ruby
39
+ require "require-hooks/setup"
40
+ ```
41
+
42
+ Then, you can add hooks:
43
+
44
+ - **around_load:** a hook that wraps code loading operation. Useful for logging and debugging purposes.
45
+
46
+ ```ruby
47
+ # Simple logging
48
+ RequireHooks.around_load do |path, &block|
49
+ puts "Loading #{path}"
50
+ block.call.tap { puts "Loaded #{path}" }
51
+ end
52
+
53
+ # Error enrichment
54
+ RequireHooks.around_load do |path, &block|
55
+ block.call
56
+ rescue SyntaxError => e
57
+ raise "Oops, your Ruby is not Ruby: #{e.message}"
58
+ end
59
+ ```
60
+
61
+ The return value MUST be a result of calling the passed block.
62
+
63
+ - **source_transform:** perform source-to-source transformations.
64
+
65
+ ```ruby
66
+ RequireHooks.source_transform do |path, source|
67
+ next unless path =~ /my_project\/.*/
68
+ source ||= File.read(path)
69
+ "# frozen_string_literal: true\n#{source}"
70
+ end
71
+ ```
72
+
73
+ The return value MUST be either String (new source code) or `nil` (indicating that no transformations were performed). The second argument (`source`) MAY be `nil``, indicating that no transformer tried to transform the source code.
74
+
75
+ - **hijack_load:** a hook that is used to manually compile byte code for VM to load it.
76
+
77
+ ```ruby
78
+ RequireHooks.hijack_load do |path, source|
79
+ next unless path =~ /my_project\/.*/
80
+
81
+ source ||= File.read(path)
82
+ if defined?(RubyVM::InstructionSequence)
83
+ RubyVM::InstructionSequence.compile(source)
84
+ elsif defined?(JRUBY_VERSION)
85
+ JRuby.compile(source)
86
+ end
87
+ end
88
+ ```
89
+
90
+ The return value is platform-specific. If there are multiple _hijackers_, the first one that returns a non-`nil` value is used, others are ignored.
91
+
92
+ ## Modes
93
+
94
+ Depending on the runtime conditions, Require Hooks picks an optimal strategy for injecting the code. You can enforce a particular _mode_ by setting the `REQUIRE_HOOKS_MODE` env variable (`patch`, `load_iseq` or `bootsnap`). In practice, only setting to `patch` may makes sense.
95
+
96
+ ### Via `#load_iseq`
97
+
98
+ If `RubyVM::InstructionSequence` is available, we use more robust way of hijacking code loading—`RubyVM::InstructionSequence#load_iseq`.
99
+
100
+ Keep in mind that if there is already a `#load_iseq` callback defined, it will only have an effect if Require Hooks hijackers return `nil`.
101
+
102
+ ### Kernel patching
103
+
104
+ In this mode, Require Hooks monkey-patches `Kernel#require` and friends. This mode is used in JRuby by default.
105
+
106
+ ### Bootsnap integration
107
+
108
+ [Bootsnap][] is a great tool to speed-up your application load and it's included into the default Rails Gemfile. And it uses `#load_iseq`. Require Hooks activates a custom Bootsnap-compatible mode, so you can benefit from both tools.
109
+
110
+ You can use require-hooks with Bootsnap to customize code loading. Just make sure you load `require-hooks/setup` after setting up Bootsnap, for example:
111
+
112
+ ```ruby
113
+ require "bootsnap/setup"
114
+ require "require-hooks/setup"
115
+ ```
116
+
117
+ The _around load_ hooks are executed for all files independently of whether they are cached or not. Source transformation and hijacking is only done for non-cached files.
118
+
119
+ Thus, if you introduce new source transformers or hijackers, you must invalidate the cache. (We plan to implement automatic invalidation in future versions.)
120
+
121
+ ## Contributing
122
+
123
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/ruby-next/require-hooks](https://github.com/ruby-next/require-hooks).
124
+
125
+ ## License
126
+
127
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
128
+
129
+ [Bootsnap]: https://github.com/Shopify/bootsnap
130
+ [ruby-next]: https://github.com/ruby-next/ruby-next
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RequireHooks
4
+ @@around_load = []
5
+ @@source_transform = []
6
+ @@hijack_load = []
7
+
8
+ class << self
9
+ # Define a block to wrap the code loading.
10
+ # The return value MUST be a result of calling the passed block.
11
+ # For example, you can use such hooks for instrumentation, debugging purposes.
12
+ #
13
+ # RequireHooks.around_load do |path, &block|
14
+ # puts "Loading #{path}"
15
+ # block.call.tap { puts "Loaded #{path}" }
16
+ # end
17
+ def around_load(&block)
18
+ @@around_load << block
19
+ end
20
+
21
+ # Define hooks to perform source-to-source transformations.
22
+ # The return value MUST be either String (new source code) or nil (indicating that no transformations were performed).
23
+ #
24
+ # NOTE: The second argument (`source`) MAY be nil, indicating that no transformer tried to transform the source code.
25
+ #
26
+ # For example, you can prepend each file with `# frozen_string_literal: true` pragma:
27
+ #
28
+ # RequireHooks.source_transform do |path, source|
29
+ # "# frozen_string_literal: true\n#{source}"
30
+ # end
31
+ def source_transform(&block)
32
+ @@source_transform << block
33
+ end
34
+
35
+ # This hook should be used to manually compile byte code to be loaded by the VM.
36
+ # The arguments are (path, source = nil), where source is only defined if transformations took place.
37
+ # Otherwise, you MUST read the source code from the file yourself.
38
+ #
39
+ # The return value MUST be either nil (continue to the next hook or default behavior) or a platform-specific bytecode object (e.g., RubyVM::InstructionSequence).
40
+ #
41
+ # RequireHooks.hijack_load do |path, source|
42
+ # source ||= File.read(path)
43
+ # if defined?(RubyVM::InstructionSequence)
44
+ # RubyVM::InstructionSequence.compile(source)
45
+ # elsif defined?(JRUBY_VERSION)
46
+ # JRuby.compile(source)
47
+ # end
48
+ # end
49
+ def hijack_load(&block)
50
+ @@hijack_load << block
51
+ end
52
+
53
+ def run_around_load_callbacks(path)
54
+ return yield if @@around_load.empty?
55
+
56
+ chain = @@around_load.reverse.inject do |acc_proc, next_proc|
57
+ proc { |path, &block| acc_proc.call(path) { next_proc.call(path, &block) } }
58
+ end
59
+
60
+ chain.call(path) { yield }
61
+ end
62
+
63
+ def perform_source_transform(path)
64
+ return unless @@source_transform.any?
65
+
66
+ source = nil
67
+
68
+ @@source_transform.each do |transform|
69
+ source = transform.call(path, source) || source
70
+ end
71
+
72
+ source
73
+ end
74
+
75
+ def try_hijack_load(path, source)
76
+ return unless @@hijack_load.any?
77
+
78
+ @@hijack_load.each do |hijack|
79
+ result = hijack.call(path, source)
80
+ return result if result
81
+ end
82
+ nil
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RequireHooks
4
+ module Bootsnap
5
+ module CompileCacheExt
6
+ def input_to_storage(source, path, *)
7
+ new_contents = RequireHooks.perform_source_transform(path)
8
+ hijacked = RequireHooks.try_hijack_load(path, new_contents)
9
+
10
+ if hijacked
11
+ raise TypeError, "Unsupported bytecode format for #{path}: #{hijack.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence)
12
+ return hijacked.to_binary
13
+ elsif new_contents
14
+ return RubyVM::InstructionSequence.compile(new_contents, path, path, 1).to_binary
15
+ end
16
+
17
+ super
18
+ rescue SyntaxError, TypeError
19
+ raise Bootsnap::CompileCache::Uncompilable
20
+ end
21
+ end
22
+
23
+ module LoadIseqExt
24
+ # Around hooks must be performed every time we trigger a file load, even if
25
+ # the file is already cached.
26
+ def load_iseq(path)
27
+ RequireHooks.run_around_load_callbacks(path) { super }
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ Bootsnap::CompileCache::ISeq.singleton_class.prepend(RequireHooks::Bootsnap::CompileCacheExt)
34
+ RubyVM::InstructionSequence.singleton_class.prepend(RequireHooks::Bootsnap::LoadIseqExt)
@@ -0,0 +1,331 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mutex_m"
4
+ require "pathname"
5
+
6
+ module RequireHooks
7
+ module KernelPatch
8
+ class << self
9
+ def load(path)
10
+ RequireHooks.run_around_load_callbacks(path) do
11
+ new_contents = RequireHooks.perform_source_transform(path)
12
+ hijacked = RequireHooks.try_hijack_load(path, new_contents)
13
+
14
+ return try_evaluate(path, hijacked) if hijacked
15
+
16
+ if new_contents
17
+ evaluate(new_contents, path)
18
+ true
19
+ else
20
+ load_without_require_hooks(path)
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def try_evaluate(path, bytecode)
28
+ if defined?(::RubyVM::InstructionSequence) && bytecode.is_a?(::RubyVM::InstructionSequence)
29
+ bytecode.eval
30
+ else
31
+ raise TypeError, "Unknown bytecode format for #{path}: #{bytecode.inspect}"
32
+ end
33
+
34
+ true
35
+ end
36
+
37
+ if defined?(JRUBY_VERSION) || defined?(TruffleRuby)
38
+ def evaluate(code, filepath)
39
+ new_toplevel.eval(code, filepath)
40
+ end
41
+
42
+ def new_toplevel
43
+ # Create new "toplevel" binding to avoid lexical scope re-use
44
+ # (aka "leaking refinements")
45
+ eval "proc{binding}.call", TOPLEVEL_BINDING, __FILE__, __LINE__
46
+ end
47
+ else
48
+ def evaluate(code, filepath)
49
+ # This is workaround to solve the "leaking refinements" problem in MRI
50
+ RubyVM::InstructionSequence.compile(code, filepath).tap do |iseq|
51
+ iseq.eval
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ module Features
58
+ class Locker
59
+ class PathLock
60
+ def initialize
61
+ @mu = Mutex.new
62
+ @resolved = false
63
+ end
64
+
65
+ def owned?
66
+ @mu.owned?
67
+ end
68
+
69
+ def locked?
70
+ @mu.locked?
71
+ end
72
+
73
+ def lock!
74
+ @mu.lock
75
+ end
76
+
77
+ def unlock!
78
+ @mu.unlock
79
+ end
80
+
81
+ def resolve!
82
+ @resolved = true
83
+ end
84
+
85
+ def resolved?
86
+ @resolved
87
+ end
88
+ end
89
+
90
+ attr_reader :features, :mu
91
+
92
+ def initialize
93
+ @mu = Mutex.new
94
+ @features = {}
95
+ end
96
+
97
+ def lock_feature(fname)
98
+ lock = mu.synchronize do
99
+ features[fname] ||= PathLock.new
100
+ end
101
+
102
+ # Can this even happen?
103
+ return yield(true) if lock.resolved?
104
+
105
+ # Recursive require
106
+ if lock.owned? && lock.locked?
107
+ warn "circular require considered harmful: #{fname}"
108
+ return yield(true)
109
+ end
110
+
111
+ lock.lock!
112
+ begin
113
+ yield(lock.resolved?).tap do
114
+ lock.resolve!
115
+ end
116
+ ensure
117
+ lock.unlock!
118
+
119
+ mu.synchronize do
120
+ features.delete(fname)
121
+ end
122
+ end
123
+ end
124
+
125
+ def locked_feature?(fname)
126
+ mu.synchronize { features.key?(fname) }
127
+ end
128
+ end
129
+
130
+ LOCK = Locker.new
131
+
132
+ class << self
133
+ def feature_path(path, implitic_ext: true)
134
+ path = resolve_feature_path(path, implitic_ext: implitic_ext)
135
+ return if path.nil?
136
+ return if File.extname(path) != ".rb" && implitic_ext
137
+ path
138
+ end
139
+
140
+ # Based on https://github.com/ruby/ruby/blob/b588fd552390c55809719100d803c36bc7430f2f/load.c#L403-L415
141
+ def feature_loaded?(feature)
142
+ return true if $LOADED_FEATURES.include?(feature) && !LOCK.locked_feature?(feature)
143
+
144
+ feature = Pathname.new(feature).cleanpath.to_s
145
+ efeature = File.expand_path(feature)
146
+
147
+ # Check absoulute and relative paths
148
+ return true if $LOADED_FEATURES.include?(efeature) && !LOCK.locked_feature?(efeature)
149
+
150
+ candidates = []
151
+
152
+ $LOADED_FEATURES.each do |lf|
153
+ candidates << lf if lf.end_with?("/#{feature}")
154
+ end
155
+
156
+ return false if candidates.empty?
157
+
158
+ $LOAD_PATH.each do |lp|
159
+ lp_feature = File.join(lp, feature)
160
+ return true if candidates.include?(lp_feature) && !LOCK.locked_feature?(lp_feature)
161
+ end
162
+
163
+ false
164
+ end
165
+
166
+ private
167
+
168
+ def lookup_feature_path(path, implitic_ext: true)
169
+ path = "#{path}.rb" if File.extname(path).empty? && implitic_ext
170
+
171
+ # Resolve relative paths only against current directory
172
+ if path.match?(/^\.\.?\//)
173
+ path = File.expand_path(path)
174
+ return path if File.file?(path)
175
+ return nil
176
+ end
177
+
178
+ if Pathname.new(path).absolute?
179
+ path = File.expand_path(path)
180
+ return File.file?(path) ? path : nil
181
+ end
182
+
183
+ # not a relative, not an absolute path — bare path; try looking relative to current dir,
184
+ # if it's in the $LOAD_PATH
185
+ if $LOAD_PATH.include?(Dir.pwd) && File.file?(path)
186
+ return File.expand_path(path)
187
+ end
188
+
189
+ $LOAD_PATH.find do |lp|
190
+ lpath = File.join(lp, path)
191
+ return File.expand_path(lpath) if File.file?(lpath)
192
+ end
193
+ end
194
+
195
+ if $LOAD_PATH.respond_to?(:resolve_feature_path)
196
+ def resolve_feature_path(feature, implitic_ext: true)
197
+ if implitic_ext
198
+ path = $LOAD_PATH.resolve_feature_path(feature)
199
+ path.last if path # rubocop:disable Style/SafeNavigation
200
+ else
201
+ lookup_feature_path(feature, implitic_ext: implitic_ext)
202
+ end
203
+ rescue LoadError
204
+ end
205
+ else
206
+ def resolve_feature_path(feature, implitic_ext: true)
207
+ lookup_feature_path(feature, implitic_ext: implitic_ext)
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ # Patch Kernel to hijack require/require_relative/load
216
+ module Kernel
217
+ module_function
218
+
219
+ alias_method :require_without_require_hooks, :require
220
+ # See https://github.com/ruby/ruby/blob/d814722fb8299c4baace3e76447a55a3d5478e3a/load.c#L1181
221
+ def require(path)
222
+ path = path.to_path if path.respond_to?(:to_path)
223
+ raise TypeError unless path.respond_to?(:to_str)
224
+
225
+ path = path.to_str
226
+
227
+ raise TypeError unless path.is_a?(::String)
228
+
229
+ realpath = nil
230
+
231
+ # if extname == ".rb" => lookup feature -> resolve feature -> load
232
+ # if extname != ".rb" => append ".rb" - lookup feature -> resolve feature -> lookup orig (no ext) -> resolve orig (no ext) -> load
233
+ if File.extname(path) != ".rb"
234
+ return false if RequireHooks::KernelPatch::Features.feature_loaded?(path + ".rb")
235
+
236
+ loaded = RequireHooks::KernelPatch::Features::LOCK.lock_feature(path + ".rb") do |loaded|
237
+ return false if loaded
238
+
239
+ realpath = RequireHooks::KernelPatch::Features.feature_path(path + ".rb")
240
+
241
+ if realpath
242
+ $LOADED_FEATURES << realpath
243
+ RequireHooks::KernelPatch.load(realpath)
244
+ true
245
+ end
246
+ end
247
+
248
+ return true if loaded
249
+ end
250
+
251
+ return false if RequireHooks::KernelPatch::Features.feature_loaded?(path)
252
+
253
+ loaded = RequireHooks::KernelPatch::Features::LOCK.lock_feature(path) do |loaded|
254
+ return false if loaded
255
+
256
+ realpath = RequireHooks::KernelPatch::Features.feature_path(path)
257
+
258
+ if realpath
259
+ $LOADED_FEATURES << realpath
260
+ RequireHooks::KernelPatch.load(realpath)
261
+ true
262
+ end
263
+ end
264
+
265
+ return true if loaded
266
+
267
+ require_without_require_hooks(path)
268
+ rescue LoadError => e
269
+ $LOADED_FEATURES.delete(realpath) if realpath
270
+ warn "RequireHooks failed to require '#{path}': #{e.message}"
271
+ require_without_require_hooks(path)
272
+ rescue Errno::ENOENT, Errno::EACCES
273
+ raise LoadError, "cannot load such file -- #{path}"
274
+ rescue
275
+ $LOADED_FEATURES.delete(realpath) if realpath
276
+ raise
277
+ end
278
+
279
+ alias_method :require_relative_without_require_hooks, :require_relative
280
+ def require_relative(path)
281
+ path = path.to_path if path.respond_to?(:to_path)
282
+ raise TypeError unless path.respond_to?(:to_str)
283
+ path = path.to_str
284
+
285
+ raise TypeError unless path.is_a?(::String)
286
+
287
+ return require(path) if Pathname.new(path).absolute?
288
+
289
+ loc = caller_locations(1..1).first
290
+ from = loc.absolute_path || loc.path || File.join(Dir.pwd, "main")
291
+ realpath = File.absolute_path(
292
+ File.join(
293
+ File.dirname(File.absolute_path(from)),
294
+ path
295
+ )
296
+ )
297
+
298
+ require(realpath)
299
+ end
300
+
301
+ alias_method :load_without_require_hooks, :load
302
+ def load(path, wrap = false)
303
+ if wrap
304
+ warn "RequireHooks does not support `load(smth, wrap: ...)`. Falling back to original `Kernel#load`"
305
+ return load_without_require_hooks(path, wrap)
306
+ end
307
+
308
+ path = path.to_path if path.respond_to?(:to_path)
309
+ raise TypeError unless path.respond_to?(:to_str)
310
+
311
+ path = path.to_str
312
+
313
+ raise TypeError unless path.is_a?(::String)
314
+
315
+ realpath =
316
+ if path =~ /^\.\.?\//
317
+ path
318
+ else
319
+ RequireHooks::KernelPatch::Features.feature_path(path, implitic_ext: false)
320
+ end
321
+
322
+ return load_without_require_hooks(path, wrap) unless realpath
323
+
324
+ RequireHooks::KernelPatch.load(realpath)
325
+ rescue Errno::ENOENT, Errno::EACCES
326
+ raise LoadError, "cannot load such file -- #{path}"
327
+ rescue LoadError => e
328
+ warn "RuquireHooks failed to load '#{path}': #{e.message}"
329
+ load_without_require_hooks(path)
330
+ end
331
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RequireHooks
4
+ module LoadIseq
5
+ def load_iseq(path)
6
+ RequireHooks.run_around_load_callbacks(path) do
7
+ new_contents = RequireHooks.perform_source_transform(path)
8
+ hijacked = RequireHooks.try_hijack_load(path, new_contents)
9
+
10
+ if hijacked
11
+ raise TypeError, "Unsupported bytecode format for #{path}: #{hijack.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence)
12
+ return hijacked
13
+ elsif new_contents
14
+ return RubyVM::InstructionSequence.compile(new_contents, path, path, 1)
15
+ end
16
+
17
+ defined?(super) ? super : RubyVM::InstructionSequence.compile_file(path)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ if RubyVM::InstructionSequence.respond_to?(:load_iseq)
24
+ warn "require-hooks: RubyVM::InstructionSequence.load_iseq is already defined. It won't be used by files processed by require-hooks."
25
+ end
26
+
27
+ RubyVM::InstructionSequence.singleton_class.prepend(RequireHooks::LoadIseq)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "require-hooks/api"
4
+
5
+ mode = ENV["REQUIRE_HOOKS_MODE"]
6
+
7
+ case mode
8
+ when "patch"
9
+ require "require-hooks/mode/kernel_patch"
10
+ when "load_iseq"
11
+ require "require-hooks/mode/load_iseq"
12
+ when "bootsnap"
13
+ require "require-hooks/mode/bootsnap"
14
+ else
15
+ if defined?(::RubyVM::InstructionSequence)
16
+ # Check if Bootsnap has been loaded.
17
+ # Based on https://github.com/kddeisz/preval/blob/master/lib/preval.rb
18
+ if RubyVM::InstructionSequence.respond_to?(:load_iseq) &&
19
+ (load_iseq = RubyVM::InstructionSequence.method(:load_iseq)) &&
20
+ load_iseq.source_location[0].include?("/bootsnap/")
21
+ require "require-hooks/mode/bootsnap"
22
+ else
23
+ require "require-hooks/mode/load_iseq"
24
+ end
25
+ else
26
+ require "require-hooks/mode/kernel_patch"
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RequireHooks
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "require-hooks/version"
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: require-hooks
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Vladimir Dementyev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-07-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Require Hooks provide infrastructure for intercepting require/load calls
14
+ in Ruby
15
+ email:
16
+ - dementiev.vm@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE.txt
23
+ - README.md
24
+ - lib/require-hooks.rb
25
+ - lib/require-hooks/api.rb
26
+ - lib/require-hooks/mode/bootsnap.rb
27
+ - lib/require-hooks/mode/kernel_patch.rb
28
+ - lib/require-hooks/mode/load_iseq.rb
29
+ - lib/require-hooks/setup.rb
30
+ - lib/require-hooks/version.rb
31
+ homepage: https://github.com/ruby-next/ruby-next
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ bug_tracker_uri: https://github.com/ruby-next/ruby-next/issues
36
+ changelog_uri: https://github.com/ruby-next/ruby-next/blob/master/CHANGELOG.md
37
+ documentation_uri: https://github.com/ruby-next/ruby-next/blob/master/README.md
38
+ homepage_uri: https://github.com/ruby-next/ruby-next
39
+ source_code_uri: https://github.com/ruby-next/ruby-next
40
+ funding_uri: https://github.com/sponsors/palkan
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '2.2'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.4.8
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: Require Hooks provide infrastructure for intercepting require/load calls
60
+ in Ruby.
61
+ test_files: []