bootsnap 1.17.0 → 1.18.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a17ea0a302554fd131e2941fa8b97b1ff9441750fb4182247bb44d91aa1174e8
4
- data.tar.gz: 9ba8386281c2dbb6896b1032e4b6a6c949b81164d54848beac032a977ee43e44
3
+ metadata.gz: 2b2271b2fa08edc313e43ec1359be530fcb6b206a7b4b63f37065290289cbc9a
4
+ data.tar.gz: c5838e7d01c33320e765a2bf668c68c09fc6b0938fb8789653b526cb3c22a244
5
5
  SHA512:
6
- metadata.gz: a1653ddc2779d492f3e5c50719c07a0cdc7a9fab27748dd82096ec923232f9c8b04ddbee738325d13a3aa8c87a38825581677408512437e12933fe18434bcdce
7
- data.tar.gz: 9c0f5d4fe058c13577d97011083b7bab23b290cd16b955937b00aaafb5a62239532df4e0d090e576c85dae45dcf504ffa63f9fedd5b2c5dca67afc4457cdbb59
6
+ metadata.gz: 77168e700a54163bf4574fbf9f82fea681a01f7d0c5c3a55340d3dabc98959fce1176bebef4c4d25d18efa257ad5314f13ca978a77f5cb4288296c63a1b36871
7
+ data.tar.gz: cc770950cc3032a5033873457a23b19823c35d9d24285f3b13fb95848e51d8f34defca5396c7b545b5530381271979c93026401e4ac32d28ec0f89ceadd57823
data/CHANGELOG.md CHANGED
@@ -1,9 +1,44 @@
1
1
  # Unreleased
2
2
 
3
+ # 1.18.4
4
+
5
+ * Allow using bootsnap without bundler. See #488.
6
+ * Fix startup failure if the cache directory points to a broken symlink.
7
+
8
+ # 1.18.3
9
+
10
+ * Fix the cache corruption issue in the revalidation feature. See #474.
11
+ The cache revalidation feature remains opt-in for now, until it is more battle tested.
12
+
13
+ # 1.18.2
14
+
15
+ * Disable stale cache entries revalidation by default as it seems to cause cache corruption issues. See #471 and #474.
16
+ Will be re-enabled in a future version once the root cause is identified.
17
+ * Fix a potential compilation issue on some systems. See #470.
18
+
19
+ # 1.18.1
20
+
21
+ * Handle `EPERM` errors when opening files with `O_NOATIME`.
22
+
23
+ # 1.18.0
24
+
25
+ * `Bootsnap.instrumentation` now receive `:hit` events.
26
+ * Add `Bootsnap.log_stats!` to print hit rate statistics on process exit. Can also be enabled with `BOOTSNAP_STATS=1`.
27
+ * Revalidate stale cache entries by digesting the source content.
28
+ This should significantly improve performance in environments where `mtime` isn't preserved (e.g. CI systems doing a git clone, etc).
29
+ See #468.
30
+ * Open source files and cache entries with `O_NOATIME` when available to reduce disk accesses. See #469.
31
+ * `bootsnap precompile --gemfile` now look for `.rb` files in the whole gem and not just the `lib/` directory. See #466.
32
+
33
+ # 1.17.1
34
+
35
+ * Fix a compatibility issue with the `prism` library that ships with Ruby 3.3. See #463.
36
+ * Improved the `Kernel#require` decorator to not cause a method redefinition warning. See #461.
37
+
3
38
  # 1.17.0
4
39
 
5
- * Ensure `$LOAD_PATH.dup` is Ractor shareable to fix an conflit with `did_you_mean`.
6
- * Allow to ignore direcotries using absolute paths.
40
+ * Ensure `$LOAD_PATH.dup` is Ractor shareable to fix a conflict with `did_you_mean`.
41
+ * Allow to ignore directories using absolute paths.
7
42
  * Support YAML and JSON CompileCache on TruffleRuby.
8
43
  * Support LoadPathCache on TruffleRuby.
9
44
 
@@ -24,7 +59,7 @@
24
59
  * Add a way to skip directories during load path scanning.
25
60
  If you have large non-ruby directories in the middle of your load path, it can severely slow down scanning.
26
61
  Typically this is a problem with `node_modules`. See #277.
