bootsnap 1.7.2 → 1.9.1

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: ea223ff0ae18f28970e2b80aa24970fe24f5eeefc7ad12b5b4b77dc921af4ca3
4
- data.tar.gz: 5f9ffdfcbeac27b128aa314749dd27c6182247fe7b6aa6492a1d99d00586ac3e
3
+ metadata.gz: 00c1b5b2c37c6a351bb7656ef17ba7f7e6143fd773ceebd6ee09d9a55e25b75a
4
+ data.tar.gz: d6d6d1d68d97e4f2c50c61f0b928714cf94f29ecab6604bb0092517779212738
5
5
  SHA512:
6
- metadata.gz: 0e3bcea4690954e6e15a3b0c10e3bf5b93b23fefff0d3d61354cb04d683bd8925d61ce32dcc9f7dd526b2f14f4d44a24196e72003fa75714bb0e6f99fbc94f62
7
- data.tar.gz: 587454d2a3a5019c56c81068e9202905328e7c2918f1786f35ff988424e2a9222b36db35f7ef6e403fc730a0752508ab248b06d6b164dc0b6e147623e5f0912e
6
+ metadata.gz: e1ade83cee1e4a917317cecd34107493424a5174b8bf92cc74db47743d4d84931de24533979480a23aa4ddb496cae8ce1f3cf30a2fbed3996b885a68138d16f0
7
+ data.tar.gz: 23e4f6c77307b30526b880bfd12983b7e66c9539556acd3bc05a2fa3fd6884a142c83dfaa590846651afad243c0bf69c96d994ca8492e5539d706ba3e6cf713f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Unreleased
2
2
 
