bootsnap 1.22.0 → 1.24.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: 1ab247855b4e6a35b2db80b44f2d3da184fd9780e9a4faa3d35d952e15398839
4
- data.tar.gz: c8fd54ed988e11df7e934e9ff4f30f7dfb6440b4b9a2ecd2eaf3dbf9aefa61fd
3
+ metadata.gz: 9680e880d828e4c61704db75e793e97b4bcaa9142e37335b697d28a14c1d7f9c
4
+ data.tar.gz: 876136914c8d89c771304fffc2d8587b3b15b0eb6abbf684ad5d4adb39b98550
5
5
  SHA512:
6
- metadata.gz: 4a7225b866cbe788281af91aa690c75191e19bc09f4d8695a58557e024193d4b0924bbf9892cbb84dad93658eda9ff46c609b113c0b588660ac318e0b9176f1f
7
- data.tar.gz: cadb844c862e9185befec65d7fa53beb6940945ebbddbea48ab30fd22b88f19aa773baa856c414522f5acf1a1c2a8aeafa75d72fb5a86cde5ebc8557172d049c
6
+ metadata.gz: 84bd60feda93f48dfeb2c8149c72a72a619050eb163decf83bf14d78173a3aa52adff84dc6a22f7821ee7d1c937a0449f96686f6199c77e53ff990f123c30359
7
+ data.tar.gz: 41d345dee52323ba80df2c8165bad4a4ea6c04780d8eb7e459a5e766973b797f68267ac63fbad38a3add47e59041aa8d7786ddb43b5ed0cfb7ac98743cd70ca5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Unreleased
2
2
 
3
+ # 1.24.0
4
+
5
+ * Added a hook API to customize Ruby compilation.
6
+
7
+ # 1.23.0
8
+
9
+ * Require Ruby 2.7.
10
+ * Fix support for absolute paths in `BOOTSNAP_IGNORE_DIRECTORIES`.
11
+
3
12
  # 1.22.0
4
13
 
5
14
  * Better fix for the `opendir` crash.
data/README.md CHANGED
@@ -75,6 +75,7 @@ well together.
75
75
  `require 'bootsnap/setup'` behavior can be changed using environment variables:
76
76
 
77
77
  - `BOOTSNAP_CACHE_DIR` allows to define the cache location.
78
+ - `BOOTSNAP_CONFIG` allows to change the default config location (`config/bootsnap.rb`).
78
79
  - `DISABLE_BOOTSNAP` allows to entirely disable bootsnap.
79
80
  - `DISABLE_BOOTSNAP_LOAD_PATH_CACHE` allows to disable load path caching.
80
81
  - `DISABLE_BOOTSNAP_COMPILE_CACHE` allows to disable ISeq and YAML caches.
@@ -320,6 +321,34 @@ open /c/nope.bundle -> -1
320
321
  # (nothing!)
