require-hooks 0.4.1 → 0.5.1
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 +8 -0
- data/README.md +9 -1
- data/lib/require-hooks/mode/bootsnap.rb +107 -42
- data/lib/require-hooks/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e75b55ac6829e71745efc569fd472288bfc17f85b569413c947f145c8f3a866
|
|
4
|
+
data.tar.gz: 72a6083f17c215f429b7f1ae14b637c38c613cd2f7dffe2a8ead2ce8be2de956
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ccb59bc078a1bd1b748b721427240fb136ccc269148472b4963d5db052f7696b2ba8356bb3a14228561ca4f6e3e830962f182a834d6ae0dd036d0c73faac22d8
|
|
7
|
+
data.tar.gz: 47cd3bc2f622f9062361099e64dd179166f2d72f02e8d1366fa548b14fd62265f1a35b4db046755b84feafc34dab3bbcaa9eb28430600da3ccb09edf2a9db5bb
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## master
|
|
4
4
|
|
|
5
|
+
## 0.5.1 (2026-08-26)
|
|
6
|
+
|
|
7
|
+
- Add `RequireHooks::Bootsnap.add_version_hash` for external cache inputs.
|
|
8
|
+
|
|
9
|
+
## 0.5.0 (2026-08-26)
|
|
10
|
+
|
|
11
|
+
- Use Bootsnap compiler namespaces for cache invalidation without changing Bootsnap's cache directory.
|
|
12
|
+
|
|
5
13
|
## 0.4.1 (2026-07-22)
|
|
6
14
|
|
|
7
15
|
- Bring back Ruby 2.3 compatibility.
|
data/README.md
CHANGED
|
@@ -124,7 +124,15 @@ require "require-hooks/setup"
|
|
|
124
124
|
|
|
125
125
|
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.
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
Require Hooks separates cached instructions by hook configuration. New source transformers and hijackers invalidate affected cache entries automatically.
|
|
128
|
+
|
|
129
|
+
If a transform depends on an external library or configuration, add that input to the cache key:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
RequireHooks::Bootsnap.add_version_hash { "parser-#{Parser::VERSION}-#{options_digest}" }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The value can be a String or a callable that returns a String. Require Hooks hashes each contribution and combines all contributions with its hook key. The callable runs when Require Hooks computes the key, so it does not freeze the key before later hooks register.
|
|
128
136
|
|
|
129
137
|
## Limitations
|
|
130
138
|
|
|
@@ -28,75 +28,136 @@ module RequireHooks
|
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
module
|
|
33
|
-
def
|
|
31
|
+
# Bootsnap before 1.24 does not support compiler namespaces.
|
|
32
|
+
module LegacyCacheExt
|
|
33
|
+
def fetch(path, cache_dir: self.cache_dir)
|
|
34
34
|
ctx = RequireHooks.context_for(path)
|
|
35
|
-
|
|
35
|
+
cache_dir = File.join(cache_dir, RequireHooks::Bootsnap.version_hash) unless ctx.empty?
|
|
36
|
+
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Bootsnap 1.24+ compiler wrapper. Its namespace isolates transformed ISeqs
|
|
42
|
+
# without changing Bootsnap's process-wide cache directory.
|
|
43
|
+
class VersionedCompiler
|
|
44
|
+
attr_reader :namespace, :version_hash
|
|
36
45
|
|
|
46
|
+
def initialize(compiler, version_hash)
|
|
47
|
+
@compiler = compiler
|
|
48
|
+
@version_hash = version_hash
|
|
49
|
+
@namespace = "#{compiler.namespace}-require-hooks-#{version_hash}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def input_to_storage(source, path, *args)
|
|
53
|
+
iseq = compile(RequireHooks.context_for(path), path, nil)
|
|
54
|
+
return @compiler.input_to_storage(source, path, *args) unless iseq
|
|
55
|
+
|
|
56
|
+
iseq.to_binary
|
|
57
|
+
rescue SyntaxError, TypeError
|
|
58
|
+
::Bootsnap::CompileCache::UNCOMPILABLE
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def storage_to_output(*args)
|
|
62
|
+
@compiler.storage_to_output(*args)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def input_to_output(source, path, args)
|
|
66
|
+
compile(RequireHooks.context_for(path), path, args) || @compiler.input_to_output(source, path, args)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def compile(ctx, path, args)
|
|
37
72
|
new_contents = ctx.perform_source_transform(path)
|
|
38
73
|
hijacked = ctx.try_hijack_load(path, new_contents)
|
|
39
74
|
|
|
40
75
|
if hijacked
|
|
41
|
-
raise TypeError, "Unsupported bytecode format for #{path}: #{
|
|
42
|
-
return hijacked
|
|
43
|
-
elsif new_contents
|
|
44
|
-
return RubyVM::InstructionSequence.compile(new_contents, path, path, 1, @compile_options).to_binary
|
|
76
|
+
raise TypeError, "Unsupported bytecode format for #{path}: #{hijacked.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence)
|
|
77
|
+
return hijacked
|
|
45
78
|
end
|
|
46
79
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
80
|
+
return unless new_contents
|
|
81
|
+
|
|
82
|
+
@compiler.input_to_output(new_contents, path, args) ||
|
|
83
|
+
RubyVM::InstructionSequence.compile(new_contents, path, path, 1)
|
|
50
84
|
end
|
|
51
85
|
end
|
|
52
86
|
|
|
53
87
|
module LoadIseqExt
|
|
54
|
-
class << self
|
|
55
|
-
attr_accessor :orig_cache_dir
|
|
56
|
-
end
|
|
57
|
-
|
|
58
88
|
# Around hooks must be performed every time we trigger a file load, even if
|
|
59
89
|
# the file is already cached.
|
|
60
90
|
def load_iseq(path)
|
|
61
91
|
ctx = RequireHooks.context_for(path)
|
|
62
|
-
|
|
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)
|
|
92
|
+
return super if ctx.empty?
|
|
70
93
|
|
|
71
94
|
ctx.run_around_load_callbacks(path) do
|
|
72
|
-
|
|
73
|
-
iseq = super
|
|
95
|
+
iseq = super
|
|
74
96
|
|
|
75
|
-
|
|
97
|
+
# Bootsnap returns nil when the coverage is on,
|
|
98
|
+
# we fallback to our custom #compile_with_coverage
|
|
99
|
+
unless iseq
|
|
100
|
+
next unless defined?(Coverage) && Coverage.running?
|
|
76
101
|
|
|
77
|
-
|
|
78
|
-
# we fallback to our custom #compile_with_coverage
|
|
79
|
-
unless iseq
|
|
80
|
-
next unless defined?(Coverage) && Coverage.running?
|
|
81
|
-
|
|
82
|
-
iseq = RequireHooks::Iseq.compile_with_coverage(ctx, path)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
iseq.eval
|
|
86
|
-
EMPTY_ISEQ
|
|
87
|
-
ensure
|
|
88
|
-
::Bootsnap::CompileCache::ISeq.cache_dir = LoadIseqExt.orig_cache_dir
|
|
102
|
+
iseq = RequireHooks::Iseq.compile_with_coverage(ctx, path)
|
|
89
103
|
end
|
|
104
|
+
|
|
105
|
+
iseq.eval
|
|
106
|
+
EMPTY_ISEQ
|
|
90
107
|
end
|
|
91
108
|
end
|
|
92
109
|
end
|
|
93
110
|
|
|
94
111
|
class << self
|
|
112
|
+
def install_compiler_selector
|
|
113
|
+
iseq = ::Bootsnap::CompileCache::ISeq
|
|
114
|
+
@original_compiler_selector = iseq.compiler_selector
|
|
115
|
+
@compilers = {}.compare_by_identity
|
|
116
|
+
iseq.compiler_selector = method(:compiler_for)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def compiler_for(path)
|
|
120
|
+
iseq = ::Bootsnap::CompileCache::ISeq
|
|
121
|
+
compiler = @original_compiler_selector&.call(path) || iseq.default_compiler
|
|
122
|
+
return compiler if RequireHooks.context_for(path).empty?
|
|
123
|
+
|
|
124
|
+
version_hash = RequireHooks::Bootsnap.version_hash
|
|
125
|
+
cached = @compilers[compiler]
|
|
126
|
+
return cached if cached&.version_hash == version_hash
|
|
127
|
+
|
|
128
|
+
@compilers[compiler] = VersionedCompiler.new(compiler, version_hash)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def add_version_hash(value = nil, &block)
|
|
132
|
+
contributor = block || value
|
|
133
|
+
unless contributor.is_a?(String) || contributor.respond_to?(:call)
|
|
134
|
+
raise ArgumentError, "version hash contribution must be a String or callable"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
@version_hash_contributors ||= []
|
|
138
|
+
@version_hash_contributors << (contributor.is_a?(String) ? contributor.dup.freeze : contributor)
|
|
139
|
+
@compilers&.clear
|
|
140
|
+
contributor
|
|
141
|
+
end
|
|
142
|
+
|
|
95
143
|
def version_hash
|
|
96
|
-
@
|
|
144
|
+
return @version_hash unless @version_hash.nil?
|
|
145
|
+
|
|
146
|
+
context_keys = RequireHooks.contexts.values.map(&:to_cache_key)
|
|
147
|
+
contribution_keys = Array(@version_hash_contributors).map do |contributor|
|
|
148
|
+
value = contributor.respond_to?(:call) ? contributor.call : contributor
|
|
149
|
+
raise TypeError, "version hash contribution must return a String" unless value.is_a?(String)
|
|
150
|
+
|
|
151
|
+
Zlib.crc32(value).to_s
|
|
152
|
+
end.sort
|
|
153
|
+
|
|
154
|
+
(context_keys + contribution_keys).join("-")
|
|
97
155
|
end
|
|
98
156
|
|
|
99
|
-
|
|
157
|
+
def version_hash=(version_hash)
|
|
158
|
+
@version_hash = version_hash
|
|
159
|
+
@compilers&.clear
|
|
160
|
+
end
|
|
100
161
|
end
|
|
101
162
|
end
|
|
102
163
|
|
|
@@ -111,6 +172,10 @@ module RequireHooks
|
|
|
111
172
|
end
|
|
112
173
|
end
|
|
113
174
|
|
|
114
|
-
Bootsnap::CompileCache::ISeq
|
|
115
|
-
|
|
175
|
+
if defined?(Bootsnap::CompileCache::ISeq::Compiler)
|
|
176
|
+
RequireHooks::Bootsnap.install_compiler_selector
|
|
177
|
+
else
|
|
178
|
+
Bootsnap::CompileCache::ISeq.singleton_class.prepend(RequireHooks::Bootsnap::CompileCacheExt)
|
|
179
|
+
Bootsnap::CompileCache::ISeq.singleton_class.prepend(RequireHooks::Bootsnap::LegacyCacheExt)
|
|
180
|
+
end
|
|
116
181
|
RubyVM::InstructionSequence.singleton_class.prepend(RequireHooks::Bootsnap::LoadIseqExt)
|