3
+ # 1.9.1
4
+
5
+ * Removed a forgotten debug statement in JSON precompilation.
6
+
7
+ # 1.9.0
8
+
9
+ * Added a compilation cache for `JSON.load_file`. (#370)
10
+
11
+ # 1.8.1
12
+
13
+ * Fixed support for older Psych. (#369)
14
+
15
+ # 1.8.0
16
+
17
+ * Improve support for Pysch 4. (#368)
18
+
19
+ # 1.7.7
20
+
21
+ * Fix `require_relative` in evaled code on latest ruby 3.1.0-dev. (#366)
22
+
23
+ # 1.7.6
24
+
25
+ * Fix reliance on `set` to be required.
26
+ * Fix `Encoding::UndefinedConversionError` error for Rails applications when precompiling cache. (#364)
27
+
28
+ # 1.7.5
29
+
30
+ * Handle a regression of Ruby 2.7.3 causing Bootsnap to call the deprecated `untaint` method. (#360)
31
+ * Gracefully handle read-only file system as well as other errors preventing to persist the load path cache. (#358)
32
+
33
+ # 1.7.4
34
+
35
+ * Stop raising errors when encoutering various file system errors. The cache is now best effort,
36
+ if somehow it can't be saved, bootsnapp will gracefully fallback to the original operation (e.g. `Kernel.require`).
37
+ (#353, #177, #262)
38
+
39
+ # 1.7.3
40
+
41
+ * Disable YAML precompilation when encountering YAML tags. (#351)
42
+
3
43
  # 1.7.2
4
44
 
5
45
  * Fix compatibility with msgpack < 1. (#349)
@@ -464,8 +464,7 @@ open_cache_file(const char * path, struct bs_cache_key * key, const char ** errn
464
464
  fd = open(path, O_RDONLY);
465
465
  if (fd < 0) {
466
466
  *errno_provenance = "bs_fetch:open_cache_file:open";
467
- if (errno == ENOENT) return CACHE_MISS;
468
- return ERROR_WITH_ERRNO;
467
+ return CACHE_MISS;
469
468
  }
470
469
  #ifdef _WIN32
471
470
  setmode(fd, O_BINARY);
@@ -763,9 +762,11 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
763
762
  /* If storage_data isn't a string, we can't cache it */
764
763
  if (!RB_TYPE_P(storage_data, T_STRING)) goto invalid_type_storage_data;
765
764
 
766
- /* Write the cache key and storage_data to the cache directory */
767
- res = atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
768
- if (res < 0) goto fail_errno;
765
+ /* Attempt to write the cache key and storage_data to the cache directory.
766
+ * We do however ignore any failures to persist the cache, as it's better
767
+ * to move along, than to interrupt the process.
768
+ */
769
+ atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
769
770
 
770
771
  /* Having written the cache, now convert storage_data to output_data */
771
772
  exception_tag = bs_storage_to_output(handler, args, storage_data, &output_data);
@@ -37,7 +37,11 @@ module Bootsnap
37
37
 
38
38
  def initialize(jobs)
39
39
  @jobs = jobs
40
- @pipe_out, @to_io = IO.pipe
40
+ @pipe_out, @to_io = IO.pipe(binmode: true)
41
+ # Set the writer encoding to binary since IO.pipe only sets it for the reader.
42
+ # https://github.com/rails/rails/issues/16514#issuecomment-52313290
43
+ @to_io.set_encoding(Encoding::BINARY)
44
+
41
45
  @pid = nil
42
46
  end
43
47
 
data/lib/bootsnap/cli.rb CHANGED
@@ -21,7 +21,7 @@ module Bootsnap
21
21
 
22
22
  attr_reader :cache_dir, :argv
23
23
 
24
- attr_accessor :compile_gemfile, :exclude, :verbose, :iseq, :yaml, :jobs
24
+ attr_accessor :compile_gemfile, :exclude, :verbose, :iseq, :yaml, :json, :jobs
25
25
 
26
26
  def initialize(argv)
27
27
  @argv = argv
@@ -32,37 +32,44 @@ module Bootsnap
32
32
  self.jobs = Etc.nprocessors
33
33
  self.iseq = true
34
34
  self.yaml = true
35
+ self.json = true
35
36
  end
36
37
 
37
38
  def precompile_command(*sources)
38
39
  require 'bootsnap/compile_cache/iseq'
39
40
  require 'bootsnap/compile_cache/yaml'
41
+ require 'bootsnap/compile_cache/json'
40
42
 
41
43
  fix_default_encoding do
42
44
  Bootsnap::CompileCache::ISeq.cache_dir = self.cache_dir
43
45
  Bootsnap::CompileCache::YAML.init!
44
46
  Bootsnap::CompileCache::YAML.cache_dir = self.cache_dir
47
+ Bootsnap::CompileCache::JSON.init!
48
+ Bootsnap::CompileCache::JSON.cache_dir = self.cache_dir
45
49
 
46
50
  @work_pool = WorkerPool.create(size: jobs, jobs: {
47
51
  ruby: method(:precompile_ruby),
48
52
  yaml: method(:precompile_yaml),
53
+ json: method(:precompile_json),
49
54
  })
50
55
  @work_pool.spawn
51
56
 
52
57
  main_sources = sources.map { |d| File.expand_path(d) }
53
58
  precompile_ruby_files(main_sources)
54
59
  precompile_yaml_files(main_sources)
60
+ precompile_json_files(main_sources)
55
61
 
56
62
  if compile_gemfile
57
63
  # Some gems embed their tests, they're very unlikely to be loaded, so not worth precompiling.
58
64
  gem_exclude = Regexp.union([exclude, '/spec/', '/test/'].compact)
59
65
  precompile_ruby_files($LOAD_PATH.map { |d| File.expand_path(d) }, exclude: gem_exclude)
60
66
 
61
- # Gems that include YAML files usually don't put them in `lib/`.
67
+ # Gems that include JSON or YAML files usually don't put them in `lib/`.
62
68
  # So we look at the gem root.
63
69
  gem_pattern = %r{^#{Regexp.escape(Bundler.bundle_path.to_s)}/?(?:bundler/)?gems\/[^/]+}
64
70
  gem_paths = $LOAD_PATH.map { |p| p[gem_pattern] }.compact.uniq
65
71
  precompile_yaml_files(gem_paths, exclude: gem_exclude)
72
+ precompile_json_files(gem_paths, exclude: gem_exclude)
66
73
  end
67
74
 
68
75
  if exitstatus = @work_pool.shutdown
@@ -137,6 +144,29 @@ module Bootsnap
137
144
  end
138
145
  end
139
146
 
147
+ def precompile_json_files(load_paths, exclude: self.exclude)
148
+ return unless json
149
+
150
+ load_paths.each do |path|
151
+ if !exclude || !exclude.match?(path)
152
+ list_files(path, '**/*.json').each do |json_file|
153
+ # We ignore hidden files to not match the various .config.json files
154
+ if !File.basename(json_file).start_with?('.') && (!exclude || !exclude.match?(json_file))
155
+ @work_pool.push(:json, json_file)
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ def precompile_json(*json_files)
163
+ Array(json_files).each do |json_file|
164
+ if CompileCache::JSON.precompile(json_file, cache_dir: cache_dir)
165
+ STDERR.puts(json_file) if verbose
166
+ end
167
+ end
168
+ end
169
+
140
170
  def precompile_ruby_files(load_paths, exclude: self.exclude)
141
171
  return unless iseq
142
172
 
@@ -240,6 +270,11 @@ module Bootsnap
240
270
  Disable YAML precompilation.
241
271
  EOS
242
272
  opts.on('--no-yaml', help) { self.yaml = false }
273
+
274
+ help = <<~EOS
275
+ Disable JSON precompilation.
276
+ EOS
277
+ opts.on('--no-json', help) { self.json = false }
243
278
  end
244
279
  end
245
280
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ require('bootsnap/bootsnap')
3
+
4
+ module Bootsnap
5
+ module CompileCache
6
+ module JSON
7
+ class << self
8
+ attr_accessor(:msgpack_factory, :cache_dir, :supported_options)
9
+
10
+ def input_to_storage(payload, _)
11
+ obj = ::JSON.parse(payload)
12
+ msgpack_factory.dump(obj)
13
+ end
14
+
15
+ def storage_to_output(data, kwargs)
16
+ if kwargs && kwargs.key?(:symbolize_names)
17
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
18
+ end
19
+ msgpack_factory.load(data, kwargs)
20
+ end
21
+
22
+ def input_to_output(data, kwargs)
23
+ ::JSON.parse(data, **(kwargs || {}))
24
+ end
25
+
26
+ def precompile(path, cache_dir: self.cache_dir)
27
+ Bootsnap::CompileCache::Native.precompile(
28
+ cache_dir,
29
+ path.to_s,
30
+ self,
31
+ )
32
+ end
33
+
34
+ def install!(cache_dir)
35
+ self.cache_dir = cache_dir
36
+ init!
37
+ if ::JSON.respond_to?(:load_file)
38
+ ::JSON.singleton_class.prepend(Patch)
39
+ end
40
+ end
41
+
42
+ def init!
43
+ require('json')
44
+ require('msgpack')
45
+
46
+ self.msgpack_factory = MessagePack::Factory.new
47
+ self.supported_options = [:symbolize_names]
48
+ if ::JSON.parse('["foo"]', freeze: true).first.frozen?
49
+ self.supported_options = [:freeze]
50
+ end
51
+ self.supported_options.freeze
52
+ end
53
+ end
54
+
55
+ module Patch
56
+ def load_file(path, *args)
57
+ return super if args.size > 1
58
+ if kwargs = args.first
59
+ return super unless kwargs.is_a?(Hash)
60
+ return super unless (kwargs.keys - ::Bootsnap::CompileCache::JSON.supported_options).empty?
61
+ end
62
+
63
+ begin
64
+ ::Bootsnap::CompileCache::Native.fetch(
65
+ Bootsnap::CompileCache::JSON.cache_dir,
66
+ File.realpath(path),
67
+ ::Bootsnap::CompileCache::JSON,
68
+ kwargs,
69
+ )
70
+ rescue Errno::EACCES
71
+ ::Bootsnap::CompileCache.permission_error(path)
72
+ end
73
+ end
74
+
75
+ ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -8,8 +8,7 @@ module Bootsnap
8
8
  attr_accessor(:msgpack_factory, :cache_dir, :supported_options)
9
9
 
10
10
  def input_to_storage(contents, _)
11
- raise(Uncompilable) if contents.index("!ruby/object")
12
- obj = ::YAML.load(contents)
11
+ obj = strict_load(contents)
13
12
  msgpack_factory.dump(obj)
14
13
  rescue NoMethodError, RangeError
15
14
  # The object included things that we can't serialize
@@ -24,8 +23,19 @@ module Bootsnap
24
23
  end
25
24
 
26
25
  def input_to_output(data, kwargs)
27
- ::YAML.load(data, **(kwargs || {}))
26
+ if ::YAML.respond_to?(:unsafe_load)
27
+ ::YAML.unsafe_load(data, **(kwargs || {}))
28
+ else
29
+ ::YAML.load(data, **(kwargs || {}))
30
+ end
31
+ end
32
+
33
+ def strict_load(payload, *args)
34
+ ast = ::YAML.parse(payload)
35
+ return ast unless ast
36
+ strict_visitor.create(*args).visit(ast)
28
37
  end
38
+ ruby2_keywords :strict_load if respond_to?(:ruby2_keywords, true)
29
39
 
30
40
  def precompile(path, cache_dir: YAML.cache_dir)
31
41
  Bootsnap::CompileCache::Native.precompile(
@@ -46,6 +56,13 @@ module Bootsnap
46
56
  require('msgpack')
47
57
  require('date')
48
58
 
59
+ if Patch.method_defined?(:unsafe_load_file) && !::YAML.respond_to?(:unsafe_load_file)
60
+ Patch.send(:remove_method, :unsafe_load_file)
61
+ end
62
+ if Patch.method_defined?(:load_file) && ::YAML::VERSION >= '4'
63
+ Patch.send(:remove_method, :load_file)
64
+ end
65
+
49
66
  # MessagePack serializes symbols as strings by default.
50
67
  # We want them to roundtrip cleanly, so we use a custom factory.
51
68
  # see: https://github.com/msgpack/msgpack-ruby/pull/122
@@ -86,6 +103,17 @@ module Bootsnap
86
103
  end
87
104
  self.supported_options.freeze
88
105
  end
106
+
107
+ def strict_visitor
108
+ self::NoTagsVisitor ||= Class.new(Psych::Visitors::ToRuby) do
109
+ def visit(target)
110
+ if target.tag
111
+ raise Uncompilable, "YAML tags are not supported: #{target.tag}"
112
+ end
113
+ super
114
+ end
115
+ end
116
+ end
89
117
  end
90
118
 
91
119
  module Patch
@@ -109,6 +137,27 @@ module Bootsnap
109
137
  end
110
138
 
111
139
  ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
140
+
141
+ def unsafe_load_file(path, *args)
142
+ return super if args.size > 1
143
+ if kwargs = args.first
144
+ return super unless kwargs.is_a?(Hash)
145
+ return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
146
+ end
147
+
148
+ begin
149
+ ::Bootsnap::CompileCache::Native.fetch(
150
+ Bootsnap::CompileCache::YAML.cache_dir,
151
+ File.realpath(path),
152
+ ::Bootsnap::CompileCache::YAML,
153
+ kwargs,
154
+ )
155
+ rescue Errno::EACCES
156
+ ::Bootsnap::CompileCache.permission_error(path)
157
+ end
158
+ end
159
+
160
+ ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
112
161
  end
113
162
  end
114
163
  end
@@ -4,7 +4,7 @@ module Bootsnap
4
4
  Error = Class.new(StandardError)
5
5
  PermissionError = Class.new(Error)
6
6
 
7
- def self.setup(cache_dir:, iseq:, yaml:)
7
+ def self.setup(cache_dir:, iseq:, yaml:, json:)
8
8
  if iseq
9
9
  if supported?
10
10
  require_relative('compile_cache/iseq')
@@ -22,6 +22,15 @@ module Bootsnap
22
22
  warn("[bootsnap/setup] YAML parsing caching is not supported on this implementation of Ruby")
23
23
  end
24
24
  end
25
+
26
+ if json
27
+ if supported?
28
+ require_relative('compile_cache/json')
29
+ Bootsnap::CompileCache::JSON.install!(cache_dir)
30
+ elsif $VERBOSE
31
+ warn("[bootsnap/setup] JSON parsing caching is not supported on this implementation of Ruby")
32
+ end
33
+ end
25
34
  end
26
35
 
27
36
  def self.permission_error(path)
@@ -196,7 +196,7 @@ module Bootsnap
196
196
 
197
197
  s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
198
198
  if s.respond_to?(:-@)
199
- if (-s).equal?(s) && (-s.dup).equal?(s)
199
+ if (-s).equal?(s) && (-s.dup).equal?(s) || RUBY_VERSION >= '2.7'
200
200
  def try_index(f)
201
201
  if (p = @index[f])
202
202
  -(File.join(p, f).freeze)
@@ -15,11 +15,13 @@ module Bootsnap
15
15
  @lpc_observer.push_paths(self, *entries.map(&:to_s))
16
16
  super
17
17
  end
18
+ alias_method :append, :push
18
19
 
19
20
  def unshift(*entries)
20
21
  @lpc_observer.unshift_paths(self, *entries.map(&:to_s))
21
22
  super
22
23
  end
24
+ alias_method :prepend, :unshift
23
25
 
24
26
  def concat(entries)
25
27
  @lpc_observer.push_paths(self, *entries.map(&:to_s))
@@ -47,8 +47,9 @@ module Kernel
47
47
 
48
48
  alias_method(:require_relative_without_bootsnap, :require_relative)
49
49
  def require_relative(path)
50
+ location = caller_locations(1..1).first
50
51
  realpath = Bootsnap::LoadPathCache.realpath_cache.call(
51
- caller_locations(1..1).first.absolute_path, path
52
+ location.absolute_path || location.path, path
52
53
  )
53
54
  require(realpath)
54
55
  end
@@ -58,9 +58,9 @@ module Bootsnap
58
58
  end
59
59
 
60
60
  def purge_multi(features)
61
- rejected_hashes = features.map(&:hash).to_set
61
+ rejected_hashes = features.each_with_object({}) { |f, h| h[f.hash] = true }
62
62
  @mutex.synchronize do
63
- @lfi.reject! { |_, hash| rejected_hashes.include?(hash) }
63
+ @lfi.reject! { |_, hash| rejected_hashes.key?(hash) }
64
64
  end
65
65
  end
66
66
 
@@ -91,6 +91,7 @@ module Bootsnap
91
91
  FileUtils.mv(tmp, @store_path)
92
92
  rescue Errno::EEXIST
93
93
  retry
94
+ rescue SystemCallError
94
95
  end
95
96
  end
96
97
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Bootsnap
3
- VERSION = "1.7.2"
3
+ VERSION = "1.9.1"
4
4
  end
data/lib/bootsnap.rb CHANGED
@@ -27,7 +27,9 @@ module Bootsnap
27
27
 
28
28
  def self.instrumentation=(callback)
29
29
  @instrumentation = callback
30
- self.instrumentation_enabled = !!callback
30
+ if respond_to?(:instrumentation_enabled=, true)
31
+ self.instrumentation_enabled = !!callback
32
+ end
31
33
  end
32
34
 
33
35
  def self._instrument(event, path)
@@ -41,7 +43,8 @@ module Bootsnap
41
43
  autoload_paths_cache: nil,
42
44
  disable_trace: nil,
43
45
  compile_cache_iseq: true,
44
- compile_cache_yaml: true
46
+ compile_cache_yaml: true,
47
+ compile_cache_json: true
45
48
  )
46
49
  unless autoload_paths_cache.nil?
47
50
  warn "[DEPRECATED] Bootsnap's `autoload_paths_cache:` option is deprecated and will be removed. " \
@@ -67,7 +70,8 @@ module Bootsnap
67
70
  Bootsnap::CompileCache.setup(
68
71
  cache_dir: cache_dir + '/bootsnap/compile-cache',
69
72
  iseq: compile_cache_iseq,
70
- yaml: compile_cache_yaml
73
+ yaml: compile_cache_yaml,
74
+ json: compile_cache_json,
71
75
  )
72
76
  end
73
77
 
@@ -111,6 +115,7 @@ module Bootsnap
111
115
  load_path_cache: !ENV['DISABLE_BOOTSNAP_LOAD_PATH_CACHE'],
112
116
  compile_cache_iseq: !ENV['DISABLE_BOOTSNAP_COMPILE_CACHE'] && iseq_cache_supported?,
113
117
  compile_cache_yaml: !ENV['DISABLE_BOOTSNAP_COMPILE_CACHE'],
118
+ compile_cache_json: !ENV['DISABLE_BOOTSNAP_COMPILE_CACHE'],
114
119
  )
115
120
 
116
121
  if ENV['BOOTSNAP_LOG']
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bootsnap
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.2
4
+ version: 1.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-02-08 00:00:00.000000000 Z
11
+ date: 2021-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -42,14 +42,14 @@ dependencies:
42
42
  name: rake-compiler
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
@@ -116,6 +116,7 @@ files:
116
116
  - lib/bootsnap/cli/worker_pool.rb
117
117
  - lib/bootsnap/compile_cache.rb
118
118
  - lib/bootsnap/compile_cache/iseq.rb
119
+ - lib/bootsnap/compile_cache/json.rb
119
120
  - lib/bootsnap/compile_cache/yaml.rb
120
121
  - lib/bootsnap/explicit_require.rb
121
122
  - lib/bootsnap/load_path_cache.rb
@@ -153,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
154
  - !ruby/object:Gem::Version
154
155
  version: '0'
155
156
  requirements: []
156
- rubygems_version: 3.0.3
157
+ rubygems_version: 3.2.20
157
158
  signing_key:
158
159
  specification_version: 4
159
160
  summary: Boot large ruby/rails apps faster