bootsnap 1.24.1 → 1.25.0

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: 86fd14b9f3a5920de24b3faf51c01e36ee19b20fc810ee6780c2ba8447871db9
4
- data.tar.gz: 4df2ffc0617e1957f264e7fb72002cec56ffb8bfe6f84534343c1b852c37e5e4
3
+ metadata.gz: e46c28f258eb76b8c90fa22a7fa2c83887695b7c12d3aff8dbbefda5051dca87
4
+ data.tar.gz: 33aaa301627c53d2f4db1b8e98db0b2f52dc8d20aa533c8de8dc9448bf0c0b8e
5
5
  SHA512:
6
- metadata.gz: 32879429b4f7473a5ed1ea6e32799a3d45d2203a46e867282d0e503396b40acce0a69faf2e83895271f80a369247fb4eeeae63b0022c25f805e5ed9fa073a581
7
- data.tar.gz: f354b078188dfb893c6690a80aa830498bce489e868c132b29317020d73b455528914ce0880f748b8c5f2da2845df61fd2d986b142c58a0d8082b42e62329392
6
+ metadata.gz: 77f885b700f2d249da6afd6ed4a70e4b37bf27303ddabefdfbed915fea6e61587d3e0fa56ee5159a95416072d58d4994498ee29bf69a3ab70c3b9e98471c3a0a
7
+ data.tar.gz: 7044ce0c6a30d7312d947da11b20755dbd496ee7b5f460293bf9188c83493ac951e6e683f75009b17860c45a7ff31e3264abffd69dcaf5a0e8c6b759b7631332
data/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Unreleased
2
2
 
3
+ # 1.25.0
4
+
5
+ * Improve YAML parsing cache to more efficiently handle `Time`, `Date` and `DateTime`.
6
+
7
+ * Don't invalidate the compile cache when YJIT is toggled. YJIT is a runtime JIT and doesn't change the
8
+ serialized instruction sequences that are cached, but enabling it (via `--yjit`, `RUBYOPT`, or
9
+ `RubyVM::YJIT.enable`) adds a ` +YJIT` marker to `RUBY_DESCRIPTION` (` +YJIT <token>` on `YJIT_SUPPORT`
10
+ builds), which is part of the cache key. This previously discarded the entire compile cache whenever YJIT
11
+ was enabled at runtime but not at precompile time (or vice versa). The marker is now stripped before hashing.
12
+
13
+ * Fix `CompileCache::Native.fetch` and `.precompile` reading a non-`String` path argument (e.g. a `Pathname`)
14
+ with `RSTRING_PTR`. Regression from 1.24.0.
15
+
16
+ # 1.24.6
17
+
18
+ * Fix detection of Ruby bug #22023 on some patch versions of Ruby 3.4, and properly apply the workaround.
19
+
20
+ # 1.24.5
21
+
22
+ * No longer load the config file by default when setup is done manually. This is so cli applications like homebrew
23
+ don't mistakenly load another app's boostnap config.
24
+
25
+ # 1.24.4
26
+
27
+ * Fix several compatibility issues with Ruby `4.0.4`, particularly the `should not compile with coverage` error. See #547.
28
+ * Fix `Bootsnap.enable_frozen_string_literal` to work even when coverage is enabled. Unfortunately only possible on Ruby `4.0.4+`.
29
+ On older rubies if coverage is enabled a warning will be issued and the feature won't work.
30
+ * Reduced cache files header size from 64 to 32 bytes, and got rid of the random padding element.
31
+ * Avoid leaking a private method in `Object` when testing for Parse.y bugs.
32
+
33
+ # 1.24.3
34
+
35
+ * Fix the `1.24.2` workaround to parse Ruby files with UTF-8 even when the `LANG` environment variable
36
+ is unset or set to `C`.
37
+
38
+ # 1.24.2
39
+
40
+ * Workaround two Ruby bugs in `RubyVM::InstructionSequence.compile_file`, that were causing
41
+ files to be loaded with the old Ruby parser instead of Prism, causing issues with some pattern matching syntax.
42
+ Ref: https://bugs.ruby-lang.org/issues/22023
43
+
3
44
  # 1.24.1
