bootsnap 1.18.3 → 1.23.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: 4fa4ab785277ee01a1c8ee75b43f0efb93db42bffcdacc1c8505a65efa03dede
4
- data.tar.gz: 8aaaca48ae257b563580023c8fa36a59463f4c30f5463c14f6b8b94bf5fe27df
3
+ metadata.gz: 4e0ad269f816c24dc901b6544acb59c5ebd65b22432f39ee69bc3387b9c2b04d
4
+ data.tar.gz: 700f525d90d4e77421ce83e38f8731981a48cc82a8b9a937c29f661972f78d7f
5
5
  SHA512:
6
- metadata.gz: 27b48d27d3330c8565952a2fbb979e71013b1e9585bcb3284656192808c304f2874c32a135b14895eec61a7ef038fa71fa111964a56e7aaedc9ff507ef307686
7
- data.tar.gz: c3d83a0b068f2908a6298c7cd8e1a660f1228a7ddbfb9409cb3f6c174319f3974388ce73756c04062b17b97a37e199a07bccbb0b8dc1c6224998c58c51194b27
6
+ metadata.gz: f5859d7fd4b81f1969c55e12b33a957b34b1dc2825d03bc3f7ab60a57d34153cbcdda7a60626ea758fd66877b1b74a58d17b276aa2055f41086488d6ced5317b
7
+ data.tar.gz: c09f2cd48da0ba3bb5e9d3c995ffb86b8b16aec987c311162fc79ca648a66f355e4051d6ae4c5ee486d5ad2d2bc3af32090e15776e1f45129c1ea8c8e6c811ed
data/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # Unreleased
2
2
 
3
+ # 1.23.0
4
+
5
+ * Require Ruby 2.7.
6
+ * Fix support for absolute paths in `BOOTSNAP_IGNORE_DIRECTORIES`.
7
+
8
+ # 1.22.0
9
+
10
+ * Better fix for the `opendir` crash.
11
+ * Add `bootsnap/rake` for cleaning the bootsnap cache as part of `rake clobber`.
12
+
13
+ # 1.21.1
14
+
15
+ * Prevent a Ruby crash while scanning load path if `opendir` fails without setting `errno`.
16
+ According to the C spec this should not happen, but according to user reports, it did.
17
+
18
+ # 1.21.0
19
+
20
+ * Fix the `require` decorator to handle `Bootsnap.unload_cache!` being called.
21
+ * Minor optimization: Eagerly clear cache buffers to appease the GC.
22
+
23
+ # 1.20.1
24
+
25
+ * Handle broken symlinks in load path scanning code.
26
+ Should fix `Errno::ENOENT fstatat` issues some users have encountered after upgrading to 1.20.0.
27
+
28
+ # 1.20.0
29
+
30
+ * Optimized load path scanning with a C extension. Should be about 2x faster on supported platforms.
31
+
32
+ # 1.19.0
33
+
34
+ * Remove JSON parsing cache. Recent versions of the `json` gem are as fast as `msgpack` if not faster.
35
+
36
+ # 1.18.6
37
+
38
+ * Fix cgroup CPU limits detection in CLI.
39
+
40
+ # 1.18.5
41
+
42
+ * Attempt to detect a QEMU bug that can cause `bootsnap precompile` to hang forever when building ARM64 docker images
43
+ from x86_64 machines. See #495.
44
+ * Improve CLI to detect cgroup CPU limits and avoid spawning too many worker processes.
45
+
46
+ # 1.18.4
47
+
48
+ * Allow using bootsnap without bundler. See #488.
49
+ * Fix startup failure if the cache directory points to a broken symlink.
50
+
3
51
  # 1.18.3
4
52
 
5
53
  * Fix the cache corruption issue in the revalidation feature. See #474.
@@ -32,7 +80,7 @@
32
80
 
33
81
  # 1.17.0
34
82
 
35
- * Ensure `$LOAD_PATH.dup` is Ractor shareable to fix an conflict with `did_you_mean`.
83
+ * Ensure `$LOAD_PATH.dup` is Ractor shareable to fix a conflict with `did_you_mean`.
36
84
  * Allow to ignore directories using absolute paths.