27
- * Fix `Bootsnap.unload_cache!`, it simply wouldn't work at all becaue of a merge mistake. See #421.
62
+ * Fix `Bootsnap.unload_cache!`, it simply wouldn't work at all because of a merge mistake. See #421.
28
63
 
29
64
  # 1.13.0
30
65
 
@@ -43,7 +78,7 @@
43
78
 
44
79
  * Stop decorating `Module#autoload` as it was only useful for supporting Ruby 2.2 and older.
45
80
 
46
- * Remove `uname` and other patform specific version from the cache keys. `RUBY_PLATFORM + RUBY_REVISION` should be
81
+ * Remove `uname` and other platform specific version from the cache keys. `RUBY_PLATFORM + RUBY_REVISION` should be
47
82
  enough to ensure bytecode compatibility. This should improve caching for alpine based setups. See #409.
48
83
 
49
84
  # 1.11.1
data/README.md CHANGED
@@ -81,6 +81,7 @@ well together.
81
81
  - `DISABLE_BOOTSNAP_COMPILE_CACHE` allows to disable ISeq and YAML caches.
82
82
  - `BOOTSNAP_READONLY` configure bootsnap to not update the cache on miss or stale entries.
83
83
  - `BOOTSNAP_LOG` configure bootsnap to log all caches misses to STDERR.
84
+ - `BOOTSNAP_STATS` log hit rate statistics on exit. Can't be used if `BOOTSNAP_LOG` is enabled.
84
85
  - `BOOTSNAP_IGNORE_DIRECTORIES` a comma separated list of directories that shouldn't be scanned.
85
86
  Useful when you have large directories of non-ruby files inside `$LOAD_PATH`.
86
87
  It defaults to ignore any directory named `node_modules`.
@@ -99,8 +100,8 @@ Bootsnap cache misses can be monitored though a callback:
99
100
  Bootsnap.instrumentation = ->(event, path) { puts "#{event} #{path}" }
