pax_global_header00006660000000000000000000000064141474637710014530gustar00rootroot0000000000000052 comment=a0aa3d2b0cbdcef9d8db9ae787c3628ac250780f bootsnap-1.9.3/000077500000000000000000000000001414746377100133675ustar00rootroot00000000000000bootsnap-1.9.3/.github/000077500000000000000000000000001414746377100147275ustar00rootroot00000000000000bootsnap-1.9.3/.github/issue_template.md000066400000000000000000000010321414746377100202700ustar00rootroot00000000000000### I made sure the issue is in bootsnap ### Steps to reproduce ### Expected behavior ### Actual behavior ### System configuration **Bootsnap version**: **Ruby version**: **Rails version**: bootsnap-1.9.3/.github/probots.yml000066400000000000000000000000211414746377100171330ustar00rootroot00000000000000enabled: - cla bootsnap-1.9.3/.github/workflows/000077500000000000000000000000001414746377100167645ustar00rootroot00000000000000bootsnap-1.9.3/.github/workflows/ci.yaml000066400000000000000000000027621414746377100202520ustar00rootroot00000000000000name: ci on: pull_request: branches: - master push: branches: - master schedule: - cron: '45 4 * * *' jobs: platforms: strategy: matrix: os: [ubuntu, macos, windows] ruby: ['2.5'] runs-on: ${{ matrix.os }}-latest steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - run: bundle install - run: bundle exec rake rubies: strategy: matrix: os: [ubuntu] ruby: ['2.3', '2.4', '2.5', '2.6', '2.7', '3.0', 'ruby-head', 'debug'] runs-on: ${{ matrix.os }}-latest steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - run: bundle install - run: bundle exec rake psych4: strategy: matrix: os: [ubuntu] ruby: ['3.0'] runs-on: ${{ matrix.os }}-latest env: PSYCH_4: "1" steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - run: bundle install - run: bundle exec rake minimal: strategy: matrix: os: [ubuntu] ruby: ['jruby', 'truffleruby'] runs-on: ${{ matrix.os }}-latest steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - run: bundle install - run: bin/test-minimal-support bootsnap-1.9.3/.gitignore000066400000000000000000000002141414746377100153540ustar00rootroot00000000000000/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ *.bundle *.so *.o *.a *.gem *.db mkmf.log .rubocop-* bootsnap-1.9.3/.rubocop.yml000066400000000000000000000007001414746377100156360ustar00rootroot00000000000000inherit_from: - http://shopify.github.io/ruby-style-guide/rubocop.yml AllCops: Exclude: - 'vendor/**/*' - 'tmp/**/*' TargetRubyVersion: '2.4' # This doesn't take into account retrying from an exception Lint/SuppressedException: Enabled: false # allow String.new to create mutable strings Style/EmptyLiteral: Enabled: false # allow the use of globals which makes sense in a CLI app like this Style/GlobalVars: Enabled: false bootsnap-1.9.3/CHANGELOG.md000066400000000000000000000151501414746377100152020ustar00rootroot00000000000000# Unreleased # 1.9.3 * Only disable the compile cache for source files impacted by [Ruby 3.0.3 [Bug 18250]](https://bugs.ruby-lang.org/issues/18250). This should keep the performance loss to a minimum. # 1.9.2 * Disable compile cache if [Ruby 3.0.3's ISeq cache bug](https://bugs.ruby-lang.org/issues/18250) is detected. AKA `iseq.rb:13 to_binary: wrong argument type false (expected Symbol)` * Fix `Kernel.load` behavior: before `load 'a'` would load `a.rb` (and other tried extensions) and wouldn't load `a` unless `development_mode: true`, now only `a` would be loaded and files with extensions wouldn't be. # 1.9.1 * Removed a forgotten debug statement in JSON precompilation. # 1.9.0 * Added a compilation cache for `JSON.load_file`. (#370) # 1.8.1 * Fixed support for older Psych. (#369) # 1.8.0 * Improve support for Pysch 4. (#368) # 1.7.7 * Fix `require_relative` in evaled code on latest ruby 3.1.0-dev. (#366) # 1.7.6 * Fix reliance on `set` to be required. * Fix `Encoding::UndefinedConversionError` error for Rails applications when precompiling cache. (#364) # 1.7.5 * Handle a regression of Ruby 2.7.3 causing Bootsnap to call the deprecated `untaint` method. (#360) * Gracefully handle read-only file system as well as other errors preventing to persist the load path cache. (#358) # 1.7.4 * Stop raising errors when encoutering various file system errors. The cache is now best effort, if somehow it can't be saved, bootsnapp will gracefully fallback to the original operation (e.g. `Kernel.require`). (#353, #177, #262) # 1.7.3 * Disable YAML precompilation when encountering YAML tags. (#351) # 1.7.2 * Fix compatibility with msgpack < 1. (#349) # 1.7.1 * Warn Ruby 2.5 users if they turn ISeq caching on. (#327, #244) * Disable ISeq caching for the whole 2.5.x series again. * Better handle hashing of Ruby strings. (#318) # 1.7.0 * Fix detection of YAML files in gems. * Adds an instrumentation API to monitor cache misses. * Allow to control the behavior of `require 'bootsnap/setup'` using environment variables. * Deprecate the `disable_trace` option. * Deprecate the `ActiveSupport::Dependencies` (AKA Classic autoloader) integration. (#344) # 1.6.0 * Fix a Ruby 2.7/3.0 issue with `YAML.load_file` keyword arguments. (#342) * `bootsnap precompile` CLI use multiple processes to complete faster. (#341) * `bootsnap precompile` CLI also precompile YAML files. (#340) * Changed the load path cache directory from `$BOOTSNAP_CACHE_DIR/bootsnap-load-path-cache` to `$BOOTSNAP_CACHE_DIR/bootsnap/load-path-cache` for ease of use. (#334) * Changed the compile cache directory from `$BOOTSNAP_CACHE_DIR/bootsnap-compile-cache` to `$BOOTSNAP_CACHE_DIR/bootsnap/compile-cache` for ease of use. (#334) # 1.5.1 * Workaround a Ruby bug in InstructionSequence.compile_file. (#332) # 1.5.0 * Add a command line to statically precompile the ISeq cache. (#326) # 1.4.9 * [Windows support](https://github.com/Shopify/bootsnap/pull/319) * [Fix potential crash](https://github.com/Shopify/bootsnap/pull/322) # 1.4.8 * [Prevent FallbackScan from polluting exception cause](https://github.com/Shopify/bootsnap/pull/314) # 1.4.7 * Various performance enhancements * Fix race condition in heavy concurrent load scenarios that would cause bootsnap to raise # 1.4.6 * Fix bug that was erroneously considering that files containing `.` in the names were being required if a different file with the same name was already being required Example: require 'foo' require 'foo.en' Before bootsnap was considering `foo.en` to be the same file as `foo` * Use glibc as part of the ruby_platform cache key # 1.4.5 * MRI 2.7 support * Fixed concurrency bugs # 1.4.4 * Disable ISeq cache in `bootsnap/setup` by default in Ruby 2.5 # 1.4.3 * Fix some cache permissions and umask issues after switch to mkstemp # 1.4.2 * Fix bug when removing features loaded by relative path from `$LOADED_FEATURES` * Fix bug with propagation of `NameError` up from nested calls to `require` # 1.4.1 * Don't register change observers to frozen objects. # 1.4.0 * When running in development mode, always fall back to a full path scan on LoadError, making bootsnap more able to detect newly-created files. (#230) * Respect `$LOADED_FEATURES.delete` in order to support code reloading, for integration with Zeitwerk. (#230) * Minor performance improvement: flow-control exceptions no longer generate backtraces. * Better support for requiring from environments where some features are not supported (especially JRuby). (#226)k * More robust handling of OS errors when creating files. (#225) # 1.3.2 * Fix Spring + Bootsnap incompatibility when there are files with similar names. * Fix `YAML.load_file` monkey patch to keep accepting File objects as arguments. * Fix the API for `ActiveSupport::Dependencies#autoloadable_module?`. * Some performance improvements. # 1.3.1 * Change load path scanning to more correctly follow symlinks. # 1.3.0 * Handle cases where load path entries are symlinked (https://github.com/Shopify/bootsnap/pull/136) # 1.2.1 * Fix method visibility of `Kernel#require`. # 1.2.0 * Add `LoadedFeaturesIndex` to preserve fix a common bug related to `LOAD_PATH` modifications after loading bootsnap. # 1.1.8 * Don't cache YAML documents with `!ruby/object` * Fix cache write mode on Windows # 1.1.7 * Create cache entries as 0775/0664 instead of 0755/0644 * Better handling around cache updates in highly-parallel workloads # 1.1.6 * Assortment of minor bugfixes # 1.1.5 * bugfix re-release of 1.1.4 # 1.1.4 (yanked) * Avoid loading a constant twice by checking if it is already defined # 1.1.3 * Properly resolve symlinked path entries # 1.1.2 * Minor fix: deprecation warning # 1.1.1 * Fix crash in `Native.compile_option_crc32=` on 32-bit platforms. # 1.1.0 * Add `bootsnap/setup` * Support jruby (without compile caching features) * Better deoptimization when Coverage is enabled * Consider `Bundler.bundle_path` to be stable # 1.0.0 * (none) # 0.3.2 * Minor performance savings around checking validity of cache in the presence of relative paths. * When coverage is enabled, skips optimization instead of exploding. # 0.3.1 * Don't whitelist paths under `RbConfig::CONFIG['prefix']` as stable; instead use `['libdir']` (#41). * Catch `EOFError` when reading load-path-cache and regenerate cache. * Support relative paths in load-path-cache. # 0.3.0 * Migrate CompileCache from xattr as a cache backend to a cache directory * Adds support for Linux and FreeBSD # 0.2.15 * Support more versions of ActiveSupport (`depend_on`'s signature varies; don't reiterate it) * Fix bug in handling autoloaded modules that raise NoMethodError bootsnap-1.9.3/CODE_OF_CONDUCT.md000066400000000000000000000062271414746377100161750ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at burke@libbey.me. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ bootsnap-1.9.3/CONTRIBUTING.md000066400000000000000000000022311414746377100156160ustar00rootroot00000000000000# Contributing to Bootsnap We love receiving pull requests! ## Standards * PR should explain what the feature does, and why the change exists. * PR should include any carrier specific documentation explaining how it works. * Code _must_ be tested, including both unit and remote tests where applicable. * Be consistent. Write clean code that follows [Ruby community standards](https://github.com/bbatsov/ruby-style-guide). * Code should be generic and reusable. If you're stuck, ask questions! ## How to contribute 1. Fork it ( https://github.com/Shopify/bootsnap/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request ## Running Tests on Windows ### Setup 1. Ensure you've installed Ruby and the MSYS2 devkit and have ran `ridk enable` in your shell. The `ridk enable` command adds make to the path so the compile rake task works. 1. Open your shell as Administrator (`Run as Administrator`), as the tests create and delete symlinks ### Running Tests > ridk enable > bundle install > bundle exec rakebootsnap-1.9.3/Gemfile000066400000000000000000000003631414746377100146640ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' # Specify your gem's dependencies in bootsnap.gemspec gemspec if ENV["PSYCH_4"] gem "psych", ">= 4" end group :development do gem 'rubocop' gem 'byebug', platform: :ruby end bootsnap-1.9.3/LICENSE.txt000066400000000000000000000020701414746377100152110ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2017 Shopify, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bootsnap-1.9.3/README.md000066400000000000000000000277751414746377100146700ustar00rootroot00000000000000# Bootsnap [![Actions Status](https://github.com/Shopify/bootsnap/workflows/ci/badge.svg)](https://github.com/Shopify/bootsnap/actions) Bootsnap is a library that plugs into Ruby, with optional support for `YAML`, to optimize and cache expensive computations. See [How Does This Work](#how-does-this-work). #### Performance - [Discourse](https://github.com/discourse/discourse) reports a boot time reduction of approximately 50%, from roughly 6 to 3 seconds on one machine; - One of our smaller internal apps also sees a reduction of 50%, from 3.6 to 1.8 seconds; - The core Shopify platform -- a rather large monolithic application -- boots about 75% faster, dropping from around 25s to 6.5s. * In Shopify core (a large app), about 25% of this gain can be attributed to `compile_cache_*` features; 75% to path caching. This is fairly representative. ## Usage This gem works on macOS and Linux. Add `bootsnap` to your `Gemfile`: ```ruby gem 'bootsnap', require: false ``` If you are using Rails, add this to `config/boot.rb` immediately after `require 'bundler/setup'`: ```ruby require 'bootsnap/setup' ``` Note that bootsnap writes to `tmp/cache` (or the path specified by `ENV['BOOTSNAP_CACHE_DIR']`), and that directory *must* be writable. Rails will fail to boot if it is not. If this is unacceptable (e.g. you are running in a read-only container and unwilling to mount in a writable tmpdir), you should remove this line or wrap it in a conditional. **Note also that bootsnap will never clean up its own cache: this is left up to you. Depending on your deployment strategy, you may need to periodically purge `tmp/cache/bootsnap*`. If you notice deploys getting progressively slower, this is almost certainly the cause.** It's technically possible to simply specify `gem 'bootsnap', require: 'bootsnap/setup'`, but it's important to load Bootsnap as early as possible to get maximum performance improvement. You can see how this require works [here](https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/setup.rb). If you are not using Rails, or if you are but want more control over things, add this to your application setup immediately after `require 'bundler/setup'` (i.e. as early as possible: the sooner this is loaded, the sooner it can start optimizing things) ```ruby require 'bootsnap' env = ENV['RAILS_ENV'] || "development" Bootsnap.setup( cache_dir: 'tmp/cache', # Path to your cache development_mode: env == 'development', # Current working environment, e.g. RACK_ENV, RAILS_ENV, etc load_path_cache: true, # Optimize the LOAD_PATH with a cache compile_cache_iseq: true, # Compile Ruby code into ISeq cache, breaks coverage reporting. compile_cache_yaml: true # Compile YAML into a cache ) ``` **Protip:** You can replace `require 'bootsnap'` with `BootLib::Require.from_gem('bootsnap', 'bootsnap')` using [this trick](https://github.com/Shopify/bootsnap/wiki/Bootlib::Require). This will help optimize boot time further if you have an extremely large `$LOAD_PATH`. Note: Bootsnap and [Spring](https://github.com/rails/spring) are orthogonal tools. While Bootsnap speeds up the loading of individual source files, Spring keeps a copy of a pre-booted Rails process on hand to completely skip parts of the boot process the next time it's needed. The two tools work well together, and are both included in a newly-generated Rails applications by default. ### Environment variables `require 'bootsnap/setup'` behavior can be changed using environment variables: - `BOOTSNAP_CACHE_DIR` allows to define the cache location. - `DISABLE_BOOTSNAP` allows to entirely disable bootsnap. - `DISABLE_BOOTSNAP_LOAD_PATH_CACHE` allows to disable load path caching. - `DISABLE_BOOTSNAP_COMPILE_CACHE` allows to disable ISeq and YAML caches. - `BOOTSNAP_LOG` configure bootsnap to log all caches misses to STDERR. ### Environments All Bootsnap features are enabled in development, test, production, and all other environments according to the configuration in the setup. At Shopify, we use this gem safely in all environments without issue. If you would like to disable any feature for a certain environment, we suggest changing the configuration to take into account the appropriate ENV var or configuration according to your needs. ### Instrumentation Bootsnap cache misses can be monitored though a callback: ```ruby Bootsnap.instrumentation = ->(event, path) { puts "#{event} #{path}" } ``` `event` is either `:miss` or `:stale`. You can also call `Bootsnap.log!` as a shortcut to log all events to STDERR. To turn instrumentation back off you can set it to nil: ```ruby Bootsnap.instrumentation = nil ``` ## How does this work? Bootsnap optimizes methods to cache results of expensive computations, and can be grouped into two broad categories: * [Path Pre-Scanning](#path-pre-scanning) * `Kernel#require` and `Kernel#load` are modified to eliminate `$LOAD_PATH` scans. * [Compilation caching](#compilation-caching) * `RubyVM::InstructionSequence.load_iseq` is implemented to cache the result of ruby bytecode compilation. * `YAML.load_file` is modified to cache the result of loading a YAML object in MessagePack format (or Marshal, if the message uses types unsupported by MessagePack). ### Path Pre-Scanning *(This work is a minor evolution of [bootscale](https://github.com/byroot/bootscale)).* Upon initialization of bootsnap or modification of the path (e.g. `$LOAD_PATH`), `Bootsnap::LoadPathCache` will fetch a list of requirable entries from a cache, or, if necessary, perform a full scan and cache the result. Later, when we run (e.g.) `require 'foo'`, ruby *would* iterate through every item on our `$LOAD_PATH` `['x', 'y', ...]`, looking for `x/foo.rb`, `y/foo.rb`, and so on. Bootsnap instead looks at all the cached requirables for each `$LOAD_PATH` entry and substitutes the full expanded path of the match ruby would have eventually chosen. If you look at the syscalls generated by this behaviour, the net effect is that what would previously look like this: ``` open x/foo.rb # (fail) # (imagine this with 500 $LOAD_PATH entries instead of two) open y/foo.rb # (success) close y/foo.rb open y/foo.rb ... ``` becomes this: ``` open y/foo.rb ... ``` The following diagram flowcharts the overrides that make the `*_path_cache` features work. ![Flowchart explaining Bootsnap](https://cloud.githubusercontent.com/assets/3074765/24532120/eed94e64-158b-11e7-9137-438d759b2ac8.png) Bootsnap classifies path entries into two categories: stable and volatile. Volatile entries are scanned each time the application boots, and their caches are only valid for 30 seconds. Stable entries do not expire -- once their contents has been scanned, it is assumed to never change. The only directories considered "stable" are things under the Ruby install prefix (`RbConfig::CONFIG['prefix']`, e.g. `/usr/local/ruby` or `~/.rubies/x.y.z`), and things under the `Gem.path` (e.g. `~/.gem/ruby/x.y.z`) or `Bundler.bundle_path`. Everything else is considered "volatile". In addition to the [`Bootsnap::LoadPathCache::Cache` source](https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/cache.rb), this diagram may help clarify how entry resolution works: ![How path searching works](https://cloud.githubusercontent.com/assets/3074765/25388270/670b5652-299b-11e7-87fb-975647f68981.png) It's also important to note how expensive `LoadError`s can be. If ruby invokes `require 'something'`, but that file isn't on `$LOAD_PATH`, it takes `2 * $LOAD_PATH.length` filesystem accesses to determine that. Bootsnap caches this result too, raising a `LoadError` without touching the filesystem at all. ### Compilation Caching *(A more readable implementation of this concept can be found in [yomikomu](https://github.com/ko1/yomikomu)).* Ruby has complex grammar and parsing it is not a particularly cheap operation. Since 1.9, Ruby has translated ruby source to an internal bytecode format, which is then executed by the Ruby VM. Since 2.3.0, Ruby [exposes an API](https://ruby-doc.org/core-2.3.0/RubyVM/InstructionSequence.html) that allows caching that bytecode. This allows us to bypass the relatively-expensive compilation step on subsequent loads of the same file. We also noticed that we spend a lot of time loading YAML documents during our application boot, and that MessagePack and Marshal are *much* faster at deserialization than YAML, even with a fast implementation. We use the same strategy of compilation caching for YAML documents, with the equivalent of Ruby's "bytecode" format being a MessagePack document (or, in the case of YAML documents with types unsupported by MessagePack, a Marshal stream). These compilation results are stored in a cache directory, with filenames generated by taking a hash of the full expanded path of the input file (FNV1a-64). Whereas before, the sequence of syscalls generated to `require` a file would look like: ``` open /c/foo.rb -> m fstat64 m close m open /c/foo.rb -> o fstat64 o fstat64 o read o read o ... close o ``` With bootsnap, we get: ``` open /c/foo.rb -> n fstat64 n close n open /c/foo.rb -> n fstat64 n open (cache) -> m read m read m close m close n ``` This may look worse at a glance, but underlies a large performance difference. *(The first three syscalls in both listings -- `open`, `fstat64`, `close` -- are not inherently useful. [This ruby patch](https://bugs.ruby-lang.org/issues/13378) optimizes them out when coupled with bootsnap.)* Bootsnap writes a cache file containing a 64 byte header followed by the cache contents. The header is a cache key including several fields: * `version`, hardcoded in bootsnap. Essentially a schema version; * `ruby_platform`, A hash of `RUBY_PLATFORM` (e.g. x86_64-linux-gnu) variable and glibc version (on Linux) or OS version (`uname -v` on BSD, macOS) * `compile_option`, which changes with `RubyVM::InstructionSequence.compile_option` does; * `ruby_revision`, the version of Ruby this was compiled with; * `size`, the size of the source file; * `mtime`, the last-modification timestamp of the source file when it was compiled; and * `data_size`, the number of bytes following the header, which we need to read it into a buffer. If the key is valid, the result is loaded from the value. Otherwise, it is regenerated and clobbers the current cache. ### Putting it all together Imagine we have this file structure: ``` / ├── a ├── b └── c └── foo.rb ``` And this `$LOAD_PATH`: ``` ["/a", "/b", "/c"] ``` When we call `require 'foo'` without bootsnap, Ruby would generate this sequence of syscalls: ``` open /a/foo.rb -> -1 open /b/foo.rb -> -1 open /c/foo.rb -> n close n open /c/foo.rb -> m fstat64 m close m open /c/foo.rb -> o fstat64 o fstat64 o read o read o ... close o ``` With bootsnap, we get: ``` open /c/foo.rb -> n fstat64 n close n open /c/foo.rb -> n fstat64 n open (cache) -> m read m read m close m close n ``` If we call `require 'nope'` without bootsnap, we get: ``` open /a/nope.rb -> -1 open /b/nope.rb -> -1 open /c/nope.rb -> -1 open /a/nope.bundle -> -1 open /b/nope.bundle -> -1 open /c/nope.bundle -> -1 ``` ...and if we call `require 'nope'` *with* bootsnap, we get... ``` # (nothing!) ``` ## Precompilation In development environments the bootsnap compilation cache is generated on the fly when source files are loaded. But in production environments, such as docker images, you might need to precompile the cache. To do so you can use the `bootsnap precompile` command. Example: ```bash $ bundle exec bootsnap precompile --gemfile app/ lib/ ``` ## When not to use Bootsnap *Alternative engines*: Bootsnap is pretty reliant on MRI features, and parts are disabled entirely on alternative ruby engines. *Non-local filesystems*: Bootsnap depends on `tmp/cache` (or whatever you set its cache directory to) being on a relatively fast filesystem. If you put it on a network mount, bootsnap is very likely to slow your application down quite a lot. bootsnap-1.9.3/Rakefile000066400000000000000000000007301414746377100150340ustar00rootroot00000000000000# frozen_string_literal: true require('rake/extensiontask') require('bundler/gem_tasks') gemspec = Gem::Specification.load('bootsnap.gemspec') Rake::ExtensionTask.new do |ext| ext.name = 'bootsnap' ext.ext_dir = 'ext/bootsnap' ext.lib_dir = 'lib/bootsnap' ext.gem_spec = gemspec end require "rake/testtask" Rake::TestTask.new(:test) do |t| t.libs << "test" t.libs << "lib" t.test_files = FileList["test/**/*_test.rb"] end task(default: %i(compile test)) bootsnap-1.9.3/bin/000077500000000000000000000000001414746377100141375ustar00rootroot00000000000000bootsnap-1.9.3/bin/console000077500000000000000000000005701414746377100155310ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require("bundler/setup") require("bootsnap") # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require("irb") IRB.start(__FILE__) bootsnap-1.9.3/bin/setup000077500000000000000000000002031414746377100152200ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here bootsnap-1.9.3/bin/test-minimal-support000077500000000000000000000002131414746377100201760ustar00rootroot00000000000000#!/bin/bash set -euxo pipefail cd test/minimal_support bundle BOOTSNAP_CACHE_DIR=/tmp bundle exec ruby -w -I ../../lib bootsnap_setup.rb bootsnap-1.9.3/bin/testunit000077500000000000000000000003201414746377100157370ustar00rootroot00000000000000#!/bin/bash if [[ $# -eq 0 ]]; then exec ruby -I"test" -w -e 'Dir.glob("./test/**/*_test.rb").each { |f| require f }' -- "$@" else path=$1 exec ruby -I"test" -w -e "require '${path#test/}'" -- "$@" fi bootsnap-1.9.3/bootsnap.gemspec000066400000000000000000000030341414746377100165610ustar00rootroot00000000000000# coding: utf-8 # frozen_string_literal: true lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require('bootsnap/version') Gem::Specification.new do |spec| spec.name = "bootsnap" spec.version = Bootsnap::VERSION spec.authors = ["Burke Libbey"] spec.email = ["burke.libbey@shopify.com"] spec.license = "MIT" spec.summary = "Boot large ruby/rails apps faster" spec.description = spec.summary spec.homepage = "https://github.com/Shopify/bootsnap" spec.metadata = { 'bug_tracker_uri' => 'https://github.com/Shopify/bootsnap/issues', 'changelog_uri' => 'https://github.com/Shopify/bootsnap/blob/master/CHANGELOG.md', 'source_code_uri' => 'https://github.com/Shopify/bootsnap', 'allowed_push_host' => 'https://rubygems.org' } spec.files = %x(git ls-files -z ext lib).split("\x0") + %w(CHANGELOG.md LICENSE.txt README.md) spec.require_paths = %w(lib) spec.bindir = 'exe' spec.executables = %w(bootsnap) spec.required_ruby_version = '>= 2.3.0' if RUBY_PLATFORM =~ /java/ spec.platform = 'java' else spec.platform = Gem::Platform::RUBY spec.extensions = ['ext/bootsnap/extconf.rb'] end spec.add_development_dependency("bundler") spec.add_development_dependency('rake') spec.add_development_dependency('rake-compiler') spec.add_development_dependency("minitest", "~> 5.0") spec.add_development_dependency("mocha", "~> 1.2") spec.add_runtime_dependency("msgpack", "~> 1.0") end bootsnap-1.9.3/dev.yml000066400000000000000000000002461414746377100146720ustar00rootroot00000000000000env: BOOTSNAP_PEDANTIC: '1' up: - ruby: 2.6.0 - bundler commands: build: rake compile test: 'rake compile && exec bin/testunit' style: 'exec rubocop -D' bootsnap-1.9.3/exe/000077500000000000000000000000001414746377100141505ustar00rootroot00000000000000bootsnap-1.9.3/exe/bootsnap000077500000000000000000000001531414746377100157220ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require 'bootsnap/cli' exit Bootsnap::CLI.new(ARGV).run bootsnap-1.9.3/ext/000077500000000000000000000000001414746377100141675ustar00rootroot00000000000000bootsnap-1.9.3/ext/bootsnap/000077500000000000000000000000001414746377100160145ustar00rootroot00000000000000bootsnap-1.9.3/ext/bootsnap/bootsnap.c000066400000000000000000000741561414746377100200220ustar00rootroot00000000000000/* * Suggested reading order: * 1. Skim Init_bootsnap * 2. Skim bs_fetch * 3. The rest of everything * * Init_bootsnap sets up the ruby objects and binds bs_fetch to * Bootsnap::CompileCache::Native.fetch. * * bs_fetch is the ultimate caller for for just about every other function in * here. */ #include "bootsnap.h" #include "ruby.h" #include #include #include #include #include #include #ifndef _WIN32 #include #endif #ifdef __GLIBC__ #include #endif /* 1000 is an arbitrary limit; FNV64 plus some slashes brings the cap down to * 981 for the cache dir */ #define MAX_CACHEPATH_SIZE 1000 #define MAX_CACHEDIR_SIZE 981 #define KEY_SIZE 64 #define MAX_CREATE_TEMPFILE_ATTEMPT 3 #ifndef RB_UNLIKELY #define RB_UNLIKELY(x) (x) #endif /* * An instance of this key is written as the first 64 bytes of each cache file. * The mtime and size members track whether the file contents have changed, and * the version, ruby_platform, compile_option, and ruby_revision members track * changes to the environment that could invalidate compile results without * file contents having changed. The data_size member is not truly part of the * "key". Really, this could be called a "header" with the first six members * being an embedded "key" struct and an additional data_size member. * * The data_size indicates the remaining number of bytes in the cache file * after the header (the size of the cached artifact). * * After data_size, the struct is padded to 64 bytes. */ struct bs_cache_key { uint32_t version; uint32_t ruby_platform; uint32_t compile_option; uint32_t ruby_revision; uint64_t size; uint64_t mtime; uint64_t data_size; /* not used for equality */ uint8_t pad[24]; } __attribute__((packed)); /* * If the struct padding isn't correct to pad the key to 64 bytes, refuse to * compile. */ #define STATIC_ASSERT(X) STATIC_ASSERT2(X,__LINE__) #define STATIC_ASSERT2(X,L) STATIC_ASSERT3(X,L) #define STATIC_ASSERT3(X,L) STATIC_ASSERT_MSG(X,at_line_##L) #define STATIC_ASSERT_MSG(COND,MSG) typedef char static_assertion_##MSG[(!!(COND))*2-1] STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE); /* Effectively a schema version. Bumping invalidates all previous caches */ static const uint32_t current_version = 3; /* hash of e.g. "x86_64-darwin17", invalidating when ruby is recompiled on a * new OS ABI, etc. */ static uint32_t current_ruby_platform; /* Invalidates cache when switching ruby versions */ static uint32_t current_ruby_revision; /* Invalidates cache when RubyVM::InstructionSequence.compile_option changes */ static uint32_t current_compile_option_crc32 = 0; /* Current umask */ static mode_t current_umask; /* Bootsnap::CompileCache::{Native, Uncompilable} */ static VALUE rb_mBootsnap; static VALUE rb_mBootsnap_CompileCache; static VALUE rb_mBootsnap_CompileCache_Native; static VALUE rb_eBootsnap_CompileCache_Uncompilable; static ID uncompilable; static ID instrumentation_method; static VALUE sym_miss; static VALUE sym_stale; static bool instrumentation_enabled = false; /* Functions exposed as module functions on Bootsnap::CompileCache::Native */ static VALUE bs_instrumentation_enabled_set(VALUE self, VALUE enabled); static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v); static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args); static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler); /* Helpers */ static void bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_CACHEPATH_SIZE]); static int bs_read_key(int fd, struct bs_cache_key * key); static int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2); static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args); static VALUE bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler); static int open_current_file(char * path, struct bs_cache_key * key, const char ** errno_provenance); 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); static uint32_t get_ruby_revision(void); static uint32_t get_ruby_platform(void); /* * Helper functions to call ruby methods on handler object without crashing on * exception. */ static int bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * output_data); static VALUE prot_storage_to_output(VALUE arg); static VALUE prot_input_to_output(VALUE arg); static void bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE * output_data, int * exception_tag); static VALUE prot_input_to_storage(VALUE arg); static int bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * storage_data); struct s2o_data; struct i2o_data; struct i2s_data; /* https://bugs.ruby-lang.org/issues/13667 */ extern VALUE rb_get_coverages(void); static VALUE bs_rb_coverage_running(VALUE self) { VALUE cov = rb_get_coverages(); return RTEST(cov) ? Qtrue : Qfalse; } /* * Ruby C extensions are initialized by calling Init_. * * This sets up the module hierarchy and attaches functions as methods. * * We also populate some semi-static information about the current OS and so on. */ void Init_bootsnap(void) { rb_mBootsnap = rb_define_module("Bootsnap"); rb_mBootsnap_CompileCache = rb_define_module_under(rb_mBootsnap, "CompileCache"); rb_mBootsnap_CompileCache_Native = rb_define_module_under(rb_mBootsnap_CompileCache, "Native"); rb_eBootsnap_CompileCache_Uncompilable = rb_define_class_under(rb_mBootsnap_CompileCache, "Uncompilable", rb_eStandardError); current_ruby_revision = get_ruby_revision(); current_ruby_platform = get_ruby_platform(); uncompilable = rb_intern("__bootsnap_uncompilable__"); instrumentation_method = rb_intern("_instrument"); sym_miss = ID2SYM(rb_intern("miss")); rb_global_variable(&sym_miss); sym_stale = ID2SYM(rb_intern("stale")); rb_global_variable(&sym_stale); rb_define_module_function(rb_mBootsnap, "instrumentation_enabled=", bs_instrumentation_enabled_set, 1); rb_define_module_function(rb_mBootsnap_CompileCache_Native, "coverage_running?", bs_rb_coverage_running, 0); rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 4); rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 3); rb_define_module_function(rb_mBootsnap_CompileCache_Native, "compile_option_crc32=", bs_compile_option_crc32_set, 1); current_umask = umask(0777); umask(current_umask); } static VALUE bs_instrumentation_enabled_set(VALUE self, VALUE enabled) { instrumentation_enabled = RTEST(enabled); return enabled; } /* * Bootsnap's ruby code registers a hook that notifies us via this function * when compile_option changes. These changes invalidate all existing caches. * * Note that on 32-bit platforms, a CRC32 can't be represented in a Fixnum, but * can be represented by a uint. */ static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v) { if (!RB_TYPE_P(crc32_v, T_BIGNUM) && !RB_TYPE_P(crc32_v, T_FIXNUM)) { Check_Type(crc32_v, T_FIXNUM); } current_compile_option_crc32 = NUM2UINT(crc32_v); return Qnil; } /* * We use FNV1a-64 to derive cache paths. The choice is somewhat arbitrary but * it has several nice properties: * * - Tiny implementation * - No external dependency * - Solid performance * - Solid randomness * - 32 bits doesn't feel collision-resistant enough; 64 is nice. */ static uint64_t fnv1a_64_iter_cstr(uint64_t h, const char *str) { unsigned char *s = (unsigned char *)str; while (*s) { h ^= (uint64_t)*s++; h += (h << 1) + (h << 4) + (h << 5) + (h << 7) + (h << 8) + (h << 40); } return h; } static uint64_t fnv1a_64_iter(uint64_t h, const VALUE str) { unsigned char *s = (unsigned char *)RSTRING_PTR(str); unsigned char *str_end = (unsigned char *)RSTRING_PTR(str) + RSTRING_LEN(str); while (s < str_end) { h ^= (uint64_t)*s++; h += (h << 1) + (h << 4) + (h << 5) + (h << 7) + (h << 8) + (h << 40); } return h; } static uint64_t fnv1a_64(const VALUE str) { uint64_t h = (uint64_t)0xcbf29ce484222325ULL; return fnv1a_64_iter(h, str); } /* * Ruby's revision may be Integer or String. CRuby 2.7 or later uses * Git commit ID as revision. It's String. */ static uint32_t get_ruby_revision(void) { VALUE ruby_revision; ruby_revision = rb_const_get(rb_cObject, rb_intern("RUBY_REVISION")); if (RB_TYPE_P(ruby_revision, RUBY_T_FIXNUM)) { return FIX2INT(ruby_revision); } else { uint64_t hash; hash = fnv1a_64(ruby_revision); return (uint32_t)(hash >> 32); } } /* * When ruby's version doesn't change, but it's recompiled on a different OS * (or OS version), we need to invalidate the cache. * * We actually factor in some extra information here, to be extra confident * that we don't try to re-use caches that will not be compatible, by factoring * in utsname.version. */ static uint32_t get_ruby_platform(void) { uint64_t hash; VALUE ruby_platform; ruby_platform = rb_const_get(rb_cObject, rb_intern("RUBY_PLATFORM")); hash = fnv1a_64(ruby_platform); #ifdef _WIN32 return (uint32_t)(hash >> 32) ^ (uint32_t)GetVersion(); #elif defined(__GLIBC__) hash = fnv1a_64_iter_cstr(hash, gnu_get_libc_version()); return (uint32_t)(hash >> 32); #else struct utsname utsname; /* Not worth crashing if this fails; lose extra cache invalidation potential */ if (uname(&utsname) >= 0) { hash = fnv1a_64_iter_cstr(hash, utsname.version); } return (uint32_t)(hash >> 32); #endif } /* * Given a cache root directory and the full path to a file being cached, * generate a path under the cache directory at which the cached artifact will * be stored. * * The path will look something like: /12/34567890abcdef */ static void bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_CACHEPATH_SIZE]) { uint64_t hash = fnv1a_64(path); uint8_t first_byte = (hash >> (64 - 8)); uint64_t remainder = hash & 0x00ffffffffffffff; sprintf(*cache_path, "%s/%02"PRIx8"/%014"PRIx64, cachedir, first_byte, remainder); } /* * Test whether a newly-generated cache key based on the file as it exists on * disk matches the one that was generated when the file was cached (or really * compare any two keys). * * The data_size member is not compared, as it serves more of a "header" * function. */ static int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2) { return ( k1->version == k2->version && k1->ruby_platform == k2->ruby_platform && k1->compile_option == k2->compile_option && k1->ruby_revision == k2->ruby_revision && k1->size == k2->size && k1->mtime == k2->mtime ); } /* * Entrypoint for Bootsnap::CompileCache::Native.fetch. The real work is done * in bs_fetch; this function just performs some basic typechecks and * conversions on the ruby VALUE arguments before passing them along. */ static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args) { FilePathValue(path_v); Check_Type(cachedir_v, T_STRING); Check_Type(path_v, T_STRING); if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) { rb_raise(rb_eArgError, "cachedir too long"); } char * cachedir = RSTRING_PTR(cachedir_v); char * path = RSTRING_PTR(path_v); char cache_path[MAX_CACHEPATH_SIZE]; /* generate cache path to cache_path */ bs_cache_path(cachedir, path_v, &cache_path); return bs_fetch(path, path_v, cache_path, handler, args); } /* * Entrypoint for Bootsnap::CompileCache::Native.precompile. * Similar to fetch, but it only generate the cache if missing * and doesn't return the content. */ static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler) { FilePathValue(path_v); Check_Type(cachedir_v, T_STRING); Check_Type(path_v, T_STRING); if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) { rb_raise(rb_eArgError, "cachedir too long"); } char * cachedir = RSTRING_PTR(cachedir_v); char * path = RSTRING_PTR(path_v); char cache_path[MAX_CACHEPATH_SIZE]; /* generate cache path to cache_path */ bs_cache_path(cachedir, path_v, &cache_path); return bs_precompile(path, path_v, cache_path, handler); } /* * Open the file we want to load/cache and generate a cache key for it if it * was loaded. */ static int open_current_file(char * path, struct bs_cache_key * key, const char ** errno_provenance) { struct stat statbuf; int fd; fd = open(path, O_RDONLY); if (fd < 0) { *errno_provenance = "bs_fetch:open_current_file:open"; return fd; } #ifdef _WIN32 setmode(fd, O_BINARY); #endif if (fstat(fd, &statbuf) < 0) { *errno_provenance = "bs_fetch:open_current_file:fstat"; close(fd); return -1; } key->version = current_version; key->ruby_platform = current_ruby_platform; key->compile_option = current_compile_option_crc32; key->ruby_revision = current_ruby_revision; key->size = (uint64_t)statbuf.st_size; key->mtime = (uint64_t)statbuf.st_mtime; return fd; } #define ERROR_WITH_ERRNO -1 #define CACHE_MISS -2 #define CACHE_STALE -3 /* * Read the cache key from the given fd, which must have position 0 (e.g. * freshly opened file). * * Possible return values: * - 0 (OK, key was loaded) * - ERROR_WITH_ERRNO (-1, errno is set) * - CACHE_MISS (-2) * - CACHE_STALE (-3) */ static int bs_read_key(int fd, struct bs_cache_key * key) { ssize_t nread = read(fd, key, KEY_SIZE); if (nread < 0) return ERROR_WITH_ERRNO; if (nread < KEY_SIZE) return CACHE_STALE; return 0; } /* * Open the cache file at a given path, if it exists, and read its key into the * struct. * * Possible return values: * - 0 (OK, key was loaded) * - CACHE_MISS (-2) * - CACHE_STALE (-3) * - ERROR_WITH_ERRNO (-1, errno is set) */ static int open_cache_file(const char * path, struct bs_cache_key * key, const char ** errno_provenance) { int fd, res; fd = open(path, O_RDONLY); if (fd < 0) { *errno_provenance = "bs_fetch:open_cache_file:open"; return CACHE_MISS; } #ifdef _WIN32 setmode(fd, O_BINARY); #endif res = bs_read_key(fd, key); if (res < 0) { *errno_provenance = "bs_fetch:open_cache_file:read"; close(fd); return res; } return fd; } /* * The cache file is laid out like: * 0...64 : bs_cache_key * 64..-1 : cached artifact * * This function takes a file descriptor whose position is pre-set to 64, and * the data_size (corresponding to the remaining number of bytes) listed in the * cache header. * * We load the text from this file into a buffer, and pass it to the ruby-land * handler with exception handling via the exception_tag param. * * Data is returned via the output_data parameter, which, if there's no error * or exception, will be the final data returnable to the user. */ 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) { char * data = NULL; ssize_t nread; int ret; VALUE storage_data; if (data_size > 100000000000) { *errno_provenance = "bs_fetch:fetch_cached_data:datasize"; errno = EINVAL; /* because wtf? */ ret = -1; goto done; } data = ALLOC_N(char, data_size); nread = read(fd, data, data_size); if (nread < 0) { *errno_provenance = "bs_fetch:fetch_cached_data:read"; ret = -1; goto done; } if (nread != data_size) { ret = CACHE_STALE; goto done; } storage_data = rb_str_new(data, data_size); *exception_tag = bs_storage_to_output(handler, args, storage_data, output_data); ret = 0; done: if (data != NULL) xfree(data); return ret; } /* * Like mkdir -p, this recursively creates directory parents of a file. e.g. * given /a/b/c, creates /a and /a/b. */ static int mkpath(char * file_path, mode_t mode) { /* It would likely be more efficient to count back until we * find a component that *does* exist, but this will only run * at most 256 times, so it seems not worthwhile to change. */ char * p; for (p = strchr(file_path + 1, '/'); p; p = strchr(p + 1, '/')) { *p = '\0'; #ifdef _WIN32 if (mkdir(file_path) == -1) { #else if (mkdir(file_path, mode) == -1) { #endif if (errno != EEXIST) { *p = '/'; return -1; } } *p = '/'; } return 0; } /* * Write a cache header/key and a compiled artifact to a given cache path by * writing to a tmpfile and then renaming the tmpfile over top of the final * path. */ static int atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data, const char ** errno_provenance) { char template[MAX_CACHEPATH_SIZE + 20]; char * tmp_path; int fd, ret, attempt; ssize_t nwrite; for (attempt = 0; attempt < MAX_CREATE_TEMPFILE_ATTEMPT; ++attempt) { tmp_path = strncpy(template, path, MAX_CACHEPATH_SIZE); strcat(tmp_path, ".tmp.XXXXXX"); // mkstemp modifies the template to be the actual created path fd = mkstemp(tmp_path); if (fd > 0) break; if (attempt == 0 && mkpath(tmp_path, 0775) < 0) { *errno_provenance = "bs_fetch:atomic_write_cache_file:mkpath"; return -1; } } if (fd < 0) { *errno_provenance = "bs_fetch:atomic_write_cache_file:mkstemp"; return -1; } if (chmod(tmp_path, 0644) < 0) { *errno_provenance = "bs_fetch:atomic_write_cache_file:chmod"; return -1; } #ifdef _WIN32 setmode(fd, O_BINARY); #endif key->data_size = RSTRING_LEN(data); nwrite = write(fd, key, KEY_SIZE); if (nwrite < 0) { *errno_provenance = "bs_fetch:atomic_write_cache_file:write"; return -1; } if (nwrite != KEY_SIZE) { *errno_provenance = "bs_fetch:atomic_write_cache_file:keysize"; errno = EIO; /* Lies but whatever */ return -1; } nwrite = write(fd, RSTRING_PTR(data), RSTRING_LEN(data)); if (nwrite < 0) return -1; if (nwrite != RSTRING_LEN(data)) { *errno_provenance = "bs_fetch:atomic_write_cache_file:writelength"; errno = EIO; /* Lies but whatever */ return -1; } close(fd); ret = rename(tmp_path, path); if (ret < 0) { *errno_provenance = "bs_fetch:atomic_write_cache_file:rename"; return -1; } ret = chmod(path, 0664 & ~current_umask); if (ret < 0) { *errno_provenance = "bs_fetch:atomic_write_cache_file:chmod"; } return ret; } /* Read contents from an fd, whose contents are asserted to be +size+ bytes * long, into a buffer */ static ssize_t bs_read_contents(int fd, size_t size, char ** contents, const char ** errno_provenance) { ssize_t nread; *contents = ALLOC_N(char, size); nread = read(fd, *contents, size); if (nread < 0) { *errno_provenance = "bs_fetch:bs_read_contents:read"; } return nread; } /* * This is the meat of the extension. bs_fetch is * Bootsnap::CompileCache::Native.fetch. * * There are three "formats" in use here: * 1. "input" format, which is what we load from the source file; * 2. "storage" format, which we write to the cache; * 3. "output" format, which is what we return. * * E.g., For ISeq compilation: * input: ruby source, as text * storage: binary string (RubyVM::InstructionSequence#to_binary) * output: Instance of RubyVM::InstructionSequence * * And for YAML: * input: yaml as text * storage: MessagePack or Marshal text * output: ruby object, loaded from yaml/messagepack/marshal * * A handler passed in must support three messages: * * storage_to_output(S) -> O * * input_to_output(I) -> O * * input_to_storage(I) -> S * (input_to_storage may raise Bootsnap::CompileCache::Uncompilable, which * will prevent caching and cause output to be generated with * input_to_output) * * The semantics of this function are basically: * * return storage_to_output(cache[path]) if cache[path] * storage = input_to_storage(input) * cache[path] = storage * return storage_to_output(storage) * * Or expanded a bit: * * - Check if the cache file exists and is up to date. * - If it is, load this data to storage_data. * - return storage_to_output(storage_data) * - Read the file to input_data * - Generate storage_data using input_to_storage(input_data) * - Write storage_data data, with a cache key, to the cache file. * - Return storage_to_output(storage_data) */ static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args) { struct bs_cache_key cached_key, current_key; char * contents = NULL; int cache_fd = -1, current_fd = -1; int res, valid_cache = 0, exception_tag = 0; const char * errno_provenance = NULL; VALUE input_data; /* data read from source file, e.g. YAML or ruby source */ VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */ VALUE output_data; /* return data, e.g. ruby hash or loaded iseq */ VALUE exception; /* ruby exception object to raise instead of returning */ /* Open the source file and generate a cache key for it */ current_fd = open_current_file(path, ¤t_key, &errno_provenance); if (current_fd < 0) goto fail_errno; /* Open the cache key if it exists, and read its cache key in */ cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance); if (cache_fd == CACHE_MISS || cache_fd == CACHE_STALE) { /* This is ok: valid_cache remains false, we re-populate it. */ if (RB_UNLIKELY(instrumentation_enabled)) { rb_funcall(rb_mBootsnap, instrumentation_method, 2, cache_fd == CACHE_MISS ? sym_miss : sym_stale, path_v); } } else if (cache_fd < 0) { goto fail_errno; } else { /* True if the cache existed and no invalidating changes have occurred since * it was generated. */ valid_cache = cache_key_equal(¤t_key, &cached_key); if (RB_UNLIKELY(instrumentation_enabled)) { if (!valid_cache) { rb_funcall(rb_mBootsnap, instrumentation_method, 2, sym_stale, path_v); } } } if (valid_cache) { /* Fetch the cache data and return it if we're able to load it successfully */ res = fetch_cached_data( cache_fd, (ssize_t)cached_key.data_size, handler, args, &output_data, &exception_tag, &errno_provenance ); if (exception_tag != 0) goto raise; else if (res == CACHE_MISS || res == CACHE_STALE) valid_cache = 0; else if (res == ERROR_WITH_ERRNO) goto fail_errno; else if (!NIL_P(output_data)) goto succeed; /* fast-path, goal */ } close(cache_fd); cache_fd = -1; /* Cache is stale, invalid, or missing. Regenerate and write it out. */ /* Read the contents of the source file into a buffer */ if (bs_read_contents(current_fd, current_key.size, &contents, &errno_provenance) < 0) goto fail_errno; input_data = rb_str_new(contents, current_key.size); /* Try to compile the input_data using input_to_storage(input_data) */ exception_tag = bs_input_to_storage(handler, args, input_data, path_v, &storage_data); if (exception_tag != 0) goto raise; /* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try * to cache anything; just return input_to_output(input_data) */ if (storage_data == uncompilable) { bs_input_to_output(handler, args, input_data, &output_data, &exception_tag); if (exception_tag != 0) goto raise; goto succeed; } /* If storage_data isn't a string, we can't cache it */ if (!RB_TYPE_P(storage_data, T_STRING)) goto invalid_type_storage_data; /* Attempt to write the cache key and storage_data to the cache directory. * We do however ignore any failures to persist the cache, as it's better * to move along, than to interrupt the process. */ atomic_write_cache_file(cache_path, ¤t_key, storage_data, &errno_provenance); /* Having written the cache, now convert storage_data to output_data */ exception_tag = bs_storage_to_output(handler, args, storage_data, &output_data); if (exception_tag != 0) goto raise; /* If output_data is nil, delete the cache entry and generate the output * using input_to_output */ if (NIL_P(output_data)) { if (unlink(cache_path) < 0) { errno_provenance = "bs_fetch:unlink"; goto fail_errno; } bs_input_to_output(handler, args, input_data, &output_data, &exception_tag); if (exception_tag != 0) goto raise; } goto succeed; /* output_data is now the correct return. */ #define CLEANUP \ if (contents != NULL) xfree(contents); \ if (current_fd >= 0) close(current_fd); \ if (cache_fd >= 0) close(cache_fd); succeed: CLEANUP; return output_data; fail_errno: CLEANUP; exception = rb_syserr_new(errno, errno_provenance); rb_exc_raise(exception); __builtin_unreachable(); raise: CLEANUP; rb_jump_tag(exception_tag); __builtin_unreachable(); invalid_type_storage_data: CLEANUP; Check_Type(storage_data, T_STRING); __builtin_unreachable(); #undef CLEANUP } static VALUE bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler) { struct bs_cache_key cached_key, current_key; char * contents = NULL; int cache_fd = -1, current_fd = -1; int res, valid_cache = 0, exception_tag = 0; const char * errno_provenance = NULL; VALUE input_data; /* data read from source file, e.g. YAML or ruby source */ VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */ /* Open the source file and generate a cache key for it */ current_fd = open_current_file(path, ¤t_key, &errno_provenance); if (current_fd < 0) goto fail; /* Open the cache key if it exists, and read its cache key in */ cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance); if (cache_fd == CACHE_MISS || cache_fd == CACHE_STALE) { /* This is ok: valid_cache remains false, we re-populate it. */ } else if (cache_fd < 0) { goto fail; } else { /* True if the cache existed and no invalidating changes have occurred since * it was generated. */ valid_cache = cache_key_equal(¤t_key, &cached_key); } if (valid_cache) { goto succeed; } close(cache_fd); cache_fd = -1; /* Cache is stale, invalid, or missing. Regenerate and write it out. */ /* Read the contents of the source file into a buffer */ if (bs_read_contents(current_fd, current_key.size, &contents, &errno_provenance) < 0) goto fail; input_data = rb_str_new(contents, current_key.size); /* Try to compile the input_data using input_to_storage(input_data) */ exception_tag = bs_input_to_storage(handler, Qnil, input_data, path_v, &storage_data); if (exception_tag != 0) goto fail; /* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try * to cache anything; just return false */ if (storage_data == uncompilable) { goto fail; } /* If storage_data isn't a string, we can't cache it */ if (!RB_TYPE_P(storage_data, T_STRING)) goto fail; /* Write the cache key and storage_data to the cache directory */ res = atomic_write_cache_file(cache_path, ¤t_key, storage_data, &errno_provenance); if (res < 0) goto fail; goto succeed; #define CLEANUP \ if (contents != NULL) xfree(contents); \ if (current_fd >= 0) close(current_fd); \ if (cache_fd >= 0) close(cache_fd); succeed: CLEANUP; return Qtrue; fail: CLEANUP; return Qfalse; #undef CLEANUP } /*****************************************************************************/ /********************* Handler Wrappers **************************************/ /***************************************************************************** * Everything after this point in the file is just wrappers to deal with ruby's * clunky method of handling exceptions from ruby methods invoked from C: * * In order to call a ruby method from C, while protecting against crashing in * the event of an exception, we must call the method with rb_protect(). * * rb_protect takes a C function and precisely one argument; however, we want * to pass multiple arguments, so we must create structs to wrap them up. * * These functions return an exception_tag, which, if non-zero, indicates an * exception that should be jumped to with rb_jump_tag after cleaning up * allocated resources. */ struct s2o_data { VALUE handler; VALUE args; VALUE storage_data; }; struct i2o_data { VALUE handler; VALUE args; VALUE input_data; }; struct i2s_data { VALUE handler; VALUE input_data; VALUE pathval; }; static VALUE prot_storage_to_output(VALUE arg) { struct s2o_data * data = (struct s2o_data *)arg; return rb_funcall(data->handler, rb_intern("storage_to_output"), 2, data->storage_data, data->args); } static int bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * output_data) { int state; struct s2o_data s2o_data = { .handler = handler, .args = args, .storage_data = storage_data, }; *output_data = rb_protect(prot_storage_to_output, (VALUE)&s2o_data, &state); return state; } static void bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE * output_data, int * exception_tag) { struct i2o_data i2o_data = { .handler = handler, .args = args, .input_data = input_data, }; *output_data = rb_protect(prot_input_to_output, (VALUE)&i2o_data, exception_tag); } static VALUE prot_input_to_output(VALUE arg) { struct i2o_data * data = (struct i2o_data *)arg; return rb_funcall(data->handler, rb_intern("input_to_output"), 2, data->input_data, data->args); } static VALUE try_input_to_storage(VALUE arg) { struct i2s_data * data = (struct i2s_data *)arg; return rb_funcall(data->handler, rb_intern("input_to_storage"), 2, data->input_data, data->pathval); } static VALUE rescue_input_to_storage(VALUE arg, VALUE e) { return uncompilable; } static VALUE prot_input_to_storage(VALUE arg) { struct i2s_data * data = (struct i2s_data *)arg; return rb_rescue2( try_input_to_storage, (VALUE)data, rescue_input_to_storage, Qnil, rb_eBootsnap_CompileCache_Uncompilable, 0); } static int bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * storage_data) { int state; struct i2s_data i2s_data = { .handler = handler, .input_data = input_data, .pathval = pathval, }; *storage_data = rb_protect(prot_input_to_storage, (VALUE)&i2s_data, &state); return state; } bootsnap-1.9.3/ext/bootsnap/bootsnap.h000066400000000000000000000001401414746377100200050ustar00rootroot00000000000000#ifndef BOOTSNAP_H #define BOOTSNAP_H 1 /* doesn't expose anything */ #endif /* BOOTSNAP_H */ bootsnap-1.9.3/ext/bootsnap/extconf.rb000066400000000000000000000013311414746377100200050ustar00rootroot00000000000000# frozen_string_literal: true require("mkmf") if RUBY_ENGINE == 'ruby' $CFLAGS << ' -O3 ' $CFLAGS << ' -std=c99' # ruby.h has some -Wpedantic fails in some cases # (e.g. https://github.com/Shopify/bootsnap/issues/15) unless ['0', '', nil].include?(ENV['BOOTSNAP_PEDANTIC']) $CFLAGS << ' -Wall' $CFLAGS << ' -Werror' $CFLAGS << ' -Wextra' $CFLAGS << ' -Wpedantic' $CFLAGS << ' -Wno-unused-parameter' # VALUE self has to be there but we don't care what it is. $CFLAGS << ' -Wno-keyword-macro' # hiding return $CFLAGS << ' -Wno-gcc-compat' # ruby.h 2.6.0 on macos 10.14, dunno end create_makefile("bootsnap/bootsnap") else File.write("Makefile", dummy_makefile($srcdir).join("")) end bootsnap-1.9.3/lib/000077500000000000000000000000001414746377100141355ustar00rootroot00000000000000bootsnap-1.9.3/lib/bootsnap.rb000066400000000000000000000077011414746377100163140ustar00rootroot00000000000000# frozen_string_literal: true require_relative('bootsnap/version') require_relative('bootsnap/bundler') require_relative('bootsnap/load_path_cache') require_relative('bootsnap/compile_cache') module Bootsnap InvalidConfiguration = Class.new(StandardError) class << self attr_reader :logger end def self.log! self.logger = $stderr.method(:puts) end def self.logger=(logger) @logger = logger if logger.respond_to?(:debug) self.instrumentation = ->(event, path) { @logger.debug("[Bootsnap] #{event} #{path}") } else self.instrumentation = ->(event, path) { @logger.call("[Bootsnap] #{event} #{path}") } end end def self.instrumentation=(callback) @instrumentation = callback if respond_to?(:instrumentation_enabled=, true) self.instrumentation_enabled = !!callback end end def self._instrument(event, path) @instrumentation.call(event, path) end def self.setup( cache_dir:, development_mode: true, load_path_cache: true, autoload_paths_cache: nil, disable_trace: nil, compile_cache_iseq: true, compile_cache_yaml: true, compile_cache_json: true ) unless autoload_paths_cache.nil? warn "[DEPRECATED] Bootsnap's `autoload_paths_cache:` option is deprecated and will be removed. " \ "If you use Zeitwerk this option is useless, and if you are still using the classic autoloader " \ "upgrading is recommended." end unless disable_trace.nil? warn "[DEPRECATED] Bootsnap's `disable_trace:` option is deprecated and will be removed. " \ "If you use Ruby 2.5 or newer this option is useless, if not upgrading is recommended." end if compile_cache_iseq && !iseq_cache_supported? warn "Ruby 2.5 has a bug that break code tracing when code is loaded from cache. It is recommened " \ "to turn `compile_cache_iseq` off on Ruby 2.5" end Bootsnap::LoadPathCache.setup( cache_path: cache_dir + '/bootsnap/load-path-cache', development_mode: development_mode, ) if load_path_cache Bootsnap::CompileCache.setup( cache_dir: cache_dir + '/bootsnap/compile-cache', iseq: compile_cache_iseq, yaml: compile_cache_yaml, json: compile_cache_json, ) end def self.iseq_cache_supported? return @iseq_cache_supported if defined? @iseq_cache_supported ruby_version = Gem::Version.new(RUBY_VERSION) @iseq_cache_supported = ruby_version < Gem::Version.new('2.5.0') || ruby_version >= Gem::Version.new('2.6.0') end def self.default_setup env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || ENV['ENV'] development_mode = ['', nil, 'development'].include?(env) unless ENV['DISABLE_BOOTSNAP'] cache_dir = ENV['BOOTSNAP_CACHE_DIR'] unless cache_dir config_dir_frame = caller.detect do |line| line.include?('/config/') end unless config_dir_frame $stderr.puts("[bootsnap/setup] couldn't infer cache directory! Either:") $stderr.puts("[bootsnap/setup] 1. require bootsnap/setup from your application's config directory; or") $stderr.puts("[bootsnap/setup] 2. Define the environment variable BOOTSNAP_CACHE_DIR") raise("couldn't infer bootsnap cache directory") end path = config_dir_frame.split(/:\d+:/).first path = File.dirname(path) until File.basename(path) == 'config' app_root = File.dirname(path) cache_dir = File.join(app_root, 'tmp', 'cache') end setup( cache_dir: cache_dir, development_mode: development_mode, load_path_cache: !ENV['DISABLE_BOOTSNAP_LOAD_PATH_CACHE'], compile_cache_iseq: !ENV['DISABLE_BOOTSNAP_COMPILE_CACHE'] && iseq_cache_supported?, compile_cache_yaml: !ENV['DISABLE_BOOTSNAP_COMPILE_CACHE'], compile_cache_json: !ENV['DISABLE_BOOTSNAP_COMPILE_CACHE'], ) if ENV['BOOTSNAP_LOG'] log! end end end end bootsnap-1.9.3/lib/bootsnap/000077500000000000000000000000001414746377100157625ustar00rootroot00000000000000bootsnap-1.9.3/lib/bootsnap/bundler.rb000066400000000000000000000004321414746377100177410ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap extend(self) def bundler? return false unless defined?(::Bundler) # Bundler environment variable %w(BUNDLE_BIN_PATH BUNDLE_GEMFILE).each do |current| return true if ENV.key?(current) end false end end bootsnap-1.9.3/lib/bootsnap/cli.rb000066400000000000000000000175061414746377100170670ustar00rootroot00000000000000# frozen_string_literal: true require 'bootsnap' require 'bootsnap/cli/worker_pool' require 'optparse' require 'fileutils' require 'etc' module Bootsnap class CLI unless Regexp.method_defined?(:match?) module RegexpMatchBackport refine Regexp do def match?(string) !!match(string) end end end using RegexpMatchBackport end attr_reader :cache_dir, :argv attr_accessor :compile_gemfile, :exclude, :verbose, :iseq, :yaml, :json, :jobs def initialize(argv) @argv = argv self.cache_dir = ENV.fetch('BOOTSNAP_CACHE_DIR', 'tmp/cache') self.compile_gemfile = false self.exclude = nil self.verbose = false self.jobs = Etc.nprocessors self.iseq = true self.yaml = true self.json = true end def precompile_command(*sources) require 'bootsnap/compile_cache/iseq' require 'bootsnap/compile_cache/yaml' require 'bootsnap/compile_cache/json' fix_default_encoding do Bootsnap::CompileCache::ISeq.cache_dir = self.cache_dir Bootsnap::CompileCache::YAML.init! Bootsnap::CompileCache::YAML.cache_dir = self.cache_dir Bootsnap::CompileCache::JSON.init! Bootsnap::CompileCache::JSON.cache_dir = self.cache_dir @work_pool = WorkerPool.create(size: jobs, jobs: { ruby: method(:precompile_ruby), yaml: method(:precompile_yaml), json: method(:precompile_json), }) @work_pool.spawn main_sources = sources.map { |d| File.expand_path(d) } precompile_ruby_files(main_sources) precompile_yaml_files(main_sources) precompile_json_files(main_sources) if compile_gemfile # Some gems embed their tests, they're very unlikely to be loaded, so not worth precompiling. gem_exclude = Regexp.union([exclude, '/spec/', '/test/'].compact) precompile_ruby_files($LOAD_PATH.map { |d| File.expand_path(d) }, exclude: gem_exclude) # Gems that include JSON or YAML files usually don't put them in `lib/`. # So we look at the gem root. gem_pattern = %r{^#{Regexp.escape(Bundler.bundle_path.to_s)}/?(?:bundler/)?gems\/[^/]+} gem_paths = $LOAD_PATH.map { |p| p[gem_pattern] }.compact.uniq precompile_yaml_files(gem_paths, exclude: gem_exclude) precompile_json_files(gem_paths, exclude: gem_exclude) end if exitstatus = @work_pool.shutdown exit(exitstatus) end end 0 end dir_sort = begin Dir[__FILE__, sort: false] true rescue ArgumentError, TypeError false end if dir_sort def list_files(path, pattern) if File.directory?(path) Dir[File.join(path, pattern), sort: false] elsif File.exist?(path) [path] else [] end end else def list_files(path, pattern) if File.directory?(path) Dir[File.join(path, pattern)] elsif File.exist?(path) [path] else [] end end end def run parser.parse!(argv) command = argv.shift method = "#{command}_command" if respond_to?(method) public_send(method, *argv) else invalid_usage!("Unknown command: #{command}") end end private def precompile_yaml_files(load_paths, exclude: self.exclude) return unless yaml load_paths.each do |path| if !exclude || !exclude.match?(path) list_files(path, '**/*.{yml,yaml}').each do |yaml_file| # We ignore hidden files to not match the various .ci.yml files if !File.basename(yaml_file).start_with?('.') && (!exclude || !exclude.match?(yaml_file)) @work_pool.push(:yaml, yaml_file) end end end end end def precompile_yaml(*yaml_files) Array(yaml_files).each do |yaml_file| if CompileCache::YAML.precompile(yaml_file, cache_dir: cache_dir) STDERR.puts(yaml_file) if verbose end end end def precompile_json_files(load_paths, exclude: self.exclude) return unless json load_paths.each do |path| if !exclude || !exclude.match?(path) list_files(path, '**/*.json').each do |json_file| # We ignore hidden files to not match the various .config.json files if !File.basename(json_file).start_with?('.') && (!exclude || !exclude.match?(json_file)) @work_pool.push(:json, json_file) end end end end end def precompile_json(*json_files) Array(json_files).each do |json_file| if CompileCache::JSON.precompile(json_file, cache_dir: cache_dir) STDERR.puts(json_file) if verbose end end end def precompile_ruby_files(load_paths, exclude: self.exclude) return unless iseq load_paths.each do |path| if !exclude || !exclude.match?(path) list_files(path, '**/*.rb').each do |ruby_file| if !exclude || !exclude.match?(ruby_file) @work_pool.push(:ruby, ruby_file) end end end end end def precompile_ruby(*ruby_files) Array(ruby_files).each do |ruby_file| if CompileCache::ISeq.precompile(ruby_file, cache_dir: cache_dir) STDERR.puts(ruby_file) if verbose end end end def fix_default_encoding if Encoding.default_external == Encoding::US_ASCII Encoding.default_external = Encoding::UTF_8 begin yield ensure Encoding.default_external = Encoding::US_ASCII end else yield end end def invalid_usage!(message) STDERR.puts message STDERR.puts STDERR.puts parser 1 end def cache_dir=(dir) @cache_dir = File.expand_path(File.join(dir, 'bootsnap/compile-cache')) end def exclude_pattern(pattern) (@exclude_patterns ||= []) << Regexp.new(pattern) self.exclude = Regexp.union(@exclude_patterns) end def parser @parser ||= OptionParser.new do |opts| opts.banner = "Usage: bootsnap COMMAND [ARGS]" opts.separator "" opts.separator "GLOBAL OPTIONS" opts.separator "" help = <<~EOS Path to the bootsnap cache directory. Defaults to tmp/cache EOS opts.on('--cache-dir DIR', help.strip) do |dir| self.cache_dir = dir end help = <<~EOS Print precompiled paths. EOS opts.on('--verbose', '-v', help.strip) do self.verbose = true end help = <<~EOS Number of workers to use. Default to number of processors, set to 0 to disable multi-processing. EOS opts.on('--jobs JOBS', '-j', help.strip) do |jobs| self.jobs = Integer(jobs) end opts.separator "" opts.separator "COMMANDS" opts.separator "" opts.separator " precompile [DIRECTORIES...]: Precompile all .rb files in the passed directories" help = <<~EOS Precompile the gems in Gemfile EOS opts.on('--gemfile', help) { self.compile_gemfile = true } help = <<~EOS Path pattern to not precompile. e.g. --exclude 'aws-sdk|google-api' EOS opts.on('--exclude PATTERN', help) { |pattern| exclude_pattern(pattern) } help = <<~EOS Disable ISeq (.rb) precompilation. EOS opts.on('--no-iseq', help) { self.iseq = false } help = <<~EOS Disable YAML precompilation. EOS opts.on('--no-yaml', help) { self.yaml = false } help = <<~EOS Disable JSON precompilation. EOS opts.on('--no-json', help) { self.json = false } end end end end bootsnap-1.9.3/lib/bootsnap/cli/000077500000000000000000000000001414746377100165315ustar00rootroot00000000000000bootsnap-1.9.3/lib/bootsnap/cli/worker_pool.rb000066400000000000000000000056041414746377100214250ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap class CLI class WorkerPool class << self def create(size:, jobs:) if size > 0 && Process.respond_to?(:fork) new(size: size, jobs: jobs) else Inline.new(jobs: jobs) end end end class Inline def initialize(jobs: {}) @jobs = jobs end def push(job, *args) @jobs.fetch(job).call(*args) nil end def spawn # noop end def shutdown # noop end end class Worker attr_reader :to_io, :pid def initialize(jobs) @jobs = jobs @pipe_out, @to_io = IO.pipe(binmode: true) # Set the writer encoding to binary since IO.pipe only sets it for the reader. # https://github.com/rails/rails/issues/16514#issuecomment-52313290 @to_io.set_encoding(Encoding::BINARY) @pid = nil end def write(message, block: true) payload = Marshal.dump(message) if block to_io.write(payload) true else to_io.write_nonblock(payload, exception: false) != :wait_writable end end def close to_io.close end def work_loop loop do job, *args = Marshal.load(@pipe_out) return if job == :exit @jobs.fetch(job).call(*args) end rescue IOError nil end def spawn @pid = Process.fork do to_io.close work_loop exit!(0) end @pipe_out.close true end end def initialize(size:, jobs: {}) @size = size @jobs = jobs @queue = Queue.new @pids = [] end def spawn @workers = @size.times.map { Worker.new(@jobs) } @workers.each(&:spawn) @dispatcher_thread = Thread.new { dispatch_loop } @dispatcher_thread.abort_on_exception = true true end def dispatch_loop loop do case job = @queue.pop when nil @workers.each do |worker| worker.write([:exit]) worker.close end return true else unless @workers.sample.write(job, block: false) free_worker.write(job) end end end end def free_worker IO.select(nil, @workers)[1].sample end def push(*args) @queue.push(args) nil end def shutdown @queue.close @dispatcher_thread.join @workers.each do |worker| _pid, status = Process.wait2(worker.pid) return status.exitstatus unless status.success? end nil end end end end bootsnap-1.9.3/lib/bootsnap/compile_cache.rb000066400000000000000000000032431414746377100210640ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module CompileCache Error = Class.new(StandardError) PermissionError = Class.new(Error) def self.setup(cache_dir:, iseq:, yaml:, json:) if iseq if supported? require_relative('compile_cache/iseq') Bootsnap::CompileCache::ISeq.install!(cache_dir) elsif $VERBOSE warn("[bootsnap/setup] bytecode caching is not supported on this implementation of Ruby") end end if yaml if supported? require_relative('compile_cache/yaml') Bootsnap::CompileCache::YAML.install!(cache_dir) elsif $VERBOSE warn("[bootsnap/setup] YAML parsing caching is not supported on this implementation of Ruby") end end if json if supported? require_relative('compile_cache/json') Bootsnap::CompileCache::JSON.install!(cache_dir) elsif $VERBOSE warn("[bootsnap/setup] JSON parsing caching is not supported on this implementation of Ruby") end end end def self.permission_error(path) cpath = Bootsnap::CompileCache::ISeq.cache_dir raise( PermissionError, "bootsnap doesn't have permission to write cache entries in '#{cpath}' " \ "(or, less likely, doesn't have permission to read '#{path}')", ) end def self.supported? # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), Windows (RubyInstaller2) and >= 2.3.0 RUBY_ENGINE == 'ruby' && RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/ && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0") end end end bootsnap-1.9.3/lib/bootsnap/compile_cache/000077500000000000000000000000001414746377100205355ustar00rootroot00000000000000bootsnap-1.9.3/lib/bootsnap/compile_cache/iseq.rb000066400000000000000000000061721414746377100220310ustar00rootroot00000000000000# frozen_string_literal: true require('bootsnap/bootsnap') require('zlib') module Bootsnap module CompileCache module ISeq class << self attr_accessor(:cache_dir) end has_ruby_bug_18250 = begin # https://bugs.ruby-lang.org/issues/18250 if defined? RubyVM::InstructionSequence RubyVM::InstructionSequence.compile("def foo(*); ->{ super }; end; def foo(**); ->{ super }; end").to_binary end false rescue TypeError true end if has_ruby_bug_18250 def self.input_to_storage(_, path) iseq = begin RubyVM::InstructionSequence.compile_file(path) rescue SyntaxError raise(Uncompilable, 'syntax error') end begin iseq.to_binary rescue TypeError raise(Uncompilable, 'ruby bug #18250') end end else def self.input_to_storage(_, path) RubyVM::InstructionSequence.compile_file(path).to_binary rescue SyntaxError raise(Uncompilable, 'syntax error') end end def self.storage_to_output(binary, _args) RubyVM::InstructionSequence.load_from_binary(binary) rescue RuntimeError => e if e.message == 'broken binary format' STDERR.puts("[Bootsnap::CompileCache] warning: rejecting broken binary") nil else raise end end def self.fetch(path, cache_dir: ISeq.cache_dir) Bootsnap::CompileCache::Native.fetch( cache_dir, path.to_s, Bootsnap::CompileCache::ISeq, nil, ) end def self.precompile(path, cache_dir: ISeq.cache_dir) Bootsnap::CompileCache::Native.precompile( cache_dir, path.to_s, Bootsnap::CompileCache::ISeq, ) end def self.input_to_output(_data, _kwargs) nil # ruby handles this end module InstructionSequenceMixin def load_iseq(path) # Having coverage enabled prevents iseq dumping/loading. return nil if defined?(Coverage) && Bootsnap::CompileCache::Native.coverage_running? Bootsnap::CompileCache::ISeq.fetch(path.to_s) rescue Errno::EACCES Bootsnap::CompileCache.permission_error(path) rescue RuntimeError => e if e.message =~ /unmatched platform/ puts("unmatched platform for file #{path}") end raise end def compile_option=(hash) super(hash) Bootsnap::CompileCache::ISeq.compile_option_updated end end def self.compile_option_updated option = RubyVM::InstructionSequence.compile_option crc = Zlib.crc32(option.inspect) Bootsnap::CompileCache::Native.compile_option_crc32 = crc end compile_option_updated def self.install!(cache_dir) Bootsnap::CompileCache::ISeq.cache_dir = cache_dir Bootsnap::CompileCache::ISeq.compile_option_updated class << RubyVM::InstructionSequence prepend(InstructionSequenceMixin) end end end end end bootsnap-1.9.3/lib/bootsnap/compile_cache/json.rb000066400000000000000000000042411414746377100220340ustar00rootroot00000000000000# frozen_string_literal: true require('bootsnap/bootsnap') module Bootsnap module CompileCache module JSON class << self attr_accessor(:msgpack_factory, :cache_dir, :supported_options) def input_to_storage(payload, _) obj = ::JSON.parse(payload) msgpack_factory.dump(obj) end def storage_to_output(data, kwargs) if kwargs && kwargs.key?(:symbolize_names) kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names) end msgpack_factory.load(data, kwargs) end def input_to_output(data, kwargs) ::JSON.parse(data, **(kwargs || {})) end def precompile(path, cache_dir: self.cache_dir) Bootsnap::CompileCache::Native.precompile( cache_dir, path.to_s, self, ) end def install!(cache_dir) self.cache_dir = cache_dir init! if ::JSON.respond_to?(:load_file) ::JSON.singleton_class.prepend(Patch) end end def init! require('json') require('msgpack') self.msgpack_factory = MessagePack::Factory.new self.supported_options = [:symbolize_names] if ::JSON.parse('["foo"]', freeze: true).first.frozen? self.supported_options = [:freeze] end self.supported_options.freeze end end module Patch def load_file(path, *args) return super if args.size > 1 if kwargs = args.first return super unless kwargs.is_a?(Hash) return super unless (kwargs.keys - ::Bootsnap::CompileCache::JSON.supported_options).empty? end begin ::Bootsnap::CompileCache::Native.fetch( Bootsnap::CompileCache::JSON.cache_dir, File.realpath(path), ::Bootsnap::CompileCache::JSON, kwargs, ) rescue Errno::EACCES ::Bootsnap::CompileCache.permission_error(path) end end ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true) end end end end bootsnap-1.9.3/lib/bootsnap/compile_cache/yaml.rb000066400000000000000000000121401414746377100220220ustar00rootroot00000000000000# frozen_string_literal: true require('bootsnap/bootsnap') module Bootsnap module CompileCache module YAML class << self attr_accessor(:msgpack_factory, :cache_dir, :supported_options) def input_to_storage(contents, _) obj = strict_load(contents) msgpack_factory.dump(obj) rescue NoMethodError, RangeError # The object included things that we can't serialize raise(Uncompilable) end def storage_to_output(data, kwargs) if kwargs && kwargs.key?(:symbolize_names) kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names) end msgpack_factory.load(data, kwargs) end def input_to_output(data, kwargs) if ::YAML.respond_to?(:unsafe_load) ::YAML.unsafe_load(data, **(kwargs || {})) else ::YAML.load(data, **(kwargs || {})) end end def strict_load(payload, *args) ast = ::YAML.parse(payload) return ast unless ast strict_visitor.create(*args).visit(ast) end ruby2_keywords :strict_load if respond_to?(:ruby2_keywords, true) def precompile(path, cache_dir: YAML.cache_dir) Bootsnap::CompileCache::Native.precompile( cache_dir, path.to_s, Bootsnap::CompileCache::YAML, ) end def install!(cache_dir) self.cache_dir = cache_dir init! ::YAML.singleton_class.prepend(Patch) end def init! require('yaml') require('msgpack') require('date') if Patch.method_defined?(:unsafe_load_file) && !::YAML.respond_to?(:unsafe_load_file) Patch.send(:remove_method, :unsafe_load_file) end if Patch.method_defined?(:load_file) && ::YAML::VERSION >= '4' Patch.send(:remove_method, :load_file) end # MessagePack serializes symbols as strings by default. # We want them to roundtrip cleanly, so we use a custom factory. # see: https://github.com/msgpack/msgpack-ruby/pull/122 factory = MessagePack::Factory.new factory.register_type(0x00, Symbol) if defined? MessagePack::Timestamp factory.register_type( MessagePack::Timestamp::TYPE, # or just -1 Time, packer: MessagePack::Time::Packer, unpacker: MessagePack::Time::Unpacker ) marshal_fallback = { packer: ->(value) { Marshal.dump(value) }, unpacker: ->(payload) { Marshal.load(payload) }, } { Date => 0x01, Regexp => 0x02, }.each do |type, code| factory.register_type(code, type, marshal_fallback) end end self.msgpack_factory = factory self.supported_options = [] params = ::YAML.method(:load).parameters if params.include?([:key, :symbolize_names]) self.supported_options << :symbolize_names end if params.include?([:key, :freeze]) if factory.load(factory.dump('yaml'), freeze: true).frozen? self.supported_options << :freeze end end self.supported_options.freeze end def strict_visitor self::NoTagsVisitor ||= Class.new(Psych::Visitors::ToRuby) do def visit(target) if target.tag raise Uncompilable, "YAML tags are not supported: #{target.tag}" end super end end end end module Patch def load_file(path, *args) return super if args.size > 1 if kwargs = args.first return super unless kwargs.is_a?(Hash) return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty? end begin ::Bootsnap::CompileCache::Native.fetch( Bootsnap::CompileCache::YAML.cache_dir, File.realpath(path), ::Bootsnap::CompileCache::YAML, kwargs, ) rescue Errno::EACCES ::Bootsnap::CompileCache.permission_error(path) end end ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true) def unsafe_load_file(path, *args) return super if args.size > 1 if kwargs = args.first return super unless kwargs.is_a?(Hash) return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty? end begin ::Bootsnap::CompileCache::Native.fetch( Bootsnap::CompileCache::YAML.cache_dir, File.realpath(path), ::Bootsnap::CompileCache::YAML, kwargs, ) rescue Errno::EACCES ::Bootsnap::CompileCache.permission_error(path) end end ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true) end end end end bootsnap-1.9.3/lib/bootsnap/explicit_require.rb000066400000000000000000000027141414746377100216700ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module ExplicitRequire ARCHDIR = RbConfig::CONFIG['archdir'] RUBYLIBDIR = RbConfig::CONFIG['rubylibdir'] DLEXT = RbConfig::CONFIG['DLEXT'] def self.from_self(feature) require_relative("../#{feature}") end def self.from_rubylibdir(feature) require(File.join(RUBYLIBDIR, "#{feature}.rb")) end def self.from_archdir(feature) require(File.join(ARCHDIR, "#{feature}.#{DLEXT}")) end # Given a set of gems, run a block with the LOAD_PATH narrowed to include # only core ruby source paths and these gems -- that is, roughly, # temporarily remove all gems not listed in this call from the LOAD_PATH. # # This is useful before bootsnap is fully-initialized to load gems that it # depends on, without forcing full LOAD_PATH traversals. def self.with_gems(*gems) orig = $LOAD_PATH.dup $LOAD_PATH.clear gems.each do |gem| pat = %r{ / (gems|extensions/[^/]+/[^/]+) # "gems" or "extensions/x64_64-darwin16/2.3.0" / #{Regexp.escape(gem)}-(\h{12}|(\d+\.)) # msgpack-1.2.3 or msgpack-1234567890ab }x $LOAD_PATH.concat(orig.grep(pat)) end $LOAD_PATH << ARCHDIR $LOAD_PATH << RUBYLIBDIR begin yield rescue LoadError $LOAD_PATH.replace(orig) yield end ensure $LOAD_PATH.replace(orig) end end end bootsnap-1.9.3/lib/bootsnap/load_path_cache.rb000066400000000000000000000041471414746377100213730ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module LoadPathCache ReturnFalse = Class.new(StandardError) FallbackScan = Class.new(StandardError) DOT_RB = '.rb' DOT_SO = '.so' SLASH = '/' # If a NameError happens several levels deep, don't re-handle it # all the way up the chain: mark it once and bubble it up without # more retries. ERROR_TAG_IVAR = :@__bootsnap_rescued DL_EXTENSIONS = ::RbConfig::CONFIG .values_at('DLEXT', 'DLEXT2') .reject { |ext| !ext || ext.empty? } .map { |ext| ".#{ext}" } .freeze DLEXT = DL_EXTENSIONS[0] # This is nil on linux and darwin, but I think it's '.o' on some other # platform. I'm not really sure which, but it seems better to replicate # ruby's semantics as faithfully as possible. DLEXT2 = DL_EXTENSIONS[1] CACHED_EXTENSIONS = DLEXT2 ? [DOT_RB, DLEXT, DLEXT2] : [DOT_RB, DLEXT] class << self attr_reader(:load_path_cache, :loaded_features_index, :realpath_cache) def setup(cache_path:, development_mode:) unless supported? warn("[bootsnap/setup] Load path caching is not supported on this implementation of Ruby") if $VERBOSE return end store = Store.new(cache_path) @loaded_features_index = LoadedFeaturesIndex.new @realpath_cache = RealpathCache.new @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode) require_relative('load_path_cache/core_ext/kernel_require') require_relative('load_path_cache/core_ext/loaded_features') end def supported? RUBY_ENGINE == 'ruby' && RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/ end end end end if Bootsnap::LoadPathCache.supported? require_relative('load_path_cache/path_scanner') require_relative('load_path_cache/path') require_relative('load_path_cache/cache') require_relative('load_path_cache/store') require_relative('load_path_cache/change_observer') require_relative('load_path_cache/loaded_features_index') require_relative('load_path_cache/realpath_cache') end bootsnap-1.9.3/lib/bootsnap/load_path_cache/000077500000000000000000000000001414746377100210405ustar00rootroot00000000000000bootsnap-1.9.3/lib/bootsnap/load_path_cache/cache.rb000066400000000000000000000170471414746377100224410ustar00rootroot00000000000000# frozen_string_literal: true require_relative('../explicit_require') module Bootsnap module LoadPathCache class Cache AGE_THRESHOLD = 30 # seconds def initialize(store, path_obj, development_mode: false) @development_mode = development_mode @store = store @mutex = Mutex.new @path_obj = path_obj.map! { |f| PathScanner.os_path(File.exist?(f) ? File.realpath(f) : f.dup) } @has_relative_paths = nil reinitialize end # What is the path item that contains the dir as child? # e.g. given "/a/b/c/d" exists, and the path is ["/a/b"], load_dir("c/d") # is "/a/b". def load_dir(dir) reinitialize if stale? @mutex.synchronize { @dirs[dir] } end # { 'enumerator' => nil, 'enumerator.so' => nil, ... } BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features| # Builtin features are of the form 'enumerator.so'. # All others include paths. next unless feat.size < 20 && !feat.include?('/') base = File.basename(feat, '.*') # enumerator.so -> enumerator ext = File.extname(feat) # .so features[feat] = nil # enumerator.so features[base] = nil # enumerator next unless [DOT_SO, *DL_EXTENSIONS].include?(ext) DL_EXTENSIONS.each do |dl_ext| features["#{base}#{dl_ext}"] = nil # enumerator.bundle end end.freeze # Try to resolve this feature to an absolute path without traversing the # loadpath. def find(feature, try_extensions: true) reinitialize if (@has_relative_paths && dir_changed?) || stale? feature = feature.to_s.freeze return feature if absolute_path?(feature) if feature.start_with?('./', '../') return try_extensions ? expand_path(feature) : File.expand_path(feature).freeze end @mutex.synchronize do x = search_index(feature, try_extensions: try_extensions) return x if x return unless try_extensions # Ruby has some built-in features that require lies about. # For example, 'enumerator' is built in. If you require it, ruby # returns false as if it were already loaded; however, there is no # file to find on disk. We've pre-built a list of these, and we # return false if any of them is loaded. raise(LoadPathCache::ReturnFalse, '', []) if BUILTIN_FEATURES.key?(feature) # The feature wasn't found on our preliminary search through the index. # We resolve this differently depending on what the extension was. case File.extname(feature) # If the extension was one of the ones we explicitly cache (.rb and the # native dynamic extension, e.g. .bundle or .so), we know it was a # failure and there's nothing more we can do to find the file. # no extension, .rb, (.bundle or .so) when '', *CACHED_EXTENSIONS nil # Ruby allows specifying native extensions as '.so' even when DLEXT # is '.bundle'. This is where we handle that case. when DOT_SO x = search_index(feature[0..-4] + DLEXT) return x if x if DLEXT2 x = search_index(feature[0..-4] + DLEXT2) return x if x end else # other, unknown extension. For example, `.rake`. Since we haven't # cached these, we legitimately need to run the load path search. raise(LoadPathCache::FallbackScan, '', []) end end # In development mode, we don't want to confidently return failures for # cases where the file doesn't appear to be on the load path. We should # be able to detect newly-created files without rebooting the # application. raise(LoadPathCache::FallbackScan, '', []) if @development_mode end if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ def absolute_path?(path) path[1] == ':' end else def absolute_path?(path) path.start_with?(SLASH) end end def unshift_paths(sender, *paths) return unless sender == @path_obj @mutex.synchronize { unshift_paths_locked(*paths) } end def push_paths(sender, *paths) return unless sender == @path_obj @mutex.synchronize { push_paths_locked(*paths) } end def reinitialize(path_obj = @path_obj) @mutex.synchronize do @path_obj = path_obj ChangeObserver.register(self, @path_obj) @index = {} @dirs = {} @generated_at = now push_paths_locked(*@path_obj) end end private def dir_changed? @prev_dir ||= Dir.pwd if @prev_dir == Dir.pwd false else @prev_dir = Dir.pwd true end end def push_paths_locked(*paths) @store.transaction do paths.map(&:to_s).each do |path| p = Path.new(path) @has_relative_paths = true if p.relative? next if p.non_directory? expanded_path = p.expanded_path entries, dirs = p.entries_and_dirs(@store) # push -> low precedence -> set only if unset dirs.each { |dir| @dirs[dir] ||= path } entries.each { |rel| @index[rel] ||= expanded_path } end end end def unshift_paths_locked(*paths) @store.transaction do paths.map(&:to_s).reverse_each do |path| p = Path.new(path) next if p.non_directory? expanded_path = p.expanded_path entries, dirs = p.entries_and_dirs(@store) # unshift -> high precedence -> unconditional set dirs.each { |dir| @dirs[dir] = path } entries.each { |rel| @index[rel] = expanded_path } end end end def expand_path(feature) maybe_append_extension(File.expand_path(feature)) end def stale? @development_mode && @generated_at + AGE_THRESHOLD < now end def now Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i end if DLEXT2 def search_index(f, try_extensions: true) if try_extensions try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f) else try_index(f) end end def maybe_append_extension(f) try_ext(f + DOT_RB) || try_ext(f + DLEXT) || try_ext(f + DLEXT2) || f end else def search_index(f, try_extensions: true) if try_extensions try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f) else try_index(f) end end def maybe_append_extension(f) try_ext(f + DOT_RB) || try_ext(f + DLEXT) || f end end s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze if s.respond_to?(:-@) if (-s).equal?(s) && (-s.dup).equal?(s) || RUBY_VERSION >= '2.7' def try_index(f) if (p = @index[f]) -(File.join(p, f).freeze) end end else def try_index(f) if (p = @index[f]) -File.join(p, f).untaint end end end else def try_index(f) if (p = @index[f]) File.join(p, f) end end end def try_ext(f) f if File.exist?(f) end end end end bootsnap-1.9.3/lib/bootsnap/load_path_cache/change_observer.rb000066400000000000000000000042571414746377100245310ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module LoadPathCache module ChangeObserver module ArrayMixin # For each method that adds items to one end or another of the array # (<<, push, unshift, concat), override that method to also notify the # observer of the change. def <<(entry) @lpc_observer.push_paths(self, entry.to_s) super end def push(*entries) @lpc_observer.push_paths(self, *entries.map(&:to_s)) super end alias_method :append, :push def unshift(*entries) @lpc_observer.unshift_paths(self, *entries.map(&:to_s)) super end alias_method :prepend, :unshift def concat(entries) @lpc_observer.push_paths(self, *entries.map(&:to_s)) super end # uniq! keeps the first occurrence of each path, otherwise preserving # order, preserving the effective load path def uniq!(*args) ret = super @lpc_observer.reinitialize if block_given? || !args.empty? ret end # For each method that modifies the array more aggressively, override # the method to also have the observer completely reconstruct its state # after the modification. Many of these could be made to modify the # internal state of the LoadPathCache::Cache more efficiently, but the # accounting cost would be greater than the hit from these, since we # actively discourage calling them. %i( []= clear collect! compact! delete delete_at delete_if fill flatten! insert keep_if map! pop reject! replace reverse! rotate! select! shift shuffle! slice! sort! sort_by! ).each do |method_name| define_method(method_name) do |*args, &block| ret = super(*args, &block) @lpc_observer.reinitialize ret end end end def self.register(observer, arr) return if arr.frozen? # can't register observer, but no need to. arr.instance_variable_set(:@lpc_observer, observer) arr.extend(ArrayMixin) end end end end bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/000077500000000000000000000000001414746377100226505ustar00rootroot00000000000000bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb000066400000000000000000000054311414746377100262140ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module LoadPathCache module CoreExt def self.make_load_error(path) err = LoadError.new(+"cannot load such file -- #{path}") err.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true) err.define_singleton_method(:path) { path } err end end end end module Kernel module_function # rubocop:disable Style/ModuleFunction alias_method(:require_without_bootsnap, :require) # Note that require registers to $LOADED_FEATURES while load does not. def require_with_bootsnap_lfi(path, resolved = nil) Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do require_without_bootsnap(resolved || path) end end def require(path) return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path) if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)) return require_with_bootsnap_lfi(path, resolved) end raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path)) rescue LoadError => e e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true) raise(e) rescue Bootsnap::LoadPathCache::ReturnFalse false rescue Bootsnap::LoadPathCache::FallbackScan fallback = true ensure if fallback require_with_bootsnap_lfi(path) end end alias_method(:require_relative_without_bootsnap, :require_relative) def require_relative(path) location = caller_locations(1..1).first realpath = Bootsnap::LoadPathCache.realpath_cache.call( location.absolute_path || location.path, path ) require(realpath) end alias_method(:load_without_bootsnap, :load) def load(path, wrap = false) if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path, try_extensions: false)) load_without_bootsnap(resolved, wrap) else load_without_bootsnap(path, wrap) end end end class Module alias_method(:autoload_without_bootsnap, :autoload) def autoload(const, path) # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately # obvious how to make it work. This feels like a pretty niche case, unclear # if it will ever burn anyone. # # The challenge is that we don't control the point at which the entry gets # added to $LOADED_FEATURES and won't be able to hook that modification # since it's done in C-land. autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path) rescue LoadError => e e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true) raise(e) rescue Bootsnap::LoadPathCache::ReturnFalse false rescue Bootsnap::LoadPathCache::FallbackScan fallback = true ensure if fallback autoload_without_bootsnap(const, path) end end end bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb000066400000000000000000000010061414746377100263200ustar00rootroot00000000000000# frozen_string_literal: true class << $LOADED_FEATURES alias_method(:delete_without_bootsnap, :delete) def delete(key) Bootsnap::LoadPathCache.loaded_features_index.purge(key) delete_without_bootsnap(key) end alias_method(:reject_without_bootsnap!, :reject!) def reject!(&block) backup = dup # FIXME: if no block is passed we'd need to return a decorated iterator reject_without_bootsnap!(&block) Bootsnap::LoadPathCache.loaded_features_index.purge_multi(backup - self) end end bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb000066400000000000000000000127371414746377100257140ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module LoadPathCache # LoadedFeaturesIndex partially mirrors an internal structure in ruby that # we can't easily obtain an interface to. # # This works around an issue where, without bootsnap, *ruby* knows that it # has already required a file by its short name (e.g. require 'bundler') if # a new instance of bundler is added to the $LOAD_PATH which resolves to a # different absolute path. This class makes bootsnap smart enough to # realize that it has already loaded 'bundler', and not just # '/path/to/bundler'. # # If you disable LoadedFeaturesIndex, you can see the problem this solves by: # # 1. `require 'a'` # 2. Prepend a new $LOAD_PATH element containing an `a.rb` # 3. `require 'a'` # # Ruby returns false from step 3. # With bootsnap but with no LoadedFeaturesIndex, this loads two different # `a.rb`s. # With bootsnap and with LoadedFeaturesIndex, this skips the second load, # returning false like ruby. class LoadedFeaturesIndex def initialize @lfi = {} @mutex = Mutex.new # In theory the user could mutate $LOADED_FEATURES and invalidate our # cache. If this ever comes up in practice — or if you, the # enterprising reader, feels inclined to solve this problem — we could # parallel the work done with ChangeObserver on $LOAD_PATH to mirror # updates to our @lfi. $LOADED_FEATURES.each do |feat| hash = feat.hash $LOAD_PATH.each do |lpe| next unless feat.start_with?(lpe) # /a/b/lib/my/foo.rb # ^^^^^^^^^ short = feat[(lpe.length + 1)..-1] stripped = strip_extension_if_elidable(short) @lfi[short] = hash @lfi[stripped] = hash end end end # We've optimized for initialize and register to be fast, and purge to be tolerable. # If access patterns make this not-okay, we can lazy-invert the LFI on # first purge and work from there. def purge(feature) @mutex.synchronize do feat_hash = feature.hash @lfi.reject! { |_, hash| hash == feat_hash } end end def purge_multi(features) rejected_hashes = features.each_with_object({}) { |f, h| h[f.hash] = true } @mutex.synchronize do @lfi.reject! { |_, hash| rejected_hashes.key?(hash) } end end def key?(feature) @mutex.synchronize { @lfi.key?(feature) } end # There is a relatively uncommon case where we could miss adding an # entry: # # If the user asked for e.g. `require 'bundler'`, and we went through the # `FallbackScan` pathway in `kernel_require.rb` and therefore did not # pass `long` (the full expanded absolute path), then we did are not able # to confidently add the `bundler.rb` form to @lfi. # # We could either: # # 1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but # not quite right; or # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching # entry. def register(short, long = nil) if long.nil? len = $LOADED_FEATURES.size ret = yield long = $LOADED_FEATURES[len..-1].detect do |feat| offset = 0 while offset = feat.index(short, offset) if feat.index(".", offset + 1) && !feat.index("/", offset + 2) break true else offset += 1 end end end else ret = yield end hash = long.hash # Do we have a filename with an elidable extension, e.g., # 'bundler.rb', or 'libgit2.so'? altname = if extension_elidable?(short) # Strip the extension off, e.g. 'bundler.rb' -> 'bundler'. strip_extension_if_elidable(short) elsif long && (ext = File.extname(long.freeze)) # We already know the extension of the actual file this # resolves to, so put that back on. short + ext end @mutex.synchronize do @lfi[short] = hash (@lfi[altname] = hash) if altname end ret end private STRIP_EXTENSION = /\.[^.]*?$/ private_constant(:STRIP_EXTENSION) # Might Ruby automatically search for this extension if # someone tries to 'require' the file without it? E.g. Ruby # will implicitly try 'x.rb' if you ask for 'x'. # # This is complex and platform-dependent, and the Ruby docs are a little # handwavy about what will be tried when and in what order. # So optimistically pretend that all known elidable extensions # will be tried on all platforms, and that people are unlikely # to name files in a way that assumes otherwise. # (E.g. It's unlikely that someone will know that their code # will _never_ run on MacOS, and therefore think they can get away # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.) # # See . def extension_elidable?(f) f.to_s.end_with?('.rb', '.so', '.o', '.dll', '.dylib') end def strip_extension_if_elidable(f) if extension_elidable?(f) f.sub(STRIP_EXTENSION, '') else f end end end end end bootsnap-1.9.3/lib/bootsnap/load_path_cache/path.rb000066400000000000000000000067661414746377100223400ustar00rootroot00000000000000# frozen_string_literal: true require_relative('path_scanner') module Bootsnap module LoadPathCache class Path # A path is considered 'stable' if it is part of a Gem.path or the ruby # distribution. When adding or removing files in these paths, the cache # must be cleared before the change will be noticed. def stable? stability == STABLE end # A path is considered volatile if it doesn't live under a Gem.path or # the ruby distribution root. These paths are scanned for new additions # more frequently. def volatile? stability == VOLATILE end attr_reader(:path) def initialize(path) @path = path.to_s.freeze end # True if the path exists, but represents a non-directory object def non_directory? !File.stat(path).directory? rescue Errno::ENOENT, Errno::ENOTDIR false end def relative? !path.start_with?(SLASH) end # Return a list of all the requirable files and all of the subdirectories # of this +Path+. def entries_and_dirs(store) if stable? # the cached_mtime field is unused for 'stable' paths, but is # set to zero anyway, just in case we change the stability heuristics. _, entries, dirs = store.get(expanded_path) return [entries, dirs] if entries # cache hit entries, dirs = scan! store.set(expanded_path, [0, entries, dirs]) return [entries, dirs] end cached_mtime, entries, dirs = store.get(expanded_path) current_mtime = latest_mtime(expanded_path, dirs || []) return [[], []] if current_mtime == -1 # path does not exist return [entries, dirs] if cached_mtime == current_mtime entries, dirs = scan! store.set(expanded_path, [current_mtime, entries, dirs]) [entries, dirs] end def expanded_path File.expand_path(path).freeze end private def scan! # (expensive) returns [entries, dirs] PathScanner.call(expanded_path) end # last time a directory was modified in this subtree. +dirs+ should be a # list of relative paths to directories under +path+. e.g. for /a/b and # /a/b/c, pass ('/a/b', ['c']) def latest_mtime(path, dirs) max = -1 ["", *dirs].each do |dir| curr = begin File.mtime("#{path}/#{dir}").to_i rescue Errno::ENOENT, Errno::ENOTDIR -1 end max = curr if curr > max end max end # a Path can be either stable of volatile, depending on how frequently we # expect its contents may change. Stable paths aren't rescanned nearly as # often. STABLE = :stable VOLATILE = :volatile # Built-in ruby lib stuff doesn't change, but things can occasionally be # installed into sitedir, which generally lives under libdir. RUBY_LIBDIR = RbConfig::CONFIG['libdir'] RUBY_SITEDIR = RbConfig::CONFIG['sitedir'] def stability @stability ||= begin if Gem.path.detect { |p| expanded_path.start_with?(p.to_s) } STABLE elsif Bootsnap.bundler? && expanded_path.start_with?(Bundler.bundle_path.to_s) STABLE elsif expanded_path.start_with?(RUBY_LIBDIR) && !expanded_path.start_with?(RUBY_SITEDIR) STABLE else VOLATILE end end end end end end bootsnap-1.9.3/lib/bootsnap/load_path_cache/path_scanner.rb000066400000000000000000000047151414746377100240410ustar00rootroot00000000000000# frozen_string_literal: true require_relative('../explicit_require') module Bootsnap module LoadPathCache module PathScanner REQUIRABLE_EXTENSIONS = [DOT_RB] + DL_EXTENSIONS NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO) ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/ BUNDLE_PATH = if Bootsnap.bundler? (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze else '' end class << self def call(path) path = File.expand_path(path.to_s).freeze return [[], []] unless File.directory?(path) # If the bundle path is a descendent of this path, we do additional # checks to prevent recursing into the bundle path as we recurse # through this path. We don't want to scan the bundle path because # anything useful in it will be present on other load path items. # # This can happen if, for example, the user adds '.' to the load path, # and the bundle path is '.bundle'. contains_bundle_path = BUNDLE_PATH.start_with?(path) dirs = [] requirables = [] walk(path, nil) do |relative_path, absolute_path, is_directory| if is_directory dirs << os_path(relative_path) !contains_bundle_path || !absolute_path.start_with?(BUNDLE_PATH) elsif relative_path.end_with?(*REQUIRABLE_EXTENSIONS) requirables << os_path(relative_path) end end [requirables, dirs] end def walk(absolute_dir_path, relative_dir_path, &block) Dir.foreach(absolute_dir_path) do |name| next if name.start_with?('.') relative_path = relative_dir_path ? File.join(relative_dir_path, name) : name absolute_path = "#{absolute_dir_path}/#{name}" if File.directory?(absolute_path) if yield relative_path, absolute_path, true walk(absolute_path, relative_path, &block) end else yield relative_path, absolute_path, false end end end if RUBY_VERSION >= '3.1' def os_path(path) path.freeze end else def os_path(path) path.force_encoding(Encoding::US_ASCII) if path.ascii_only? path.freeze end end end end end end bootsnap-1.9.3/lib/bootsnap/load_path_cache/realpath_cache.rb000066400000000000000000000013211414746377100243050ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module LoadPathCache class RealpathCache def initialize @cache = Hash.new { |h, k| h[k] = realpath(*k) } end def call(*key) @cache[key] end private def realpath(caller_location, path) base = File.dirname(caller_location) abspath = File.expand_path(path, base).freeze find_file(abspath) end def find_file(name) return File.realpath(name).freeze if File.exist?(name) CACHED_EXTENSIONS.each do |ext| filename = "#{name}#{ext}" return File.realpath(filename).freeze if File.exist?(filename) end name end end end end bootsnap-1.9.3/lib/bootsnap/load_path_cache/store.rb000066400000000000000000000051441414746377100225250ustar00rootroot00000000000000# frozen_string_literal: true require_relative('../explicit_require') Bootsnap::ExplicitRequire.with_gems('msgpack') { require('msgpack') } Bootsnap::ExplicitRequire.from_rubylibdir('fileutils') module Bootsnap module LoadPathCache class Store NestedTransactionError = Class.new(StandardError) SetOutsideTransactionNotAllowed = Class.new(StandardError) def initialize(store_path) @store_path = store_path @txn_mutex = Mutex.new @dirty = false load_data end def get(key) @data[key] end def fetch(key) raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned? v = get(key) unless v @dirty = true v = yield @data[key] = v end v end def set(key, value) raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned? if value != @data[key] @dirty = true @data[key] = value end end def transaction raise(NestedTransactionError) if @txn_mutex.owned? @txn_mutex.synchronize do begin yield ensure commit_transaction end end end private def commit_transaction if @dirty dump_data @dirty = false end end def load_data @data = begin File.open(@store_path, encoding: Encoding::BINARY) do |io| MessagePack.load(io) end # handle malformed data due to upgrade incompatibility rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError {} rescue ArgumentError => error if error.message =~ /negative array size/ {} else raise end end end def dump_data # Change contents atomically so other processes can't get invalid # caches if they read at an inopportune time. tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100000).to_i}.tmp" FileUtils.mkpath(File.dirname(tmp)) exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY # `encoding:` looks redundant wrt `binwrite`, but necessary on windows # because binary is part of mode. File.open(tmp, mode: exclusive_write, encoding: Encoding::BINARY) do |io| MessagePack.dump(@data, io, freeze: true) end FileUtils.mv(tmp, @store_path) rescue Errno::EEXIST retry rescue SystemCallError end end end end bootsnap-1.9.3/lib/bootsnap/setup.rb000066400000000000000000000001261414746377100174460ustar00rootroot00000000000000# frozen_string_literal: true require_relative('../bootsnap') Bootsnap.default_setup bootsnap-1.9.3/lib/bootsnap/version.rb000066400000000000000000000001061414746377100177710ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap VERSION = "1.9.3" end bootsnap-1.9.3/shipit.rubygems.yml000066400000000000000000000000001414746377100172340ustar00rootroot00000000000000bootsnap-1.9.3/test/000077500000000000000000000000001414746377100143465ustar00rootroot00000000000000bootsnap-1.9.3/test/bundler_test.rb000066400000000000000000000022341414746377100173660ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') class BundlerTest < Minitest::Test def test_bundler_with_bundle_bin_path_env without_required_env_keys do ENV['BUNDLE_BIN_PATH'] = 'foo' assert_predicate(Bootsnap, :bundler?) end end def test_bundler_with_bundle_gemfile_env without_required_env_keys do ENV['BUNDLE_GEMFILE'] = 'foo' assert_predicate(Bootsnap, :bundler?) end end def test_bundler_without_bundler_const without_bundler do refute_predicate(Bootsnap, :bundler?) end end def test_bundler_without_required_env_keys without_required_env_keys do assert(defined?(::Bundler)) refute_predicate(Bootsnap, :bundler?) end end private def without_bundler b = ::Bundler begin Object.send(:remove_const, :Bundler) yield ensure Object.send(:const_set, :Bundler, b) end end def without_required_env_keys original_env = {} begin %w(BUNDLE_BIN_PATH BUNDLE_GEMFILE).each do |k| original_env[k] = ENV[k] ENV[k] = nil end yield ensure original_env.each { |k, v| ENV[k] = v } end end end bootsnap-1.9.3/test/cli_test.rb000066400000000000000000000040571414746377100165070ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') require('bootsnap/cli') module Bootsnap class CLITest < Minitest::Test include(TmpdirHelper) def setup super @cache_dir = File.expand_path('tmp/cache/bootsnap/compile-cache') end def test_precompile_single_file path = Help.set_file('a.rb', 'a = a = 3', 100) CompileCache::ISeq.expects(:precompile).with(File.expand_path(path), cache_dir: @cache_dir) assert_equal 0, CLI.new(['precompile', '-j', '0', path]).run end def test_no_iseq path = Help.set_file('a.rb', 'a = a = 3', 100) CompileCache::ISeq.expects(:precompile).never assert_equal 0, CLI.new(['precompile', '-j', '0', '--no-iseq', path]).run end def test_precompile_directory path_a = Help.set_file('foo/a.rb', 'a = a = 3', 100) path_b = Help.set_file('foo/b.rb', 'b = b = 3', 100) CompileCache::ISeq.expects(:precompile).with(File.expand_path(path_a), cache_dir: @cache_dir) CompileCache::ISeq.expects(:precompile).with(File.expand_path(path_b), cache_dir: @cache_dir) assert_equal 0, CLI.new(['precompile', '-j', '0', 'foo']).run end def test_precompile_exclude path_a = Help.set_file('foo/a.rb', 'a = a = 3', 100) Help.set_file('foo/b.rb', 'b = b = 3', 100) CompileCache::ISeq.expects(:precompile).with(File.expand_path(path_a), cache_dir: @cache_dir) assert_equal 0, CLI.new(['precompile', '-j', '0', '--exclude', 'b.rb', 'foo']).run end def test_precompile_gemfile assert_equal 0, CLI.new(['precompile', '--gemfile']).run end def test_precompile_yaml path = Help.set_file('a.yaml', 'foo: bar', 100) CompileCache::YAML.expects(:precompile).with(File.expand_path(path), cache_dir: @cache_dir) assert_equal 0, CLI.new(['precompile', '-j', '0', path]).run end def test_no_yaml path = Help.set_file('a.yaml', 'foo: bar', 100) CompileCache::YAML.expects(:precompile).never assert_equal 0, CLI.new(['precompile', '-j', '0', '--no-yaml', path]).run end end end bootsnap-1.9.3/test/compile_cache/000077500000000000000000000000001414746377100171215ustar00rootroot00000000000000bootsnap-1.9.3/test/compile_cache/iseq_cache_test.rb000066400000000000000000000004541414746377100225740ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') class CompileCacheISeqTest < Minitest::Test include(TmpdirHelper) def test_ruby_bug_18250 Help.set_file('a.rb', 'def foo(*); ->{ super }; end; def foo(**); ->{ super }; end', 100) Bootsnap::CompileCache::ISeq.fetch('a.rb') end end bootsnap-1.9.3/test/compile_cache/json_test.rb000066400000000000000000000040721414746377100214610ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') class CompileCacheJSONTest < Minitest::Test include(TmpdirHelper) module FakeJson Fallback = Class.new(StandardError) class << self def load_file(path, symbolize_names: false, freeze: false, fallback: nil) raise Fallback end end end def setup super Bootsnap::CompileCache::JSON.init! FakeJson.singleton_class.prepend(Bootsnap::CompileCache::JSON::Patch) end def test_json_input_to_output document = ::Bootsnap::CompileCache::JSON.input_to_output(<<~JSON, {}) { "foo": 42, "bar": [1] } JSON expected = { 'foo' => 42, 'bar' => [1], } assert_equal expected, document end def test_load_file Help.set_file('a.json', '{"foo": "bar"}', 100) assert_equal({'foo' => 'bar'}, FakeJson.load_file('a.json')) end def test_load_file_symbolize_names Help.set_file('a.json', '{"foo": "bar"}', 100) FakeJson.load_file('a.json') if ::Bootsnap::CompileCache::JSON.supported_options.include?(:symbolize_names) 2.times do assert_equal({foo: 'bar'}, FakeJson.load_file('a.json', symbolize_names: true)) end else assert_raises(FakeJson::Fallback) do # would call super FakeJson.load_file('a.json', symbolize_names: true) end end end def test_load_file_freeze Help.set_file('a.json', '["foo"]', 100) FakeJson.load_file('a.json') if ::Bootsnap::CompileCache::JSON.supported_options.include?(:freeze) 2.times do string = FakeJson.load_file('a.json', freeze: true).first assert_equal("foo", string) assert_predicate(string, :frozen?) end else assert_raises(FakeJson::Fallback) do # would call super FakeJson.load_file('a.json', freeze: true) end end end def test_load_file_unknown_option Help.set_file('a.json', '["foo"]', 100) FakeJson.load_file('a.json') assert_raises(FakeJson::Fallback) do # would call super FakeJson.load_file('a.json', fallback: true) end end end bootsnap-1.9.3/test/compile_cache/yaml_test.rb000066400000000000000000000073421414746377100214550ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') class CompileCacheYAMLTest < Minitest::Test include(TmpdirHelper) module FakeYaml Fallback = Class.new(StandardError) class << self def load_file(path, symbolize_names: false, freeze: false, fallback: nil) raise Fallback end def unsafe_load_file(path, symbolize_names: false, freeze: false, fallback: nil) raise Fallback end end end def setup super Bootsnap::CompileCache::YAML.init! FakeYaml.singleton_class.prepend(Bootsnap::CompileCache::YAML::Patch) end def test_yaml_strict_load document = ::Bootsnap::CompileCache::YAML.strict_load(<<~YAML) --- :foo: 42 bar: [1] YAML expected = { foo: 42, 'bar' => [1], } assert_equal expected, document end def test_yaml_input_to_output document = ::Bootsnap::CompileCache::YAML.input_to_output(<<~YAML, {}) --- :foo: 42 bar: [1] YAML expected = { foo: 42, 'bar' => [1], } assert_equal expected, document end def test_yaml_tags error = assert_raises Bootsnap::CompileCache::Uncompilable do ::Bootsnap::CompileCache::YAML.strict_load('!many Boolean') end assert_equal "YAML tags are not supported: !many", error.message error = assert_raises Bootsnap::CompileCache::Uncompilable do ::Bootsnap::CompileCache::YAML.strict_load('!ruby/object {}') end assert_equal "YAML tags are not supported: !ruby/object", error.message end if YAML::VERSION >= '4' def test_load_psych_4 # Until we figure out a proper strategy, only `YAML.unsafe_load_file` # is cached with Psych >= 4 Help.set_file('a.yml', "foo: &foo\n bar: 42\nplop:\n <<: *foo", 100) assert_raises FakeYaml::Fallback do FakeYaml.load_file('a.yml') end end else def test_load_file Help.set_file('a.yml', "---\nfoo: bar", 100) assert_equal({'foo' => 'bar'}, FakeYaml.load_file('a.yml')) end def test_load_file_aliases Help.set_file('a.yml', "foo: &foo\n bar: 42\nplop:\n <<: *foo", 100) assert_equal({"foo" => { "bar" => 42 }, "plop" => { "bar" => 42} }, FakeYaml.load_file('a.yml')) end def test_load_file_symbolize_names Help.set_file('a.yml', "---\nfoo: bar", 100) FakeYaml.load_file('a.yml') if ::Bootsnap::CompileCache::YAML.supported_options.include?(:symbolize_names) 2.times do assert_equal({foo: 'bar'}, FakeYaml.load_file('a.yml', symbolize_names: true)) end else assert_raises(FakeYaml::Fallback) do # would call super FakeYaml.load_file('a.yml', symbolize_names: true) end end end def test_load_file_freeze Help.set_file('a.yml', "---\nfoo", 100) FakeYaml.load_file('a.yml') if ::Bootsnap::CompileCache::YAML.supported_options.include?(:freeze) 2.times do string = FakeYaml.load_file('a.yml', freeze: true) assert_equal("foo", string) assert_predicate(string, :frozen?) end else assert_raises(FakeYaml::Fallback) do # would call super FakeYaml.load_file('a.yml', freeze: true) end end end def test_load_file_unknown_option Help.set_file('a.yml', "---\nfoo", 100) FakeYaml.load_file('a.yml') assert_raises(FakeYaml::Fallback) do # would call super FakeYaml.load_file('a.yml', fallback: true) end end end if YAML.respond_to?(:unsafe_load_file) def test_unsafe_load_file Help.set_file('a.yml', "foo: &foo\n bar: 42\nplop:\n <<: *foo", 100) assert_equal({"foo" => { "bar" => 42 }, "plop" => { "bar" => 42} }, FakeYaml.unsafe_load_file('a.yml')) end end end bootsnap-1.9.3/test/compile_cache_handler_errors_test.rb000066400000000000000000000054331414746377100236030ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') class CompileCacheHandlerErrorsTest < Minitest::Test include(TmpdirHelper) # now test three failure modes of each handler method: # 1. unexpected type # 2. invalid instance of expected type # 3. exception def test_input_to_storage_unexpected_type path = Help.set_file('a.rb', 'a = 3', 100) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).returns(nil) # this could be made slightly more obvious though. assert_raises(TypeError) { load(path) } end def test_input_to_storage_invalid_instance_of_expected_type path = Help.set_file('a.rb', 'a = 3', 100) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).returns('broken') Bootsnap::CompileCache::ISeq.expects(:input_to_output).with('a = 3', nil).returns('whatever') _, err = capture_subprocess_io do load(path) end assert_match(/broken binary/, err) end def test_input_to_storage_raises path = Help.set_file('a.rb', 'a = 3', 100) klass = Class.new(StandardError) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).raises(klass, 'oops') assert_raises(klass) { load(path) } end def test_storage_to_output_unexpected_type path = Help.set_file('a.rb', 'a = a = 3', 100) Bootsnap::CompileCache::ISeq.expects(:storage_to_output).returns(Object.new) # It seems like ruby doesn't really care. load(path) end # not really a thing. Really, we just return whatever. It's a problem with # the handler if that's invalid. # def test_storage_to_output_invalid_instance_of_expected_type def test_storage_to_output_raises path = Help.set_file('a.rb', 'a = a = 3', 100) klass = Class.new(StandardError) Bootsnap::CompileCache::ISeq.expects(:storage_to_output).times(2).raises(klass, 'oops') assert_raises(klass) { load(path) } # called from two paths; this tests the second. assert_raises(klass) { load(path) } end def test_input_to_output_unexpected_type path = Help.set_file('a.rb', 'a = a = 3', 100) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).raises(Bootsnap::CompileCache::Uncompilable) Bootsnap::CompileCache::ISeq.expects(:input_to_output).returns(Object.new) # It seems like ruby doesn't really care. load(path) end # not really a thing. Really, we just return whatever. It's a problem with # the handler if that's invalid. # def test_input_to_output_invalid_instance_of_expected_type def test_input_to_output_raises path = Help.set_file('a.rb', 'a = 3', 100) klass = Class.new(StandardError) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).raises(Bootsnap::CompileCache::Uncompilable) Bootsnap::CompileCache::ISeq.expects(:input_to_output).raises(klass, 'oops') assert_raises(klass) { load(path) } end end bootsnap-1.9.3/test/compile_cache_key_format_test.rb000066400000000000000000000050361414746377100227310ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') require('tempfile') require('tmpdir') require('fileutils') class CompileCacheKeyFormatTest < Minitest::Test FILE = File.expand_path(__FILE__) include(TmpdirHelper) R = { version: 0...4, ruby_platform: 4...8, compile_option: 8...12, ruby_revision: 12...16, size: 16...24, mtime: 24...32, data_size: 32...40, } def test_key_version key = cache_key_for_file(FILE) exp = [3].pack("L") assert_equal(exp, key[R[:version]]) end def test_key_compile_option_stable k1 = cache_key_for_file(FILE) k2 = cache_key_for_file(FILE) RubyVM::InstructionSequence.compile_option = { tailcall_optimization: true } k3 = cache_key_for_file(FILE) assert_equal(k1[R[:compile_option]], k2[R[:compile_option]]) refute_equal(k1[R[:compile_option]], k3[R[:compile_option]]) ensure RubyVM::InstructionSequence.compile_option = { tailcall_optimization: false } end def test_key_ruby_revision key = cache_key_for_file(FILE) exp = if RUBY_REVISION.is_a?(String) [Help.fnv1a_64(RUBY_REVISION) >> 32].pack("L") else [RUBY_REVISION].pack("L") end assert_equal(exp, key[R[:ruby_revision]]) end def test_key_size key = cache_key_for_file(FILE) exp = File.size(FILE) act = key[R[:size]].unpack("Q")[0] assert_equal(exp, act) end def test_key_mtime key = cache_key_for_file(FILE) exp = File.mtime(FILE).to_i act = key[R[:mtime]].unpack("Q")[0] assert_equal(exp, act) end def test_fetch if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ target = 'NUL' expected_file = "#{@tmp_dir}/36/9eba19c29ffe00" else target = '/dev/null' expected_file = "#{@tmp_dir}/8c/d2d180bbd995df" end actual = Bootsnap::CompileCache::Native.fetch(@tmp_dir, target, TestHandler, nil) assert_equal("NEATO #{target.upcase}", actual) data = File.read(expected_file) assert_equal("neato #{target}", data.force_encoding(Encoding::BINARY)[64..-1]) actual = Bootsnap::CompileCache::Native.fetch(@tmp_dir, target, TestHandler, nil) assert_equal("NEATO #{target.upcase}", actual) end def test_unexistent_fetch assert_raises(Errno::ENOENT) do Bootsnap::CompileCache::Native.fetch(@tmp_dir, '123', Bootsnap::CompileCache::ISeq, nil) end end private def cache_key_for_file(file) Bootsnap::CompileCache::Native.fetch(@tmp_dir, file, TestHandler, nil) data = File.binread(Help.cache_path(@tmp_dir, file)) data.byteslice(0..31) end end bootsnap-1.9.3/test/compile_cache_test.rb000066400000000000000000000122431414746377100205070ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') class CompileCacheTest < Minitest::Test include(TmpdirHelper) def test_compile_option_crc32 # Just assert that this works. Bootsnap::CompileCache::Native.compile_option_crc32 = 0xffffffff assert_raises(RangeError) do Bootsnap::CompileCache::Native.compile_option_crc32 = 0xffffffff + 1 end end def test_coverage_running? refute(Bootsnap::CompileCache::Native.coverage_running?) require('coverage') begin Coverage.start assert(Bootsnap::CompileCache::Native.coverage_running?) ensure Coverage.result end end def test_no_write_permission_to_cache if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ # Always pass this test on Windows because directories aren't read, only # listed. You can restrict the ability to list directory contents on # Windows or you can set ACLS on a folder such that it is not allowed to # list contents. # # Since we can't read directories on windows, this specific test doesn't # make sense. In addtion we test read-only files in # `test_can_open_read_only_cache` so we are covered testing reading # read-only files. pass else path = Help.set_file('a.rb', 'a = 3', 100) folder = File.dirname(Help.cache_path(@tmp_dir, path)) FileUtils.mkdir_p(folder) FileUtils.chmod(0400, folder) load(path) end end def test_can_open_read_only_cache path = Help.set_file('a.rb', 'a = a = 3', 100) # Load once to create the cache file load(path) FileUtils.chmod(0400, path) # Loading again after the file is marked read-only should still succeed load(path) end def test_file_is_only_read_once path = Help.set_file('a.rb', 'a = a = 3', 100) storage = RubyVM::InstructionSequence.compile_file(path).to_binary output = RubyVM::InstructionSequence.load_from_binary(storage) # This doesn't really *prove* the file is only read once, but # it at least asserts the input is only cached once. Bootsnap::CompileCache::ISeq.expects(:input_to_storage).times(1).returns(storage) Bootsnap::CompileCache::ISeq.expects(:storage_to_output).times(2).returns(output) load(path) load(path) end def test_raises_syntax_error path = Help.set_file('a.rb', 'a = (3', 100) assert_raises(SyntaxError) do # SyntaxError emits directly to stderr in addition to raising, it seems. capture_io { load(path) } end end def test_no_recache_when_mtime_and_size_same path = Help.set_file('a.rb', 'a = a = 3', 100) storage = RubyVM::InstructionSequence.compile_file(path).to_binary output = RubyVM::InstructionSequence.load_from_binary(storage) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).times(1).returns(storage) Bootsnap::CompileCache::ISeq.expects(:storage_to_output).times(2).returns(output) load(path) Help.set_file(path, 'a = a = 4', 100) load(path) end def test_recache_when_mtime_different path = Help.set_file('a.rb', 'a = a = 3', 100) storage = RubyVM::InstructionSequence.compile_file(path).to_binary output = RubyVM::InstructionSequence.load_from_binary(storage) # Totally lies the second time but that's not the point. Bootsnap::CompileCache::ISeq.expects(:input_to_storage).times(2).returns(storage) Bootsnap::CompileCache::ISeq.expects(:storage_to_output).times(2).returns(output) load(path) Help.set_file(path, 'a = a = 2', 101) load(path) end def test_recache_when_size_different path = Help.set_file('a.rb', 'a = a = 3', 100) storage = RubyVM::InstructionSequence.compile_file(path).to_binary output = RubyVM::InstructionSequence.load_from_binary(storage) # Totally lies the second time but that's not the point. Bootsnap::CompileCache::ISeq.expects(:input_to_storage).times(2).returns(storage) Bootsnap::CompileCache::ISeq.expects(:storage_to_output).times(2).returns(output) load(path) Help.set_file(path, 'a = 33', 100) load(path) end def test_invalid_cache_file path = Help.set_file('a.rb', 'a = a = 3', 100) cp = Help.cache_path(@tmp_dir, path) FileUtils.mkdir_p(File.dirname(cp)) File.write(cp, 'nope') load(path) assert(File.size(cp) > 32) # cache was overwritten end def test_instrumentation_hit path = Help.set_file('a.rb', 'a = a = 3', 100) load(path) calls = [] Bootsnap.instrumentation = ->(event, path) { calls << [event, path] } load(path) assert_equal [], calls ensure Bootsnap.instrumentation = nil end def test_instrumentation_miss path = Help.set_file('a.rb', 'a = a = 3', 100) calls = [] Bootsnap.instrumentation = ->(event, path) { calls << [event, path] } load(path) assert_equal [[:miss, 'a.rb']], calls ensure Bootsnap.instrumentation = nil end def test_instrumentation_stale path = Help.set_file('a.rb', 'a = a = 3', 100) load(path) path = Help.set_file('a.rb', 'a = a = 4', 101) calls = [] Bootsnap.instrumentation = ->(event, path) { calls << [event, path] } load(path) assert_equal [[:stale, 'a.rb']], calls ensure Bootsnap.instrumentation = nil end end bootsnap-1.9.3/test/helper_test.rb000066400000000000000000000004541414746377100172140ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') class HelperTest < MiniTest::Test include(TmpdirHelper) def test_validate_cache_path path = Help.set_file('a.rb', 'a = a = 3', 100) cp = Help.cache_path(@tmp_dir, path) load(path) assert_equal(true, File.file?(cp)) end end bootsnap-1.9.3/test/load_path_cache/000077500000000000000000000000001414746377100174245ustar00rootroot00000000000000bootsnap-1.9.3/test/load_path_cache/cache_test.rb000066400000000000000000000166311414746377100220620ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') module Bootsnap module LoadPathCache class CacheTest < MiniTest::Test def setup @dir1 = File.realpath(Dir.mktmpdir) @dir2 = File.realpath(Dir.mktmpdir) FileUtils.touch("#{@dir1}/a.rb") FileUtils.mkdir_p("#{@dir1}/foo/bar") FileUtils.touch("#{@dir1}/foo/bar/baz.rb") FileUtils.touch("#{@dir2}/b.rb") FileUtils.touch("#{@dir1}/conflict.rb") FileUtils.touch("#{@dir2}/conflict.rb") FileUtils.touch("#{@dir1}/dl#{DLEXT}") FileUtils.touch("#{@dir1}/both.rb") FileUtils.touch("#{@dir1}/both#{DLEXT}") FileUtils.touch("#{@dir1}/béé.rb") end def teardown FileUtils.rm_rf(@dir1) FileUtils.rm_rf(@dir2) end # dev.yml specifies 2.3.3 and this test assumes it. Failures on other # versions aren't a big deal, but feel free to fix the test. def test_builtin_features cache = Cache.new(NullCache, []) assert_raises(ReturnFalse) { cache.find('thread') } assert_raises(ReturnFalse) { cache.find('thread.rb') } assert_raises(ReturnFalse) { cache.find('enumerator') } assert_raises(ReturnFalse) { cache.find('enumerator.so') } if RUBY_PLATFORM =~ /darwin/ assert_raises(ReturnFalse) { cache.find('enumerator.bundle') } else assert_raises(FallbackScan) { cache.find('enumerator.bundle') } end bundle = RUBY_PLATFORM =~ /darwin/ ? 'bundle' : 'so' refute(cache.find('thread.' + bundle)) refute(cache.find('enumerator.rb')) refute(cache.find('encdb.' + bundle)) end def test_simple po = [@dir1] cache = Cache.new(NullCache, po) assert_equal("#{@dir1}/a.rb", cache.find('a')) refute(cache.find('a', try_extensions: false)) cache.push_paths(po, @dir2) assert_equal("#{@dir2}/b.rb", cache.find('b')) refute(cache.find('b', try_extensions: false)) end def test_extension_append_for_relative_paths po = [@dir1] cache = Cache.new(NullCache, po) dir1_basename = File.basename(@dir1) Dir.chdir(@dir1) do assert_equal("#{@dir1}/a.rb", cache.find('./a')) assert_equal("#{@dir1}/a", cache.find('./a', try_extensions: false)) assert_equal("#{@dir1}/a.rb", cache.find("../#{dir1_basename}/a")) assert_equal("#{@dir1}/a", cache.find("../#{dir1_basename}/a", try_extensions: false)) assert_equal("#{@dir1}/dl#{DLEXT}", cache.find('./dl')) assert_equal("#{@dir1}/dl", cache.find('./dl', try_extensions: false)) assert_equal("#{@dir1}/enoent", cache.find('./enoent')) assert_equal("#{@dir1}/enoent", cache.find('./enoent', try_extensions: false)) end end def test_unshifted_paths_have_higher_precedence po = [@dir1] cache = Cache.new(NullCache, po) assert_equal("#{@dir1}/conflict.rb", cache.find('conflict')) assert_equal("#{@dir1}/conflict.rb", cache.find('conflict.rb', try_extensions: false)) cache.unshift_paths(po, @dir2) assert_equal("#{@dir2}/conflict.rb", cache.find('conflict')) assert_equal("#{@dir2}/conflict.rb", cache.find('conflict.rb', try_extensions: false)) end def test_pushed_paths_have_lower_precedence po = [@dir1] cache = Cache.new(NullCache, po) assert_equal("#{@dir1}/conflict.rb", cache.find('conflict')) assert_equal("#{@dir1}/conflict.rb", cache.find('conflict.rb', try_extensions: false)) cache.push_paths(po, @dir2) assert_equal("#{@dir1}/conflict.rb", cache.find('conflict')) assert_equal("#{@dir1}/conflict.rb", cache.find('conflict.rb', try_extensions: false)) end def test_directory_caching cache = Cache.new(NullCache, [@dir1]) assert_equal(@dir1, cache.load_dir("foo")) assert_equal(@dir1, cache.load_dir("foo/bar")) assert_nil(cache.load_dir("bar")) end def test_extension_permutations cache = Cache.new(NullCache, [@dir1]) assert_equal("#{@dir1}/dl#{DLEXT}", cache.find('dl')) refute(cache.find('dl', try_extensions: false)) assert_equal("#{@dir1}/dl#{DLEXT}", cache.find("dl#{DLEXT}")) assert_equal("#{@dir1}/both.rb", cache.find("both")) refute(cache.find("both", try_extensions: false)) assert_equal("#{@dir1}/both.rb", cache.find("both.rb")) assert_equal("#{@dir1}/both.rb", cache.find("both.rb", try_extensions: false)) assert_equal("#{@dir1}/both#{DLEXT}", cache.find("both#{DLEXT}")) assert_equal("#{@dir1}/both#{DLEXT}", cache.find("both#{DLEXT}", try_extensions: false)) end def test_relative_paths_rescanned Dir.chdir(@dir2) do cache = Cache.new(NullCache, %w(foo)) refute(cache.find('bar/baz')) Dir.chdir(@dir1) do # one caveat here is that you get the actual path back when # resolving relative paths. On darwin, this means that # /var/folders/... comes back as /private/var/folders/... -- In # production, this should be fine, but for this test to pass, we # have to resolve it. assert_equal(File.realpath("#{@dir1}/foo/bar/baz.rb"), cache.find('bar/baz')) end end end def test_development_mode time = Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i # without development_mode, no refresh dev_no_cache = Cache.new(NullCache, [@dir1], development_mode: false) dev_yes_cache = Cache.new(NullCache, [@dir1], development_mode: true) FileUtils.touch("#{@dir1}/new.rb") dev_no_cache.stubs(:now).returns(time + 31) refute(dev_no_cache.find('new')) dev_yes_cache.stubs(:now).returns(time + 28) assert_raises(Bootsnap::LoadPathCache::FallbackScan) do dev_yes_cache.find('new') end dev_yes_cache.stubs(:now).returns(time + 31) assert(dev_yes_cache.find('new')) end def test_path_obj_equal? path_obj = [] cache = Cache.new(NullCache, path_obj) path_obj.unshift(@dir1) assert_equal("#{@dir1}/a.rb", cache.find('a')) end if RUBY_VERSION >= '2.5' && !(RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/) # https://github.com/ruby/ruby/pull/4061 # https://bugs.ruby-lang.org/issues/17517 OS_ASCII_PATH_ENCODING = RUBY_VERSION >= '3.1' ? Encoding::UTF_8 : Encoding::US_ASCII def test_path_encoding po = [@dir1] cache = Cache.new(NullCache, po) path = cache.find('a') assert_equal("#{@dir1}/a.rb", path) require path internal_path = $LOADED_FEATURES.last assert_equal(OS_ASCII_PATH_ENCODING, internal_path.encoding) assert_equal(OS_ASCII_PATH_ENCODING, path.encoding) File.write(path, '') assert_same path, internal_path utf8_path = cache.find('béé') require utf8_path internal_utf8_path = $LOADED_FEATURES.last assert_equal("#{@dir1}/béé.rb", utf8_path) assert_equal(Encoding::UTF_8, internal_utf8_path.encoding) assert_equal(Encoding::UTF_8, utf8_path.encoding) File.write(utf8_path, '') assert_same utf8_path, internal_utf8_path end end end end end bootsnap-1.9.3/test/load_path_cache/change_observer_test.rb000066400000000000000000000034021414746377100241430ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') module Bootsnap module LoadPathCache class ChangeObserverTest < MiniTest::Test def setup @observer = Object.new @arr = [] ChangeObserver.register(@observer, @arr) end def test_observes_changes @observer.expects(:push_paths).with(@arr, 'a') @arr << 'a' @observer.expects(:push_paths).with(@arr, 'b', 'c') @arr.push('b', 'c') @observer.expects(:push_paths).with(@arr, 'd', 'e') @arr.append('d', 'e') @observer.expects(:unshift_paths).with(@arr, 'f', 'g') @arr.unshift('f', 'g') @observer.expects(:push_paths).with(@arr, 'h', 'i') @arr.concat(%w(h i)) @observer.expects(:unshift_paths).with(@arr, 'j', 'k') @arr.prepend('j', 'k') end def test_reinitializes_on_aggressive_modifications @observer.expects(:push_paths).with(@arr, 'a', 'b', 'c') @arr.push('a', 'b', 'c') @observer.expects(:reinitialize).times(4) @arr.delete(3) @arr.compact! @arr.map!(&:upcase) assert_equal('C', @arr.pop) assert_equal(%w(A B), @arr) end def test_register_frozen # just assert no crash ChangeObserver.register(@observer, @arr.dup.freeze) end def test_register_twice_observes_once ChangeObserver.register(@observer, @arr) @observer.expects(:push_paths).with(@arr, 'a').once @arr << 'a' assert_equal(%w(a), @arr) end def test_uniq_without_block @observer.expects(:reinitialize).never @arr.uniq! end def test_uniq_with_block @observer.expects(:reinitialize).once @arr.uniq! {} end end end end bootsnap-1.9.3/test/load_path_cache/core_ext/000077500000000000000000000000001414746377100212345ustar00rootroot00000000000000bootsnap-1.9.3/test/load_path_cache/core_ext/kernel_require_test.rb000066400000000000000000000016761414746377100256460ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') module Bootsnap module KernelRequireTest class KernelLoadTest < MiniTest::Test def setup @initial_dir = Dir.pwd @dir1 = File.realpath(Dir.mktmpdir) FileUtils.touch("#{@dir1}/a.rb") FileUtils.touch("#{@dir1}/no_ext") @dir2 = File.realpath(Dir.mktmpdir) File.open("#{@dir2}/loads.rb", "wb") { |f| f.write("load 'subdir/loaded'\nload './subdir/loaded'\n") } FileUtils.mkdir("#{@dir2}/subdir") FileUtils.touch("#{@dir2}/subdir/loaded") $LOAD_PATH.push(@dir1) end def teardown $LOAD_PATH.pop Dir.chdir(@initial_dir) FileUtils.rm_rf(@dir1) FileUtils.rm_rf(@dir2) end def test_no_exstensions_for_kernel_load assert_raises(LoadError) { load 'a' } assert(load 'no_ext') Dir.chdir(@dir2) assert(load 'loads.rb') end end end end bootsnap-1.9.3/test/load_path_cache/loaded_features_index_test.rb000066400000000000000000000120701414746377100253250ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') module Bootsnap module LoadPathCache class LoadedFeaturesIndexTest < MiniTest::Test def setup @index = LoadedFeaturesIndex.new # not really necessary but let's just make it a clean slate @index.instance_variable_set(:@lfi, {}) end def test_successful_addition refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.register('bundler', '/a/b/bundler.rb') {} assert(@index.key?('bundler')) assert(@index.key?('bundler.rb')) refute(@index.key?('foo')) end def test_no_add_on_raise refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) assert_raises(RuntimeError) do @index.register('bundler', '/a/b/bundler.rb') { raise } end refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) end def test_infer_base_from_ext refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.register('bundler.rb') {} assert(@index.key?('bundler')) assert(@index.key?('bundler.rb')) refute(@index.key?('foo')) end def test_only_strip_elidable_ext # It is only valid to strip a '.rb' or shared library extension from the # end of a filename, not anything else. # # E.g. 'descriptor.pb.rb' if required via 'descriptor.pb' # should never be shortened to merely 'descriptor'! refute(@index.key?('descriptor.pb')) refute(@index.key?('descriptor.pb.rb')) refute(@index.key?('descriptor.rb')) refute(@index.key?('descriptor')) refute(@index.key?('foo')) @index.register('descriptor.pb.rb') {} assert(@index.key?('descriptor.pb')) assert(@index.key?('descriptor.pb.rb')) refute(@index.key?('descriptor.rb')) refute(@index.key?('descriptor')) refute(@index.key?('foo')) end def test_shared_library_ext_considered_elidable # Check that '.dylib' (token shared library extension) is treated as elidable, # and doesn't get mixed up with Ruby '.rb' files. refute(@index.key?('libgit2.dylib')) refute(@index.key?('libgit2.dylib.rb')) refute(@index.key?('descriptor.rb')) refute(@index.key?('descriptor')) refute(@index.key?('foo')) @index.register('libgit2.dylib') {} assert(@index.key?('libgit2.dylib')) refute(@index.key?('libgit2.dylib.rb')) refute(@index.key?('libgit2.rb')) refute(@index.key?('foo')) end def test_cannot_infer_ext_from_base # Current limitation refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.register('bundler') {} assert(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) end def test_purge_loaded_feature refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.register('bundler', '/a/b/bundler.rb') {} assert(@index.key?('bundler')) assert(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.purge('/a/b/bundler.rb') refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) end def test_purge_multi_loaded_feature refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.register('bundler', '/a/b/bundler.rb') {} assert(@index.key?('bundler')) assert(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.purge_multi(['/a/b/bundler.rb', '/a/b/does-not-exist.rb']) refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) end def test_register_finds_correct_feature refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.register('bundler', nil) { $LOADED_FEATURES << '/a/b/bundler.rb' } assert(@index.key?('bundler')) assert(@index.key?('bundler.rb')) refute(@index.key?('foo')) @index.purge('/a/b/bundler.rb') refute(@index.key?('bundler')) refute(@index.key?('bundler.rb')) refute(@index.key?('foo')) end def test_derives_initial_state_from_loaded_features index = LoadedFeaturesIndex.new assert(index.key?('minitest/autorun')) assert(index.key?('minitest/autorun.rb')) refute(index.key?('minitest/autorun.so')) end def test_works_with_pathname path = '/tmp/bundler.rb' pathname = Pathname.new(path) @index.register(pathname, path) { true } assert(@index.key?(pathname)) end end end end bootsnap-1.9.3/test/load_path_cache/path_scanner_test.rb000066400000000000000000000024401414746377100234550ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') module Bootsnap module LoadPathCache class PathScannerTest < MiniTest::Test DLEXT = RbConfig::CONFIG['DLEXT'] OTHER_DLEXT = DLEXT == 'bundle' ? 'so' : 'bundle' def test_scans_requirables_and_dirs Dir.mktmpdir do |dir| FileUtils.mkdir_p("#{dir}/ruby/a") FileUtils.mkdir_p("#{dir}/ruby/b/c") FileUtils.mkdir_p("#{dir}/support/h/i") FileUtils.mkdir_p("#{dir}/ruby/l") FileUtils.mkdir_p("#{dir}/support/l/m") FileUtils.touch("#{dir}/ruby/d.rb") FileUtils.touch("#{dir}/ruby/e.#{DLEXT}") FileUtils.touch("#{dir}/ruby/f.#{OTHER_DLEXT}") FileUtils.touch("#{dir}/ruby/a/g.rb") FileUtils.touch("#{dir}/support/h/j.rb") FileUtils.touch("#{dir}/support/h/i/k.rb") FileUtils.touch("#{dir}/support/l/m/n.rb") FileUtils.ln_s("#{dir}/support/h", "#{dir}/ruby/h") FileUtils.ln_s("#{dir}/support/l/m", "#{dir}/ruby/l/m") entries, dirs = PathScanner.call("#{dir}/ruby") assert_equal(["a/g.rb", "d.rb", "e.#{DLEXT}", "h/i/k.rb", "h/j.rb", "l/m/n.rb"], entries.sort) assert_equal(["a", "b", "b/c", "h", "h/i", "l", "l/m"], dirs.sort) end end end end end bootsnap-1.9.3/test/load_path_cache/path_test.rb000066400000000000000000000112701414746377100217450ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') require('bootsnap/load_path_cache') module Bootsnap module LoadPathCache class PathTest < MiniTest::Test def setup @cache = Object.new end def test_stability require('time') time_file = Time.method(:rfc2822).source_location[0] volatile = Path.new(__FILE__) stable = Path.new(time_file) unknown = Path.new('/who/knows') lib = Path.new(RbConfig::CONFIG['libdir'] + '/a') site = Path.new(RbConfig::CONFIG['sitedir'] + '/b') absolute_prefix = RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ ? ENV['SystemDrive'] : '' bundler = Path.new(absolute_prefix + '/bp/3') Bundler.stubs(:bundle_path).returns(absolute_prefix + '/bp') assert(stable.stable?, "The stable path #{stable.path.inspect} was unexpectedly not stable.") refute(stable.volatile?, "The stable path #{stable.path.inspect} was unexpectedly volatile.") assert(volatile.volatile?, "The volatile path #{volatile.path.inspect} was unexpectedly not volatile.") refute(volatile.stable?, "The volatile path #{volatile.path.inspect} was unexpectedly stable.") assert(unknown.volatile?, "The unknown path #{unknown.path.inspect} was unexpectedly not volatile.") refute(unknown.stable?, "The unknown path #{unknown.path.inspect} was unexpectedly stable.") assert(lib.stable?, "The lib path #{lib.path.inspect} was unexpectedly not stable.") refute(site.stable?, "The site path #{site.path.inspect} was unexpectedly stable.") assert(bundler.stable?, "The bundler path #{bundler.path.inspect} was unexpectedly not stable.") end def test_non_directory? if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ refute(Path.new('c:/dev').non_directory?) refute(Path.new('c:/nope').non_directory?) # there isn't a direct analog i could think of # assert(Path.new('/dev/null').non_directory?) assert(Path.new("#{ENV['WinDir']}/System32/Drivers/Etc/hosts").non_directory?) else refute(Path.new('/dev').non_directory?) refute(Path.new('/nope').non_directory?) assert(Path.new('/dev/null').non_directory?) assert(Path.new('/etc/hosts').non_directory?) end end def test_volatile_cache_valid_when_mtime_has_not_changed with_caching_fixtures do |dir, _a, _a_b, _a_b_c| entries, dirs = PathScanner.call(dir) path = Path.new(dir) # volatile, since it'll be in /tmp @cache.expects(:get).with(dir).returns([100, entries, dirs]) path.entries_and_dirs(@cache) end end def test_volatile_cache_invalid_when_mtime_changed with_caching_fixtures do |dir, _a, a_b, _a_b_c| entries, dirs = PathScanner.call(dir) path = Path.new(dir) # volatile, since it'll be in /tmp FileUtils.touch(a_b, mtime: Time.at(101)) @cache.expects(:get).with(dir).returns([100, entries, dirs]) @cache.expects(:set).with(dir, [101, entries, dirs]) # next read doesn't regen @cache.expects(:get).with(dir).returns([101, entries, dirs]) path.entries_and_dirs(@cache) path.entries_and_dirs(@cache) end end def test_volatile_cache_generated_when_missing with_caching_fixtures do |dir, _a, _a_b, _a_b_c| entries, dirs = PathScanner.call(dir) path = Path.new(dir) # volatile, since it'll be in /tmp @cache.expects(:get).with(dir).returns(nil) @cache.expects(:set).with(dir, [100, entries, dirs]) path.entries_and_dirs(@cache) end end def test_stable_cache_does_not_notice_when_mtime_changes with_caching_fixtures do |dir, _a, a_b, _a_b_c| entries, dirs = PathScanner.call(dir) path = Path.new(dir) # volatile, since it'll be in /tmp path.expects(:stable?).returns(true) FileUtils.touch(a_b, mtime: Time.at(101)) # It's unfortunate that we're stubbing the impl of #fetch here. PathScanner.expects(:call).never @cache.expects(:get).with(dir).returns([100, entries, dirs]) path.entries_and_dirs(@cache) end end private def with_caching_fixtures Dir.mktmpdir do |dir| a = "#{dir}/a" a_b = "#{dir}/a/b" a_b_c = "#{dir}/a/b/c.rb" FileUtils.mkdir_p(a_b) [a_b_c, a_b, a, dir].each { |f| FileUtils.touch(f, mtime: Time.at(100)) } yield(dir, a, a_b, a_b_c) end end end end end bootsnap-1.9.3/test/load_path_cache/realpath_cache_test.rb000066400000000000000000000055351414746377100237430ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') module Bootsnap module LoadPathCache class RealpathCacheTest < MiniTest::Test EXTENSIONS = ['', *CACHED_EXTENSIONS] def setup @cache = RealpathCache.new @base_dir = File.realpath(Dir.mktmpdir) @absolute_dir = "#{@base_dir}/absolute" Dir.mkdir(@absolute_dir) @symlinked_dir = "#{@base_dir}/symlink" FileUtils.ln_s(@absolute_dir, @symlinked_dir) real_caller = File.new("#{@absolute_dir}/real_caller.rb", 'w').tap(&:close).path symlinked_caller = "#{@absolute_dir}/symlinked_caller.rb" FileUtils.ln_s(real_caller, symlinked_caller) EXTENSIONS.each do |ext| real_required = File.new("#{@absolute_dir}/real_required#{ext}", 'w').tap(&:close).path symlinked_required = "#{@absolute_dir}/symlinked_required#{ext}" FileUtils.ln_s(real_required, symlinked_required) end end def teardown FileUtils.remove_entry(@base_dir) end def remove_required(extensions) extensions.each do |ext| FileUtils.rm("#{@absolute_dir}/real_required#{ext}") FileUtils.rm("#{@absolute_dir}/symlinked_required#{ext}") end end variants = %w(absolute symlink).product(%w(absolute symlink), %w(real_caller symlinked_caller), %w(real_required symlinked_required)) variants.each do |caller_dir, required_dir, caller_file, required_file| method_name = "test_with_#{caller_dir}_caller_dir_" \ "#{required_dir}_require_dir_" \ "#{caller_file}_#{required_file}" define_method(method_name) do caller_path = "#{@base_dir}/#{caller_dir}/#{caller_file}" require_path = "../#{required_dir}/#{required_file}.rb" expected = "#{@absolute_dir}/real_required.rb" assert(@cache.call(caller_path, require_path).eql?(expected)) end (EXTENSIONS.size - 1).times do |n| removing = EXTENSIONS[0..n] define_method("#{method_name}_no#{removing.join('_')}_extensions") do caller_path = "#{@base_dir}/#{caller_dir}/#{caller_file}" require_path = "../#{required_dir}/#{required_file}" remove_required(removing) expected = "#{@absolute_dir}/real_required#{EXTENSIONS[n + 1]}" assert(@cache.call(caller_path, require_path).eql?(expected)) end end define_method("#{method_name}_no_files") do caller_path = "#{@base_dir}/#{caller_dir}/#{caller_file}" require_path = "../#{required_dir}/#{required_file}" remove_required(EXTENSIONS) expected = "#{@base_dir}/#{required_dir}/#{required_file}" assert(@cache.call(caller_path, require_path).eql?(expected)) end end end end end bootsnap-1.9.3/test/load_path_cache/store_test.rb000066400000000000000000000043771414746377100221570ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') require('tmpdir') require('fileutils') module Bootsnap module LoadPathCache class StoreTest < MiniTest::Test def setup @dir = Dir.mktmpdir @path = "#{@dir}/store" @store = Store.new(@path) end def teardown FileUtils.rm_rf(@dir) end attr_reader(:store) def test_persistence store.transaction { store.set('a', 'b') } store2 = Store.new(@path) assert_equal('b', store2.get('a')) end def test_modification store.transaction { store.set('a', 'b') } store2 = Store.new(@path) assert_equal('b', store2.get('a')) store.transaction { store.set('a', 'c') } store3 = Store.new(@path) assert_equal('c', store3.get('a')) end def test_stores_arrays store.transaction { store.set('a', [1234, %w(a b)]) } store2 = Store.new(@path) assert_equal([1234, %w(a b)], store2.get('a')) end def test_transaction_required_to_set assert_raises(Store::SetOutsideTransactionNotAllowed) do store.set('a', 'b') end assert_raises(Store::SetOutsideTransactionNotAllowed) do store.fetch('a') { 'b' } end end def test_nested_transaction_fails assert_raises(Store::NestedTransactionError) do store.transaction { store.transaction } end end def test_no_commit_unless_dirty store.transaction { store.set('a', nil) } refute(File.exist?(@path)) store.transaction { store.set('a', 1) } assert(File.exist?(@path)) end def test_retry_on_collision retries = sequence('retries') MessagePack.expects(:dump).in_sequence(retries).raises(Errno::EEXIST.new("File exists @ rb_sysopen")) MessagePack.expects(:dump).in_sequence(retries).returns(1) FileUtils.expects(:mv).in_sequence(retries) store.transaction { store.set('a', 1) } end def test_ignore_read_only_filesystem MessagePack.expects(:dump).raises(Errno::EROFS.new("Read-only file system @ rb_sysopen")) store.transaction { store.set('a', 1) } refute(File.exist?(@path)) end end end end bootsnap-1.9.3/test/minimal_support/000077500000000000000000000000001414746377100175705ustar00rootroot00000000000000bootsnap-1.9.3/test/minimal_support/bootsnap_setup.rb000066400000000000000000000001211414746377100231540ustar00rootroot00000000000000# frozen_string_literal: true require('bundler/setup') require('bootsnap/setup') bootsnap-1.9.3/test/setup_test.rb000066400000000000000000000046531414746377100171020ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') module Bootsnap class SetupTest < Minitest::Test def setup @_old_env = ENV.to_h @tmp_dir = Dir.mktmpdir('bootsnap-test') ENV['BOOTSNAP_CACHE_DIR'] = @tmp_dir end def teardown ENV.replace(@_old_env) end def test_default_setup Bootsnap.expects(:setup).with( cache_dir: @tmp_dir, development_mode: true, load_path_cache: true, compile_cache_iseq: Bootsnap.iseq_cache_supported?, compile_cache_yaml: true, compile_cache_json: true, ) Bootsnap.default_setup end def test_default_setup_with_ENV_not_dev ENV['ENV'] = 'something' Bootsnap.expects(:setup).with( cache_dir: @tmp_dir, development_mode: false, load_path_cache: true, compile_cache_iseq: Bootsnap.iseq_cache_supported?, compile_cache_yaml: true, compile_cache_json: true, ) Bootsnap.default_setup end def test_default_setup_with_DISABLE_BOOTSNAP_LOAD_PATH_CACHE ENV['DISABLE_BOOTSNAP_LOAD_PATH_CACHE'] = 'something' Bootsnap.expects(:setup).with( cache_dir: @tmp_dir, development_mode: true, load_path_cache: false, compile_cache_iseq: Bootsnap.iseq_cache_supported?, compile_cache_yaml: true, compile_cache_json: true, ) Bootsnap.default_setup end def test_default_setup_with_DISABLE_BOOTSNAP_COMPILE_CACHE ENV['DISABLE_BOOTSNAP_COMPILE_CACHE'] = 'something' Bootsnap.expects(:setup).with( cache_dir: @tmp_dir, development_mode: true, load_path_cache: true, compile_cache_iseq: false, compile_cache_yaml: false, compile_cache_json: false, ) Bootsnap.default_setup end def test_default_setup_with_DISABLE_BOOTSNAP ENV['DISABLE_BOOTSNAP'] = 'something' Bootsnap.expects(:setup).never Bootsnap.default_setup end def test_default_setup_with_BOOTSNAP_LOG ENV['BOOTSNAP_LOG'] = 'something' Bootsnap.expects(:setup).with( cache_dir: @tmp_dir, development_mode: true, load_path_cache: true, compile_cache_iseq: Bootsnap.iseq_cache_supported?, compile_cache_yaml: true, compile_cache_json: true, ) Bootsnap.expects(:logger=).with($stderr.method(:puts)) Bootsnap.default_setup end end end bootsnap-1.9.3/test/test_helper.rb000066400000000000000000000050271414746377100172150ustar00rootroot00000000000000# frozen_string_literal: true $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__)) if defined? Warning if Warning.respond_to?(:[]=) Warning[:deprecated] = true end end require('bundler/setup') require('bootsnap') require('bootsnap/compile_cache/yaml') require('bootsnap/compile_cache/json') require('tmpdir') require('fileutils') require('minitest/autorun') require('mocha/minitest') cache_dir = File.expand_path('../../tmp/bootsnap/compile-cache', __FILE__) Bootsnap::CompileCache.setup(cache_dir: cache_dir, iseq: true, yaml: false, json: false) if GC.respond_to?(:verify_compaction_references) # This method was added in Ruby 3.0.0. Calling it this way asks the GC to # move objects around, helping to find object movement bugs. GC.verify_compaction_references(double_heap: true, toward: :empty) end module TestHandler def self.input_to_storage(_i, p) 'neato ' + p end def self.storage_to_output(d, _a) d.upcase end def self.input_to_output(_d, _a) raise('but why tho') end end module NullCache def self.get(*) end def self.set(*) end def self.transaction(*) yield end def self.fetch(*) yield end end module MiniTest class Test module Help class << self def cache_path(dir, file, args_key = nil) hash = fnv1a_64(file) unless args_key.nil? hash ^= fnv1a_64(args_key) end hex = hash.to_s(16).rjust(16, '0') "#{dir}/#{hex[0..1]}/#{hex[2..-1]}" end def fnv1a_64(data) hash = 0xcbf29ce484222325 data.bytes.each do |byte| hash = hash ^ byte hash = (hash * 0x100000001b3) % (2**64) end hash end def set_file(path, contents, mtime) FileUtils.mkdir_p(File.dirname(path)) File.write(path, contents) FileUtils.touch(path, mtime: mtime) path end end end end end module TmpdirHelper def setup super @prev_dir = Dir.pwd @tmp_dir = Dir.mktmpdir('bootsnap-test') Dir.chdir(@tmp_dir) @prev = Bootsnap::CompileCache::ISeq.cache_dir Bootsnap::CompileCache::ISeq.cache_dir = @tmp_dir Bootsnap::CompileCache::YAML.cache_dir = @tmp_dir Bootsnap::CompileCache::JSON.cache_dir = @tmp_dir end def teardown super Dir.chdir(@prev_dir) FileUtils.remove_entry(@tmp_dir) Bootsnap::CompileCache::ISeq.cache_dir = @prev Bootsnap::CompileCache::YAML.cache_dir = @prev Bootsnap::CompileCache::JSON.cache_dir = @prev end end bootsnap-1.9.3/test/worker_pool_test.rb000066400000000000000000000010771414746377100203010ustar00rootroot00000000000000# frozen_string_literal: true require('test_helper') require('bootsnap/cli') module Bootsnap class WorkerPoolTestTest < Minitest::Test def test_dispatch @pool = CLI::WorkerPool.create(size: 2, jobs: { touch: ->(path) { File.write(path, $$.to_s) } }) @pool.spawn Dir.mktmpdir('bootsnap-test') do |tmpdir| 10.times do |i| @pool.push(:touch, File.join(tmpdir, i.to_s)) end @pool.shutdown files = Dir.chdir(tmpdir) { Dir['*'] }.sort assert_equal 10.times.map(&:to_s), files end end end end