37
85
  * Support YAML and JSON CompileCache on TruffleRuby.
38
86
  * Support LoadPathCache on TruffleRuby.
@@ -90,7 +138,7 @@
90
138
 
91
139
  * Get rid of the `Kernel.require_relative` decorator by resolving `$LOAD_PATH` members to their real path.
92
140
  This way we handle symlinks in `$LOAD_PATH` much more efficiently. See #402 for the detailed explanation.
93
-
141
+
94
142
  * Drop support for Ruby 2.3 (to allow getting rid of the `Kernel.require_relative` decorator).
95
143
 
96
144
  # 1.10.3
@@ -216,7 +264,7 @@
216
264
  * Adds an instrumentation API to monitor cache misses.
217
265
  * Allow to control the behavior of `require 'bootsnap/setup'` using environment variables.
218
266
  * Deprecate the `disable_trace` option.
219
- * Deprecate the `ActiveSupport::Dependencies` (AKA Classic autoloader) integration. (#344)
267
+ * Deprecate the `ActiveSupport::Dependencies` (AKA Classic autoloader) integration. (#344)
220
268
 
221
269
  # 1.6.0
222
270
 
@@ -236,12 +284,12 @@
236
284
 
237
285
  # 1.4.9
238
286
 
239
- * [Windows support](https://github.com/Shopify/bootsnap/pull/319)
240
- * [Fix potential crash](https://github.com/Shopify/bootsnap/pull/322)
287
+ * [Windows support](https://github.com/rails/bootsnap/pull/319)
288
+ * [Fix potential crash](https://github.com/rails/bootsnap/pull/322)
241
289
 
242
290
  # 1.4.8
243
291
 
244
- * [Prevent FallbackScan from polluting exception cause](https://github.com/Shopify/bootsnap/pull/314)
292
+ * [Prevent FallbackScan from polluting exception cause](https://github.com/rails/bootsnap/pull/314)
245
293
 
246
294
  # 1.4.7
247
295
 
@@ -254,7 +302,7 @@
254
302
  required if a different file with the same name was already being required
255
303
 
256
304
  Example:
257
-
305
+
258
306
  require 'foo'
259
307
  require 'foo.en'
260
308
 
@@ -308,7 +356,7 @@
308
356
 
309
357
  # 1.3.0
310
358
 
311
- * Handle cases where load path entries are symlinked (https://github.com/Shopify/bootsnap/pull/136)
359
+ * Handle cases where load path entries are symlinked (https://github.com/rails/bootsnap/pull/136)
312
360
 
313
361
  # 1.2.1
314
362
 
data/LICENSE.txt CHANGED
@@ -1,6 +1,7 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2017-present Shopify, Inc.
3
+ Copyright (c) 2017-2025 Shopify, Inc.
4
+ Copyright (c) 2025-present Rails Foundation
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Bootsnap [![Actions Status](https://github.com/Shopify/bootsnap/workflows/ci/badge.svg)](https://github.com/Shopify/bootsnap/actions)
1
+ # Bootsnap [![Actions Status](https://github.com/rails/bootsnap/workflows/ci/badge.svg)](https://github.com/rails/bootsnap/actions)
2
2
 
3
3
  Bootsnap is a library that plugs into Ruby, with optional support for `YAML` and `JSON`,
4
4
  to optimize and cache expensive computations. See [How Does This Work](#how-does-this-work).
@@ -41,7 +41,7 @@ getting progressively slower, this is almost certainly the cause.**
41
41
  It's technically possible to simply specify `gem 'bootsnap', require: 'bootsnap/setup'`, but it's
42
42
  important to load Bootsnap as early as possible to get maximum performance improvement.
43
43
 
44
- You can see how this require works [here](https://github.com/Shopify/bootsnap/blob/main/lib/bootsnap/setup.rb).
44
+ You can see how this require works [here](https://github.com/rails/bootsnap/blob/main/lib/bootsnap/setup.rb).
45
45
 
46
46
  If you are not using Rails, or if you are but want more control over things, add this to your
47
47
  application setup immediately after `require 'bundler/setup'` (i.e. as early as possible: the sooner
@@ -57,13 +57,12 @@ Bootsnap.setup(
57
57
  load_path_cache: true, # Optimize the LOAD_PATH with a cache
58
58
  compile_cache_iseq: true, # Compile Ruby code into ISeq cache, breaks coverage reporting.
59
59
  compile_cache_yaml: true, # Compile YAML into a cache
60
- compile_cache_json: true, # Compile JSON into a cache
61
60
  readonly: true, # Use the caches but don't update them on miss or stale entries.
62
61
  )
63
62
  ```
64
63
 
65
64
  **Protip:** You can replace `require 'bootsnap'` with `BootLib::Require.from_gem('bootsnap',
66
- 'bootsnap')` using [this trick](https://github.com/Shopify/bootsnap/wiki/Bootlib::Require). This
65
+ 'bootsnap')` using [this trick](https://github.com/rails/bootsnap/wiki/Bootlib::Require). This
67
66
  will help optimize boot time further if you have an extremely large `$LOAD_PATH`.
68
67
 
69
68
  Note: Bootsnap and [Spring](https://github.com/rails/spring) are orthogonal tools. While Bootsnap
@@ -170,7 +169,7 @@ The only directories considered "stable" are things under the Ruby install prefi
170
169
  "volatile".
171
170
 
172
171
  In addition to the [`Bootsnap::LoadPathCache::Cache`
173
- source](https://github.com/Shopify/bootsnap/blob/main/lib/bootsnap/load_path_cache/cache.rb),
172
+ source](https://github.com/rails/bootsnap/blob/main/lib/bootsnap/load_path_cache/cache.rb),
174
173
  this diagram may help clarify how entry resolution works:
175
174
 
176
175
  ![How path searching works](https://cloud.githubusercontent.com/assets/3074765/25388270/670b5652-299b-11e7-87fb-975647f68981.png)
@@ -188,7 +187,7 @@ result too, raising a `LoadError` without touching the filesystem at all.
188
187
 
189
188
  Ruby has complex grammar and parsing it is not a particularly cheap operation. Since 1.9, Ruby has
190
189
  translated ruby source to an internal bytecode format, which is then executed by the Ruby VM. Since
191
- 2.3.0, Ruby [exposes an API](https://ruby-doc.org/core-2.3.0/RubyVM/InstructionSequence.html) that
190
+ 2.3.0, Ruby [exposes an API](https://docs.ruby-lang.org/en/master/RubyVM/InstructionSequence.html) that
192
191
  allows caching that bytecode. This allows us to bypass the relatively-expensive compilation step on
193
192
  subsequent loads of the same file.
194
193
 
@@ -331,9 +330,23 @@ To do so you can use the `bootsnap precompile` command.
331
330
  Example:
332
331
 
333
332
  ```bash
334
- $ bundle exec bootsnap precompile --gemfile app/ lib/
333
+ $ bundle exec bootsnap precompile --gemfile app/ lib/ config/
335
334
  ```
336
335
 
336
+ ## Known issues
337
+
338
+ ### QEMU environments
339
+
340
+ When building cross-platform Docker images, QEMU is often used for emulation and can be the source of a limitation that causes forked processes to hang. While Bootsnap includes automatic detection for this issue (as of [PR #501](https://github.com/rails/bootsnap/pull/501)), the detection may not always be sufficient.
341
+
342
+ If you encounter hangs during precompilation in QEMU-based environments (such as when using Docker buildx for cross-platform builds), you can work around this by disabling parallelization with the `-j 0` option:
343
+
344
+ ```bash
345
+ $ bundle exec bootsnap precompile -j 0 --gemfile app/ lib/ config/
346
+ ```
347
+
348
+ See [Issue #495](https://github.com/rails/bootsnap/issues/495) for more details about this QEMU-related issue.
349
+
337
350
  ## When not to use Bootsnap
338
351
 
339
352
  *Alternative engines*: Bootsnap is pretty reliant on MRI features, and parts are disabled entirely on alternative ruby
@@ -11,7 +11,6 @@
11
11
  * here.
12
12
  */
13
13
 
14
- #include "bootsnap.h"
15
14
  #include "ruby.h"
16
15
  #include <stdint.h>
17
16
  #include <stdbool.h>
@@ -20,10 +19,15 @@
20
19
  #include <fcntl.h>
21
20
  #include <unistd.h>
22
21
  #include <sys/stat.h>
22
+ #include <dirent.h>
23
+
24
+ #ifndef RBIMPL_ATTR_NORETURN
25
+ #define RBIMPL_ATTR_NORETURN()
26
+ #endif
23
27
 
24
28
  #ifdef __APPLE__
25
29
  // The symbol is present, however not in the headers
26
- // See: https://github.com/Shopify/bootsnap/issues/470
30
+ // See: https://github.com/rails/bootsnap/issues/470
27
31
  extern int fdatasync(int);
28
32
  #endif
29
33
 
@@ -82,7 +86,7 @@ struct bs_cache_key {
82
86
  STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE);
83
87
 
84
88
  /* Effectively a schema version. Bumping invalidates all previous caches */
85
- static const uint32_t current_version = 5;
89
+ static const uint32_t current_version = 6;
86
90
 
87
91
  /* hash of e.g. "x86_64-darwin17", invalidating when ruby is recompiled on a
88
92
  * new OS ABI, etc. */
@@ -96,7 +100,6 @@ static mode_t current_umask;
96
100
 
97
101
  /* Bootsnap::CompileCache::{Native, Uncompilable} */
98
102
  static VALUE rb_mBootsnap;
99
- static VALUE rb_mBootsnap_CompileCache;
100
103
  static VALUE rb_mBootsnap_CompileCache_Native;
101
104
  static VALUE rb_cBootsnap_CompileCache_UNCOMPILABLE;
102
105
  static ID instrumentation_method;
@@ -146,20 +149,119 @@ struct s2o_data;
146
149
  struct i2o_data;
147
150
  struct i2s_data;
148
151
 
149
- /* https://bugs.ruby-lang.org/issues/13667 */
150
- extern VALUE rb_get_coverages(void);
151
152
  static VALUE
152
- bs_rb_coverage_running(VALUE self)
153
+ bs_rb_get_path(VALUE self, VALUE fname)
154
+ {
155
+ return rb_get_path(fname);
156
+ }
157
+
158
+ #ifdef HAVE_FSTATAT
159
+
160
+ RBIMPL_ATTR_NORETURN()
161
+ static void
162
+ bs_syserr_fail_path(const char *func_name, int n, VALUE path)
153
163
  {
154
- VALUE cov = rb_get_coverages();
155
- return RTEST(cov) ? Qtrue : Qfalse;
164
+ rb_syserr_fail_str(n, rb_sprintf("%s @ %s", func_name, RSTRING_PTR(path)));
165
+ }
166
+
167
+ RBIMPL_ATTR_NORETURN()
168
+ static void
169
+ bs_syserr_fail_dir_entry(const char *func_name, int n, VALUE dir, const char *d_name)
170
+ {
171
+ rb_syserr_fail_str(n, rb_sprintf("%s @ %s/%s", func_name, RSTRING_PTR(dir), d_name));
156
172
  }
157
173
 
158
174
  static VALUE
159
- bs_rb_get_path(VALUE self, VALUE fname)
175
+ bs_rb_scan_dir(VALUE self, VALUE abspath)
160
176
  {
161
- return rb_get_path(fname);
177
+ Check_Type(abspath, T_STRING);
178
+
179
+ VALUE dirs = rb_ary_new();
180
+ VALUE requirables = rb_ary_new();
181
+ VALUE result = rb_ary_new_from_args(2, requirables, dirs);
182
+
183
+ DIR *dirp = opendir(RSTRING_PTR(abspath));
184
+ if (dirp == NULL) {
185
+ if (errno == ENOTDIR || errno == ENOENT) {
186
+ return result;
187
+ }
188
+
189
+ bs_syserr_fail_path("opendir", errno, abspath);
190
+ return Qundef;
191
+ }
192
+
193
+ struct dirent *entry;
194
+ struct stat st;
195
+ int dfd = -1;
196
+
197
+ while (1) {
198
+ errno = 0;
199
+
200
+ entry = readdir(dirp);
201
+ if (entry == NULL) break;
202
+
203
+ if (entry->d_name[0] == '.') continue;
204
+
205
+ if (RB_UNLIKELY(entry->d_type == DT_UNKNOWN || entry->d_type == DT_LNK)) {
206
+ // Note: the original implementation of LoadPathCache did follow symlink.
207
+ // So this is replicated here, but I'm not sure it's a good idea.
208
+ if (dfd < 0) {
209
+ dfd = dirfd(dirp);
210
+ if (dfd < 0) {
211
+ int err = errno;
212
+ closedir(dirp);
213
+ bs_syserr_fail_path("dirfd", err, abspath);
214
+ return Qundef;
215
+ }
216
+ }
217
+
218
+ if (fstatat(dfd, entry->d_name, &st, 0)) {
219
+ if (errno == ENOENT) continue; // Broken symlink
220
+
221
+ int err = errno;
222
+ closedir(dirp);
223
+ bs_syserr_fail_dir_entry("fstatat", err, abspath, entry->d_name);
224
+ return Qundef;
225
+ }
226
+
227
+ if (S_ISREG(st.st_mode)) {
228
+ entry->d_type = DT_REG;
229
+ } else if (S_ISDIR(st.st_mode)) {
230
+ entry->d_type = DT_DIR;
231
+ }
232
+ }
233
+
234
+ if (entry->d_type == DT_DIR) {
235
+ rb_ary_push(dirs, rb_utf8_str_new_cstr(entry->d_name));
236
+ continue;
237
+ } else if (entry->d_type == DT_REG) {
238
+ size_t len = strlen(entry->d_name);
239
+ bool is_requirable = (
240
+ // Comparing 4B allows compiler to optimize this into a single 32b integer comparison.
241
+ (len > 3 && memcmp(entry->d_name + (len - 3), ".rb", 4) == 0)
242
+ || (len > DLEXT_MAXLEN && memcmp(entry->d_name + (len - DLEXT_MAXLEN), DLEXT, DLEXT_MAXLEN + 1) == 0)
243
+ #ifdef DLEXT2
244
+ || (len > DLEXT2_MAXLEN && memcmp(entry->d_name + (len - DLEXT2_MAXLEN), DLEXT2, DLEXT2_MAXLEN + 1) == 0)
245
+ #endif
246
+ );
247
+ if (is_requirable) {
248
+ rb_ary_push(requirables, rb_utf8_str_new(entry->d_name, len));
249
+ }
250
+ }
251
+ }
252
+
253
+ if (errno) {
254
+ int err = errno;
255
+ closedir(dirp);
256
+ bs_syserr_fail_path("readdir", err, abspath);
257
+ } else if (closedir(dirp)) {
258
+ bs_syserr_fail_path("closedir", errno, abspath);
259
+ return Qundef;
260
+ }
261
+
262
+ return result;
162
263
  }
264
+ #endif
163
265
 
164
266
  /*
165
267
  * Ruby C extensions are initialized by calling Init_<extname>.
@@ -175,7 +277,14 @@ Init_bootsnap(void)
175
277
 
176
278
  rb_define_singleton_method(rb_mBootsnap, "rb_get_path", bs_rb_get_path, 1);
177
279
 
178
- rb_mBootsnap_CompileCache = rb_define_module_under(rb_mBootsnap, "CompileCache");
280
+ #ifdef HAVE_FSTATAT
281
+ VALUE rb_mBootsnap_LoadPathCache = rb_define_module_under(rb_mBootsnap, "LoadPathCache");
282
+ VALUE rb_mBootsnap_LoadPathCache_Native = rb_define_module_under(rb_mBootsnap_LoadPathCache, "Native");
283
+
284
+ rb_define_singleton_method(rb_mBootsnap_LoadPathCache_Native, "scan_dir", bs_rb_scan_dir, 1);
285
+ #endif
286
+
287
+ VALUE rb_mBootsnap_CompileCache = rb_define_module_under(rb_mBootsnap, "CompileCache");
179
288
  rb_mBootsnap_CompileCache_Native = rb_define_module_under(rb_mBootsnap_CompileCache, "Native");
180
289
  rb_cBootsnap_CompileCache_UNCOMPILABLE = rb_const_get(rb_mBootsnap_CompileCache, rb_intern("UNCOMPILABLE"));
181
290
  rb_global_variable(&rb_cBootsnap_CompileCache_UNCOMPILABLE);
@@ -193,7 +302,6 @@ Init_bootsnap(void)
193
302
  rb_define_module_function(rb_mBootsnap, "instrumentation_enabled=", bs_instrumentation_enabled_set, 1);
194
303
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "readonly=", bs_readonly_set, 1);
195
304
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "revalidation=", bs_revalidation_set, 1);
196
- rb_define_module_function(rb_mBootsnap_CompileCache_Native, "coverage_running?", bs_rb_coverage_running, 0);
197
305
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 4);
198
306
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 3);
199
307
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "compile_option_crc32=", bs_compile_option_crc32_set, 1);
@@ -922,9 +1030,9 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
922
1030
  goto succeed; /* output_data is now the correct return. */
923
1031
 
924
1032
  #define CLEANUP \
925
- if (status != Qfalse) bs_instrumentation(status, path_v); \
926
1033
  if (current_fd >= 0) close(current_fd); \
927
- if (cache_fd >= 0) close(cache_fd);
1034
+ if (cache_fd >= 0) close(cache_fd); \
1035
+ if (status != Qfalse) bs_instrumentation(status, path_v);
928
1036
 
929
1037
  succeed:
930
1038
  CLEANUP;
@@ -4,6 +4,7 @@ require "mkmf"
4
4
 
5
5
  if %w[ruby truffleruby].include?(RUBY_ENGINE)
6
6
  have_func "fdatasync", "unistd.h"
7
+ have_func "fstatat", "sys/stat.h"
7
8
 
8
9
  unless RUBY_PLATFORM.match?(/mswin|mingw|cygwin/)
9
10
  append_cppflags ["-D_GNU_SOURCE"] # Needed of O_NOATIME
@@ -12,7 +13,7 @@ if %w[ruby truffleruby].include?(RUBY_ENGINE)
12
13
  append_cflags ["-O3", "-std=c99"]
13
14
 
14
15
  # ruby.h has some -Wpedantic fails in some cases
15
- # (e.g. https://github.com/Shopify/bootsnap/issues/15)
16
+ # (e.g. https://github.com/rails/bootsnap/issues/15)
16
17
  unless ["0", "", nil].include?(ENV["BOOTSNAP_PEDANTIC"])
17
18
  append_cflags([
18
19
  "-Wall",
@@ -1,16 +1,88 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "etc"
4
+ require "rbconfig"
5
+ require "io/wait" unless IO.method_defined?(:wait_readable)
6
+
3
7
  module Bootsnap
4
8
  class CLI
5
9
  class WorkerPool
6
10
  class << self
7
11
  def create(size:, jobs:)
12
+ size ||= default_size
8
13
  if size > 0 && Process.respond_to?(:fork)
9
14
  new(size: size, jobs: jobs)
10
15
  else
11
16
  Inline.new(jobs: jobs)
12
17
  end
13
18
  end
19
+
20
+ def default_size
21
+ nprocessors = Etc.nprocessors
22
+ size = [nprocessors, cpu_quota&.to_i || nprocessors].min
23
+ case size
24
+ when 0, 1
25
+ 0
26
+ else
27
+ if fork_defunct?
28
+ $stderr.puts "warning: faulty fork(2) detected, probably in cross platform docker builds. " \
29
+ "Disabling parallel compilation."
30
+ 0
31
+ else
32
+ size
33
+ end
34
+ end
35
+ end
36
+
37
+ def cpu_quota
38
+ if RbConfig::CONFIG["target_os"].include?("linux")
39
+ if File.exist?("/sys/fs/cgroup/cpu.max")
40
+ # cgroups v2: https://docs.kernel.org/admin-guide/cgroup-v2.html#cpu-interface-files
41
+ cpu_max = File.read("/sys/fs/cgroup/cpu.max")
42
+ return nil if cpu_max.start_with?("max ") # no limit
43
+
44
+ max, period = cpu_max.split.map(&:to_f)
45
+ max / period
46
+ elsif File.exist?("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us")
47
+ # cgroups v1: https://kernel.googlesource.com/pub/scm/linux/kernel/git/glommer/memcg/+/cpu_stat/Documentation/cgroups/cpu.txt
48
+ max = File.read("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us").to_i
49
+ # If the cpu.cfs_quota_us is -1, cgroup does not adhere to any CPU time restrictions
50
+ # https://docs.kernel.org/scheduler/sched-bwc.html#management
51
+ return nil if max <= 0
52
+
53
+ period = File.read("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us").to_f
54
+ max / period
55
+ end
56
+ end
57
+ end
58
+
59
+ def fork_defunct?
60
+ return true unless ::Process.respond_to?(:fork)
61
+
62
+ # Ref: https://github.com/rails/bootsnap/issues/495
63
+ # The second forked process will hang on some QEMU environments
64
+ r, w = IO.pipe
65
+ pids = 2.times.map do
66
+ ::Process.fork do
67
+ exit!(true)
68
+ end
69
+ end
70
+ w.close
71
+ r.wait_readable(1) # Wait at most 1s
72
+
73
+ defunct = false
74
+
75
+ pids.each do |pid|
76
+ _pid, status = ::Process.wait2(pid, ::Process::WNOHANG)
77
+ if status.nil? # Didn't exit in 1s
78
+ defunct = true
79
+ Process.kill(:KILL, pid)
80
+ ::Process.wait2(pid)
81
+ end
82
+ end
83
+
84
+ defunct
85
+ end
14
86
  end
15
87
 
16
88
  class Inline
data/lib/bootsnap/cli.rb CHANGED
@@ -4,7 +4,6 @@ require "bootsnap"
4
4
  require "bootsnap/cli/worker_pool"
5
5
  require "optparse"
6
6
  require "fileutils"
7
- require "etc"
8
7
 
9
8
  module Bootsnap
10
9
  class CLI
@@ -21,7 +20,7 @@ module Bootsnap
21
20
 
22
21
  attr_reader :cache_dir, :argv
23
22
 
24
- attr_accessor :compile_gemfile, :exclude, :verbose, :iseq, :yaml, :json, :jobs
23
+ attr_accessor :compile_gemfile, :exclude, :verbose, :iseq, :yaml, :jobs
25
24
 
26
25
  def initialize(argv)
27
26
  @argv = argv
@@ -29,35 +28,31 @@ module Bootsnap
29
28
  self.compile_gemfile = false
30
29
  self.exclude = nil
31
30
  self.verbose = false
32
- self.jobs = Etc.nprocessors
31
+ self.jobs = nil
33
32
  self.iseq = true
34
33
  self.yaml = true
35
- self.json = true
36
34
  end
37
35
 
38
36
  def precompile_command(*sources)
39
- require "bootsnap/compile_cache/iseq"
40
- require "bootsnap/compile_cache/yaml"
41
- require "bootsnap/compile_cache/json"
37
+ require "bootsnap/compile_cache"
42
38
 
43
39
  fix_default_encoding do
44
- Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
45
- Bootsnap::CompileCache::YAML.init!
46
- Bootsnap::CompileCache::YAML.cache_dir = cache_dir
47
- Bootsnap::CompileCache::JSON.init!
48
- Bootsnap::CompileCache::JSON.cache_dir = cache_dir
40
+ Bootsnap::CompileCache.setup(
41
+ cache_dir: cache_dir,
42
+ iseq: iseq,
43
+ yaml: yaml,
44
+ revalidation: true,
45
+ )
49
46
 
50
47
  @work_pool = WorkerPool.create(size: jobs, jobs: {
51
48
  ruby: method(:precompile_ruby),
52
49
  yaml: method(:precompile_yaml),
53
- json: method(:precompile_json),
54
50
  })
55
51
  @work_pool.spawn
56
52
 
57
53
  main_sources = sources.map { |d| File.expand_path(d) }
58
54
  precompile_ruby_files(main_sources)
59
55
  precompile_yaml_files(main_sources)
60
- precompile_json_files(main_sources)
61
56
 
62
57
  if compile_gemfile
63
58
  # Gems that include JSON or YAML files usually don't put them in `lib/`.
@@ -71,7 +66,6 @@ module Bootsnap
71
66
 
72
67
  precompile_ruby_files(gem_paths, exclude: gem_exclude)
73
68
  precompile_yaml_files(gem_paths, exclude: gem_exclude)
74
- precompile_json_files(gem_paths, exclude: gem_exclude)
75
69
  end
76
70
 
77
71
  if (exitstatus = @work_pool.shutdown)
@@ -146,29 +140,6 @@ module Bootsnap
146
140
  end
147
141
  end
148
142
 
149
- def precompile_json_files(load_paths, exclude: self.exclude)
150
- return unless json
151
-
152
- load_paths.each do |path|
153
- if !exclude || !exclude.match?(path)
154
- list_files(path, "**/*.json").each do |json_file|
155
- # We ignore hidden files to not match the various .config.json files
156
- if !File.basename(json_file).start_with?(".") && (!exclude || !exclude.match?(json_file))
157
- @work_pool.push(:json, json_file)
158
- end
159
- end
160
- end
161
- end
162
- end
163
-
164
- def precompile_json(*json_files)
165
- Array(json_files).each do |json_file|
166
- if CompileCache::JSON.precompile(json_file) && verbose
167
- $stderr.puts(json_file)
168
- end
169
- end
170
- end
171
-
172
143
  def precompile_ruby_files(load_paths, exclude: self.exclude)
173
144
  return unless iseq
174
145
 
@@ -222,6 +193,9 @@ module Bootsnap
222
193
 
223
194
  def parser
224
195
  @parser ||= OptionParser.new do |opts|
196
+ opts.version = Bootsnap::VERSION
197
+ opts.program_name = "bootsnap"
198
+
225
199
  opts.banner = "Usage: bootsnap COMMAND [ARGS]"
226
200
  opts.separator ""
227
201
  opts.separator "GLOBAL OPTIONS"
@@ -274,9 +248,9 @@ module Bootsnap
274
248
  opts.on("--no-yaml", help) { self.yaml = false }
275
249
 
276
250
  help = <<~HELP
277
- Disable JSON precompilation.
251
+ Disable JSON precompilation. Deprecated.
278
252
  HELP
279
- opts.on("--no-json", help) { self.json = false }
253
+ opts.on("--no-json", help) { $stderr.puts("The --no-json option is deprecated and now a noop.") }
280
254
  end
281
255
  end
282
256
  end
@@ -50,7 +50,9 @@ module Bootsnap
50
50
  end
51
51
 
52
52
  def self.storage_to_output(binary, _args)
53
- RubyVM::InstructionSequence.load_from_binary(binary)
53
+ iseq = RubyVM::InstructionSequence.load_from_binary(binary)
54
+ binary.clear
55
+ iseq
54
56
  rescue RuntimeError => error
55
57
  if error.message == "broken binary format"
56
58
  $stderr.puts("[Bootsnap::CompileCache] warning: rejecting broken binary")
@@ -84,7 +86,7 @@ module Bootsnap
84
86
  module InstructionSequenceMixin
85
87
  def load_iseq(path)
86
88
  # Having coverage enabled prevents iseq dumping/loading.
87
- return nil if defined?(Coverage) && Bootsnap::CompileCache::Native.coverage_running?
89
+ return nil if defined?(Coverage) && Coverage.running?
88
90
 
89
91
  Bootsnap::CompileCache::ISeq.fetch(path.to_s)
90
92
  rescue RuntimeError => error
@@ -95,7 +97,7 @@ module Bootsnap
95
97
  end
96
98
 
97
99
  def compile_option=(hash)
98
- super(hash)
100
+ super
99
101
  Bootsnap::CompileCache::ISeq.compile_option_updated
100
102
  end
101
103
  end
@@ -170,7 +170,9 @@ module Bootsnap
170
170
  unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
171
171
  unpacker.feed(data)
172
172
  _safe_loaded = unpacker.unpack
173
- unpacker.unpack
173
+ result = unpacker.unpack
174
+ data.clear
175
+ result
174
176
  end
175
177
 
176
178
  def input_to_output(data, kwargs)