321
322
  ```
322
323
 
324
+ ## Custom Compilers
325
+
326
+ Bootsnap allows substituing the default Ruby compiler by another one.
327
+ This can be configured from the bootsnap config file (defaults to `config/bootsnap.rb`).
328
+
329
+ The main use case is to programmatically enable frozen string literals for your project without impacting dependencies:
330
+
331
+ ```ruby
332
+ Bootsnap.enable_frozen_string_literal(app_only: true)
333
+ ```
334
+
335
+ But it can also be used for more fine grained logic, or to implement all sort of Ruby code preprocessing:
336
+
337
+ ```ruby
338
+ # config/bootsnap.rb
339
+ gems_root = File.join(Bundler.bundle_path.cleanpath, "")
340
+ app_root = File.join(Dir.pwd, "")
341
+ Bootsnap::CompileCache::ISeq.compiler_selector = ->(path) do
342
+ # Enable `frozen_string_literal: true` for app code, but not gems.
343
+
344
+ if path.start_with?(app_root) && !path.start_with?(gems_root)
345
+ Bootsnap::CompileCache::ISeq::FROZEN_STRING_LITERAL
346
+ else
347
+ Bootsnap::CompileCache::ISeq::DEFAULT
348
+ end
349
+ end
350
+ ```
351
+
323
352
  ## Precompilation
324
353
 
325
354
  In development environments the bootsnap compilation cache is generated on the fly when source files are loaded.
@@ -109,13 +109,15 @@ static bool readonly = false;
109
109
  static bool revalidation = false;
110
110
  static bool perm_issue = false;
111
111
 
112
+ static ID id_storage_to_output, id_input_to_output, id_input_to_storage;
113
+
112
114
  /* Functions exposed as module functions on Bootsnap::CompileCache::Native */
113
115
  static VALUE bs_instrumentation_enabled_set(VALUE self, VALUE enabled);
114
116
  static VALUE bs_readonly_set(VALUE self, VALUE enabled);
115
117
  static VALUE bs_revalidation_set(VALUE self, VALUE enabled);
116
118
  static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v);
117
- static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args);
118
- static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
119
+ static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler, VALUE args);
120
+ static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler);
119
121
 
120
122
  /* Helpers */
121
123
  enum cache_status {
@@ -123,7 +125,7 @@ enum cache_status {
123
125
  hit,
124
126
  stale,
125
127
  };
126
- static void bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_CACHEPATH_SIZE]);
128
+ static void bs_cache_path(VALUE cachedir_v, VALUE namespace_v, VALUE path_v, char (* cache_path)[MAX_CACHEPATH_SIZE]);
127
129
  static int bs_read_key(int fd, struct bs_cache_key * key);
128
130
  static enum cache_status cache_key_equal_fast_path(struct bs_cache_key * k1, struct bs_cache_key * k2);
129
131
  static int cache_key_equal_slow_path(struct bs_cache_key * current_key, struct bs_cache_key * cached_key, const VALUE input_data);
@@ -143,7 +145,7 @@ static uint32_t get_ruby_platform(void);
143
145
  */
144
146
  static int bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * output_data);
145
147
  static VALUE prot_input_to_output(VALUE arg);
146
- static void bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE * output_data, int * exception_tag);
148
+ static void bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * output_data, int * exception_tag);
147
149
  static int bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * storage_data);
148
150
  struct s2o_data;
149
151
  struct i2o_data;
@@ -194,8 +196,12 @@ bs_rb_scan_dir(VALUE self, VALUE abspath)
194
196
  struct stat st;
195
197
  int dfd = -1;
196
198
 
197
- errno = 0;
198
- while ((entry = readdir(dirp))) {
199
+ while (1) {
200
+ errno = 0;
201
+
202
+ entry = readdir(dirp);
203
+ if (entry == NULL) break;
204
+
199
205
  if (entry->d_name[0] == '.') continue;
200
206
 
201
207
  if (RB_UNLIKELY(entry->d_type == DT_UNKNOWN || entry->d_type == DT_LNK)) {
@@ -212,11 +218,8 @@ bs_rb_scan_dir(VALUE self, VALUE abspath)
212
218
  }
213
219
 
214
220
  if (fstatat(dfd, entry->d_name, &st, 0)) {
215
- if (errno == ENOENT) {
216
- // Broken symlinK
217
- errno = 0;
218
- continue;
219
- }
221
+ if (errno == ENOENT) continue; // Broken symlink
222
+
220
223
  int err = errno;
221
224
  closedir(dirp);
222
225
  bs_syserr_fail_dir_entry("fstatat", err, abspath, entry->d_name);
@@ -249,10 +252,15 @@ bs_rb_scan_dir(VALUE self, VALUE abspath)
249
252
  }
250
253
  }
251
254
 
252
- if (closedir(dirp)) {
255
+ if (errno) {
256
+ int err = errno;
257
+ closedir(dirp);
258
+ bs_syserr_fail_path("readdir", err, abspath);
259
+ } else if (closedir(dirp)) {
253
260
  bs_syserr_fail_path("closedir", errno, abspath);
254
261
  return Qundef;
255
262
  }
263
+
256
264
  return result;
257
265
  }
258
266
  #endif
@@ -269,6 +277,10 @@ Init_bootsnap(void)
269
277
  {
270
278
  rb_mBootsnap = rb_define_module("Bootsnap");
271
279
 
280
+ id_storage_to_output = rb_intern("storage_to_output");
281
+ id_input_to_output = rb_intern("input_to_output");
282
+ id_input_to_storage = rb_intern("input_to_storage");
283
+
272
284
  rb_define_singleton_method(rb_mBootsnap, "rb_get_path", bs_rb_get_path, 1);
273
285
 
274
286
  #ifdef HAVE_FSTATAT
@@ -296,8 +308,8 @@ Init_bootsnap(void)
296
308
  rb_define_module_function(rb_mBootsnap, "instrumentation_enabled=", bs_instrumentation_enabled_set, 1);
297
309
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "readonly=", bs_readonly_set, 1);
298
310
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "revalidation=", bs_revalidation_set, 1);
299
- rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 4);
300
- rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 3);
311
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 5);
312
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 4);
301
313
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "compile_option_crc32=", bs_compile_option_crc32_set, 1);
302
314
 
303
315
  current_umask = umask(0777);
@@ -414,13 +426,28 @@ get_ruby_platform(void)
414
426
  * The path will look something like: <cachedir>/12/34567890abcdef
415
427
  */
416
428
  static void
417
- bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_CACHEPATH_SIZE])
429
+ bs_cache_path(VALUE cachedir_v, VALUE namespace_v, VALUE path_v, char (* cache_path)[MAX_CACHEPATH_SIZE])
418
430
  {
419
- uint64_t hash = fnv1a_64(path);
431
+ FilePathValue(path_v);
432
+
433
+ Check_Type(cachedir_v, T_STRING);
434
+ Check_Type(path_v, T_STRING);
435
+ if (!NIL_P(namespace_v)) {
436
+ Check_Type(namespace_v, T_STRING);
437
+ }
438
+
439
+ if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
440
+ rb_raise(rb_eArgError, "cachedir too long");
441
+ }
442
+
443
+ const char * cachedir = RSTRING_PTR(cachedir_v);
444
+ const char * namespace = NIL_P(namespace_v) ? "" : RSTRING_PTR(namespace_v);
445
+
446
+ uint64_t hash = fnv1a_64(path_v);
420
447
  uint8_t first_byte = (hash >> (64 - 8));
421
448
  uint64_t remainder = hash & 0x00ffffffffffffff;
422
449
 
423
- sprintf(*cache_path, "%s/%02"PRIx8"/%014"PRIx64, cachedir, first_byte, remainder);
450
+ sprintf(*cache_path, "%s%s/%02"PRIx8"/%014"PRIx64, cachedir, namespace, first_byte, remainder);
424
451
  }
425
452
 
426
453
  /*
@@ -492,25 +519,14 @@ static void bs_cache_key_digest(struct bs_cache_key *key,
492
519
  * conversions on the ruby VALUE arguments before passing them along.
493
520
  */
494
521
  static VALUE
495
- bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args)
522
+ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler, VALUE args)
496
523
  {
497
- FilePathValue(path_v);
498
-
499
- Check_Type(cachedir_v, T_STRING);
500
- Check_Type(path_v, T_STRING);
501
-
502
- if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
503
- rb_raise(rb_eArgError, "cachedir too long");
504
- }
505
-
506
- char * cachedir = RSTRING_PTR(cachedir_v);
507
- char * path = RSTRING_PTR(path_v);
508
524
  char cache_path[MAX_CACHEPATH_SIZE];
509
525
 
510
526
  /* generate cache path to cache_path */
511
- bs_cache_path(cachedir, path_v, &cache_path);
527
+ bs_cache_path(cachedir_v, namespace_v, path_v, &cache_path);
512
528
 
513
- return bs_fetch(path, path_v, cache_path, handler, args);
529
+ return bs_fetch(RSTRING_PTR(path_v), path_v, cache_path, handler, args);
514
530
  }
515
531
 
516
532
  /*
@@ -519,25 +535,13 @@ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE arg
519
535
  * and doesn't return the content.
520
536
  */
521
537
  static VALUE
522
- bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
538
+ bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler)
523
539
  {
524
- FilePathValue(path_v);
525
-
526
- Check_Type(cachedir_v, T_STRING);
527
- Check_Type(path_v, T_STRING);
528
-
529
- if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
530
- rb_raise(rb_eArgError, "cachedir too long");
531
- }
532
-
533
- char * cachedir = RSTRING_PTR(cachedir_v);
534
- char * path = RSTRING_PTR(path_v);
535
540
  char cache_path[MAX_CACHEPATH_SIZE];
536
-
537
541
  /* generate cache path to cache_path */
538
- bs_cache_path(cachedir, path_v, &cache_path);
542
+ bs_cache_path(cachedir_v, namespace_v, path_v, &cache_path);
539
543
 
540
- return bs_precompile(path, path_v, cache_path, handler);
544
+ return bs_precompile(RSTRING_PTR(path_v), path_v, cache_path, handler);
541
545
  }
542
546
 
543
547
  static int bs_open_noatime(const char *path, int flags) {
@@ -957,7 +961,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
957
961
  exception_message = path_v;
958
962
  goto fail_errno;
959
963
  }
960
- bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
964
+ bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
961
965
  if (exception_tag != 0) goto raise;
962
966
  goto succeed;
963
967
  } else if (res == CACHE_MISS || res == CACHE_STALE) valid_cache = 0;
@@ -983,7 +987,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
983
987
  /* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try
984
988
  * to cache anything; just return input_to_output(input_data) */
985
989
  if (storage_data == rb_cBootsnap_CompileCache_UNCOMPILABLE) {
986
- bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
990
+ bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
987
991
  if (exception_tag != 0) goto raise;
988
992
  goto succeed;
989
993
  }
@@ -1003,7 +1007,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
1003
1007
 
1004
1008
  if (output_data == rb_cBootsnap_CompileCache_UNCOMPILABLE) {
1005
1009
  /* If storage_to_output returned `Uncompilable` we fallback to `input_to_output` */
1006
- bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
1010
+ bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
1007
1011
  if (exception_tag != 0) goto raise;
1008
1012
  } else if (NIL_P(output_data)) {
1009
1013
  /* If output_data is nil, delete the cache entry and generate the output
@@ -1017,7 +1021,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
1017
1021
  goto fail_errno;
1018
1022
  }
1019
1023
  }
1020
- bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
1024
+ bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
1021
1025
  if (exception_tag != 0) goto raise;
1022
1026
  }
1023
1027
 
@@ -1175,6 +1179,7 @@ struct i2o_data {
1175
1179
  VALUE handler;
1176
1180
  VALUE args;
1177
1181
  VALUE input_data;
1182
+ VALUE pathval;
1178
1183
  };
1179
1184
 
1180
1185
  struct i2s_data {
@@ -1187,7 +1192,7 @@ static VALUE
1187
1192
  try_storage_to_output(VALUE arg)
1188
1193
  {
1189
1194
  struct s2o_data * data = (struct s2o_data *)arg;
1190
- return rb_funcall(data->handler, rb_intern("storage_to_output"), 2, data->storage_data, data->args);
1195
+ return rb_funcall(data->handler, id_storage_to_output, 2, data->storage_data, data->args);
1191
1196
  }
1192
1197
 
1193
1198
  static int
@@ -1204,12 +1209,13 @@ bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * outp
1204
1209
  }
1205
1210
 
1206
1211
  static void
1207
- bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE * output_data, int * exception_tag)
1212
+ bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE path_v, VALUE * output_data, int * exception_tag)
1208
1213
  {
1209
1214
  struct i2o_data i2o_data = {
1210
1215
  .handler = handler,
1211
1216
  .args = args,
1212
1217
  .input_data = input_data,
1218
+ .pathval = path_v,
1213
1219
  };
1214
1220
  *output_data = rb_protect(prot_input_to_output, (VALUE)&i2o_data, exception_tag);
1215
1221
  }
@@ -1218,14 +1224,14 @@ static VALUE
1218
1224
  prot_input_to_output(VALUE arg)
1219
1225
  {
1220
1226
  struct i2o_data * data = (struct i2o_data *)arg;
1221
- return rb_funcall(data->handler, rb_intern("input_to_output"), 2, data->input_data, data->args);
1227
+ return rb_funcall(data->handler, id_input_to_output, 3, data->input_data, data->pathval, data->args);
1222
1228
  }
1223
1229
 
1224
1230
  static VALUE
1225
1231
  try_input_to_storage(VALUE arg)
1226
1232
  {
1227
1233
  struct i2s_data * data = (struct i2s_data *)arg;
1228
- return rb_funcall(data->handler, rb_intern("input_to_storage"), 2, data->input_data, data->pathval);
1234
+ return rb_funcall(data->handler, id_input_to_storage, 2, data->input_data, data->pathval);
1229
1235
  }
1230
1236
 
1231
1237
  static int
data/lib/bootsnap/cli.rb CHANGED
@@ -43,6 +43,7 @@ module Bootsnap
43
43
  yaml: yaml,
44
44
  revalidation: true,
45
45
  )
46
+ Bootsnap.load_config
46
47
 
47
48
  @work_pool = WorkerPool.create(size: jobs, jobs: {
48
49
  ruby: method(:precompile_ruby),
@@ -7,7 +7,8 @@ module Bootsnap
7
7
  module CompileCache
8
8
  module ISeq
9
9
  class << self
10
- attr_reader(:cache_dir)
10
+ attr_reader :cache_dir
11
+ attr_accessor :compiler_selector, :default_compiler
11
12
 
12
13
  def cache_dir=(cache_dir)
13
14
  @cache_dir = cache_dir.end_with?("/") ? "#{cache_dir}iseq" : "#{cache_dir}-iseq"
@@ -18,71 +19,83 @@ module Bootsnap
18
19
  end
19
20
  end
20
21
 
21
- has_ruby_bug_18250 = begin # https://bugs.ruby-lang.org/issues/18250
22
- if defined? RubyVM::InstructionSequence
23
- RubyVM::InstructionSequence.compile("def foo(*); ->{ super }; end; def foo(**); ->{ super }; end").to_binary
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
24
28
  end
25
- false
26
- rescue TypeError
27
- true
28
- end
29
29
 
30
- if has_ruby_bug_18250
31
- def self.input_to_storage(_, path)
32
- iseq = begin
33
- RubyVM::InstructionSequence.compile_file(path)
34
- rescue SyntaxError
35
- return UNCOMPILABLE # syntax error
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
36
33
  end
34
+ false
35
+ rescue TypeError
36
+ true
37
+ end
38
+
39
+ if has_ruby_bug_18250
40
+ def input_to_storage(_, path)
41
+ iseq = RubyVM::InstructionSequence.compile_file(path, @compile_options)
37
42
 
38
- begin
39
- iseq.to_binary
40
- rescue TypeError
41
- UNCOMPILABLE # ruby bug #18250
43
+ begin
44
+ iseq.to_binary
45
+ rescue TypeError
46
+ UNCOMPILABLE # ruby bug #18250
47
+ end
48
+ end
49
+ else
50
+ def input_to_storage(_, path)
51
+ RubyVM::InstructionSequence.compile_file(path, @compile_options).to_binary
42
52
  end
43
53
  end
44
- else
45
- def self.input_to_storage(_, path)
46
- RubyVM::InstructionSequence.compile_file(path).to_binary
47
- rescue SyntaxError
48
- UNCOMPILABLE # syntax error
54
+
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
65
+ end
49
66
  end
50
- end
51
67
 
52
- def self.storage_to_output(binary, _args)
53
- iseq = RubyVM::InstructionSequence.load_from_binary(binary)
54
- binary.clear
55
- iseq
56
- rescue RuntimeError => error
57
- if error.message == "broken binary format"
58
- $stderr.puts("[Bootsnap::CompileCache] warning: rejecting broken binary")
59
- nil
60
- else
61
- raise
68
+ def input_to_output(source, path, _kwargs)
69
+ RubyVM::InstructionSequence.compile(source, path, path, nil, @compile_options)
62
70
  end
63
71
  end
64
72
 
73
+ DEFAULT = Compiler.new
74
+ FROZEN_STRING_LITERAL = Compiler.new("-fstr", {frozen_string_literal: true}.freeze)
75
+ MUTABLE_STRING_LITERAL = Compiler.new("-no-fstr", {frozen_string_literal: false}.freeze)
76
+ @default_compiler = DEFAULT
77
+
65
78
  def self.fetch(path, cache_dir: ISeq.cache_dir)
79
+ compiler = compiler_selector&.call(path) || default_compiler
66
80
  Bootsnap::CompileCache::Native.fetch(
67
81
  cache_dir,
82
+ compiler.namespace,
68
83
  path.to_s,
69
- Bootsnap::CompileCache::ISeq,
84
+ compiler,
70
85
  nil,
71
86
  )
72
87
  end
73
88
 
74
89
  def self.precompile(path)
90
+ compiler = compiler_selector&.call(path) || default_compiler
75
91
  Bootsnap::CompileCache::Native.precompile(
76
92
  cache_dir,
93
+ compiler.namespace,
77
94
  path.to_s,
78
- Bootsnap::CompileCache::ISeq,
95
+ compiler,
79
96
  )
80
97
  end
81
98
 
82
- def self.input_to_output(_data, _kwargs)
83
- nil # ruby handles this
84
- end
85
-
86
99
  module InstructionSequenceMixin
87
100
  def load_iseq(path)
88
101
  # Having coverage enabled prevents iseq dumping/loading.
@@ -97,7 +110,7 @@ module Bootsnap
97
110
  end
98
111
 
99
112
  def compile_option=(hash)
100
- super(hash)
113
+ super
101
114
  Bootsnap::CompileCache::ISeq.compile_option_updated
102
115
  end
103
116
  end
@@ -28,6 +28,7 @@ module Bootsnap
28
28
 
29
29
  CompileCache::Native.precompile(
30
30
  cache_dir,
31
+ nil,
31
32
  path.to_s,
32
33
  @implementation,
33
34
  )
@@ -175,7 +176,7 @@ module Bootsnap
175
176
  result
176
177
  end
177
178
 
178
- def input_to_output(data, kwargs)
179
+ def input_to_output(data, _path, kwargs)
179
180
  ::YAML.unsafe_load(data, **(kwargs || {}))
180
181
  end
181
182
  end
@@ -215,7 +216,7 @@ module Bootsnap
215
216
  end
216
217
  end
217
218
 
218
- def input_to_output(data, kwargs)
219
+ def input_to_output(data, _path, kwargs)
219
220
  ::YAML.load(data, **(kwargs || {}))
220
221
  end
221
222
  end
@@ -233,6 +234,7 @@ module Bootsnap
233
234
 
234
235
  CompileCache::Native.fetch(
235
236
  CompileCache::YAML.cache_dir,
237
+ nil,
236
238
  File.realpath(path),
237
239
  CompileCache::YAML::Psych4::SafeLoad,
238
240
  kwargs,
@@ -253,6 +255,7 @@ module Bootsnap
253
255
 
254
256
  CompileCache::Native.fetch(
255
257
  CompileCache::YAML.cache_dir,
258
+ nil,
256
259
  File.realpath(path),
257
260
  CompileCache::YAML::Psych4::UnsafeLoad,
258
261
  kwargs,
@@ -288,7 +291,7 @@ module Bootsnap
288
291
  unpacker.unpack
289
292
  end
290
293
 
291
- def input_to_output(data, kwargs)
294
+ def input_to_output(data, _path, kwargs)
292
295
  ::YAML.load(data, **(kwargs || {}))
293
296
  end
294
297
 
@@ -305,6 +308,7 @@ module Bootsnap
305
308
 
306
309
  CompileCache::Native.fetch(
307
310
  CompileCache::YAML.cache_dir,
311
+ nil,
308
312
  File.realpath(path),
309
313
  CompileCache::YAML::Psych3,
310
314
  kwargs,
@@ -325,6 +329,7 @@ module Bootsnap
325
329
 
326
330
  CompileCache::Native.fetch(
327
331
  CompileCache::YAML.cache_dir,
332
+ nil,
328
333
  File.realpath(path),
329
334
  CompileCache::YAML::Psych3,
330
335
  kwargs,
@@ -5,7 +5,7 @@ module Kernel
5
5
 
6
6
  alias_method :require, :require # Avoid method redefinition warnings
7
7
 
8
- def require(path) # rubocop:disable Lint/DuplicateMethods
8
+ def require(path)
9
9
  return require_without_bootsnap(path) unless Bootsnap::LoadPathCache.enabled?
10
10
 
11
11
  string_path = Bootsnap.rb_get_path(path)
@@ -59,7 +59,7 @@ module Bootsnap
59
59
  end
60
60
 
61
61
  def purge_multi(features)
62
- rejected_hashes = features.each_with_object({}) { |f, h| h[f.hash] = true }
62
+ rejected_hashes = features.to_h { |f| [f.hash, true] }
63
63
  @mutex.synchronize do
64
64
  @lfi.reject! { |_, hash| rejected_hashes.key?(hash) }
65
65
  end
@@ -18,21 +18,12 @@ module Bootsnap
18
18
  class << self
19
19
  attr_accessor :ignored_directories
20
20
 
21
- def ruby_call(path)
22
- path = File.expand_path(path.to_s).freeze
23
- return [] unless File.directory?(path)
24
-
25
- # If the bundle path is a descendent of this path, we do additional
26
- # checks to prevent recursing into the bundle path as we recurse
27
- # through this path. We don't want to scan the bundle path because
28
- # anything useful in it will be present on other load path items.
29
- #
30
- # This can happen if, for example, the user adds '.' to the load path,
31
- # and the bundle path is '.bundle'.
32
- contains_bundle_path = BUNDLE_PATH.start_with?(path)
21
+ def ruby_call(root_path)
22
+ root_path, contains_bundle_path, ignored_abs_paths, ignored_dir_names = prepare_scan(root_path)
23
+ return [] unless File.directory?(root_path)
33
24
 
34
25
  requirables = []
35
- walk(path, nil) do |relative_path, absolute_path, is_directory|
26
+ walk(root_path, nil, ignored_abs_paths, ignored_dir_names) do |relative_path, absolute_path, is_directory|
36
27
  if is_directory
37
28
  !contains_bundle_path || !absolute_path.start_with?(BUNDLE_PATH)
38
29
  elsif relative_path.end_with?(*REQUIRABLE_EXTENSIONS)
@@ -42,25 +33,6 @@ module Bootsnap
42
33
  requirables
43
34
  end
44
35
 
45
- def walk(absolute_dir_path, relative_dir_path, &block)
46
- Dir.foreach(absolute_dir_path) do |name|
47
- next if name.start_with?(".")
48
-
49
- relative_path = relative_dir_path ? File.join(relative_dir_path, name) : name
50
-
51
- absolute_path = "#{absolute_dir_path}/#{name}"
52
- if File.directory?(absolute_path)
53
- next if ignored_directories.include?(name) || ignored_directories.include?(absolute_path)
54
-
55
- if yield relative_path, absolute_path, true
56
- walk(absolute_path, relative_path, &block)
57
- end
58
- else
59
- yield relative_path, absolute_path, false
60
- end
61
- end
62
- end
63
-
64
36
  if RUBY_ENGINE == "ruby" && RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/)
65
37
  require "bootsnap/bootsnap"
66
38
  end
@@ -69,33 +41,35 @@ module Bootsnap
69
41
  def native_call(root_path)
70
42
  # NOTE: if https://bugs.ruby-lang.org/issues/21800 is accepted we should be able
71
43
  # to have similar performance with pure Ruby
72
-
73
- # If the bundle path is a descendent of this path, we do additional
74
- # checks to prevent recursing into the bundle path as we recurse
75
- # through this path. We don't want to scan the bundle path because
76
- # anything useful in it will be present on other load path items.
77
- #
78
- # This can happen if, for example, the user adds '.' to the load path,
79
- # and the bundle path is '.bundle'.
80
- contains_bundle_path = BUNDLE_PATH.start_with?(root_path)
44
+ root_path, contains_bundle_path, ignored_abs_paths, ignored_dir_names = prepare_scan(root_path)
81
45
 
82
46
  all_requirables, queue = Native.scan_dir(root_path)
83
47
  all_requirables.each(&:freeze)
84
48
 
85
49
  queue.reject! do |dir|
86
- ignored_directories.include?(dir) ||
87
- (contains_bundle_path && dir.start_with?(BUNDLE_PATH))
50
+ if ignored_dir_names&.include?(dir)
51
+ true
52
+ elsif ignored_abs_paths || contains_bundle_path
53
+ absolute_dir = File.join(root_path, dir)
54
+ ignored_abs_paths&.include?(absolute_dir) ||
55
+ (contains_bundle_path && absolute_dir.start_with?(BUNDLE_PATH))
56
+ end
88
57
  end
89
58
 
90
- while (path = queue.pop)
91
- requirables, dirs = Native.scan_dir(File.join(root_path, path))
92
- dirs.reject! { |dir| ignored_directories.include?(dir) }
93
- dirs.map! { |f| File.join(path, f).freeze }
94
- requirables.map! { |f| File.join(path, f).freeze }
95
-
96
- if contains_bundle_path
97
- dirs.reject! { |dir| dir.start_with?(BUNDLE_PATH) }
59
+ while (relative_path = queue.pop)
60
+ absolute_base = File.join(root_path, relative_path)
61
+ requirables, dirs = Native.scan_dir(absolute_base)
62
+ dirs.reject! do |dir|
63
+ if ignored_dir_names&.include?(dir)
64
+ true
65
+ elsif ignored_abs_paths || contains_bundle_path
66
+ absolute_dir = File.join(absolute_base, dir)
67
+ ignored_abs_paths&.include?(absolute_dir) ||
68
+ (contains_bundle_path && absolute_dir.start_with?(BUNDLE_PATH))
69
+ end
98
70
  end
71
+ dirs.map! { |f| File.join(relative_path, f).freeze }
72
+ requirables.map! { |f| File.join(relative_path, f).freeze }
99
73
 
100
74
  all_requirables.concat(requirables)
101
75
  queue.concat(dirs)
@@ -107,6 +81,46 @@ module Bootsnap
107
81
  else
108
82
  alias_method :call, :ruby_call
109
83
  end
84
+
85
+ private
86
+
87
+ def prepare_scan(root_path)
88
+ root_path = File.expand_path(root_path.to_s).freeze
89
+
90
+ # If the bundle path is a descendent of this path, we do additional
91
+ # checks to prevent recursing into the bundle path as we recurse
92
+ # through this path. We don't want to scan the bundle path because
93
+ # anything useful in it will be present on other load path items.
94
+ #
95
+ # This can happen if, for example, the user adds '.' to the load path,
96
+ # and the bundle path is '.bundle'.
97
+ contains_bundle_path = BUNDLE_PATH.start_with?(root_path)
98
+
99
+ ignored_abs_paths, ignored_dir_names = ignored_directories.partition { |p| File.absolute_path?(p) }
100
+ ignored_abs_paths = nil if ignored_abs_paths.empty?
101
+ ignored_dir_names = nil if ignored_dir_names.empty?
102
+
103
+ [root_path, contains_bundle_path, ignored_abs_paths, ignored_dir_names]
104
+ end
105
+
106
+ def walk(absolute_dir_path, relative_dir_path, ignored_abs_paths, ignored_dir_names, &block)
107
+ Dir.foreach(absolute_dir_path) do |name|
108
+ next if name.start_with?(".")
109
+
110
+ relative_path = relative_dir_path ? File.join(relative_dir_path, name) : name
111
+
112
+ absolute_path = File.join(absolute_dir_path, name)
113
+ if File.directory?(absolute_path)
114
+ next if ignored_dir_names&.include?(name) || ignored_abs_paths&.include?(absolute_path)
115
+
116
+ if yield relative_path, absolute_path, true
117
+ walk(absolute_path, relative_path, ignored_abs_paths, ignored_dir_names, &block)
118
+ end
119
+ else
120
+ yield relative_path, absolute_path, false
121
+ end
122
+ end
123
+ end
110
124
  end
111
125
  end
112
126
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bootsnap
4
- VERSION = "1.22.0"
4
+ VERSION = "1.24.0"
5
5
  end
data/lib/bootsnap.rb CHANGED
@@ -43,6 +43,13 @@ 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
+
46
53
  def setup(
47
54
  cache_dir:,
48
55
  development_mode: true,
@@ -76,6 +83,28 @@ module Bootsnap
76
83
  readonly: readonly,
77
84
  revalidation: revalidation,
78
85
  )
86
+
87
+ load_config
88
+ end
89
+
90
+ def enable_frozen_string_literal(app_only: false)
91
+ if app_only
92
+ gems_root = File.join(Bundler.bundle_path.cleanpath, "")
93
+ app_root = File.join(Dir.pwd, "")
94
+ Bootsnap::CompileCache::ISeq.default_compiler = Bootsnap::CompileCache::ISeq::DEFAULT
95
+ Bootsnap::CompileCache::ISeq.compiler_selector = lambda { |path|
96
+ # Enable `frozen_string_literal: true` for app code, but not gems.
97
+
98
+ if path.start_with?(app_root) && !path.start_with?(gems_root)
99
+ Bootsnap::CompileCache::ISeq::FROZEN_STRING_LITERAL
100
+ else
101
+ Bootsnap::CompileCache::ISeq::DEFAULT
102
+ end
103
+ }
104
+ else
105
+ Bootsnap::CompileCache::ISeq.compiler_selector = nil
106
+ Bootsnap::CompileCache::ISeq.default_compiler = Bootsnap::CompileCache::ISeq::FROZEN_STRING_LITERAL
107
+ end
79
108
  end
80
109
 
81
110
  def unload_cache!
@@ -150,7 +179,7 @@ module Bootsnap
150
179
  end
151
180
 
152
181
  # Allow the C extension to redefine `rb_get_path` without warning.
153
- alias_method :rb_get_path, :rb_get_path # rubocop:disable Lint/DuplicateMethods
182
+ alias_method :rb_get_path, :rb_get_path
154
183
 
155
184
  private
156
185
 
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.22.0
4
+ version: 1.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
@@ -73,14 +73,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
73
73
  requirements:
74
74
  - - ">="
75
75
  - !ruby/object:Gem::Version
76
- version: 2.6.0
76
+ version: 2.7.0
77
77
  required_rubygems_version: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0'
82
82
  requirements: []
83
- rubygems_version: 4.0.3
83
+ rubygems_version: 4.0.6
84
84
  specification_version: 4
85
85
  summary: Boot large ruby/rails apps faster
86
86
  test_files: []