bootsnap 1.24.3 → 1.24.5

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: 31d64e50bd32368c8dffe927b63fa6e62243b697a2606ca3e2330c4694ec816b
4
- data.tar.gz: 5dfc374040409d1d20e893abc6a8c6052de02cf560c15e7678a5db2084bd722c
3
+ metadata.gz: 9f514d9a08cf2cbfa470c7d6745fe6388bfb4800a8cf659b9939bc57c93a67e1
4
+ data.tar.gz: e623cc84b0ce559e15eeb5ade64617ab6faff50a5004256ecdac5e93267ac5e0
5
5
  SHA512:
6
- metadata.gz: ff9e6b5b2c47d0337fa8d46c487fd6881bc567f5ae01a17f85aa3821df9b7f8416283764f8309de6207d94eb00e6cd6ee845a1307dd601659e4d26cd6abbc7ea
7
- data.tar.gz: e34adc7c5162da87d94358a99a5990d0dfbc6497fb141e7407614e181328e1091bd708fac0aa96e6bce8731283a367db6a5aa6e8a2afe3b848b2d0f6b73e3ba4
6
+ metadata.gz: 517950fbb71bbfc7e9ecbc0220849bc4c6fba0f410edb3d08db13d7e1e55f6602cef80c0636ed656cd76eab3c100da317a40d7cf7527dbf97bb4e2b250bc820f
7
+ data.tar.gz: 97fdc6fee1ac2945fba9b79ec3e6b8d65f265c5e90fc9da16ced08aff3b09f6f516bcdec46aa703452666dfbfaff3db319b912282f0bb1b5ed3d355d0a24e5a0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Unreleased
2
2
 
3
+ # 1.24.5
4
+
5
+ * No longer load the config file by default when setup is done manually. This is so cli applications like homebrew
6
+ don't mistakenly load another app's boostnap config.
7
+
8
+ # 1.24.4
9
+
10
+ * Fix several compatibility issues with Ruby `4.0.4`, particularly the `should not compile with coverage` error. See #547.
11
+ * Fix `Bootsnap.enable_frozen_string_literal` to work even when coverage is enabled. Unfortunately only possible on Ruby `4.0.4+`.
12
+ On older rubies if coverage is enabled a warning will be issued and the feature won't work.
13
+ * Reduced cache files header size from 64 to 32 bytes, and got rid of the random padding element.
14
+ * Avoid leaking a private method in `Object` when testing for Parse.y bugs.
15
+
3
16
  # 1.24.3
4
17
 
5
18
  * Fix the `1.24.2` workaround to parse Ruby files with UTF-8 even when the `LANG` environment variable
data/README.md CHANGED
@@ -237,14 +237,15 @@ This may look worse at a glance, but underlies a large performance difference.
237
237
  useful. [This ruby patch](https://bugs.ruby-lang.org/issues/13378) optimizes them out when coupled
238
238
  with bootsnap.)*
239
239
 
240
- Bootsnap writes a cache file containing a 64 byte header followed by the cache contents. The header
240
+ Bootsnap writes a cache file containing a 32 byte header followed by the cache contents. The header
241
241
  is a cache key including several fields:
242
242
 
243
- * `version`, hardcoded in bootsnap. Essentially a schema version;
244
- * `ruby_platform`, A hash of `RUBY_PLATFORM` (e.g. x86_64-linux-gnu) variable.
245
- * `compile_option`, which changes with `RubyVM::InstructionSequence.compile_option` does;
246
- * `ruby_revision`, A hash of `RUBY_REVISION`, the exact version of Ruby;
243
+ * `ruby_version_digest`, a digest of:
244
+ * The `RUBY_DESCRIPTION` constant e.g. `"ruby 4.0.2 (2026-03-17 revision d3da9fec82) +PRISM [arm64-darwin25]"`.
245
+ * Bootsnap's cache version. Hardcoded in bootsnap. Essentially a schema version;
246
+ * The content of `RubyVM::InstructionSequence.compile_option`.
247
247
  * `size`, the size of the source file;
248
+ * `digest`, a fnv1a_64 hash of the source file;
248
249
  * `mtime`, the last-modification timestamp of the source file when it was compiled; and
249
250
  * `data_size`, the number of bytes following the header, which we need to read it into a buffer.
250
251
 
@@ -349,6 +350,10 @@ Bootsnap::CompileCache::ISeq.compiler_selector = ->(path) do
349
350
  end
