bootsnap 1.16.0 → 1.18.4
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 +44 -2
- data/README.md +9 -6
- data/ext/bootsnap/bootsnap.c +233 -69
- data/ext/bootsnap/extconf.rb +19 -12
- data/lib/bootsnap/bundler.rb +1 -1
- data/lib/bootsnap/cli.rb +18 -13
- data/lib/bootsnap/compile_cache/iseq.rb +12 -6
- data/lib/bootsnap/compile_cache/json.rb +9 -13
- data/lib/bootsnap/compile_cache/yaml.rb +28 -44
- data/lib/bootsnap/compile_cache.rb +8 -16
- data/lib/bootsnap/explicit_require.rb +5 -0
- data/lib/bootsnap/load_path_cache/cache.rb +9 -1
- data/lib/bootsnap/load_path_cache/change_observer.rb +6 -0
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +6 -6
- data/lib/bootsnap/load_path_cache/path.rb +1 -1
- data/lib/bootsnap/load_path_cache/path_scanner.rb +2 -2
- data/lib/bootsnap/load_path_cache/store.rb +4 -2
- data/lib/bootsnap/load_path_cache.rb +21 -12
- data/lib/bootsnap/setup.rb +1 -1
- data/lib/bootsnap/version.rb +1 -1
- data/lib/bootsnap.rb +38 -12
- metadata +6 -6
data/ext/bootsnap/bootsnap.c
CHANGED
@@ -18,8 +18,19 @@
|
|
18
18
|
#include <sys/types.h>
|
19
19
|
#include <errno.h>
|
20
20
|
#include <fcntl.h>
|
21
|
+
#include <unistd.h>
|
21
22
|
#include <sys/stat.h>
|
22
23
|
|
24
|
+
#ifdef __APPLE__
|
25
|
+
// The symbol is present, however not in the headers
|
26
|
+
// See: https://github.com/Shopify/bootsnap/issues/470
|
27
|
+
extern int fdatasync(int);
|
28
|
+
#endif
|
29
|
+
|
30
|
+
#ifndef O_NOATIME
|
31
|
+
#define O_NOATIME 0
|
32
|
+
#endif
|
33
|
+
|
23
34
|
/* 1000 is an arbitrary limit; FNV64 plus some slashes brings the cap down to
|
24
35
|
* 981 for the cache dir */
|
25
36
|
#define MAX_CACHEPATH_SIZE 1000
|
@@ -30,7 +41,7 @@
|
|
30
41
|
#define MAX_CREATE_TEMPFILE_ATTEMPT 3
|
31
42
|
|
32
43
|
#ifndef RB_UNLIKELY
|
33
|
-
|
44
|
+
#define RB_UNLIKELY(x) (x)
|
34
45
|
#endif
|
35
46
|
|
36
47
|
/*
|
@@ -54,8 +65,10 @@ struct bs_cache_key {
|
|
54
65
|
uint32_t ruby_revision;
|
55
66
|
uint64_t size;
|
56
67
|
uint64_t mtime;
|
57
|
-
uint64_t data_size;
|
58
|
-
|
68
|
+
uint64_t data_size; //
|
69
|
+
uint64_t digest;
|
70
|
+
uint8_t digest_set;
|
71
|
+
uint8_t pad[15];
|
59
72
|
} __attribute__((packed));
|
60
73
|
|
61
74
|
/*
|
@@ -69,7 +82,7 @@ struct bs_cache_key {
|
|
69
82
|
STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE);
|
70
83
|
|
71
84
|
/* Effectively a schema version. Bumping invalidates all previous caches */
|
72
|
-
static const uint32_t current_version =
|
85
|
+
static const uint32_t current_version = 6;
|
73
86
|
|
74
87
|
/* hash of e.g. "x86_64-darwin17", invalidating when ruby is recompiled on a
|
75
88
|
* new OS ABI, etc. */
|
@@ -87,25 +100,36 @@ static VALUE rb_mBootsnap_CompileCache;
|
|
87
100
|
static VALUE rb_mBootsnap_CompileCache_Native;
|
88
101
|
static VALUE rb_cBootsnap_CompileCache_UNCOMPILABLE;
|
89
102
|
static ID instrumentation_method;
|
90
|
-
static VALUE sym_miss;
|
91
|
-
static VALUE sym_stale;
|
103
|
+
static VALUE sym_hit, sym_miss, sym_stale, sym_revalidated;
|
92
104
|
static bool instrumentation_enabled = false;
|
93
105
|
static bool readonly = false;
|
106
|
+
static bool revalidation = false;
|
107
|
+
static bool perm_issue = false;
|
94
108
|
|
95
109
|
/* Functions exposed as module functions on Bootsnap::CompileCache::Native */
|
96
110
|
static VALUE bs_instrumentation_enabled_set(VALUE self, VALUE enabled);
|
97
111
|
static VALUE bs_readonly_set(VALUE self, VALUE enabled);
|
112
|
+
static VALUE bs_revalidation_set(VALUE self, VALUE enabled);
|
98
113
|
static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v);
|
99
114
|
static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args);
|
100
115
|
static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
|
101
116
|
|
102
117
|
/* Helpers */
|
118
|
+
enum cache_status {
|
119
|
+
miss,
|
120
|
+
hit,
|
121
|
+
stale,
|
122
|
+
};
|
103
123
|
static void bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_CACHEPATH_SIZE]);
|
104
124
|
static int bs_read_key(int fd, struct bs_cache_key * key);
|
105
|
-
static
|
125
|
+
static enum cache_status cache_key_equal_fast_path(struct bs_cache_key * k1, struct bs_cache_key * k2);
|
126
|
+
static int cache_key_equal_slow_path(struct bs_cache_key * current_key, struct bs_cache_key * cached_key, const VALUE input_data);
|
127
|
+
static int update_cache_key(struct bs_cache_key *current_key, struct bs_cache_key *old_key, int cache_fd, const char ** errno_provenance);
|
128
|
+
|
129
|
+
static void bs_cache_key_digest(struct bs_cache_key * key, const VALUE input_data);
|
106
130
|
static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args);
|
107
131
|
static VALUE bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler);
|
108
|
-
static int open_current_file(char * path, struct bs_cache_key * key, const char ** errno_provenance);
|
132
|
+
static int open_current_file(const char * path, struct bs_cache_key * key, const char ** errno_provenance);
|
109
133
|
static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE * output_data, int * exception_tag, const char ** errno_provenance);
|
110
134
|
static uint32_t get_ruby_revision(void);
|
111
135
|
static uint32_t get_ruby_platform(void);
|
@@ -122,15 +146,6 @@ struct s2o_data;
|
|
122
146
|
struct i2o_data;
|
123
147
|
struct i2s_data;
|
124
148
|
|
125
|
-
/* https://bugs.ruby-lang.org/issues/13667 */
|
126
|
-
extern VALUE rb_get_coverages(void);
|
127
|
-
static VALUE
|
128
|
-
bs_rb_coverage_running(VALUE self)
|
129
|
-
{
|
130
|
-
VALUE cov = rb_get_coverages();
|
131
|
-
return RTEST(cov) ? Qtrue : Qfalse;
|
132
|
-
}
|
133
|
-
|
134
149
|
static VALUE
|
135
150
|
bs_rb_get_path(VALUE self, VALUE fname)
|
136
151
|
{
|
@@ -161,15 +176,14 @@ Init_bootsnap(void)
|
|
161
176
|
|
162
177
|
instrumentation_method = rb_intern("_instrument");
|
163
178
|
|
179
|
+
sym_hit = ID2SYM(rb_intern("hit"));
|
164
180
|
sym_miss = ID2SYM(rb_intern("miss"));
|
165
|
-
rb_global_variable(&sym_miss);
|
166
|
-
|
167
181
|
sym_stale = ID2SYM(rb_intern("stale"));
|
168
|
-
|
182
|
+
sym_revalidated = ID2SYM(rb_intern("revalidated"));
|
169
183
|
|
170
184
|
rb_define_module_function(rb_mBootsnap, "instrumentation_enabled=", bs_instrumentation_enabled_set, 1);
|
171
185
|
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "readonly=", bs_readonly_set, 1);
|
172
|
-
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "
|
186
|
+
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "revalidation=", bs_revalidation_set, 1);
|
173
187
|
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 4);
|
174
188
|
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 3);
|
175
189
|
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "compile_option_crc32=", bs_compile_option_crc32_set, 1);
|
@@ -185,6 +199,14 @@ bs_instrumentation_enabled_set(VALUE self, VALUE enabled)
|
|
185
199
|
return enabled;
|
186
200
|
}
|
187
201
|
|
202
|
+
static inline void
|
203
|
+
bs_instrumentation(VALUE event, VALUE path)
|
204
|
+
{
|
205
|
+
if (RB_UNLIKELY(instrumentation_enabled)) {
|
206
|
+
rb_funcall(rb_mBootsnap, instrumentation_method, 2, event, path);
|
207
|
+
}
|
208
|
+
}
|
209
|
+
|
188
210
|
static VALUE
|
189
211
|
bs_readonly_set(VALUE self, VALUE enabled)
|
190
212
|
{
|
@@ -192,6 +214,13 @@ bs_readonly_set(VALUE self, VALUE enabled)
|
|
192
214
|
return enabled;
|
193
215
|
}
|
194
216
|
|
217
|
+
static VALUE
|
218
|
+
bs_revalidation_set(VALUE self, VALUE enabled)
|
219
|
+
{
|
220
|
+
revalidation = RTEST(enabled);
|
221
|
+
return enabled;
|
222
|
+
}
|
223
|
+
|
195
224
|
/*
|
196
225
|
* Bootsnap's ruby code registers a hook that notifies us via this function
|
197
226
|
* when compile_option changes. These changes invalidate all existing caches.
|
@@ -290,17 +319,59 @@ bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_C
|
|
290
319
|
* The data_size member is not compared, as it serves more of a "header"
|
291
320
|
* function.
|
292
321
|
*/
|
293
|
-
static
|
294
|
-
|
322
|
+
static enum cache_status cache_key_equal_fast_path(struct bs_cache_key *k1,
|
323
|
+
struct bs_cache_key *k2) {
|
324
|
+
if (k1->version == k2->version &&
|
325
|
+
k1->ruby_platform == k2->ruby_platform &&
|
326
|
+
k1->compile_option == k2->compile_option &&
|
327
|
+
k1->ruby_revision == k2->ruby_revision && k1->size == k2->size) {
|
328
|
+
if (k1->mtime == k2->mtime) {
|
329
|
+
return hit;
|
330
|
+
}
|
331
|
+
if (revalidation) {
|
332
|
+
return stale;
|
333
|
+
}
|
334
|
+
}
|
335
|
+
return miss;
|
336
|
+
}
|
337
|
+
|
338
|
+
static int cache_key_equal_slow_path(struct bs_cache_key *current_key,
|
339
|
+
struct bs_cache_key *cached_key,
|
340
|
+
const VALUE input_data)
|
295
341
|
{
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
);
|
342
|
+
bs_cache_key_digest(current_key, input_data);
|
343
|
+
return current_key->digest == cached_key->digest;
|
344
|
+
}
|
345
|
+
|
346
|
+
static int update_cache_key(struct bs_cache_key *current_key, struct bs_cache_key *old_key, int cache_fd, const char ** errno_provenance)
|
347
|
+
{
|
348
|
+
old_key->mtime = current_key->mtime;
|
349
|
+
lseek(cache_fd, 0, SEEK_SET);
|
350
|
+
ssize_t nwrite = write(cache_fd, old_key, KEY_SIZE);
|
351
|
+
if (nwrite < 0) {
|
352
|
+
*errno_provenance = "update_cache_key:write";
|
353
|
+
return -1;
|
354
|
+
}
|
355
|
+
|
356
|
+
#ifdef HAVE_FDATASYNC
|
357
|
+
if (fdatasync(cache_fd) < 0) {
|
358
|
+
*errno_provenance = "update_cache_key:fdatasync";
|
359
|
+
return -1;
|
360
|
+
}
|
361
|
+
#endif
|
362
|
+
|
363
|
+
return 0;
|
364
|
+
}
|
365
|
+
|
366
|
+
/*
|
367
|
+
* Fills the cache key digest.
|
368
|
+
*/
|
369
|
+
static void bs_cache_key_digest(struct bs_cache_key *key,
|
370
|
+
const VALUE input_data) {
|
371
|
+
if (key->digest_set)
|
372
|
+
return;
|
373
|
+
key->digest = fnv1a_64(input_data);
|
374
|
+
key->digest_set = 1;
|
304
375
|
}
|
305
376
|
|
306
377
|
/*
|
@@ -356,17 +427,34 @@ bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
|
|
356
427
|
|
357
428
|
return bs_precompile(path, path_v, cache_path, handler);
|
358
429
|
}
|
430
|
+
|
431
|
+
static int bs_open_noatime(const char *path, int flags) {
|
432
|
+
int fd = 1;
|
433
|
+
if (!perm_issue) {
|
434
|
+
fd = open(path, flags | O_NOATIME);
|
435
|
+
if (fd < 0 && errno == EPERM) {
|
436
|
+
errno = 0;
|
437
|
+
perm_issue = true;
|
438
|
+
}
|
439
|
+
}
|
440
|
+
|
441
|
+
if (perm_issue) {
|
442
|
+
fd = open(path, flags);
|
443
|
+
}
|
444
|
+
return fd;
|
445
|
+
}
|
446
|
+
|
359
447
|
/*
|
360
448
|
* Open the file we want to load/cache and generate a cache key for it if it
|
361
449
|
* was loaded.
|
362
450
|
*/
|
363
451
|
static int
|
364
|
-
open_current_file(char * path, struct bs_cache_key * key, const char ** errno_provenance)
|
452
|
+
open_current_file(const char * path, struct bs_cache_key * key, const char ** errno_provenance)
|
365
453
|
{
|
366
454
|
struct stat statbuf;
|
367
455
|
int fd;
|
368
456
|
|
369
|
-
fd =
|
457
|
+
fd = bs_open_noatime(path, O_RDONLY);
|
370
458
|
if (fd < 0) {
|
371
459
|
*errno_provenance = "bs_fetch:open_current_file:open";
|
372
460
|
return fd;
|
@@ -377,7 +465,9 @@ open_current_file(char * path, struct bs_cache_key * key, const char ** errno_pr
|
|
377
465
|
|
378
466
|
if (fstat(fd, &statbuf) < 0) {
|
379
467
|
*errno_provenance = "bs_fetch:open_current_file:fstat";
|
468
|
+
int previous_errno = errno;
|
380
469
|
close(fd);
|
470
|
+
errno = previous_errno;
|
381
471
|
return -1;
|
382
472
|
}
|
383
473
|
|
@@ -387,6 +477,7 @@ open_current_file(char * path, struct bs_cache_key * key, const char ** errno_pr
|
|
387
477
|
key->ruby_revision = current_ruby_revision;
|
388
478
|
key->size = (uint64_t)statbuf.st_size;
|
389
479
|
key->mtime = (uint64_t)statbuf.st_mtime;
|
480
|
+
key->digest_set = false;
|
390
481
|
|
391
482
|
return fd;
|
392
483
|
}
|
@@ -430,7 +521,12 @@ open_cache_file(const char * path, struct bs_cache_key * key, const char ** errn
|
|
430
521
|
{
|
431
522
|
int fd, res;
|
432
523
|
|
433
|
-
|
524
|
+
if (readonly || !revalidation) {
|
525
|
+
fd = bs_open_noatime(path, O_RDONLY);
|
526
|
+
} else {
|
527
|
+
fd = bs_open_noatime(path, O_RDWR);
|
528
|
+
}
|
529
|
+
|
434
530
|
if (fd < 0) {
|
435
531
|
*errno_provenance = "bs_fetch:open_cache_file:open";
|
436
532
|
return CACHE_MISS;
|
@@ -467,7 +563,6 @@ open_cache_file(const char * path, struct bs_cache_key * key, const char ** errn
|
|
467
563
|
static int
|
468
564
|
fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE * output_data, int * exception_tag, const char ** errno_provenance)
|
469
565
|
{
|
470
|
-
char * data = NULL;
|
471
566
|
ssize_t nread;
|
472
567
|
int ret;
|
473
568
|
|
@@ -479,8 +574,8 @@ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE *
|
|
479
574
|
ret = ERROR_WITH_ERRNO;
|
480
575
|
goto done;
|
481
576
|
}
|
482
|
-
|
483
|
-
nread = read(fd,
|
577
|
+
storage_data = rb_str_buf_new(data_size);
|
578
|
+
nread = read(fd, RSTRING_PTR(storage_data), data_size);
|
484
579
|
if (nread < 0) {
|
485
580
|
*errno_provenance = "bs_fetch:fetch_cached_data:read";
|
486
581
|
ret = ERROR_WITH_ERRNO;
|
@@ -491,7 +586,7 @@ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE *
|
|
491
586
|
goto done;
|
492
587
|
}
|
493
588
|
|
494
|
-
storage_data
|
589
|
+
rb_str_set_len(storage_data, nread);
|
495
590
|
|
496
591
|
*exception_tag = bs_storage_to_output(handler, args, storage_data, output_data);
|
497
592
|
if (*output_data == rb_cBootsnap_CompileCache_UNCOMPILABLE) {
|
@@ -500,7 +595,6 @@ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE *
|
|
500
595
|
}
|
501
596
|
ret = 0;
|
502
597
|
done:
|
503
|
-
if (data != NULL) xfree(data);
|
504
598
|
return ret;
|
505
599
|
}
|
506
600
|
|
@@ -607,17 +701,22 @@ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data, cons
|
|
607
701
|
|
608
702
|
|
609
703
|
/* Read contents from an fd, whose contents are asserted to be +size+ bytes
|
610
|
-
* long,
|
611
|
-
static
|
612
|
-
bs_read_contents(int fd, size_t size,
|
704
|
+
* long, returning a Ruby string on success and Qfalse on failure */
|
705
|
+
static VALUE
|
706
|
+
bs_read_contents(int fd, size_t size, const char ** errno_provenance)
|
613
707
|
{
|
708
|
+
VALUE contents;
|
614
709
|
ssize_t nread;
|
615
|
-
|
616
|
-
nread = read(fd,
|
710
|
+
contents = rb_str_buf_new(size);
|
711
|
+
nread = read(fd, RSTRING_PTR(contents), size);
|
712
|
+
|
617
713
|
if (nread < 0) {
|
618
714
|
*errno_provenance = "bs_fetch:bs_read_contents:read";
|
715
|
+
return Qfalse;
|
716
|
+
} else {
|
717
|
+
rb_str_set_len(contents, nread);
|
718
|
+
return contents;
|
619
719
|
}
|
620
|
-
return nread;
|
621
720
|
}
|
622
721
|
|
623
722
|
/*
|
@@ -668,38 +767,67 @@ static VALUE
|
|
668
767
|
bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args)
|
669
768
|
{
|
670
769
|
struct bs_cache_key cached_key, current_key;
|
671
|
-
char * contents = NULL;
|
672
770
|
int cache_fd = -1, current_fd = -1;
|
673
771
|
int res, valid_cache = 0, exception_tag = 0;
|
674
772
|
const char * errno_provenance = NULL;
|
675
773
|
|
676
|
-
VALUE
|
774
|
+
VALUE status = Qfalse;
|
775
|
+
VALUE input_data = Qfalse; /* data read from source file, e.g. YAML or ruby source */
|
677
776
|
VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
|
678
777
|
VALUE output_data; /* return data, e.g. ruby hash or loaded iseq */
|
679
778
|
|
680
779
|
VALUE exception; /* ruby exception object to raise instead of returning */
|
780
|
+
VALUE exception_message; /* ruby exception string to use instead of errno_provenance */
|
681
781
|
|
682
782
|
/* Open the source file and generate a cache key for it */
|
683
783
|
current_fd = open_current_file(path, ¤t_key, &errno_provenance);
|
684
|
-
if (current_fd < 0)
|
784
|
+
if (current_fd < 0) {
|
785
|
+
exception_message = path_v;
|
786
|
+
goto fail_errno;
|
787
|
+
}
|
685
788
|
|
686
789
|
/* Open the cache key if it exists, and read its cache key in */
|
687
790
|
cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance);
|
688
791
|
if (cache_fd == CACHE_MISS || cache_fd == CACHE_STALE) {
|
689
792
|
/* This is ok: valid_cache remains false, we re-populate it. */
|
690
|
-
|
691
|
-
rb_funcall(rb_mBootsnap, instrumentation_method, 2, cache_fd == CACHE_MISS ? sym_miss : sym_stale, path_v);
|
692
|
-
}
|
793
|
+
bs_instrumentation(cache_fd == CACHE_MISS ? sym_miss : sym_stale, path_v);
|
693
794
|
} else if (cache_fd < 0) {
|
795
|
+
exception_message = rb_str_new_cstr(cache_path);
|
694
796
|
goto fail_errno;
|
695
797
|
} else {
|
696
798
|
/* True if the cache existed and no invalidating changes have occurred since
|
697
799
|
* it was generated. */
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
800
|
+
|
801
|
+
switch(cache_key_equal_fast_path(¤t_key, &cached_key)) {
|
802
|
+
case hit:
|
803
|
+
status = sym_hit;
|
804
|
+
valid_cache = true;
|
805
|
+
break;
|
806
|
+
case miss:
|
807
|
+
valid_cache = false;
|
808
|
+
break;
|
809
|
+
case stale:
|
810
|
+
valid_cache = false;
|
811
|
+
if ((input_data = bs_read_contents(current_fd, current_key.size,
|
812
|
+
&errno_provenance)) == Qfalse) {
|
813
|
+
exception_message = path_v;
|
814
|
+
goto fail_errno;
|
702
815
|
}
|
816
|
+
valid_cache = cache_key_equal_slow_path(¤t_key, &cached_key, input_data);
|
817
|
+
if (valid_cache) {
|
818
|
+
if (!readonly) {
|
819
|
+
if (update_cache_key(¤t_key, &cached_key, cache_fd, &errno_provenance)) {
|
820
|
+
exception_message = path_v;
|
821
|
+
goto fail_errno;
|
822
|
+
}
|
823
|
+
}
|
824
|
+
status = sym_revalidated;
|
825
|
+
}
|
826
|
+
break;
|
827
|
+
};
|
828
|
+
|
829
|
+
if (!valid_cache) {
|
830
|
+
status = sym_stale;
|
703
831
|
}
|
704
832
|
}
|
705
833
|
|
@@ -713,13 +841,18 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
|
|
713
841
|
else if (res == CACHE_UNCOMPILABLE) {
|
714
842
|
/* If fetch_cached_data returned `Uncompilable` we fallback to `input_to_output`
|
715
843
|
This happens if we have say, an unsafe YAML cache, but try to load it in safe mode */
|
716
|
-
if (bs_read_contents(current_fd, current_key.size, &
|
717
|
-
|
844
|
+
if (input_data == Qfalse && (input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) {
|
845
|
+
exception_message = path_v;
|
846
|
+
goto fail_errno;
|
847
|
+
}
|
718
848
|
bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
|
719
849
|
if (exception_tag != 0) goto raise;
|
720
850
|
goto succeed;
|
721
851
|
} else if (res == CACHE_MISS || res == CACHE_STALE) valid_cache = 0;
|
722
|
-
else if (res == ERROR_WITH_ERRNO)
|
852
|
+
else if (res == ERROR_WITH_ERRNO){
|
853
|
+
exception_message = rb_str_new_cstr(cache_path);
|
854
|
+
goto fail_errno;
|
855
|
+
}
|
723
856
|
else if (!NIL_P(output_data)) goto succeed; /* fast-path, goal */
|
724
857
|
}
|
725
858
|
close(cache_fd);
|
@@ -727,8 +860,10 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
|
|
727
860
|
/* Cache is stale, invalid, or missing. Regenerate and write it out. */
|
728
861
|
|
729
862
|
/* Read the contents of the source file into a buffer */
|
730
|
-
if (bs_read_contents(current_fd, current_key.size, &
|
731
|
-
|
863
|
+
if (input_data == Qfalse && (input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) {
|
864
|
+
exception_message = path_v;
|
865
|
+
goto fail_errno;
|
866
|
+
}
|
732
867
|
|
733
868
|
/* Try to compile the input_data using input_to_storage(input_data) */
|
734
869
|
exception_tag = bs_input_to_storage(handler, args, input_data, path_v, &storage_data);
|
@@ -747,6 +882,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
|
|
747
882
|
* We do however ignore any failures to persist the cache, as it's better
|
748
883
|
* to move along, than to interrupt the process.
|
749
884
|
*/
|
885
|
+
bs_cache_key_digest(¤t_key, input_data);
|
750
886
|
atomic_write_cache_file(cache_path, ¤t_key, storage_data, &errno_provenance);
|
751
887
|
|
752
888
|
/* Having written the cache, now convert storage_data to output_data */
|
@@ -765,6 +901,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
|
|
765
901
|
* No point raising an error */
|
766
902
|
if (errno != ENOENT) {
|
767
903
|
errno_provenance = "bs_fetch:unlink";
|
904
|
+
exception_message = rb_str_new_cstr(cache_path);
|
768
905
|
goto fail_errno;
|
769
906
|
}
|
770
907
|
}
|
@@ -775,16 +912,22 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
|
|
775
912
|
goto succeed; /* output_data is now the correct return. */
|
776
913
|
|
777
914
|
#define CLEANUP \
|
778
|
-
if (contents != NULL) xfree(contents); \
|
779
915
|
if (current_fd >= 0) close(current_fd); \
|
780
|
-
if (cache_fd >= 0) close(cache_fd);
|
916
|
+
if (cache_fd >= 0) close(cache_fd); \
|
917
|
+
if (status != Qfalse) bs_instrumentation(status, path_v);
|
781
918
|
|
782
919
|
succeed:
|
783
920
|
CLEANUP;
|
784
921
|
return output_data;
|
785
922
|
fail_errno:
|
786
923
|
CLEANUP;
|
787
|
-
|
924
|
+
if (errno_provenance) {
|
925
|
+
exception_message = rb_str_concat(
|
926
|
+
rb_str_new_cstr(errno_provenance),
|
927
|
+
rb_str_concat(rb_str_new_cstr(": "), exception_message)
|
928
|
+
);
|
929
|
+
}
|
930
|
+
exception = rb_syserr_new_str(errno, exception_message);
|
788
931
|
rb_exc_raise(exception);
|
789
932
|
__builtin_unreachable();
|
790
933
|
raise:
|
@@ -802,13 +945,16 @@ invalid_type_storage_data:
|
|
802
945
|
static VALUE
|
803
946
|
bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
|
804
947
|
{
|
948
|
+
if (readonly) {
|
949
|
+
return Qfalse;
|
950
|
+
}
|
951
|
+
|
805
952
|
struct bs_cache_key cached_key, current_key;
|
806
|
-
char * contents = NULL;
|
807
953
|
int cache_fd = -1, current_fd = -1;
|
808
954
|
int res, valid_cache = 0, exception_tag = 0;
|
809
955
|
const char * errno_provenance = NULL;
|
810
956
|
|
811
|
-
VALUE input_data; /* data read from source file, e.g. YAML or ruby source */
|
957
|
+
VALUE input_data = Qfalse; /* data read from source file, e.g. YAML or ruby source */
|
812
958
|
VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
|
813
959
|
|
814
960
|
/* Open the source file and generate a cache key for it */
|
@@ -824,7 +970,26 @@ bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
|
|
824
970
|
} else {
|
825
971
|
/* True if the cache existed and no invalidating changes have occurred since
|
826
972
|
* it was generated. */
|
827
|
-
|
973
|
+
switch(cache_key_equal_fast_path(¤t_key, &cached_key)) {
|
974
|
+
case hit:
|
975
|
+
valid_cache = true;
|
976
|
+
break;
|
977
|
+
case miss:
|
978
|
+
valid_cache = false;
|
979
|
+
break;
|
980
|
+
case stale:
|
981
|
+
valid_cache = false;
|
982
|
+
if ((input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) {
|
983
|
+
goto fail;
|
984
|
+
}
|
985
|
+
valid_cache = cache_key_equal_slow_path(¤t_key, &cached_key, input_data);
|
986
|
+
if (valid_cache) {
|
987
|
+
if (update_cache_key(¤t_key, &cached_key, cache_fd, &errno_provenance)) {
|
988
|
+
goto fail;
|
989
|
+
}
|
990
|
+
}
|
991
|
+
break;
|
992
|
+
};
|
828
993
|
}
|
829
994
|
|
830
995
|
if (valid_cache) {
|
@@ -836,8 +1001,7 @@ bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
|
|
836
1001
|
/* Cache is stale, invalid, or missing. Regenerate and write it out. */
|
837
1002
|
|
838
1003
|
/* Read the contents of the source file into a buffer */
|
839
|
-
if (bs_read_contents(current_fd, current_key.size, &
|
840
|
-
input_data = rb_str_new(contents, current_key.size);
|
1004
|
+
if ((input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) goto fail;
|
841
1005
|
|
842
1006
|
/* Try to compile the input_data using input_to_storage(input_data) */
|
843
1007
|
exception_tag = bs_input_to_storage(handler, Qnil, input_data, path_v, &storage_data);
|
@@ -852,13 +1016,13 @@ bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
|
|
852
1016
|
if (!RB_TYPE_P(storage_data, T_STRING)) goto fail;
|
853
1017
|
|
854
1018
|
/* Write the cache key and storage_data to the cache directory */
|
1019
|
+
bs_cache_key_digest(¤t_key, input_data);
|
855
1020
|
res = atomic_write_cache_file(cache_path, ¤t_key, storage_data, &errno_provenance);
|
856
1021
|
if (res < 0) goto fail;
|
857
1022
|
|
858
1023
|
goto succeed;
|
859
1024
|
|
860
1025
|
#define CLEANUP \
|
861
|
-
if (contents != NULL) xfree(contents); \
|
862
1026
|
if (current_fd >= 0) close(current_fd); \
|
863
1027
|
if (cache_fd >= 0) close(cache_fd);
|
864
1028
|
|
data/ext/bootsnap/extconf.rb
CHANGED
@@ -1,23 +1,30 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "mkmf"
|
4
4
|
|
5
|
-
if RUBY_ENGINE
|
6
|
-
|
7
|
-
|
5
|
+
if %w[ruby truffleruby].include?(RUBY_ENGINE)
|
6
|
+
have_func "fdatasync", "unistd.h"
|
7
|
+
|
8
|
+
unless RUBY_PLATFORM.match?(/mswin|mingw|cygwin/)
|
9
|
+
append_cppflags ["-D_GNU_SOURCE"] # Needed of O_NOATIME
|
10
|
+
end
|
11
|
+
|
12
|
+
append_cflags ["-O3", "-std=c99"]
|
8
13
|
|
9
14
|
# ruby.h has some -Wpedantic fails in some cases
|
10
15
|
# (e.g. https://github.com/Shopify/bootsnap/issues/15)
|
11
16
|
unless ["0", "", nil].include?(ENV["BOOTSNAP_PEDANTIC"])
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
17
|
+
append_cflags([
|
18
|
+
"-Wall",
|
19
|
+
"-Werror",
|
20
|
+
"-Wextra",
|
21
|
+
"-Wpedantic",
|
16
22
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
23
|
+
"-Wno-unused-parameter", # VALUE self has to be there but we don't care what it is.
|
24
|
+
"-Wno-keyword-macro", # hiding return
|
25
|
+
"-Wno-gcc-compat", # ruby.h 2.6.0 on macos 10.14, dunno
|
26
|
+
"-Wno-compound-token-split-by-macro",
|
27
|
+
])
|
21
28
|
end
|
22
29
|
|
23
30
|
create_makefile("bootsnap/bootsnap")
|
data/lib/bootsnap/bundler.rb
CHANGED
data/lib/bootsnap/cli.rb
CHANGED
@@ -36,16 +36,16 @@ module Bootsnap
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def precompile_command(*sources)
|
39
|
-
require "bootsnap/compile_cache
|
40
|
-
require "bootsnap/compile_cache/yaml"
|
41
|
-
require "bootsnap/compile_cache/json"
|
39
|
+
require "bootsnap/compile_cache"
|
42
40
|
|
43
41
|
fix_default_encoding do
|
44
|
-
Bootsnap::CompileCache
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
42
|
+
Bootsnap::CompileCache.setup(
|
43
|
+
cache_dir: cache_dir,
|
44
|
+
iseq: iseq,
|
45
|
+
yaml: yaml,
|
46
|
+
json: json,
|
47
|
+
revalidation: true,
|
48
|
+
)
|
49
49
|
|
50
50
|
@work_pool = WorkerPool.create(size: jobs, jobs: {
|
51
51
|
ruby: method(:precompile_ruby),
|
@@ -60,14 +60,16 @@ module Bootsnap
|
|
60
60
|
precompile_json_files(main_sources)
|
61
61
|
|
62
62
|
if compile_gemfile
|
63
|
-
# Some gems embed their tests, they're very unlikely to be loaded, so not worth precompiling.
|
64
|
-
gem_exclude = Regexp.union([exclude, "/spec/", "/test/"].compact)
|
65
|
-
precompile_ruby_files($LOAD_PATH.map { |d| File.expand_path(d) }, exclude: gem_exclude)
|
66
|
-
|
67
63
|
# Gems that include JSON or YAML files usually don't put them in `lib/`.
|
68
64
|
# So we look at the gem root.
|
65
|
+
# Similarly, gems that include Rails engines generally file Ruby files in `app/`.
|
66
|
+
# However some gems embed their tests, they're very unlikely to be loaded, so not worth precompiling.
|
67
|
+
gem_exclude = Regexp.union([exclude, "/spec/", "/test/", "/features/"].compact)
|
68
|
+
|
69
69
|
gem_pattern = %r{^#{Regexp.escape(Bundler.bundle_path.to_s)}/?(?:bundler/)?gems/[^/]+}
|
70
|
-
gem_paths = $LOAD_PATH.map { |p| p[gem_pattern] }.
|
70
|
+
gem_paths = $LOAD_PATH.map { |p| p[gem_pattern] || p }.uniq
|
71
|
+
|
72
|
+
precompile_ruby_files(gem_paths, exclude: gem_exclude)
|
71
73
|
precompile_yaml_files(gem_paths, exclude: gem_exclude)
|
72
74
|
precompile_json_files(gem_paths, exclude: gem_exclude)
|
73
75
|
end
|
@@ -220,6 +222,9 @@ module Bootsnap
|
|
220
222
|
|
221
223
|
def parser
|
222
224
|
@parser ||= OptionParser.new do |opts|
|
225
|
+
opts.version = Bootsnap::VERSION
|
226
|
+
opts.program_name = "bootsnap"
|
227
|
+
|
223
228
|
opts.banner = "Usage: bootsnap COMMAND [ARGS]"
|
224
229
|
opts.separator ""
|
225
230
|
opts.separator "GLOBAL OPTIONS"
|