4
45
 
5
46
  * Fix encoding of Ruby source files loaded when `BOOTSNAP_READONLY` is set.
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 = 8;
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_mBootsnap, rb_intern("RUBY_CACHE_KEY")));
388
+ hash = fnv1a_64_iter(hash, (unsigned char *)&bootsnap_cache_version, sizeof(bootsnap_cache_version));
389
+ return hash;
419
390
  }
420
391
 
421
392
  /*
@@ -424,26 +395,31 @@ get_ruby_platform(void)
424
395
  * be stored.
425
396
  *
426
397
  * The path will look something like: <cachedir>/12/34567890abcdef
398
+ *
399
+ * `path_v` must already have been through FilePathValue(): the callers read it
400
+ * with RSTRING_PTR() too, so the conversion has to happen in the frame that
401
+ * owns the variable.
427
402
  */
428
403
  static void
429
404
  bs_cache_path(VALUE cachedir_v, VALUE namespace_v, VALUE path_v, char (* cache_path)[MAX_CACHEPATH_SIZE])
430
405
  {
431
- FilePathValue(path_v);
432
-
433
406
  Check_Type(cachedir_v, T_STRING);
434
407
  Check_Type(path_v, T_STRING);
408
+
409
+ long namespace_len = 0;
435
410
  if (!NIL_P(namespace_v)) {
436
411
  Check_Type(namespace_v, T_STRING);
412
+ namespace_len = RSTRING_LEN(namespace_v);
437
413
  }
438
414
 
439
- if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
415
+ if (RSTRING_LEN(cachedir_v) + namespace_len > MAX_CACHEDIR_SIZE) {
440
416
  rb_raise(rb_eArgError, "cachedir too long");
441
417
  }
442
418
 
443
419
  const char * cachedir = RSTRING_PTR(cachedir_v);
444
420
  const char * namespace = NIL_P(namespace_v) ? "" : RSTRING_PTR(namespace_v);
445
421
 
446
- uint64_t hash = fnv1a_64(path_v);
422
+ uint64_t hash = fnv1a_64_str(path_v);
447
423
  uint8_t first_byte = (hash >> (64 - 8));
448
424
  uint64_t remainder = hash & 0x00ffffffffffffff;
449
425
 
@@ -460,10 +436,8 @@ bs_cache_path(VALUE cachedir_v, VALUE namespace_v, VALUE path_v, char (* cache_p
460
436
  */
461
437
  static enum cache_status cache_key_equal_fast_path(struct bs_cache_key *k1,
462
438
  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) {
439
+ if (k1->ruby_version_digest == k2->ruby_version_digest &&
440
+ k1->size == k2->size) {
467
441
  if (k1->mtime == k2->mtime) {
468
442
  return hit;
469
443
  }
@@ -507,10 +481,9 @@ static int update_cache_key(struct bs_cache_key *current_key, struct bs_cache_ke
507
481
  */
508
482
  static void bs_cache_key_digest(struct bs_cache_key *key,
509
483
  const VALUE input_data) {
510
- if (key->digest_set)
484
+ if (key->digest)
511
485
  return;
512
- key->digest = fnv1a_64(input_data);
513
- key->digest_set = 1;
486
+ key->digest = fnv1a_64_str(input_data);
514
487
  }
515
488
 
516
489
  /*
@@ -521,6 +494,8 @@ static void bs_cache_key_digest(struct bs_cache_key *key,
521
494
  static VALUE
522
495
  bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler, VALUE args)
523
496
  {
497
+ FilePathValue(path_v);
498
+
524
499
  char cache_path[MAX_CACHEPATH_SIZE];
525
500
 
526
501
  /* generate cache path to cache_path */
@@ -537,6 +512,8 @@ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE
537
512
  static VALUE
538
513
  bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler)
539
514
  {
515
+ FilePathValue(path_v);
516
+
540
517
  char cache_path[MAX_CACHEPATH_SIZE];
541
518
  /* generate cache path to cache_path */
542
519
  bs_cache_path(cachedir_v, namespace_v, path_v, &cache_path);
@@ -587,13 +564,19 @@ open_current_file(const char * path, struct bs_cache_key * key, const char ** er
587
564
  return -1;
588
565
  }
589
566
 
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;
567
+ key->ruby_version_digest = current_ruby_version_digest;
568
+
569
+ // We're limited to file of 4GiB or less. Hopefully that's enough for everyone.
570
+ if (statbuf.st_size > (uint32_t)-1) {
571
+ *errno_provenance = "bs_fetch:open_current_file:file_too_big";
572
+ close(fd);
573
+ errno = EFBIG;
574
+ return -1;
575
+ }
576
+
577
+ key->size = (uint32_t)statbuf.st_size;
578
+ key->mtime = (uint64_t)statbuf.st_mtime;
579
+ key->digest = 0;
597
580
 
598
581
  return fd;
599
582
  }
@@ -663,10 +646,10 @@ open_cache_file(const char * path, struct bs_cache_key * key, const char ** errn
663
646
 
664
647
  /*
665
648
  * The cache file is laid out like:
666
- * 0...64 : bs_cache_key
667
- * 64..-1 : cached artifact
649
+ * 0...`KEY_SIZE` : bs_cache_key
650
+ * `KEY_SIZE`..-1 : cached artifact
668
651
  *
669
- * This function takes a file descriptor whose position is pre-set to 64, and
652
+ * This function takes a file descriptor whose position is pre-set to `KEY_SIZE`, and
670
653
  * the data_size (corresponding to the remaining number of bytes) listed in the
671
654
  * cache header.
672
655
  *
@@ -782,7 +765,12 @@ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data, cons
782
765
  setmode(fd, O_BINARY);
783
766
  #endif
784
767
 
785
- key->data_size = RSTRING_LEN(data);
768
+ uint64_t data_size = RSTRING_LEN(data);
769
+ if (data_size > (uint32_t)-1) {
770
+ return 0; // Don't cache.
771
+ }
772
+
773
+ key->data_size = (uint32_t)data_size;
786
774
  nwrite = write(fd, key, KEY_SIZE);
787
775
  if (nwrite < 0) {
788
776
  *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,124 +19,209 @@ module Bootsnap
19
19
  end
20
20
  end
21
21
 
22
- class Compiler
23
- attr_reader :namespace, :compile_options
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
24
31
 
25
- def initialize(namespace = nil, compile_options = nil)
26
- @namespace = namespace
27
- @compile_options = compile_options
28
- end
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
38
+ end
29
39
 
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
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
46
+ false
47
+ rescue TypeError
48
+ true
33
49
  end
34
- false
35
- rescue TypeError
36
- true
37
- end
38
50
 
39
- if has_ruby_bug_18250
40
- def input_to_storage(_, path)
41
- iseq = RubyVM::InstructionSequence.compile_file(path, @compile_options)
51
+ has_ruby_bug_22023 = case RUBY_VERSION
52
+ when /^3\.3\./
53
+ true
54
+ when /^3\.4\.(\d+)/
55
+ $1.to_i < 10
56
+ when /^4\.0\.(\d+)/
57
+ $1.to_i < 4
58
+ else
59
+ false
60
+ end
61
+
62
+ if has_ruby_bug_22023 && RUBY_DESCRIPTION.include?("+PRISM")
63
+ module PatchRubyBug22023
64
+ def compile_file(path, options = nil)
65
+ compile_file_prism(path, options)
66
+ end
67
+
68
+ has_ruby_bug_22023_bis = !RubyVM::InstructionSequence.compile_file_prism(
69
+ File.expand_path("../ruby_bug_22023_canary.rb", __FILE__),
70
+ {frozen_string_literal: true},
71
+ ).eval.frozen?
72
+
73
+ if has_ruby_bug_22023_bis
74
+ def compile_file_prism(path, options = nil)
75
+ compile_prism(::File.read(path, encoding: Encoding::UTF_8), path, path, nil, options)
76
+ end
77
+ end
78
+ end
79
+ RubyVM::InstructionSequence.singleton_class.prepend(PatchRubyBug22023)
80
+ end
42
81
 
43
- begin
82
+ if has_ruby_bug_18250
83
+ def input_to_storage(_, path)
84
+ iseq = RubyVM::InstructionSequence.compile_file(path, @compile_options)
44
85
  iseq.to_binary
45
- rescue TypeError
46
- UNCOMPILABLE # ruby bug #18250
86
+ rescue TypeError, SyntaxError # Ruby [Bug #18250] & [Bug #22023]
87
+ UNCOMPILABLE
88
+ end
89
+ else
90
+ def input_to_storage(_, path)
91
+ RubyVM::InstructionSequence.compile_file(path, @compile_options).to_binary
92
+ rescue SyntaxError # Ruby [Bug #22023]
93
+ UNCOMPILABLE
47
94
  end
48
95
  end
49
- else
50
- def input_to_storage(_, path)
51
- RubyVM::InstructionSequence.compile_file(path, @compile_options).to_binary
96
+
97
+ def storage_to_output(binary, _args)
98
+ iseq = RubyVM::InstructionSequence.load_from_binary(binary)
99
+ binary.clear
100
+ iseq
101
+ rescue RuntimeError => error
102
+ if error.message == "broken binary format"
103
+ $stderr.puts("[Bootsnap::CompileCache] warning: rejecting broken binary")
104
+ nil
105
+ else
106
+ raise
107
+ end
52
108
  end
53
- end
54
109
 
55
- def storage_to_output(binary, _args)
56
- iseq = RubyVM::InstructionSequence.load_from_binary(binary)
57
- binary.clear
58
- iseq
59
- rescue RuntimeError => error
60
- if error.message == "broken binary format"
61
- $stderr.puts("[Bootsnap::CompileCache] warning: rejecting broken binary")
62
- nil
63
- else
64
- raise
110
+ def input_to_output(source, path, _kwargs)
111
+ if @compile_options
112
+ if source
113
+ RubyVM::InstructionSequence.compile(
114
+ source.force_encoding(Encoding.default_external),
115
+ path,
116
+ path,
117
+ nil,
118
+ @compile_options,
119
+ )
120
+ else
121
+ RubyVM::InstructionSequence.compile_file(path, @compile_options)
122
+ end
123
+ end
65
124
  end
66
125
  end
67
126
 
68
- def input_to_output(source, path, _kwargs)
69
- RubyVM::InstructionSequence.compile(
70
- source.force_encoding(Encoding.default_external),
71
- path,
72
- path,
127
+ DEFAULT = Compiler.new
128
+ FROZEN_STRING_LITERAL = Compiler.new("-fstr", {frozen_string_literal: true}.freeze)
129
+ COVERAGE_SUPPORTED = RUBY_VERSION >= "4.0.4"
130
+
131
+ @default_compiler = DEFAULT
132
+ @coverage_support_warning_emitted = false
133
+
134
+ def self.fetch(path, cache_dir: ISeq.cache_dir)
135
+ compiler = compiler_selector&.call(path) || default_compiler
136
+
137
+ # Having coverage enabled prevents iseq dumping/loading.
138
+ if coverage_on?
139
+ return nil if compiler.equal?(DEFAULT)
140
+
141
+ if COVERAGE_SUPPORTED
142
+ return compiler.input_to_output(nil, path.to_s, nil)
143
+ elsif !@coverage_support_warning_emitted
144
+ @coverage_support_warning_emitted = true
145
+ warn(<<~MSG)
146
+ Using `Bootsnap.enable_frozen_string_literal` with code coverage enabled is only supported on Ruby 4.0.4+.
147
+ Files loaded while coverage is on, will have mutable string literals.
148
+ MSG
149
+ end
150
+
151
+ return nil
152
+ end
153
+
154
+ Bootsnap::CompileCache::Native.fetch(
155
+ cache_dir,
156
+ compiler.namespace,
157
+ path.to_s,
158
+ compiler,
73
159
  nil,
74
- @compile_options,
75
160
  )
76
161
  end
77
- end
78
162
 
79
- DEFAULT = Compiler.new
80
- FROZEN_STRING_LITERAL = Compiler.new("-fstr", {frozen_string_literal: true}.freeze)
81
- MUTABLE_STRING_LITERAL = Compiler.new("-no-fstr", {frozen_string_literal: false}.freeze)
82
- @default_compiler = DEFAULT
83
-
84
- def self.fetch(path, cache_dir: ISeq.cache_dir)
85
- compiler = compiler_selector&.call(path) || default_compiler
86
- Bootsnap::CompileCache::Native.fetch(
87
- cache_dir,
88
- compiler.namespace,
89
- path.to_s,
90
- compiler,
91
- nil,
92
- )
93
- end
163
+ def self.precompile(path)
164
+ compiler = compiler_selector&.call(path) || default_compiler
165
+ Bootsnap::CompileCache::Native.precompile(
166
+ cache_dir,
167
+ compiler.namespace,
168
+ path.to_s,
169
+ compiler,
170
+ )
171
+ end
94
172
 
95
- def self.precompile(path)
96
- compiler = compiler_selector&.call(path) || default_compiler
97
- Bootsnap::CompileCache::Native.precompile(
98
- cache_dir,
99
- compiler.namespace,
100
- path.to_s,
101
- compiler,
102
- )
103
- end
173
+ if RUBY_VERSION < "3.1."
174
+ def self.coverage_on?
175
+ defined?(Coverage) && Coverage.running?
176
+ end
177
+ else
178
+ def self.coverage_on?
179
+ defined?(Coverage) && Coverage.state != :idle
180
+ end
181
+ end
104
182
 
105
- module InstructionSequenceMixin
106
- def load_iseq(path)
107
- # Having coverage enabled prevents iseq dumping/loading.
108
- return nil if defined?(Coverage) && Coverage.running?
183
+ module InstructionSequenceMixin
184
+ def load_iseq(path)
185
+ Bootsnap::CompileCache::ISeq.fetch(path.to_s)
186
+ rescue RuntimeError => error
187
+ if error.message.include?("unmatched platform")
188
+ puts("unmatched platform for file #{path}")
189
+ end
190
+ raise
191
+ end
109
192
 
110
- Bootsnap::CompileCache::ISeq.fetch(path.to_s)
111
- rescue RuntimeError => error
112
- if error.message =~ /unmatched platform/
113
- puts("unmatched platform for file #{path}")
193
+ def compile_option=(hash)
194
+ super
195
+ Bootsnap::CompileCache::ISeq.compile_option_updated
114
196
  end
115
- raise
116
197
  end
117
198
 
118
- def compile_option=(hash)
119
- super
120
- Bootsnap::CompileCache::ISeq.compile_option_updated
199
+ def self.compile_option_updated
200
+ option = RubyVM::InstructionSequence.compile_option
201
+ crc = Zlib.crc32(option.inspect)
202
+ Bootsnap::CompileCache::Native.compile_option_crc32 = crc
203
+ FROZEN_STRING_LITERAL.update_options
121
204
  end
122
- end
205
+ compile_option_updated if supported?
123
206
 
124
- def self.compile_option_updated
125
- option = RubyVM::InstructionSequence.compile_option
126
- crc = Zlib.crc32(option.inspect)
127
- Bootsnap::CompileCache::Native.compile_option_crc32 = crc
128
- end
129
- compile_option_updated if supported?
207
+ def self.install!(cache_dir)
208
+ Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
130
209
 
131
- def self.install!(cache_dir)
132
- Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
210
+ return unless supported?
133
211
 
134
- return unless supported?
212
+ Bootsnap::CompileCache::ISeq.compile_option_updated
135
213
 
136
- Bootsnap::CompileCache::ISeq.compile_option_updated
214
+ class << RubyVM::InstructionSequence
215
+ prepend(InstructionSequenceMixin)
216
+ end
217
+ end
218
+ else
219
+ def self.install!(...)
220
+ # noop
221
+ end
137
222
 
138
- class << RubyVM::InstructionSequence
139
- prepend(InstructionSequenceMixin)
223
+ def self.precompile(...)
224
+ # noop
140
225
  end
141
226
  end
142
227
  end
@@ -0,0 +1 @@
1
+ _ = "test"
@@ -47,14 +47,6 @@ module Bootsnap
47
47
  SUPPORTED_INTERNAL_ENCODINGS.include?(Encoding.default_internal)
48
48
  end
49
49
 
50
- module EncodingAwareSymbols
51
- extend self
52
-
53
- def unpack(payload)
54
- (+payload).force_encoding(Encoding::UTF_8).to_sym
55
- end
56
- end
57
-
58
50
  def init!
59
51
  require "yaml"
60
52
  require "msgpack"
@@ -86,28 +78,78 @@ module Bootsnap
86
78
  0x00,
87
79
  Symbol,
88
80
  packer: :to_msgpack_ext,
89
- unpacker: EncodingAwareSymbols.method(:unpack).to_proc,
81
+ unpacker: :from_msgpack_ext,
82
+ optimized_symbol_parsing: true,
90
83
  )
91
84
 
92
- if defined? MessagePack::Timestamp
93
- factory.register_type(
94
- MessagePack::Timestamp::TYPE, # or just -1
95
- Time,
96
- packer: MessagePack::Time::Packer,
97
- unpacker: MessagePack::Time::Unpacker,
98
- )
85
+ factory.register_type(
86
+ MessagePack::Timestamp::TYPE, # or just -1
87
+ Time,
88
+ packer: MessagePack::Time::Packer,
89
+ unpacker: MessagePack::Time::Unpacker,
90
+ )
99
91
 
100
- marshal_fallback = {
101
- packer: ->(value) { Marshal.dump(value) },
102
- unpacker: ->(payload) { Marshal.load(payload) },
103
- }
104
- {
105
- Date => 0x01,
106
- Regexp => 0x02,
107
- }.each do |type, code|
108
- factory.register_type(code, type, marshal_fallback)
109
- end
110
- end
92
+ factory.register_type(
93
+ 0x01,
94
+ Date,
95
+ packer: lambda { |date, packer|
96
+ packer.write(date.year)
97
+ packer.write(date.month)
98
+ packer.write(date.day)
99
+ },
100
+ unpacker: lambda { |unpacker|
101
+ ::Date.new(unpacker.read, unpacker.read, unpacker.read)
102
+ },
103
+ recursive: true,
104
+ )
105
+
106
+ factory.register_type(
107
+ 0x02,
108
+ Regexp,
109
+ packer: ->(value) { Marshal.dump(value) },
110
+ unpacker: ->(payload) { Marshal.load(payload) },
111
+ )
112
+
113
+ factory.register_type(
114
+ 0x03,
115
+ DateTime,
116
+ packer: lambda { |dt, packer|
117
+ packer.write(dt.year)
118
+ packer.write(dt.month)
119
+ packer.write(dt.day)
120
+ packer.write(dt.hour)
121
+ packer.write(dt.minute)
122
+
123
+ sec = dt.sec + dt.sec_fraction
124
+ packer.write(sec.numerator)
125
+ packer.write(sec.denominator)
126
+
127
+ offset = dt.offset
128
+ packer.write(offset.numerator)
129
+ packer.write(offset.denominator)
130
+ },
131
+ unpacker: lambda { |unpacker|
132
+ ::DateTime.new(
133
+ unpacker.read, # year
134
+ unpacker.read, # month
135
+ unpacker.read, # day
136
+ unpacker.read, # hour
137
+ unpacker.read, # minute
138
+ Rational(unpacker.read, unpacker.read), # sec fraction
139
+ Rational(unpacker.read, unpacker.read), # offset fraction
140
+ )
141
+ },
142
+ recursive: true,
143
+ )
144
+
145
+ require "msgpack/bigint"
146
+ factory.register_type(
147
+ 0x04,
148
+ Integer,
149
+ packer: MessagePack::Bigint.method(:to_msgpack_ext),
150
+ unpacker: MessagePack::Bigint.method(:from_msgpack_ext),
151
+ oversized_integer_extension: true,
152
+ )
111
153
 
112
154
  self.msgpack_factory = factory
113
155
 
@@ -116,7 +158,7 @@ module Bootsnap
116
158
  if params.include?([:key, :symbolize_names])
117
159
  supported_options << :symbolize_names
118
160
  end
119
- if params.include?([:key, :freeze]) && factory.load(factory.dump("yaml"), freeze: true).frozen?
161
+ if params.include?([:key, :freeze])
120
162
  supported_options << :freeze
121
163
  end
122
164
  supported_options.freeze
@@ -135,6 +177,14 @@ module Bootsnap
135
177
 
136
178
  NoTagsVisitor.new(scanner, loader).visit(ast)
137
179
  end
180
+
181
+ def msgpack_unpacker_options(kwargs)
182
+ return kwargs unless kwargs&.key?(:symbolize_names)
183
+
184
+ kwargs = kwargs.dup
185
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
186
+ kwargs
187
+ end
138
188
  end
139
189
 
140
190
  module Psych4
@@ -164,11 +214,9 @@ module Bootsnap
164
214
  end
165
215
 
166
216
  def storage_to_output(data, kwargs)
167
- if kwargs&.key?(:symbolize_names)
168
- kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
169
- end
170
-
171
- unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
217
+ unpacker = CompileCache::YAML.msgpack_factory.unpacker(
218
+ CompileCache::YAML.msgpack_unpacker_options(kwargs),
219
+ )
172
220
  unpacker.feed(data)
173
221
  _safe_loaded = unpacker.unpack
174
222
  result = unpacker.unpack
@@ -202,11 +250,9 @@ module Bootsnap
202
250
  end
203
251
 
204
252
  def storage_to_output(data, kwargs)
205
- if kwargs&.key?(:symbolize_names)
206
- kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
207
- end
208
-
209
- unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
253
+ unpacker = CompileCache::YAML.msgpack_factory.unpacker(
254
+ CompileCache::YAML.msgpack_unpacker_options(kwargs),
255
+ )
210
256
  unpacker.feed(data)
211
257
  safe_loaded = unpacker.unpack
212
258
  if safe_loaded
@@ -282,10 +328,9 @@ module Bootsnap
282
328
  end
283
329
 
284
330
  def storage_to_output(data, kwargs)
285
- if kwargs&.key?(:symbolize_names)
286
- kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
287
- end
288
- unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
331
+ unpacker = CompileCache::YAML.msgpack_factory.unpacker(
332
+ CompileCache::YAML.msgpack_unpacker_options(kwargs),
333
+ )
289
334
  unpacker.feed(data)
290
335
  _safe_loaded = unpacker.unpack
291
336
  unpacker.unpack
@@ -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.1"
4
+ VERSION = "1.25.0"
5
5
  end
data/lib/bootsnap.rb CHANGED
@@ -6,6 +6,9 @@ require_relative "bootsnap/bundler"
6
6
  module Bootsnap
7
7
  InvalidConfiguration = Class.new(StandardError)
8
8
 
9
+ # JITs shouldn't change the cache key as they have no impact on iseq generation
10
+ RUBY_CACHE_KEY = RUBY_DESCRIPTION.gsub(/\s\+\wJIT/, "").freeze
11
+
9
12
  class << self
10
13
  attr_reader :cache_dir, :logger
11
14
 
@@ -43,13 +46,6 @@ module Bootsnap
43
46
  @instrumentation.call(event, path)
44
47
  end
45
48
 
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
49
  def setup(
54
50
  cache_dir:,
55
51
  development_mode: true,
@@ -59,7 +55,8 @@ module Bootsnap
59
55
  revalidation: false,
60
56
  compile_cache_iseq: true,
61
57
  compile_cache_yaml: true,
62
- compile_cache_json: (compile_cache_json_unset = true)
58
+ compile_cache_json: (compile_cache_json_unset = true),
59
+ config_path: nil
63
60
  )
64
61
  unless compile_cache_json_unset
65
62
  warn("Bootsnap.setup `compile_cache_json` argument is deprecated and has no effect")
@@ -84,7 +81,16 @@ module Bootsnap
84
81
  revalidation: revalidation,
85
82
  )
86
83
 
87
- load_config
84
+ load_config(config_path)
85
+ end
86
+
87
+ def load_config(config_path)
88
+ if config_path
89
+ config_path = File.expand_path(config_path)
90
+ if File.exist?(config_path)
91
+ require(config_path)
92
+ end
93
+ end
88
94
  end
89
95
 
90
96
  def enable_frozen_string_literal(app_only: false)
@@ -102,8 +108,8 @@ module Bootsnap
102
108
  end
103
109
  }
104
110
  else
105
- Bootsnap::CompileCache::ISeq.compiler_selector = nil
106
- Bootsnap::CompileCache::ISeq.default_compiler = Bootsnap::CompileCache::ISeq::FROZEN_STRING_LITERAL
111
+ options = RubyVM::InstructionSequence.compile_option.merge(frozen_string_literal: true)
112
+ RubyVM::InstructionSequence.compile_option = options
107
113
  end
108
114
  end
109
115
 
@@ -150,6 +156,7 @@ module Bootsnap
150
156
  readonly: bool_env("BOOTSNAP_READONLY"),
151
157
  revalidation: bool_env("BOOTSNAP_REVALIDATE"),
152
158
  ignore_directories: ignore_directories,
159
+ config_path: ENV["BOOTSNAP_CONFIG"] || "config/bootsnap.rb",
153
160
  )
154
161
 
155
162
  if ENV["BOOTSNAP_LOG"]
@@ -160,7 +167,7 @@ module Bootsnap
160
167
  end
161
168
  end
162
169
 
163
- if RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/
170
+ if /mswin|mingw|cygwin/.match?(RbConfig::CONFIG["host_os"])
164
171
  def absolute_path?(path)
165
172
  path[1] == ":"
166
173
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bootsnap
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.24.1
4
+ version: 1.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '1.2'
18
+ version: '1.5'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '1.2'
25
+ version: '1.5'
26
26
  description: Boot large ruby/rails apps faster
27
27
  email:
28
28
  - burke.libbey@shopify.com
@@ -44,6 +44,7 @@ files:
44
44
  - lib/bootsnap/cli/worker_pool.rb
45
45
  - lib/bootsnap/compile_cache.rb
46
46
  - lib/bootsnap/compile_cache/iseq.rb
47
+ - lib/bootsnap/compile_cache/ruby_bug_22023_canary.rb
47
48
  - lib/bootsnap/compile_cache/yaml.rb
48
49
  - lib/bootsnap/explicit_require.rb
49
50
  - lib/bootsnap/load_path_cache.rb
@@ -80,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
80
81
  - !ruby/object:Gem::Version
81
82
  version: '0'
82
83
  requirements: []
83
- rubygems_version: 4.0.6
84
+ rubygems_version: 4.0.16
84
85
  specification_version: 4
85
86
  summary: Boot large ruby/rails apps faster
86
87
  test_files: []