350
351
  ```
351
352
 
353
+ *Important note*: This feature is only fully supported by Ruby `4.0.4` and newer.
354
+ On older rubies the feature work except if the `Coverage` module is enabled, in which case a warning will be emitted
355
+ and the default Ruby compiler will be used.
356
+
352
357
  ## Precompilation
353
358
 
354
359
  In development environments the bootsnap compilation cache is generated on the fly when source files are loaded.
@@ -40,7 +40,7 @@
40
40
  #define MAX_CACHEPATH_SIZE 1000
41
41
  #define MAX_CACHEDIR_SIZE 981
42
42
 
43
- #define KEY_SIZE 64
43
+ #define KEY_SIZE 32
44
44
 
45
45
  #define MAX_CREATE_TEMPFILE_ATTEMPT 3
46
46
 
@@ -49,34 +49,27 @@
49
49
  #endif
50
50
 
51
51
  /*
52
- * An instance of this key is written as the first 64 bytes of each cache file.
52
+ * An instance of this key is written as the first `KEY_SIZE` bytes of each cache file.
53
53
  * The mtime and size members track whether the file contents have changed, and
54
- * the version, ruby_platform, compile_option, and ruby_revision members track
55
- * changes to the environment that could invalidate compile results without
54
+ * the ruby_version_digest (bootsnap_cache_version + RUBY_DESCRIPTION) and compile_option
55
+ * members track changes to the environment that could invalidate compile results without
56
56
  * file contents having changed. The data_size member is not truly part of the
57
57
  * "key". Really, this could be called a "header" with the first six members
58
58
  * being an embedded "key" struct and an additional data_size member.
59
59
  *
60
60
  * The data_size indicates the remaining number of bytes in the cache file
61
61
  * after the header (the size of the cached artifact).
62
- *
63
- * After data_size, the struct is padded to 64 bytes.
64
62
  */
65
63
  struct bs_cache_key {
66
- uint32_t version;
67
- uint32_t ruby_platform;
68
- uint32_t compile_option;
69
- uint32_t ruby_revision;
70
- uint64_t size;
64
+ uint64_t ruby_version_digest;
71
65
  uint64_t mtime;
72
- uint64_t data_size; //
73
66
  uint64_t digest;
74
- uint8_t digest_set;
75
- uint8_t pad[15];
67
+ uint32_t size;
68
+ uint32_t data_size;
76
69
  } __attribute__((packed));
77
70
 
78
71
  /*
79
- * If the struct padding isn't correct to pad the key to 64 bytes, refuse to
72
+ * If the struct padding isn't correct to pad the key to `KEY_SIZE` bytes, refuse to
80
73
  * compile.
81
74
  */
82
75
  #define STATIC_ASSERT(X) STATIC_ASSERT2(X,__LINE__)
@@ -86,15 +79,15 @@ struct bs_cache_key {
86
79
  STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE);
87
80
 
88
81
  /* Effectively a schema version. Bumping invalidates all previous caches */
89
- static const uint32_t current_version = 6;
90
-
91
- /* hash of e.g. "x86_64-darwin17", invalidating when ruby is recompiled on a
92
- * new OS ABI, etc. */
93
- static uint32_t current_ruby_platform;
94
- /* Invalidates cache when switching ruby versions */
95
- static uint32_t current_ruby_revision;
96
- /* Invalidates cache when RubyVM::InstructionSequence.compile_option changes */
97
- static uint32_t current_compile_option_crc32 = 0;
82
+ static const uint32_t bootsnap_cache_version = 7;
83
+
84
+ /* Invalidates cache when switching ruby version, platform or ABI */
85
+ static uint64_t base_ruby_version_digest = 0;
86
+
87
+ // `base_ruby_version_digest` combined with RubyVM::InstructionSequence.compile_option
88
+ // Invalidates cache when RubyVM::InstructionSequence.compile_option changes
89
+ static uint64_t current_ruby_version_digest = 0;
90
+
98
91
  /* Current umask */
99
92
  static mode_t current_umask;
100
93
 
@@ -136,8 +129,7 @@ static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handle
136
129
  static VALUE bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler);
137
130
  static int open_current_file(const char * path, struct bs_cache_key * key, const char ** errno_provenance);
138
131
  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);
139
- static uint32_t get_ruby_revision(void);
140
- static uint32_t get_ruby_platform(void);
132
+ static uint64_t get_ruby_version_digest(void);
141
133
 
142
134
  /*
143
135
  * Helper functions to call ruby methods on handler object without crashing on
@@ -295,8 +287,7 @@ Init_bootsnap(void)
295
287
  rb_cBootsnap_CompileCache_UNCOMPILABLE = rb_const_get(rb_mBootsnap_CompileCache, rb_intern("UNCOMPILABLE"));
296
288
  rb_global_variable(&rb_cBootsnap_CompileCache_UNCOMPILABLE);
297
289
 
298
- current_ruby_revision = get_ruby_revision();
299
- current_ruby_platform = get_ruby_platform();
290
+ base_ruby_version_digest = get_ruby_version_digest();
300
291
 
301
292
  instrumentation_method = rb_intern("_instrument");
302
293
 
@@ -345,28 +336,10 @@ bs_revalidation_set(VALUE self, VALUE enabled)
345
336
  return enabled;
346
337
  }
347
338
 
348
- /*
349
- * Bootsnap's ruby code registers a hook that notifies us via this function
350
- * when compile_option changes. These changes invalidate all existing caches.
351
- *
352
- * Note that on 32-bit platforms, a CRC32 can't be represented in a Fixnum, but
353
- * can be represented by a uint.
354
- */
355
- static VALUE
356
- bs_compile_option_crc32_set(VALUE self, VALUE crc32_v)
357
- {
358
- if (!RB_TYPE_P(crc32_v, T_BIGNUM) && !RB_TYPE_P(crc32_v, T_FIXNUM)) {
359
- Check_Type(crc32_v, T_FIXNUM);
360
- }
361
- current_compile_option_crc32 = NUM2UINT(crc32_v);
362
- return Qnil;
363
- }
364
-
365
339
  static uint64_t
366
- fnv1a_64_iter(uint64_t h, const VALUE str)
340
+ fnv1a_64_iter(uint64_t h, const unsigned char *s, size_t len)
367
341
  {
368
- unsigned char *s = (unsigned char *)RSTRING_PTR(str);
369
- unsigned char *str_end = (unsigned char *)RSTRING_PTR(str) + RSTRING_LEN(str);
342
+ const unsigned char *str_end = s + len;
370
343
 
371
344
  while (s < str_end) {
372
345
  h ^= (uint64_t)*s++;
@@ -377,45 +350,43 @@ fnv1a_64_iter(uint64_t h, const VALUE str)
377
350
  }
378
351
 
379
352
  static uint64_t
380
- fnv1a_64(const VALUE str)
353
+ fnv1a_64_iter_str(uint64_t h, const VALUE str)
354
+ {
355
+ Check_Type(str, T_STRING);
356
+ return fnv1a_64_iter(h, (unsigned char *)RSTRING_PTR(str), RSTRING_LEN(str));
357
+ }
358
+
359
+ static uint64_t
360
+ fnv1a_64_str(const VALUE str)
381
361
  {
382
362
  uint64_t h = (uint64_t)0xcbf29ce484222325ULL;
383
- return fnv1a_64_iter(h, str);
363
+ return fnv1a_64_iter_str(h, str);
384
364
  }
385
365
 
386
366
  /*
387
- * Ruby's revision may be Integer or String. CRuby 2.7 or later uses
388
- * Git commit ID as revision. It's String.
367
+ * Bootsnap's ruby code registers a hook that notifies us via this function
368
+ * when compile_option changes. These changes invalidate all existing caches.
369
+ *
370
+ * Note that on 32-bit platforms, a CRC32 can't be represented in a Fixnum, but
371
+ * can be represented by a uint.
389
372
  */
390
- static uint32_t
391
- get_ruby_revision(void)
373
+ static VALUE
374
+ bs_compile_option_crc32_set(VALUE self, VALUE crc32_v)
392
375
  {
393
- VALUE ruby_revision;
394
-
395
- ruby_revision = rb_const_get(rb_cObject, rb_intern("RUBY_REVISION"));
396
- if (RB_TYPE_P(ruby_revision, RUBY_T_FIXNUM)) {
397
- return FIX2INT(ruby_revision);
398
- } else {
399
- uint64_t hash;
400
-
401
- hash = fnv1a_64(ruby_revision);
402
- return (uint32_t)(hash >> 32);
376
+ if (!RB_TYPE_P(crc32_v, T_BIGNUM) && !RB_TYPE_P(crc32_v, T_FIXNUM)) {
377
+ Check_Type(crc32_v, T_FIXNUM);
403
378
  }
379
+ uint32_t crc32 = (uint32_t)NUM2UINT(crc32_v);
380
+ current_ruby_version_digest = fnv1a_64_iter(base_ruby_version_digest, (unsigned char *)&crc32, sizeof(crc32));
381
+ return Qnil;
404
382
  }
405
383
 
406
- /*
407
- * When ruby's version doesn't change, but it's recompiled on a different OS
408
- * (or OS version), we need to invalidate the cache.
409
- */
410
- static uint32_t
411
- get_ruby_platform(void)
384
+ static uint64_t
385
+ get_ruby_version_digest(void)
412
386
  {
413
- uint64_t hash;
414
- VALUE ruby_platform;
415
-
416
- ruby_platform = rb_const_get(rb_cObject, rb_intern("RUBY_PLATFORM"));
417
- hash = fnv1a_64(ruby_platform);
418
- return (uint32_t)(hash >> 32);
387
+ uint64_t hash = fnv1a_64_str(rb_const_get(rb_cObject, rb_intern("RUBY_DESCRIPTION")));
388
+ hash = fnv1a_64_iter(hash, (unsigned char *)&bootsnap_cache_version, sizeof(bootsnap_cache_version));
389
+ return hash;
419
390
  }
420
391
 
421
392
  /*
@@ -443,7 +414,7 @@ bs_cache_path(VALUE cachedir_v, VALUE namespace_v, VALUE path_v, char (* cache_p
443
414
  const char * cachedir = RSTRING_PTR(cachedir_v);
444
415
  const char * namespace = NIL_P(namespace_v) ? "" : RSTRING_PTR(namespace_v);
445
416
 
446
- uint64_t hash = fnv1a_64(path_v);
417
+ uint64_t hash = fnv1a_64_str(path_v);
447
418
  uint8_t first_byte = (hash >> (64 - 8));
448
419
  uint64_t remainder = hash & 0x00ffffffffffffff;
449
420
 
@@ -460,10 +431,8 @@ bs_cache_path(VALUE cachedir_v, VALUE namespace_v, VALUE path_v, char (* cache_p
460
431
  */
461
432
  static enum cache_status cache_key_equal_fast_path(struct bs_cache_key *k1,
462
433
  struct bs_cache_key *k2) {
463
- if (k1->version == k2->version &&
464
- k1->ruby_platform == k2->ruby_platform &&
465
- k1->compile_option == k2->compile_option &&
466
- k1->ruby_revision == k2->ruby_revision && k1->size == k2->size) {
434
+ if (k1->ruby_version_digest == k2->ruby_version_digest &&
435
+ k1->size == k2->size) {
467
436
  if (k1->mtime == k2->mtime) {
468
437
  return hit;
469
438
  }
@@ -507,10 +476,9 @@ static int update_cache_key(struct bs_cache_key *current_key, struct bs_cache_ke
507
476
  */
508
477
  static void bs_cache_key_digest(struct bs_cache_key *key,
509
478
  const VALUE input_data) {
510
- if (key->digest_set)
479
+ if (key->digest)
511
480
  return;
512
- key->digest = fnv1a_64(input_data);
513
- key->digest_set = 1;
481
+ key->digest = fnv1a_64_str(input_data);
514
482
  }
515
483
 
516
484
  /*
@@ -587,13 +555,19 @@ open_current_file(const char * path, struct bs_cache_key * key, const char ** er
587
555
  return -1;
588
556
  }
589
557
 
590
- key->version = current_version;
591
- key->ruby_platform = current_ruby_platform;
592
- key->compile_option = current_compile_option_crc32;
593
- key->ruby_revision = current_ruby_revision;
594
- key->size = (uint64_t)statbuf.st_size;
595
- key->mtime = (uint64_t)statbuf.st_mtime;
596
- key->digest_set = false;
558
+ key->ruby_version_digest = current_ruby_version_digest;
559
+
560
+ // We're limited to file of 4GiB or less. Hopefully that's enough for everyone.
561
+ if (statbuf.st_size > (uint32_t)-1) {
562
+ *errno_provenance = "bs_fetch:open_current_file:file_too_big";
563
+ close(fd);
564
+ errno = EFBIG;
565
+ return -1;
566
+ }
567
+
568
+ key->size = (uint32_t)statbuf.st_size;
569
+ key->mtime = (uint64_t)statbuf.st_mtime;
570
+ key->digest = 0;
597
571
 
598
572
  return fd;
599
573
  }
@@ -663,10 +637,10 @@ open_cache_file(const char * path, struct bs_cache_key * key, const char ** errn
663
637
 
664
638
  /*
665
639
  * The cache file is laid out like:
666
- * 0...64 : bs_cache_key
667
- * 64..-1 : cached artifact
640
+ * 0...`KEY_SIZE` : bs_cache_key
641
+ * `KEY_SIZE`..-1 : cached artifact
668
642
  *
669
- * This function takes a file descriptor whose position is pre-set to 64, and
643
+ * This function takes a file descriptor whose position is pre-set to `KEY_SIZE`, and
670
644
  * the data_size (corresponding to the remaining number of bytes) listed in the
671
645
  * cache header.
672
646
  *
@@ -782,7 +756,12 @@ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data, cons
782
756
  setmode(fd, O_BINARY);
783
757
  #endif
784
758
 
785
- key->data_size = RSTRING_LEN(data);
759
+ uint64_t data_size = RSTRING_LEN(data);
760
+ if (data_size > (uint32_t)-1) {
761
+ return 0; // Don't cache.
762
+ }
763
+
764
+ key->data_size = (uint32_t)data_size;
786
765
  nwrite = write(fd, key, KEY_SIZE);
787
766
  if (nwrite < 0) {
788
767
  *errno_provenance = "bs_fetch:atomic_write_cache_file:write";
@@ -62,7 +62,7 @@ module Bootsnap
62
62
  # Ref: https://github.com/rails/bootsnap/issues/495
63
63
  # The second forked process will hang on some QEMU environments
64
64
  r, w = IO.pipe
65
- pids = 2.times.map do
65
+ pids = Array.new(2) do
66
66
  ::Process.fork do
67
67
  exit!(true)
68
68
  end
@@ -161,7 +161,7 @@ module Bootsnap
161
161
  end
162
162
 
163
163
  def spawn
164
- @workers = @size.times.map { Worker.new(@jobs) }
164
+ @workers = Array.new(@size) { Worker.new(@jobs) }
165
165
  @workers.each(&:spawn)
166
166
  @dispatcher_thread = Thread.new { dispatch_loop }
167
167
  @dispatcher_thread.abort_on_exception = true
data/lib/bootsnap/cli.rb CHANGED
@@ -43,7 +43,7 @@ module Bootsnap
43
43
  yaml: yaml,
44
44
  revalidation: true,
45
45
  )
46
- Bootsnap.load_config
46
+ Bootsnap.load_config(ENV["BOOTSNAP_CONFIG"] || "config/bootsnap.rb")
47
47
 
48
48
  @work_pool = WorkerPool.create(size: jobs, jobs: {
49
49
  ruby: method(:precompile_ruby),
@@ -19,152 +19,207 @@ module Bootsnap
19
19
  end
20
20
  end
21
21
 
22
- class Compiler
23
- attr_reader :namespace, :compile_options
24
-
25
- def initialize(namespace = nil, compile_options = nil)
26
- @namespace = namespace
27
- @compile_options = compile_options
28
- end
22
+ if supported?
23
+ class Compiler
24
+ attr_reader :namespace
25
+
26
+ def initialize(namespace = nil, compile_options = nil)
27
+ @namespace = namespace
28
+ @options = compile_options.freeze
29
+ update_options
30
+ end
29
31
 
30
- has_ruby_bug_18250 = begin # https://bugs.ruby-lang.org/issues/18250
31
- if defined? RubyVM::InstructionSequence
32
- RubyVM::InstructionSequence.compile("def foo(*); ->{ super }; end; def foo(**); ->{ super }; end").to_binary
32
+ def update_options
33
+ @compile_options = if @options.nil? || @options < RubyVM::InstructionSequence.compile_option
34
+ nil
35
+ else
36
+ RubyVM::InstructionSequence.compile_option.merge(@options).freeze
37
+ end
33
38
  end
34
- false
35
- rescue TypeError
36
- true
37
- end
38
39
 
39
- has_ruby_bug_22023 = if defined?(RubyVM::InstructionSequence) && RubyVM::InstructionSequence.respond_to?(:compile_file_prism)
40
- begin
41
- RubyVM::InstructionSequence.compile_file(File.expand_path("../ruby_bug_22023_canary.rb", __FILE__))
40
+ has_ruby_bug_18250 = RUBY_VERSION.start_with?("3.0.") && begin # https://bugs.ruby-lang.org/issues/18250
41
+ if defined? RubyVM::InstructionSequence
42
+ RubyVM::InstructionSequence.compile(<<~RUBY).to_binary
43
+ def foo(*); ->{ super }; end; def foo(**); ->{ super }; end
44
+ RUBY
45
+ end
42
46
  false
43
- rescue SyntaxError
47
+ rescue TypeError
44
48
  true
45
49
  end
46
- end
47
50
 
48
- if has_ruby_bug_22023 && RUBY_DESCRIPTION.include?("+PRISM")
49
- module PatchRubyBug22023
50
- def compile_file(path, options = nil)
51
- compile_file_prism(path, options)
51
+ has_ruby_bug_22023 = if defined?(RubyVM::InstructionSequence) && RubyVM::InstructionSequence.respond_to?(:compile_file_prism)
52
+ begin
53
+ RubyVM::InstructionSequence.compile_file(File.expand_path("../ruby_bug_22023_canary.rb", __FILE__))
54
+ false
55
+ rescue SyntaxError
56
+ true
52
57
  end
58
+ end
59
+
60
+ if has_ruby_bug_22023 && RUBY_DESCRIPTION.include?("+PRISM")
61
+ module PatchRubyBug22023
62
+ def compile_file(path, options = nil)
63
+ compile_file_prism(path, options)
64
+ end
53
65
 
54
- has_ruby_bug_22023_bis = !RubyVM::InstructionSequence.compile_file_prism(
55
- File.expand_path("../ruby_bug_22023_canary.rb", __FILE__),
56
- {frozen_string_literal: true},
57
- ).eval.frozen?
66
+ has_ruby_bug_22023_bis = !RubyVM::InstructionSequence.compile_file_prism(
67
+ File.expand_path("../ruby_bug_22023_canary.rb", __FILE__),
68
+ {frozen_string_literal: true},
69
+ ).eval.frozen?
58
70
 
59
- if has_ruby_bug_22023_bis
60
- def compile_file_prism(path, options = nil)
61
- compile_prism(::File.read(path, encoding: Encoding::UTF_8), path, path, nil, options)
71
+ if has_ruby_bug_22023_bis
72
+ def compile_file_prism(path, options = nil)
73
+ compile_prism(::File.read(path, encoding: Encoding::UTF_8), path, path, nil, options)
74
+ end
62
75
  end
63
76
  end
77
+ RubyVM::InstructionSequence.singleton_class.prepend(PatchRubyBug22023)
64
78
  end
65
- RubyVM::InstructionSequence.singleton_class.prepend(PatchRubyBug22023)
66
- end
67
79
 
68
- if has_ruby_bug_18250
69
- def input_to_storage(_, path)
70
- iseq = RubyVM::InstructionSequence.compile_file(path, @compile_options)
71
- iseq.to_binary
72
- rescue TypeError, SyntaxError # Ruby [Bug #18250] & [Bug #22023]
73
- UNCOMPILABLE
80
+ if has_ruby_bug_18250
81
+ def input_to_storage(_, path)
82
+ iseq = RubyVM::InstructionSequence.compile_file(path, @compile_options)
83
+ iseq.to_binary
84
+ rescue TypeError, SyntaxError # Ruby [Bug #18250] & [Bug #22023]
85
+ UNCOMPILABLE
86
+ end
87
+ else
88
+ def input_to_storage(_, path)
89
+ RubyVM::InstructionSequence.compile_file(path, @compile_options).to_binary
90
+ rescue SyntaxError # Ruby [Bug #22023]
91
+ UNCOMPILABLE
92
+ end
74
93
  end
75
- else
76
- def input_to_storage(_, path)
77
- RubyVM::InstructionSequence.compile_file(path, @compile_options).to_binary
78
- rescue SyntaxError # Ruby [Bug #22023]
79
- UNCOMPILABLE
94
+
95
+ def storage_to_output(binary, _args)
96
+ iseq = RubyVM::InstructionSequence.load_from_binary(binary)
97
+ binary.clear
98
+ iseq
99
+ rescue RuntimeError => error
100
+ if error.message == "broken binary format"
101
+ $stderr.puts("[Bootsnap::CompileCache] warning: rejecting broken binary")
102
+ nil
103
+ else
104
+ raise
105
+ end
80
106
  end
81
- end
82
107
 
83
- def storage_to_output(binary, _args)
84
- iseq = RubyVM::InstructionSequence.load_from_binary(binary)
85
- binary.clear
86
- iseq
87
- rescue RuntimeError => error
88
- if error.message == "broken binary format"
89
- $stderr.puts("[Bootsnap::CompileCache] warning: rejecting broken binary")
90
- nil
91
- else
92
- raise
108
+ def input_to_output(source, path, _kwargs)
109
+ if @compile_options
110
+ if source
111
+ RubyVM::InstructionSequence.compile(
112
+ source.force_encoding(Encoding.default_external),
113
+ path,
114
+ path,
115
+ nil,
116
+ @compile_options,
117
+ )
118
+ else
119
+ RubyVM::InstructionSequence.compile_file(path, @compile_options)
120
+ end
121
+ end
93
122
  end
94
123
  end
95
124
 
96
- def input_to_output(source, path, _kwargs)
97
- RubyVM::InstructionSequence.compile(
98
- source.force_encoding(Encoding.default_external),
99
- path,
100
- path,
125
+ DEFAULT = Compiler.new
126
+ FROZEN_STRING_LITERAL = Compiler.new("-fstr", {frozen_string_literal: true}.freeze)
127
+ COVERAGE_SUPPORTED = RUBY_VERSION >= "4.0.4"
128
+
129
+ @default_compiler = DEFAULT
130
+ @coverage_support_warning_emitted = false
131
+
132
+ def self.fetch(path, cache_dir: ISeq.cache_dir)
133
+ compiler = compiler_selector&.call(path) || default_compiler
134
+
135
+ # Having coverage enabled prevents iseq dumping/loading.
136
+ if coverage_on?
137
+ return nil if compiler.equal?(DEFAULT)
138
+
139
+ if COVERAGE_SUPPORTED
140
+ return compiler.input_to_output(nil, path.to_s, nil)
141
+ elsif !@coverage_support_warning_emitted
142
+ @coverage_support_warning_emitted = true
143
+ warn(<<~MSG)
144
+ Using `Bootsnap.enable_frozen_string_literal` with code coverage enabled is only supported on Ruby 4.0.4+.
145
+ Files loaded while coverage is on, will have mutable string literals.
146
+ MSG
147
+ end
148
+
149
+ return nil
150
+ end
151
+
152
+ Bootsnap::CompileCache::Native.fetch(
153
+ cache_dir,
154
+ compiler.namespace,
155
+ path.to_s,
156
+ compiler,
101
157
  nil,
102
- @compile_options,
103
158
  )
104
159
  end
105
- end
106
160
 
107
- DEFAULT = Compiler.new
108
- FROZEN_STRING_LITERAL = Compiler.new("-fstr", {frozen_string_literal: true}.freeze)
109
- MUTABLE_STRING_LITERAL = Compiler.new("-no-fstr", {frozen_string_literal: false}.freeze)
110
- @default_compiler = DEFAULT
111
-
112
- def self.fetch(path, cache_dir: ISeq.cache_dir)
113
- compiler = compiler_selector&.call(path) || default_compiler
114
- Bootsnap::CompileCache::Native.fetch(
115
- cache_dir,
116
- compiler.namespace,
117
- path.to_s,
118
- compiler,
119
- nil,
120
- )
121
- end
161
+ def self.precompile(path)
162
+ compiler = compiler_selector&.call(path) || default_compiler
163
+ Bootsnap::CompileCache::Native.precompile(
164
+ cache_dir,
165
+ compiler.namespace,
166
+ path.to_s,
167
+ compiler,
168
+ )
169
+ end
122
170
 
123
- def self.precompile(path)
124
- compiler = compiler_selector&.call(path) || default_compiler
125
- Bootsnap::CompileCache::Native.precompile(
126
- cache_dir,
127
- compiler.namespace,
128
- path.to_s,
129
- compiler,
130
- )
131
- end
171
+ if RUBY_VERSION < "3.1."
172
+ def self.coverage_on?
173
+ defined?(Coverage) && Coverage.running?
174
+ end
175
+ else
176
+ def self.coverage_on?
177
+ defined?(Coverage) && Coverage.state != :idle
178
+ end
179
+ end
132
180
 
133
- module InstructionSequenceMixin
134
- def load_iseq(path)
135
- # Having coverage enabled prevents iseq dumping/loading.
136
- return nil if defined?(Coverage) && Coverage.running?
181
+ module InstructionSequenceMixin
182
+ def load_iseq(path)
183
+ Bootsnap::CompileCache::ISeq.fetch(path.to_s)
184
+ rescue RuntimeError => error
185
+ if error.message.include?("unmatched platform")
186
+ puts("unmatched platform for file #{path}")
187
+ end
188
+ raise
189
+ end
137
190
 
138
- Bootsnap::CompileCache::ISeq.fetch(path.to_s)
139
- rescue RuntimeError => error
140
- if error.message =~ /unmatched platform/
141
- puts("unmatched platform for file #{path}")
191
+ def compile_option=(hash)
192
+ super
193
+ Bootsnap::CompileCache::ISeq.compile_option_updated
142
194
  end
143
- raise
144
195
  end
145
196
 
146
- def compile_option=(hash)
147
- super
148
- Bootsnap::CompileCache::ISeq.compile_option_updated
197
+ def self.compile_option_updated
198
+ option = RubyVM::InstructionSequence.compile_option
199
+ crc = Zlib.crc32(option.inspect)
200
+ Bootsnap::CompileCache::Native.compile_option_crc32 = crc
201
+ FROZEN_STRING_LITERAL.update_options
149
202
  end
150
- end
203
+ compile_option_updated if supported?
151
204
 
152
- def self.compile_option_updated
153
- option = RubyVM::InstructionSequence.compile_option
154
- crc = Zlib.crc32(option.inspect)
155
- Bootsnap::CompileCache::Native.compile_option_crc32 = crc
156
- end
157
- compile_option_updated if supported?
205
+ def self.install!(cache_dir)
206
+ Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
158
207
 
159
- def self.install!(cache_dir)
160
- Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
208
+ return unless supported?
161
209
 
162
- return unless supported?
210
+ Bootsnap::CompileCache::ISeq.compile_option_updated
163
211
 
164
- Bootsnap::CompileCache::ISeq.compile_option_updated
212
+ class << RubyVM::InstructionSequence
213
+ prepend(InstructionSequenceMixin)
214
+ end
215
+ end
216
+ else
217
+ def self.install!(...)
218
+ # noop
219
+ end
165
220
 
166
- class << RubyVM::InstructionSequence
167
- prepend(InstructionSequenceMixin)
221
+ def self.precompile(...)
222
+ # noop
168
223
  end
169
224
  end
170
225
  end
@@ -1,10 +1,7 @@
1
- # rubocop:disable Style/FrozenStringLiteralComment
2
- def foo(bars)
3
- case bars
1
+ _f = -> {
2
+ case foo
4
3
  in [one, "a" | "b" => two]
5
4
  puts "#{one} - #{two}"
6
5
  end
7
- end
8
-
6
+ }
9
7
  _ = "test"
10
- # rubocop:enable Style/FrozenStringLiteralComment
@@ -84,7 +84,7 @@ module Bootsnap
84
84
  rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
85
85
  default_data
86
86
  rescue ArgumentError => error
87
- if error.message =~ /negative array size/
87
+ if error.message.include?("negative array size")
88
88
  default_data
89
89
  else
90
90
  raise
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bootsnap
4
- VERSION = "1.24.3"
4
+ VERSION = "1.24.5"
5
5
  end
data/lib/bootsnap.rb CHANGED
@@ -43,13 +43,6 @@ module Bootsnap
43
43
  @instrumentation.call(event, path)
44
44
  end
45
45
 
46
- def load_config
47
- config_path = File.expand_path(ENV["BOOTSNAP_CONFIG"] || "config/bootsnap.rb")
48
- if File.exist?(config_path)
49
- require(config_path)
50
- end
51
- end
52
-
53
46
  def setup(
54
47
  cache_dir:,
55
48
  development_mode: true,
@@ -59,7 +52,8 @@ module Bootsnap
59
52
  revalidation: false,
60
53
  compile_cache_iseq: true,
61
54
  compile_cache_yaml: true,
62
- compile_cache_json: (compile_cache_json_unset = true)
55
+ compile_cache_json: (compile_cache_json_unset = true),
56
+ config_path: nil
63
57
  )
64
58
  unless compile_cache_json_unset
65
59
  warn("Bootsnap.setup `compile_cache_json` argument is deprecated and has no effect")
@@ -84,7 +78,16 @@ module Bootsnap
84
78
  revalidation: revalidation,
85
79
  )
86
80
 
87
- load_config
81
+ load_config(config_path)
82
+ end
83
+
84
+ def load_config(config_path)
85
+ if config_path
86
+ config_path = File.expand_path(config_path)
87
+ if File.exist?(config_path)
88
+ require(config_path)
89
+ end
90
+ end
88
91
  end
89
92
 
90
93
  def enable_frozen_string_literal(app_only: false)
@@ -102,8 +105,8 @@ module Bootsnap
102
105
  end
103
106
  }
104
107
  else
105
- Bootsnap::CompileCache::ISeq.compiler_selector = nil
106
- Bootsnap::CompileCache::ISeq.default_compiler = Bootsnap::CompileCache::ISeq::FROZEN_STRING_LITERAL
108
+ options = RubyVM::InstructionSequence.compile_option.merge(frozen_string_literal: true)
109
+ RubyVM::InstructionSequence.compile_option = options
107
110
  end
108
111
  end
109
112
 
@@ -150,6 +153,7 @@ module Bootsnap
150
153
  readonly: bool_env("BOOTSNAP_READONLY"),
151
154
  revalidation: bool_env("BOOTSNAP_REVALIDATE"),
152
155
  ignore_directories: ignore_directories,
156
+ config_path: ENV["BOOTSNAP_CONFIG"] || "config/bootsnap.rb",
153
157
  )
154
158
 
155
159
  if ENV["BOOTSNAP_LOG"]
@@ -160,7 +164,7 @@ module Bootsnap
160
164
  end
161
165
  end
162
166
 
163
- if RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/
167
+ if /mswin|mingw|cygwin/.match?(RbConfig::CONFIG["host_os"])
164
168
  def absolute_path?(path)
165
169
  path[1] == ":"
166
170
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bootsnap
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.24.3
4
+ version: 1.24.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-05-22 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: msgpack
@@ -67,6 +68,7 @@ metadata:
67
68
  changelog_uri: https://github.com/rails/bootsnap/blob/main/CHANGELOG.md
68
69
  source_code_uri: https://github.com/rails/bootsnap
69
70
  allowed_push_host: https://rubygems.org
71
+ post_install_message:
70
72
  rdoc_options: []
71
73
  require_paths:
72
74
  - lib
@@ -81,7 +83,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
83
  - !ruby/object:Gem::Version
82
84
  version: '0'
83
85
  requirements: []
84
- rubygems_version: 4.0.6
86
+ rubygems_version: 3.5.22
87
+ signing_key:
85
88
  specification_version: 4
86
89
  summary: Boot large ruby/rails apps faster
87
90
  test_files: []