100
101
  ```
101
102
 
102
- `event` is either `:miss` or `:stale`. You can also call `Bootsnap.log!` as a shortcut to
103
- log all events to STDERR.
103
+ `event` is either `:hit`, `:miss`, `:stale` or `:revalidated`.
104
+ You can also call `Bootsnap.log!` as a shortcut to log all events to STDERR.
104
105
 
105
106
  To turn instrumentation back off you can set it to nil:
106
107
 
@@ -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
- #define RB_UNLIKELY(x) (x)
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; /* not used for equality */
58
- uint8_t pad[24];
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 = 4;
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 int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2);
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
- rb_global_variable(&sym_stale);
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, "coverage_running?", bs_rb_coverage_running, 0);
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 int
294
- cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2)
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
- return (
297
- k1->version == k2->version &&
298
- k1->ruby_platform == k2->ruby_platform &&
299
- k1->compile_option == k2->compile_option &&
300
- k1->ruby_revision == k2->ruby_revision &&
301
- k1->size == k2->size &&
302
- k1->mtime == k2->mtime
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 = open(path, O_RDONLY);
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;
@@ -389,6 +477,7 @@ open_current_file(char * path, struct bs_cache_key * key, const char ** errno_pr
389
477
  key->ruby_revision = current_ruby_revision;
390
478
  key->size = (uint64_t)statbuf.st_size;
391
479
  key->mtime = (uint64_t)statbuf.st_mtime;
480
+ key->digest_set = false;
392
481
 
393
482
  return fd;
394
483
  }
@@ -432,7 +521,12 @@ open_cache_file(const char * path, struct bs_cache_key * key, const char ** errn
432
521
  {
433
522
  int fd, res;
434
523
 
435
- fd = open(path, O_RDONLY);
524
+ if (readonly || !revalidation) {
525
+ fd = bs_open_noatime(path, O_RDONLY);
526
+ } else {
527
+ fd = bs_open_noatime(path, O_RDWR);
528
+ }
529
+
436
530
  if (fd < 0) {
437
531
  *errno_provenance = "bs_fetch:open_cache_file:open";
438
532
  return CACHE_MISS;
@@ -677,7 +771,8 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
677
771
  int res, valid_cache = 0, exception_tag = 0;
678
772
  const char * errno_provenance = NULL;
679
773
 
680
- VALUE input_data; /* data read from source file, e.g. YAML or ruby source */
774
+ VALUE status = Qfalse;
775
+ VALUE input_data = Qfalse; /* data read from source file, e.g. YAML or ruby source */
681
776
  VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
682
777
  VALUE output_data; /* return data, e.g. ruby hash or loaded iseq */
683
778
 
@@ -695,20 +790,44 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
695
790
  cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance);
696
791
  if (cache_fd == CACHE_MISS || cache_fd == CACHE_STALE) {
697
792
  /* This is ok: valid_cache remains false, we re-populate it. */
698
- if (RB_UNLIKELY(instrumentation_enabled)) {
699
- rb_funcall(rb_mBootsnap, instrumentation_method, 2, cache_fd == CACHE_MISS ? sym_miss : sym_stale, path_v);
700
- }
793
+ bs_instrumentation(cache_fd == CACHE_MISS ? sym_miss : sym_stale, path_v);
701
794
  } else if (cache_fd < 0) {
702
795
  exception_message = rb_str_new_cstr(cache_path);
703
796
  goto fail_errno;
704
797
  } else {
705
798
  /* True if the cache existed and no invalidating changes have occurred since
706
799
  * it was generated. */
707
- valid_cache = cache_key_equal(&current_key, &cached_key);
708
- if (RB_UNLIKELY(instrumentation_enabled)) {
709
- if (!valid_cache) {
710
- rb_funcall(rb_mBootsnap, instrumentation_method, 2, sym_stale, path_v);
800
+
801
+ switch(cache_key_equal_fast_path(&current_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;
711
815
  }
816
+ valid_cache = cache_key_equal_slow_path(&current_key, &cached_key, input_data);
817
+ if (valid_cache) {
818
+ if (!readonly) {
819
+ if (update_cache_key(&current_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;
712
831
  }
713
832
  }
714
833
 
@@ -722,7 +841,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
722
841
  else if (res == CACHE_UNCOMPILABLE) {
723
842
  /* If fetch_cached_data returned `Uncompilable` we fallback to `input_to_output`
724
843
  This happens if we have say, an unsafe YAML cache, but try to load it in safe mode */
725
- if ((input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse){
844
+ if (input_data == Qfalse && (input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) {
726
845
  exception_message = path_v;
727
846
  goto fail_errno;
728
847
  }
@@ -741,7 +860,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
741
860
  /* Cache is stale, invalid, or missing. Regenerate and write it out. */
742
861
 
743
862
  /* Read the contents of the source file into a buffer */
744
- if ((input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse){
863
+ if (input_data == Qfalse && (input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) {
745
864
  exception_message = path_v;
746
865
  goto fail_errno;
747
866
  }
@@ -763,6 +882,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
763
882
  * We do however ignore any failures to persist the cache, as it's better
764
883
  * to move along, than to interrupt the process.
765
884
  */
885
+ bs_cache_key_digest(&current_key, input_data);
766
886
  atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
767
887
 
768
888
  /* Having written the cache, now convert storage_data to output_data */
@@ -793,13 +913,20 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
793
913
 
794
914
  #define CLEANUP \
795
915
  if (current_fd >= 0) close(current_fd); \
796
- if (cache_fd >= 0) close(cache_fd);
916
+ if (cache_fd >= 0) close(cache_fd); \
917
+ if (status != Qfalse) bs_instrumentation(status, path_v);
797
918
 
798
919
  succeed:
799
920
  CLEANUP;
800
921
  return output_data;
801
922
  fail_errno:
802
923
  CLEANUP;
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
+ }
803
930
  exception = rb_syserr_new_str(errno, exception_message);
804
931
  rb_exc_raise(exception);
805
932
  __builtin_unreachable();
@@ -818,13 +945,16 @@ invalid_type_storage_data:
818
945
  static VALUE
819
946
  bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
820
947
  {
948
+ if (readonly) {
949
+ return Qfalse;
950
+ }
951
+
821
952
  struct bs_cache_key cached_key, current_key;
822
- char * contents = NULL;
823
953
  int cache_fd = -1, current_fd = -1;
824
954
  int res, valid_cache = 0, exception_tag = 0;
825
955
  const char * errno_provenance = NULL;
826
956
 
827
- 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 */
828
958
  VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
829
959
 
830
960
  /* Open the source file and generate a cache key for it */
@@ -840,7 +970,26 @@ bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
840
970
  } else {
841
971
  /* True if the cache existed and no invalidating changes have occurred since
842
972
  * it was generated. */
843
- valid_cache = cache_key_equal(&current_key, &cached_key);
973
+ switch(cache_key_equal_fast_path(&current_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(&current_key, &cached_key, input_data);
986
+ if (valid_cache) {
987
+ if (update_cache_key(&current_key, &cached_key, cache_fd, &errno_provenance)) {
988
+ goto fail;
989
+ }
990
+ }
991
+ break;
992
+ };
844
993
  }
845
994
 
846
995
  if (valid_cache) {
@@ -867,6 +1016,7 @@ bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
867
1016
  if (!RB_TYPE_P(storage_data, T_STRING)) goto fail;
868
1017
 
869
1018
  /* Write the cache key and storage_data to the cache directory */
1019
+ bs_cache_key_digest(&current_key, input_data);
870
1020
  res = atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
871
1021
  if (res < 0) goto fail;
872
1022
 
@@ -1,23 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require("mkmf")
3
+ require "mkmf"
4
4
 
5
5
  if %w[ruby truffleruby].include?(RUBY_ENGINE)
6
- $CFLAGS << " -O3 "
7
- $CFLAGS << " -std=c99"
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
- $CFLAGS << " -Wall"
13
- $CFLAGS << " -Werror"
14
- $CFLAGS << " -Wextra"
15
- $CFLAGS << " -Wpedantic"
17
+ append_cflags([
18
+ "-Wall",
19
+ "-Werror",
20
+ "-Wextra",
21
+ "-Wpedantic",
16
22
 
17
- $CFLAGS << " -Wno-unused-parameter" # VALUE self has to be there but we don't care what it is.
18
- $CFLAGS << " -Wno-keyword-macro" # hiding return
19
- $CFLAGS << " -Wno-gcc-compat" # ruby.h 2.6.0 on macos 10.14, dunno
20
- $CFLAGS << " -Wno-compound-token-split-by-macro"
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")
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bootsnap
4
- extend(self)
4
+ extend self
5
5
 
6
6
  def bundler?
7
7
  return false unless defined?(::Bundler)
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/iseq"
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::ISeq.cache_dir = cache_dir
45
- Bootsnap::CompileCache::YAML.init!
46
- Bootsnap::CompileCache::YAML.cache_dir = cache_dir
47
- Bootsnap::CompileCache::JSON.init!
48
- Bootsnap::CompileCache::JSON.cache_dir = cache_dir
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] }.compact.uniq
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"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require("bootsnap/bootsnap")
4
- require("zlib")
3
+ require "bootsnap/bootsnap"
4
+ require "zlib"
5
5
 
6
6
  module Bootsnap
7
7
  module CompileCache
@@ -84,7 +84,7 @@ module Bootsnap
84
84
  module InstructionSequenceMixin
85
85
  def load_iseq(path)
86
86
  # Having coverage enabled prevents iseq dumping/loading.
87
- return nil if defined?(Coverage) && Bootsnap::CompileCache::Native.coverage_running?
87
+ return nil if defined?(Coverage) && Coverage.running?
88
88
 
89
89
  Bootsnap::CompileCache::ISeq.fetch(path.to_s)
90
90
  rescue RuntimeError => error
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require("bootsnap/bootsnap")
3
+ require "bootsnap/bootsnap"
4
4
 
5
5
  module Bootsnap
6
6
  module CompileCache
@@ -46,8 +46,8 @@ module Bootsnap
46
46
  end
47
47
 
48
48
  def init!
49
- require("json")
50
- require("msgpack")
49
+ require "json"
50
+ require "msgpack"
51
51
 
52
52
  self.msgpack_factory = MessagePack::Factory.new
53
53
  self.supported_options = [:symbolize_names]
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require("bootsnap/bootsnap")
3
+ require "bootsnap/bootsnap"
4
4
 
5
5
  module Bootsnap
6
6
  module CompileCache
@@ -55,9 +55,9 @@ module Bootsnap
55
55
  end
56
56
 
57
57
  def init!
58
- require("yaml")
59
- require("msgpack")
60
- require("date")
58
+ require "yaml"
59
+ require "msgpack"
60
+ require "date"
61
61
 
62
62
  @implementation = ::YAML::VERSION >= "4" ? Psych4 : Psych3
63
63
  if @implementation::Patch.method_defined?(:unsafe_load_file) && !::YAML.respond_to?(:unsafe_load_file)
@@ -9,10 +9,10 @@ module Bootsnap
9
9
 
10
10
  Error = Class.new(StandardError)
11
11
 
12
- def self.setup(cache_dir:, iseq:, yaml:, json:, readonly: false)
12
+ def self.setup(cache_dir:, iseq:, yaml:, json:, readonly: false, revalidation: false)
13
13
  if iseq
14
14
  if supported?
15
- require_relative("compile_cache/iseq")
15
+ require_relative "compile_cache/iseq"
16
16
  Bootsnap::CompileCache::ISeq.install!(cache_dir)
17
17
  elsif $VERBOSE
18
18
  warn("[bootsnap/setup] bytecode caching is not supported on this implementation of Ruby")
@@ -21,7 +21,7 @@ module Bootsnap
21
21
 
22
22
  if yaml
23
23
  if supported?
24
- require_relative("compile_cache/yaml")
24
+ require_relative "compile_cache/yaml"
25
25
  Bootsnap::CompileCache::YAML.install!(cache_dir)
26
26
  elsif $VERBOSE
27
27
  warn("[bootsnap/setup] YAML parsing caching is not supported on this implementation of Ruby")
@@ -30,7 +30,7 @@ module Bootsnap
30
30
 
31
31
  if json
32
32
  if supported?
33
- require_relative("compile_cache/json")
33
+ require_relative "compile_cache/json"
34
34
  Bootsnap::CompileCache::JSON.install!(cache_dir)
35
35
  elsif $VERBOSE
36
36
  warn("[bootsnap/setup] JSON parsing caching is not supported on this implementation of Ruby")
@@ -39,6 +39,7 @@ module Bootsnap
39
39
 
40
40
  if supported? && defined?(Bootsnap::CompileCache::Native)
41
41
  Bootsnap::CompileCache::Native.readonly = readonly
42
+ Bootsnap::CompileCache::Native.revalidation = revalidation
42
43
  end
43
44
  end
44
45
 
@@ -25,6 +25,11 @@ module Bootsnap
25
25
  # This is useful before bootsnap is fully-initialized to load gems that it
26
26
  # depends on, without forcing full LOAD_PATH traversals.
27
27
  def self.with_gems(*gems)
28
+ # Ensure the gems are activated (their paths are in $LOAD_PATH)
29
+ gems.each do |gem_name|
30
+ gem gem_name
31
+ end
32
+
28
33
  orig = $LOAD_PATH.dup
29
34
  $LOAD_PATH.clear
30
35
  gems.each do |gem|
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("../explicit_require")
3
+ require_relative "../explicit_require"
4
4
 
5
5
  module Bootsnap
6
6
  module LoadPathCache
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kernel
4
- module_function
4
+ alias_method :require_without_bootsnap, :require
5
5
 
6
- alias_method(:require_without_bootsnap, :require)
6
+ alias_method :require, :require # Avoid method redefinition warnings
7
7
 
8
- def require(path)
8
+ def require(path) # rubocop:disable Lint/DuplicateMethods
9
9
  return require_without_bootsnap(path) unless Bootsnap::LoadPathCache.enabled?
10
10
 
11
11
  string_path = Bootsnap.rb_get_path(path)
@@ -24,9 +24,7 @@ module Kernel
24
24
  elsif false == resolved
25
25
  return false
26
26
  elsif resolved.nil?
27
- error = LoadError.new(+"cannot load such file -- #{path}")
28
- error.instance_variable_set(:@path, path)
29
- raise error
27
+ return require_without_bootsnap(path)
30
28
  else
31
29
  # Note that require registers to $LOADED_FEATURES while load does not.
32
30
  ret = require_without_bootsnap(resolved)
@@ -34,4 +32,6 @@ module Kernel
34
32
  return ret
35
33
  end
36
34
  end
35
+
36
+ private :require
37
37
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("path_scanner")
3
+ require_relative "path_scanner"
4
4
 
5
5
  module Bootsnap
6
6
  module LoadPathCache
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("../explicit_require")
3
+ require_relative "../explicit_require"
4
4
 
5
5
  module Bootsnap
6
6
  module LoadPathCache
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("../explicit_require")
3
+ require_relative "../explicit_require"
4
4
 
5
- Bootsnap::ExplicitRequire.with_gems("msgpack") { require("msgpack") }
5
+ Bootsnap::ExplicitRequire.with_gems("msgpack") { require "msgpack" }
6
6
 
7
7
  module Bootsnap
8
8
  module LoadPathCache
@@ -122,6 +122,8 @@ module Bootsnap
122
122
  stack.reverse_each do |dir|
123
123
  Dir.mkdir(dir)
124
124
  rescue SystemCallError
125
+ # Check for broken symlinks. Calling File.realpath will raise Errno::ENOENT if that is the case
126
+ File.realpath(dir) if File.symlink?(dir)
125
127
  raise unless File.directory?(dir)
126
128
  end
127
129
  end
@@ -41,8 +41,8 @@ module Bootsnap
41
41
  PathScanner.ignored_directories = ignore_directories if ignore_directories
42
42
  @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode)
43
43
  @enabled = true
44
- require_relative("load_path_cache/core_ext/kernel_require")
45
- require_relative("load_path_cache/core_ext/loaded_features")
44
+ require_relative "load_path_cache/core_ext/kernel_require"
45
+ require_relative "load_path_cache/core_ext/loaded_features"
46
46
  end
47
47
 
48
48
  def unload!
@@ -71,10 +71,10 @@ module Bootsnap
71
71
  end
72
72
 
73
73
  if Bootsnap::LoadPathCache.supported?
74
- require_relative("load_path_cache/path_scanner")
75
- require_relative("load_path_cache/path")
76
- require_relative("load_path_cache/cache")
77
- require_relative("load_path_cache/store")
78
- require_relative("load_path_cache/change_observer")
79
- require_relative("load_path_cache/loaded_features_index")
74
+ require_relative "load_path_cache/path_scanner"
75
+ require_relative "load_path_cache/path"
76
+ require_relative "load_path_cache/cache"
77
+ require_relative "load_path_cache/store"
78
+ require_relative "load_path_cache/change_observer"
79
+ require_relative "load_path_cache/loaded_features_index"
80
80
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("../bootsnap")
3
+ require_relative "../bootsnap"
4
4
 
5
5
  Bootsnap.default_setup
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bootsnap
4
- VERSION = "1.17.0"
4
+ VERSION = "1.18.4"
5
5
  end
data/lib/bootsnap.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("bootsnap/version")
4
- require_relative("bootsnap/bundler")
5
- require_relative("bootsnap/load_path_cache")
6
- require_relative("bootsnap/compile_cache")
3
+ require_relative "bootsnap/version"
4
+ require_relative "bootsnap/bundler"
5
+ require_relative "bootsnap/load_path_cache"
6
+ require_relative "bootsnap/compile_cache"
7
7
 
8
8
  module Bootsnap
9
9
  InvalidConfiguration = Class.new(StandardError)
@@ -11,6 +11,16 @@ module Bootsnap
11
11
  class << self
12
12
  attr_reader :logger
13
13
 
14
+ def log_stats!
15
+ stats = {hit: 0, revalidated: 0, miss: 0, stale: 0}
16
+ self.instrumentation = ->(event, _path) { stats[event] += 1 }
17
+ Kernel.at_exit do
18
+ stats.each do |event, count|
19
+ $stderr.puts "bootsnap #{event}: #{count}"
20
+ end
21
+ end
22
+ end
23
+
14
24
  def log!
15
25
  self.logger = $stderr.method(:puts)
16
26
  end
@@ -18,9 +28,9 @@ module Bootsnap
18
28
  def logger=(logger)
19
29
  @logger = logger
20
30
  self.instrumentation = if logger.respond_to?(:debug)
21
- ->(event, path) { @logger.debug("[Bootsnap] #{event} #{path}") }
31
+ ->(event, path) { @logger.debug("[Bootsnap] #{event} #{path}") unless event == :hit }
22
32
  else
23
- ->(event, path) { @logger.call("[Bootsnap] #{event} #{path}") }
33
+ ->(event, path) { @logger.call("[Bootsnap] #{event} #{path}") unless event == :hit }
24
34
  end
25
35
  end
26
36
 
@@ -41,6 +51,7 @@ module Bootsnap
41
51
  load_path_cache: true,
42
52
  ignore_directories: nil,
43
53
  readonly: false,
54
+ revalidation: false,
44
55
  compile_cache_iseq: true,
45
56
  compile_cache_yaml: true,
46
57
  compile_cache_json: true
@@ -60,6 +71,7 @@ module Bootsnap
60
71
  yaml: compile_cache_yaml,
61
72
  json: compile_cache_json,
62
73
  readonly: readonly,
74
+ revalidation: revalidation,
63
75
  )
64
76
  end
65
77
 
@@ -71,7 +83,7 @@ module Bootsnap
71
83
  env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || ENV["ENV"]
72
84
  development_mode = ["", nil, "development"].include?(env)
73
85
 
74
- unless ENV["DISABLE_BOOTSNAP"]
86
+ if enabled?("BOOTSNAP")
75
87
  cache_dir = ENV["BOOTSNAP_CACHE_DIR"]
76
88
  unless cache_dir
77
89
  config_dir_frame = caller.detect do |line|
@@ -100,16 +112,19 @@ module Bootsnap
100
112
  setup(
101
113
  cache_dir: cache_dir,
102
114
  development_mode: development_mode,
103
- load_path_cache: !ENV["DISABLE_BOOTSNAP_LOAD_PATH_CACHE"],
104
- compile_cache_iseq: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
105
- compile_cache_yaml: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
106
- compile_cache_json: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
107
- readonly: !!ENV["BOOTSNAP_READONLY"],
115
+ load_path_cache: enabled?("BOOTSNAP_LOAD_PATH_CACHE"),
116
+ compile_cache_iseq: enabled?("BOOTSNAP_COMPILE_CACHE"),
117
+ compile_cache_yaml: enabled?("BOOTSNAP_COMPILE_CACHE"),
118
+ compile_cache_json: enabled?("BOOTSNAP_COMPILE_CACHE"),
119
+ readonly: bool_env("BOOTSNAP_READONLY"),
120
+ revalidation: bool_env("BOOTSNAP_REVALIDATE"),
108
121
  ignore_directories: ignore_directories,
109
122
  )
110
123
 
111
124
  if ENV["BOOTSNAP_LOG"]
112
125
  log!
126
+ elsif ENV["BOOTSNAP_STATS"]
127
+ log_stats!
113
128
  end
114
129
  end
115
130
  end
@@ -134,5 +149,16 @@ module Bootsnap
134
149
 
135
150
  # Allow the C extension to redefine `rb_get_path` without warning.
136
151
  alias_method :rb_get_path, :rb_get_path # rubocop:disable Lint/DuplicateMethods
152
+
153
+ private
154
+
155
+ def enabled?(key)
156
+ !ENV["DISABLE_#{key}"]
157
+ end
158
+
159
+ def bool_env(key, default: false)
160
+ value = ENV.fetch(key) { default }
161
+ !["0", "false", false].include?(value)
162
+ end
137
163
  end
138
164
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bootsnap
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.17.0
4
+ version: 1.18.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-10-30 00:00:00.000000000 Z
11
+ date: 2024-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -68,7 +68,7 @@ metadata:
68
68
  changelog_uri: https://github.com/Shopify/bootsnap/blob/main/CHANGELOG.md
69
69
  source_code_uri: https://github.com/Shopify/bootsnap
70
70
  allowed_push_host: https://rubygems.org
71
- post_install_message:
71
+ post_install_message:
72
72
  rdoc_options: []
73
73
  require_paths:
74
74
  - lib
@@ -83,8 +83,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
83
  - !ruby/object:Gem::Version
84
84
  version: '0'
85
85
  requirements: []
86
- rubygems_version: 3.4.21
87
- signing_key:
86
+ rubygems_version: 3.5.16
87
+ signing_key:
88
88
  specification_version: 4
89
89
  summary: Boot large ruby/rails apps faster
90
90
  test_files: []