bootsnap 1.17.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 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: []