bootsnap 1.1.0-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +7 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +31 -0
- data/CONTRIBUTING.md +21 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +284 -0
- data/Rakefile +11 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/testunit +8 -0
- data/bootsnap.gemspec +39 -0
- data/dev.yml +8 -0
- data/ext/bootsnap/bootsnap.c +742 -0
- data/ext/bootsnap/bootsnap.h +6 -0
- data/ext/bootsnap/extconf.rb +17 -0
- data/lib/bootsnap.rb +39 -0
- data/lib/bootsnap/compile_cache.rb +15 -0
- data/lib/bootsnap/compile_cache/iseq.rb +71 -0
- data/lib/bootsnap/compile_cache/yaml.rb +57 -0
- data/lib/bootsnap/explicit_require.rb +44 -0
- data/lib/bootsnap/load_path_cache.rb +52 -0
- data/lib/bootsnap/load_path_cache/cache.rb +191 -0
- data/lib/bootsnap/load_path_cache/change_observer.rb +56 -0
- data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +73 -0
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +88 -0
- data/lib/bootsnap/load_path_cache/path.rb +113 -0
- data/lib/bootsnap/load_path_cache/path_scanner.rb +42 -0
- data/lib/bootsnap/load_path_cache/store.rb +77 -0
- data/lib/bootsnap/setup.rb +47 -0
- data/lib/bootsnap/version.rb +3 -0
- metadata +160 -0
data/bootsnap.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'bootsnap/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "bootsnap"
|
8
|
+
spec.version = Bootsnap::VERSION
|
9
|
+
spec.authors = ["Burke Libbey"]
|
10
|
+
spec.email = ["burke.libbey@shopify.com"]
|
11
|
+
|
12
|
+
spec.license = "MIT"
|
13
|
+
|
14
|
+
spec.summary = "wip"
|
15
|
+
spec.description = "wip."
|
16
|
+
spec.homepage = "https://github.com/Shopify/bootsnap"
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.required_ruby_version = '>= 2.0.0'
|
24
|
+
|
25
|
+
if RUBY_PLATFORM =~ /java/
|
26
|
+
spec.platform = 'java'
|
27
|
+
else
|
28
|
+
spec.platform = Gem::Platform::RUBY
|
29
|
+
spec.extensions = ['ext/bootsnap/extconf.rb']
|
30
|
+
end
|
31
|
+
|
32
|
+
spec.add_development_dependency "bundler", '~> 1'
|
33
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
34
|
+
spec.add_development_dependency 'rake-compiler', '~> 0'
|
35
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
36
|
+
spec.add_development_dependency "mocha", "~> 1.2"
|
37
|
+
|
38
|
+
spec.add_runtime_dependency "msgpack", "~> 1.0"
|
39
|
+
end
|
data/dev.yml
ADDED
@@ -0,0 +1,742 @@
|
|
1
|
+
/*
|
2
|
+
* Suggested reading order:
|
3
|
+
* 1. Skim Init_bootsnap
|
4
|
+
* 2. Skim bs_fetch
|
5
|
+
* 3. The rest of everything
|
6
|
+
*
|
7
|
+
* Init_bootsnap sets up the ruby objects and binds bs_fetch to
|
8
|
+
* Bootsnap::CompileCache::Native.fetch.
|
9
|
+
*
|
10
|
+
* bs_fetch is the ultimate caller for for just about every other function in
|
11
|
+
* here.
|
12
|
+
*/
|
13
|
+
|
14
|
+
#include "bootsnap.h"
|
15
|
+
#include "ruby.h"
|
16
|
+
#include <stdint.h>
|
17
|
+
#include <sys/types.h>
|
18
|
+
#include <errno.h>
|
19
|
+
#include <fcntl.h>
|
20
|
+
#include <sys/stat.h>
|
21
|
+
#ifndef _WIN32
|
22
|
+
#include <sys/utsname.h>
|
23
|
+
#endif
|
24
|
+
|
25
|
+
/* 1000 is an arbitrary limit; FNV64 plus some slashes brings the cap down to
|
26
|
+
* 981 for the cache dir */
|
27
|
+
#define MAX_CACHEPATH_SIZE 1000
|
28
|
+
#define MAX_CACHEDIR_SIZE 981
|
29
|
+
|
30
|
+
#define KEY_SIZE 64
|
31
|
+
|
32
|
+
/*
|
33
|
+
* An instance of this key is written as the first 64 bytes of each cache file.
|
34
|
+
* The mtime and size members track whether the file contents have changed, and
|
35
|
+
* the version, os_version, compile_option, and ruby_revision members track
|
36
|
+
* changes to the environment that could invalidate compile results without
|
37
|
+
* file contents having changed. The data_size member is not truly part of the
|
38
|
+
* "key". Really, this could be called a "header" with the first six members
|
39
|
+
* being an embedded "key" struct and an additional data_size member.
|
40
|
+
*
|
41
|
+
* The data_size indicates the remaining number of bytes in the cache file
|
42
|
+
* after the header (the size of the cached artifact).
|
43
|
+
*
|
44
|
+
* After data_size, the struct is padded to 64 bytes.
|
45
|
+
*/
|
46
|
+
struct bs_cache_key {
|
47
|
+
uint32_t version;
|
48
|
+
uint32_t os_version;
|
49
|
+
uint32_t compile_option;
|
50
|
+
uint32_t ruby_revision;
|
51
|
+
uint64_t size;
|
52
|
+
uint64_t mtime;
|
53
|
+
uint64_t data_size; /* not used for equality */
|
54
|
+
uint8_t pad[24];
|
55
|
+
} __attribute__((packed));
|
56
|
+
|
57
|
+
/*
|
58
|
+
* If the struct padding isn't correct to pad the key to 64 bytes, refuse to
|
59
|
+
* compile.
|
60
|
+
*/
|
61
|
+
#define STATIC_ASSERT(X) STATIC_ASSERT2(X,__LINE__)
|
62
|
+
#define STATIC_ASSERT2(X,L) STATIC_ASSERT3(X,L)
|
63
|
+
#define STATIC_ASSERT3(X,L) STATIC_ASSERT_MSG(X,at_line_##L)
|
64
|
+
#define STATIC_ASSERT_MSG(COND,MSG) typedef char static_assertion_##MSG[(!!(COND))*2-1]
|
65
|
+
STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE);
|
66
|
+
|
67
|
+
/* Effectively a schema version. Bumping invalidates all previous caches */
|
68
|
+
static const uint32_t current_version = 2;
|
69
|
+
|
70
|
+
/* Derived from kernel or libc version; intended to roughly correspond to when
|
71
|
+
* ABIs have changed, requiring recompilation of native gems. */
|
72
|
+
static uint32_t current_os_version;
|
73
|
+
/* Invalidates cache when switching ruby versions */
|
74
|
+
static uint32_t current_ruby_revision;
|
75
|
+
/* Invalidates cache when RubyVM::InstructionSequence.compile_option changes */
|
76
|
+
static uint32_t current_compile_option_crc32 = 0;
|
77
|
+
|
78
|
+
/* Bootsnap::CompileCache::{Native, Uncompilable} */
|
79
|
+
static VALUE rb_mBootsnap;
|
80
|
+
static VALUE rb_mBootsnap_CompileCache;
|
81
|
+
static VALUE rb_mBootsnap_CompileCache_Native;
|
82
|
+
static VALUE rb_eBootsnap_CompileCache_Uncompilable;
|
83
|
+
static ID uncompilable;
|
84
|
+
|
85
|
+
/* Functions exposed as module functions on Bootsnap::CompileCache::Native */
|
86
|
+
static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v);
|
87
|
+
static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
|
88
|
+
|
89
|
+
/* Helpers */
|
90
|
+
static uint64_t fnv1a_64(const char *str);
|
91
|
+
static void bs_cache_path(const char * cachedir, const char * path, char ** cache_path);
|
92
|
+
static int bs_read_key(int fd, struct bs_cache_key * key);
|
93
|
+
static int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2);
|
94
|
+
static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler);
|
95
|
+
static int open_current_file(char * path, struct bs_cache_key * key);
|
96
|
+
static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag);
|
97
|
+
static VALUE prot_exception_for_errno(VALUE err);
|
98
|
+
static uint32_t get_os_version(void);
|
99
|
+
|
100
|
+
/*
|
101
|
+
* Helper functions to call ruby methods on handler object without crashing on
|
102
|
+
* exception.
|
103
|
+
*/
|
104
|
+
static int bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data);
|
105
|
+
static VALUE prot_storage_to_output(VALUE arg);
|
106
|
+
static VALUE prot_input_to_output(VALUE arg);
|
107
|
+
static void bs_input_to_output(VALUE handler, VALUE input_data, VALUE * output_data, int * exception_tag);
|
108
|
+
static VALUE prot_input_to_storage(VALUE arg);
|
109
|
+
static int bs_input_to_storage(VALUE handler, VALUE input_data, VALUE pathval, VALUE * storage_data);
|
110
|
+
struct s2o_data;
|
111
|
+
struct i2o_data;
|
112
|
+
struct i2s_data;
|
113
|
+
|
114
|
+
/* https://bugs.ruby-lang.org/issues/13667 */
|
115
|
+
extern VALUE rb_get_coverages(void);
|
116
|
+
static VALUE
|
117
|
+
bs_rb_coverage_running(VALUE self)
|
118
|
+
{
|
119
|
+
VALUE cov = rb_get_coverages();
|
120
|
+
return RTEST(cov) ? Qtrue : Qfalse;
|
121
|
+
}
|
122
|
+
|
123
|
+
/*
|
124
|
+
* Ruby C extensions are initialized by calling Init_<extname>.
|
125
|
+
*
|
126
|
+
* This sets up the module hierarchy and attaches functions as methods.
|
127
|
+
*
|
128
|
+
* We also populate some semi-static information about the current OS and so on.
|
129
|
+
*/
|
130
|
+
void
|
131
|
+
Init_bootsnap(void)
|
132
|
+
{
|
133
|
+
rb_mBootsnap = rb_define_module("Bootsnap");
|
134
|
+
rb_mBootsnap_CompileCache = rb_define_module_under(rb_mBootsnap, "CompileCache");
|
135
|
+
rb_mBootsnap_CompileCache_Native = rb_define_module_under(rb_mBootsnap_CompileCache, "Native");
|
136
|
+
rb_eBootsnap_CompileCache_Uncompilable = rb_define_class_under(rb_mBootsnap_CompileCache, "Uncompilable", rb_eStandardError);
|
137
|
+
|
138
|
+
current_ruby_revision = FIX2INT(rb_const_get(rb_cObject, rb_intern("RUBY_REVISION")));
|
139
|
+
current_os_version = get_os_version();
|
140
|
+
|
141
|
+
uncompilable = rb_intern("__bootsnap_uncompilable__");
|
142
|
+
|
143
|
+
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "coverage_running?", bs_rb_coverage_running, 0);
|
144
|
+
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 3);
|
145
|
+
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "compile_option_crc32=", bs_compile_option_crc32_set, 1);
|
146
|
+
}
|
147
|
+
|
148
|
+
/*
|
149
|
+
* Bootsnap's ruby code registers a hook that notifies us via this function
|
150
|
+
* when compile_option changes. These changes invalidate all existing caches.
|
151
|
+
*/
|
152
|
+
static VALUE
|
153
|
+
bs_compile_option_crc32_set(VALUE self, VALUE crc32_v)
|
154
|
+
{
|
155
|
+
Check_Type(crc32_v, T_FIXNUM);
|
156
|
+
current_compile_option_crc32 = FIX2UINT(crc32_v);
|
157
|
+
return Qnil;
|
158
|
+
}
|
159
|
+
|
160
|
+
/*
|
161
|
+
* We use FNV1a-64 to derive cache paths. The choice is somewhat arbitrary but
|
162
|
+
* it has several nice properties:
|
163
|
+
*
|
164
|
+
* - Tiny implementation
|
165
|
+
* - No external dependency
|
166
|
+
* - Solid performance
|
167
|
+
* - Solid randomness
|
168
|
+
* - 32 bits doesn't feel collision-resistant enough; 64 is nice.
|
169
|
+
*/
|
170
|
+
static uint64_t
|
171
|
+
fnv1a_64(const char *str)
|
172
|
+
{
|
173
|
+
unsigned char *s = (unsigned char *)str;
|
174
|
+
uint64_t h = (uint64_t)0xcbf29ce484222325ULL;
|
175
|
+
|
176
|
+
while (*s) {
|
177
|
+
h ^= (uint64_t)*s++;
|
178
|
+
h += (h << 1) + (h << 4) + (h << 5) + (h << 7) + (h << 8) + (h << 40);
|
179
|
+
}
|
180
|
+
|
181
|
+
return h;
|
182
|
+
}
|
183
|
+
|
184
|
+
/*
|
185
|
+
* The idea here is that we want a cache key member that changes when the OS
|
186
|
+
* changes in such a way as to make existing compiled ISeqs unloadable.
|
187
|
+
*/
|
188
|
+
static uint32_t
|
189
|
+
get_os_version(void)
|
190
|
+
{
|
191
|
+
#ifdef _WIN32
|
192
|
+
return (uint32_t)GetVersion();
|
193
|
+
#else
|
194
|
+
uint64_t hash;
|
195
|
+
struct utsname utsname;
|
196
|
+
|
197
|
+
/* Not worth crashing if this fails; lose cache invalidation potential */
|
198
|
+
if (uname(&utsname) < 0) return 0;
|
199
|
+
|
200
|
+
hash = fnv1a_64(utsname.version);
|
201
|
+
|
202
|
+
return (uint32_t)(hash >> 32);
|
203
|
+
#endif
|
204
|
+
}
|
205
|
+
|
206
|
+
/*
|
207
|
+
* Given a cache root directory and the full path to a file being cached,
|
208
|
+
* generate a path under the cache directory at which the cached artifact will
|
209
|
+
* be stored.
|
210
|
+
*
|
211
|
+
* The path will look something like: <cachedir>/12/34567890abcdef
|
212
|
+
*/
|
213
|
+
static void
|
214
|
+
bs_cache_path(const char * cachedir, const char * path, char ** cache_path)
|
215
|
+
{
|
216
|
+
uint64_t hash = fnv1a_64(path);
|
217
|
+
|
218
|
+
uint8_t first_byte = (hash >> (64 - 8));
|
219
|
+
uint64_t remainder = hash & 0x00ffffffffffffff;
|
220
|
+
|
221
|
+
sprintf(*cache_path, "%s/%02x/%014llx", cachedir, first_byte, remainder);
|
222
|
+
}
|
223
|
+
|
224
|
+
/*
|
225
|
+
* Test whether a newly-generated cache key based on the file as it exists on
|
226
|
+
* disk matches the one that was generated when the file was cached (or really
|
227
|
+
* compare any two keys).
|
228
|
+
*
|
229
|
+
* The data_size member is not compared, as it serves more of a "header"
|
230
|
+
* function.
|
231
|
+
*/
|
232
|
+
static int
|
233
|
+
cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2)
|
234
|
+
{
|
235
|
+
return (
|
236
|
+
k1->version == k2->version &&
|
237
|
+
k1->os_version == k2->os_version &&
|
238
|
+
k1->compile_option == k2->compile_option &&
|
239
|
+
k1->ruby_revision == k2->ruby_revision &&
|
240
|
+
k1->size == k2->size &&
|
241
|
+
k1->mtime == k2->mtime
|
242
|
+
);
|
243
|
+
}
|
244
|
+
|
245
|
+
/*
|
246
|
+
* Entrypoint for Bootsnap::CompileCache::Native.fetch. The real work is done
|
247
|
+
* in bs_fetch; this function just performs some basic typechecks and
|
248
|
+
* conversions on the ruby VALUE arguments before passing them along.
|
249
|
+
*/
|
250
|
+
static VALUE
|
251
|
+
bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
|
252
|
+
{
|
253
|
+
Check_Type(cachedir_v, T_STRING);
|
254
|
+
Check_Type(path_v, T_STRING);
|
255
|
+
|
256
|
+
if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
|
257
|
+
rb_raise(rb_eArgError, "cachedir too long");
|
258
|
+
}
|
259
|
+
|
260
|
+
char * cachedir = RSTRING_PTR(cachedir_v);
|
261
|
+
char * path = RSTRING_PTR(path_v);
|
262
|
+
char cache_path[MAX_CACHEPATH_SIZE];
|
263
|
+
|
264
|
+
{ /* generate cache path to cache_path */
|
265
|
+
char * tmp = (char *)&cache_path;
|
266
|
+
bs_cache_path(cachedir, path, &tmp);
|
267
|
+
}
|
268
|
+
|
269
|
+
return bs_fetch(path, path_v, cache_path, handler);
|
270
|
+
}
|
271
|
+
|
272
|
+
/*
|
273
|
+
* Open the file we want to load/cache and generate a cache key for it if it
|
274
|
+
* was loaded.
|
275
|
+
*/
|
276
|
+
static int
|
277
|
+
open_current_file(char * path, struct bs_cache_key * key)
|
278
|
+
{
|
279
|
+
struct stat statbuf;
|
280
|
+
int fd;
|
281
|
+
|
282
|
+
fd = open(path, O_RDONLY);
|
283
|
+
if (fd < 0) return fd;
|
284
|
+
#ifdef _WIN32
|
285
|
+
setmode(fd, O_BINARY);
|
286
|
+
#endif
|
287
|
+
|
288
|
+
if (fstat(fd, &statbuf) < 0) {
|
289
|
+
close(fd);
|
290
|
+
return -1;
|
291
|
+
}
|
292
|
+
|
293
|
+
key->version = current_version;
|
294
|
+
key->os_version = current_os_version;
|
295
|
+
key->compile_option = current_compile_option_crc32;
|
296
|
+
key->ruby_revision = current_ruby_revision;
|
297
|
+
key->size = (uint64_t)statbuf.st_size;
|
298
|
+
key->mtime = (uint64_t)statbuf.st_mtime;
|
299
|
+
|
300
|
+
return fd;
|
301
|
+
}
|
302
|
+
|
303
|
+
#define ERROR_WITH_ERRNO -1
|
304
|
+
#define CACHE_MISSING_OR_INVALID -2
|
305
|
+
|
306
|
+
/*
|
307
|
+
* Read the cache key from the given fd, which must have position 0 (e.g.
|
308
|
+
* freshly opened file).
|
309
|
+
*
|
310
|
+
* Possible return values:
|
311
|
+
* - 0 (OK, key was loaded)
|
312
|
+
* - CACHE_MISSING_OR_INVALID (-2)
|
313
|
+
* - ERROR_WITH_ERRNO (-1, errno is set)
|
314
|
+
*/
|
315
|
+
static int
|
316
|
+
bs_read_key(int fd, struct bs_cache_key * key)
|
317
|
+
{
|
318
|
+
ssize_t nread = read(fd, key, KEY_SIZE);
|
319
|
+
if (nread < 0) return ERROR_WITH_ERRNO;
|
320
|
+
if (nread < KEY_SIZE) return CACHE_MISSING_OR_INVALID;
|
321
|
+
return 0;
|
322
|
+
}
|
323
|
+
|
324
|
+
/*
|
325
|
+
* Open the cache file at a given path, if it exists, and read its key into the
|
326
|
+
* struct.
|
327
|
+
*
|
328
|
+
* Possible return values:
|
329
|
+
* - 0 (OK, key was loaded)
|
330
|
+
* - CACHE_MISSING_OR_INVALID (-2)
|
331
|
+
* - ERROR_WITH_ERRNO (-1, errno is set)
|
332
|
+
*/
|
333
|
+
static int
|
334
|
+
open_cache_file(const char * path, struct bs_cache_key * key)
|
335
|
+
{
|
336
|
+
int fd, res;
|
337
|
+
|
338
|
+
fd = open(path, O_RDONLY);
|
339
|
+
if (fd < 0) {
|
340
|
+
if (errno == ENOENT) return CACHE_MISSING_OR_INVALID;
|
341
|
+
return ERROR_WITH_ERRNO;
|
342
|
+
}
|
343
|
+
#ifdef _WIN32
|
344
|
+
setmode(fd, O_BINARY);
|
345
|
+
#endif
|
346
|
+
|
347
|
+
res = bs_read_key(fd, key);
|
348
|
+
if (res < 0) {
|
349
|
+
close(fd);
|
350
|
+
return res;
|
351
|
+
}
|
352
|
+
|
353
|
+
return fd;
|
354
|
+
}
|
355
|
+
|
356
|
+
/*
|
357
|
+
* The cache file is laid out like:
|
358
|
+
* 0...64 : bs_cache_key
|
359
|
+
* 64..-1 : cached artifact
|
360
|
+
*
|
361
|
+
* This function takes a file descriptor whose position is pre-set to 64, and
|
362
|
+
* the data_size (corresponding to the remaining number of bytes) listed in the
|
363
|
+
* cache header.
|
364
|
+
*
|
365
|
+
* We load the text from this file into a buffer, and pass it to the ruby-land
|
366
|
+
* handler with exception handling via the exception_tag param.
|
367
|
+
*
|
368
|
+
* Data is returned via the output_data parameter, which, if there's no error
|
369
|
+
* or exception, will be the final data returnable to the user.
|
370
|
+
*/
|
371
|
+
static int
|
372
|
+
fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag)
|
373
|
+
{
|
374
|
+
char * data = NULL;
|
375
|
+
ssize_t nread;
|
376
|
+
int ret;
|
377
|
+
|
378
|
+
VALUE storage_data;
|
379
|
+
|
380
|
+
if (data_size > 100000000000) {
|
381
|
+
errno = EINVAL; /* because wtf? */
|
382
|
+
ret = -1;
|
383
|
+
goto done;
|
384
|
+
}
|
385
|
+
data = ALLOC_N(char, data_size);
|
386
|
+
nread = read(fd, data, data_size);
|
387
|
+
if (nread < 0) {
|
388
|
+
ret = -1;
|
389
|
+
goto done;
|
390
|
+
}
|
391
|
+
if (nread != data_size) {
|
392
|
+
ret = CACHE_MISSING_OR_INVALID;
|
393
|
+
goto done;
|
394
|
+
}
|
395
|
+
|
396
|
+
storage_data = rb_str_new_static(data, data_size);
|
397
|
+
|
398
|
+
*exception_tag = bs_storage_to_output(handler, storage_data, output_data);
|
399
|
+
ret = 0;
|
400
|
+
done:
|
401
|
+
if (data != NULL) xfree(data);
|
402
|
+
return ret;
|
403
|
+
}
|
404
|
+
|
405
|
+
/*
|
406
|
+
* Like mkdir -p, this recursively creates directory parents of a file. e.g.
|
407
|
+
* given /a/b/c, creates /a and /a/b.
|
408
|
+
*/
|
409
|
+
static int
|
410
|
+
mkpath(char * file_path, mode_t mode)
|
411
|
+
{
|
412
|
+
/* It would likely be more efficient to count back until we
|
413
|
+
* find a component that *does* exist, but this will only run
|
414
|
+
* at most 256 times, so it seems not worthwhile to change. */
|
415
|
+
char * p;
|
416
|
+
for (p = strchr(file_path + 1, '/'); p; p = strchr(p + 1, '/')) {
|
417
|
+
*p = '\0';
|
418
|
+
#ifdef _WIN32
|
419
|
+
if (mkdir(file_path) == -1) {
|
420
|
+
#else
|
421
|
+
if (mkdir(file_path, mode) == -1) {
|
422
|
+
#endif
|
423
|
+
if (errno != EEXIST) {
|
424
|
+
*p = '/';
|
425
|
+
return -1;
|
426
|
+
}
|
427
|
+
}
|
428
|
+
*p = '/';
|
429
|
+
}
|
430
|
+
return 0;
|
431
|
+
}
|
432
|
+
|
433
|
+
/*
|
434
|
+
* Write a cache header/key and a compiled artifact to a given cache path by
|
435
|
+
* writing to a tmpfile and then renaming the tmpfile over top of the final
|
436
|
+
* path.
|
437
|
+
*/
|
438
|
+
static int
|
439
|
+
atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data)
|
440
|
+
{
|
441
|
+
char template[MAX_CACHEPATH_SIZE + 20];
|
442
|
+
char * dest;
|
443
|
+
char * tmp_path;
|
444
|
+
int fd;
|
445
|
+
ssize_t nwrite;
|
446
|
+
|
447
|
+
dest = strncpy(template, path, MAX_CACHEPATH_SIZE);
|
448
|
+
strcat(dest, ".tmp.XXXXXX");
|
449
|
+
|
450
|
+
tmp_path = mktemp(template);
|
451
|
+
fd = open(tmp_path, O_WRONLY | O_CREAT, 0644);
|
452
|
+
if (fd < 0) {
|
453
|
+
if (mkpath(path, 0755) < 0) return -1;
|
454
|
+
fd = open(tmp_path, O_WRONLY | O_CREAT, 0644);
|
455
|
+
if (fd < 0) return -1;
|
456
|
+
}
|
457
|
+
#ifdef _WIN32
|
458
|
+
setmode(fd, O_BINARY);
|
459
|
+
#endif
|
460
|
+
|
461
|
+
key->data_size = RSTRING_LEN(data);
|
462
|
+
nwrite = write(fd, key, KEY_SIZE);
|
463
|
+
if (nwrite < 0) return -1;
|
464
|
+
if (nwrite != KEY_SIZE) {
|
465
|
+
errno = EIO; /* Lies but whatever */
|
466
|
+
return -1;
|
467
|
+
}
|
468
|
+
|
469
|
+
nwrite = write(fd, RSTRING_PTR(data), RSTRING_LEN(data));
|
470
|
+
if (nwrite < 0) return -1;
|
471
|
+
if (nwrite != RSTRING_LEN(data)) {
|
472
|
+
errno = EIO; /* Lies but whatever */
|
473
|
+
return -1;
|
474
|
+
}
|
475
|
+
|
476
|
+
close(fd);
|
477
|
+
return rename(tmp_path, path);
|
478
|
+
}
|
479
|
+
|
480
|
+
/*
|
481
|
+
* Given an errno value (converted to a ruby Fixnum), return the corresponding
|
482
|
+
* Errno::* constant. If none is found, return StandardError instead.
|
483
|
+
*/
|
484
|
+
static VALUE
|
485
|
+
prot_exception_for_errno(VALUE err)
|
486
|
+
{
|
487
|
+
if (err != INT2FIX(0)) {
|
488
|
+
VALUE mErrno = rb_const_get(rb_cObject, rb_intern("Errno"));
|
489
|
+
VALUE constants = rb_funcall(mErrno, rb_intern("constants"), 0);
|
490
|
+
VALUE which = rb_funcall(constants, rb_intern("[]"), 1, err);
|
491
|
+
return rb_funcall(mErrno, rb_intern("const_get"), 1, which);
|
492
|
+
}
|
493
|
+
return rb_eStandardError;
|
494
|
+
}
|
495
|
+
|
496
|
+
|
497
|
+
/* Read contents from an fd, whose contents are asserted to be +size+ bytes
|
498
|
+
* long, into a buffer */
|
499
|
+
static ssize_t
|
500
|
+
bs_read_contents(int fd, size_t size, char ** contents)
|
501
|
+
{
|
502
|
+
*contents = ALLOC_N(char, size);
|
503
|
+
return read(fd, *contents, size);
|
504
|
+
}
|
505
|
+
|
506
|
+
/*
|
507
|
+
* This is the meat of the extension. bs_fetch is
|
508
|
+
* Bootsnap::CompileCache::Native.fetch.
|
509
|
+
*
|
510
|
+
* There are three "formats" in use here:
|
511
|
+
* 1. "input" fomat, which is what we load from the source file;
|
512
|
+
* 2. "storage" format, which we write to the cache;
|
513
|
+
* 3. "output" format, which is what we return.
|
514
|
+
*
|
515
|
+
* E.g., For ISeq compilation:
|
516
|
+
* input: ruby source, as text
|
517
|
+
* storage: binary string (RubyVM::InstructionSequence#to_binary)
|
518
|
+
* output: Instance of RubyVM::InstructionSequence
|
519
|
+
*
|
520
|
+
* And for YAML:
|
521
|
+
* input: yaml as text
|
522
|
+
* storage: MessagePack or Marshal text
|
523
|
+
* output: ruby object, loaded from yaml/messagepack/marshal
|
524
|
+
*
|
525
|
+
* The handler passed in must support three messages:
|
526
|
+
* * storage_to_output(s) -> o
|
527
|
+
* * input_to_output(i) -> o
|
528
|
+
* * input_to_storage(i) -> s
|
529
|
+
* (input_to_storage may raise Bootsnap::CompileCache::Uncompilable, which
|
530
|
+
* will prevent caching and cause output to be generated with
|
531
|
+
* input_to_output)
|
532
|
+
*
|
533
|
+
* The semantics of this function are basically:
|
534
|
+
*
|
535
|
+
* return storage_to_output(cache[path]) if cache[path]
|
536
|
+
* storage = input_to_storage(input)
|
537
|
+
* cache[path] = storage
|
538
|
+
* return storage_to_output(storage)
|
539
|
+
*
|
540
|
+
* Or expanded a bit:
|
541
|
+
*
|
542
|
+
* - Check if the cache file exists and is up to date.
|
543
|
+
* - If it is, load this data to storage_data.
|
544
|
+
* - return storage_to_output(storage_data)
|
545
|
+
* - Read the file to input_data
|
546
|
+
* - Generate storage_data using input_to_storage(input_data)
|
547
|
+
* - Write storage_data data, with a cache key, to the cache file.
|
548
|
+
* - Return storage_to_output(storage_data)
|
549
|
+
*/
|
550
|
+
static VALUE
|
551
|
+
bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
|
552
|
+
{
|
553
|
+
struct bs_cache_key cached_key, current_key;
|
554
|
+
char * contents = NULL;
|
555
|
+
int cache_fd = -1, current_fd = -1;
|
556
|
+
int res, valid_cache, exception_tag = 0;
|
557
|
+
|
558
|
+
VALUE input_data; /* data read from source file, e.g. YAML or ruby source */
|
559
|
+
VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
|
560
|
+
VALUE output_data; /* return data, e.g. ruby hash or loaded iseq */
|
561
|
+
|
562
|
+
VALUE exception; /* ruby exception object to raise instead of returning */
|
563
|
+
|
564
|
+
/* Open the source file and generate a cache key for it */
|
565
|
+
current_fd = open_current_file(path, ¤t_key);
|
566
|
+
if (current_fd < 0) goto fail_errno;
|
567
|
+
|
568
|
+
/* Open the cache key if it exists, and read its cache key in */
|
569
|
+
cache_fd = open_cache_file(cache_path, &cached_key);
|
570
|
+
if (cache_fd < 0 && cache_fd != CACHE_MISSING_OR_INVALID) goto fail_errno;
|
571
|
+
|
572
|
+
/* True if the cache existed and no invalidating changes have occurred since
|
573
|
+
* it was generated. */
|
574
|
+
valid_cache = cache_key_equal(¤t_key, &cached_key);
|
575
|
+
|
576
|
+
if (valid_cache) {
|
577
|
+
/* Fetch the cache data and return it if we're able to load it successfully */
|
578
|
+
res = fetch_cached_data(cache_fd, (ssize_t)cached_key.data_size, handler, &output_data, &exception_tag);
|
579
|
+
if (exception_tag != 0) goto raise;
|
580
|
+
else if (res == CACHE_MISSING_OR_INVALID) valid_cache = 0;
|
581
|
+
else if (res == ERROR_WITH_ERRNO) goto fail_errno;
|
582
|
+
else if (!NIL_P(output_data)) goto succeed; /* fast-path, goal */
|
583
|
+
}
|
584
|
+
close(cache_fd);
|
585
|
+
cache_fd = -1;
|
586
|
+
/* Cache is stale, invalid, or missing. Regenerate and write it out. */
|
587
|
+
|
588
|
+
/* Read the contents of the source file into a buffer */
|
589
|
+
if (bs_read_contents(current_fd, current_key.size, &contents) < 0) goto fail_errno;
|
590
|
+
input_data = rb_str_new_static(contents, current_key.size);
|
591
|
+
|
592
|
+
/* Try to compile the input_data using input_to_storage(input_data) */
|
593
|
+
exception_tag = bs_input_to_storage(handler, input_data, path_v, &storage_data);
|
594
|
+
if (exception_tag != 0) goto raise;
|
595
|
+
/* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try
|
596
|
+
* to cache anything; just return input_to_output(input_data) */
|
597
|
+
if (storage_data == uncompilable) {
|
598
|
+
bs_input_to_output(handler, input_data, &output_data, &exception_tag);
|
599
|
+
if (exception_tag != 0) goto raise;
|
600
|
+
goto succeed;
|
601
|
+
}
|
602
|
+
/* If storage_data isn't a string, we can't cache it */
|
603
|
+
if (!RB_TYPE_P(storage_data, T_STRING)) goto invalid_type_storage_data;
|
604
|
+
|
605
|
+
/* Write the cache key and storage_data to the cache directory */
|
606
|
+
res = atomic_write_cache_file(cache_path, ¤t_key, storage_data);
|
607
|
+
if (res < 0) goto fail_errno;
|
608
|
+
|
609
|
+
/* Having written the cache, now convert storage_data to output_data */
|
610
|
+
exception_tag = bs_storage_to_output(handler, storage_data, &output_data);
|
611
|
+
if (exception_tag != 0) goto raise;
|
612
|
+
|
613
|
+
/* If output_data is nil, delete the cache entry and generate the output
|
614
|
+
* using input_to_output */
|
615
|
+
if (NIL_P(output_data)) {
|
616
|
+
if (unlink(cache_path) < 0) goto fail_errno;
|
617
|
+
bs_input_to_output(handler, input_data, &output_data, &exception_tag);
|
618
|
+
if (exception_tag != 0) goto raise;
|
619
|
+
}
|
620
|
+
|
621
|
+
goto succeed; /* output_data is now the correct return. */
|
622
|
+
|
623
|
+
#define CLEANUP \
|
624
|
+
if (contents != NULL) xfree(contents); \
|
625
|
+
if (current_fd >= 0) close(current_fd); \
|
626
|
+
if (cache_fd >= 0) close(cache_fd);
|
627
|
+
|
628
|
+
succeed:
|
629
|
+
CLEANUP;
|
630
|
+
return output_data;
|
631
|
+
fail_errno:
|
632
|
+
CLEANUP;
|
633
|
+
exception = rb_protect(prot_exception_for_errno, INT2FIX(errno), &res);
|
634
|
+
if (res) exception = rb_eStandardError;
|
635
|
+
rb_exc_raise(exception);
|
636
|
+
__builtin_unreachable();
|
637
|
+
raise:
|
638
|
+
CLEANUP;
|
639
|
+
rb_jump_tag(exception_tag);
|
640
|
+
__builtin_unreachable();
|
641
|
+
invalid_type_storage_data:
|
642
|
+
CLEANUP;
|
643
|
+
Check_Type(storage_data, T_STRING);
|
644
|
+
__builtin_unreachable();
|
645
|
+
|
646
|
+
#undef CLEANUP
|
647
|
+
}
|
648
|
+
|
649
|
+
/*****************************************************************************/
|
650
|
+
/********************* Handler Wrappers **************************************/
|
651
|
+
/*****************************************************************************
|
652
|
+
* Everything after this point in the file is just wrappers to deal with ruby's
|
653
|
+
* clunky method of handling exceptions from ruby methods invoked from C.
|
654
|
+
*/
|
655
|
+
|
656
|
+
struct s2o_data {
|
657
|
+
VALUE handler;
|
658
|
+
VALUE storage_data;
|
659
|
+
};
|
660
|
+
|
661
|
+
struct i2o_data {
|
662
|
+
VALUE handler;
|
663
|
+
VALUE input_data;
|
664
|
+
};
|
665
|
+
|
666
|
+
struct i2s_data {
|
667
|
+
VALUE handler;
|
668
|
+
VALUE input_data;
|
669
|
+
VALUE pathval;
|
670
|
+
};
|
671
|
+
|
672
|
+
static VALUE
|
673
|
+
prot_storage_to_output(VALUE arg)
|
674
|
+
{
|
675
|
+
struct s2o_data * data = (struct s2o_data *)arg;
|
676
|
+
return rb_funcall(data->handler, rb_intern("storage_to_output"), 1, data->storage_data);
|
677
|
+
}
|
678
|
+
|
679
|
+
static int
|
680
|
+
bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data)
|
681
|
+
{
|
682
|
+
int state;
|
683
|
+
struct s2o_data s2o_data = {
|
684
|
+
.handler = handler,
|
685
|
+
.storage_data = storage_data,
|
686
|
+
};
|
687
|
+
*output_data = rb_protect(prot_storage_to_output, (VALUE)&s2o_data, &state);
|
688
|
+
return state;
|
689
|
+
}
|
690
|
+
|
691
|
+
static void
|
692
|
+
bs_input_to_output(VALUE handler, VALUE input_data, VALUE * output_data, int * exception_tag)
|
693
|
+
{
|
694
|
+
struct i2o_data i2o_data = {
|
695
|
+
.handler = handler,
|
696
|
+
.input_data = input_data,
|
697
|
+
};
|
698
|
+
*output_data = rb_protect(prot_input_to_output, (VALUE)&i2o_data, exception_tag);
|
699
|
+
}
|
700
|
+
|
701
|
+
static VALUE
|
702
|
+
prot_input_to_output(VALUE arg)
|
703
|
+
{
|
704
|
+
struct i2o_data * data = (struct i2o_data *)arg;
|
705
|
+
return rb_funcall(data->handler, rb_intern("input_to_output"), 1, data->input_data);
|
706
|
+
}
|
707
|
+
|
708
|
+
static VALUE
|
709
|
+
try_input_to_storage(VALUE arg)
|
710
|
+
{
|
711
|
+
struct i2s_data * data = (struct i2s_data *)arg;
|
712
|
+
return rb_funcall(data->handler, rb_intern("input_to_storage"), 2, data->input_data, data->pathval);
|
713
|
+
}
|
714
|
+
|
715
|
+
static VALUE
|
716
|
+
rescue_input_to_storage(VALUE arg)
|
717
|
+
{
|
718
|
+
return uncompilable;
|
719
|
+
}
|
720
|
+
|
721
|
+
static VALUE
|
722
|
+
prot_input_to_storage(VALUE arg)
|
723
|
+
{
|
724
|
+
struct i2s_data * data = (struct i2s_data *)arg;
|
725
|
+
return rb_rescue2(
|
726
|
+
try_input_to_storage, (VALUE)data,
|
727
|
+
rescue_input_to_storage, Qnil,
|
728
|
+
rb_eBootsnap_CompileCache_Uncompilable, 0);
|
729
|
+
}
|
730
|
+
|
731
|
+
static int
|
732
|
+
bs_input_to_storage(VALUE handler, VALUE input_data, VALUE pathval, VALUE * storage_data)
|
733
|
+
{
|
734
|
+
int state;
|
735
|
+
struct i2s_data i2s_data = {
|
736
|
+
.handler = handler,
|
737
|
+
.input_data = input_data,
|
738
|
+
.pathval = pathval,
|
739
|
+
};
|
740
|
+
*storage_data = rb_protect(prot_input_to_storage, (VALUE)&i2s_data, &state);
|
741
|
+
return state;
|
742
|
+
}
|