pax_global_header00006660000000000000000000000064145564555750014537gustar00rootroot0000000000000052 comment=48c08b0dd9340f8a694174b3d1065d51b2ef7499 bootsnap-1.18.3/000077500000000000000000000000001455645557500134565ustar00rootroot00000000000000bootsnap-1.18.3/.github/000077500000000000000000000000001455645557500150165ustar00rootroot00000000000000bootsnap-1.18.3/.github/issue_template.md000066400000000000000000000010321455645557500203570ustar00rootroot00000000000000### 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.18.3/.github/workflows/000077500000000000000000000000001455645557500170535ustar00rootroot00000000000000bootsnap-1.18.3/.github/workflows/ci.yaml000066400000000000000000000035401455645557500203340ustar00rootroot00000000000000name: ci on: pull_request: branches: - main push: branches: - main schedule: - cron: '45 4 * * *' jobs: platforms: strategy: fail-fast: false matrix: os: [ubuntu, macos, windows] ruby: ['2.6'] runs-on: ${{ matrix.os }}-latest steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rake rubocop: strategy: fail-fast: false runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: '3.3' bundler-cache: true - run: bundle exec rubocop rubies: strategy: fail-fast: false matrix: os: [ubuntu] ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', 'ruby-head', 'debug', 'truffleruby', 'truffleruby-head'] runs-on: ${{ matrix.os }}-latest steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rake psych4: strategy: fail-fast: false matrix: os: [ubuntu] ruby: ['3.3'] runs-on: ${{ matrix.os }}-latest env: PSYCH_4: "1" steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rake minimal: strategy: fail-fast: false matrix: os: [ubuntu] ruby: ['jruby'] runs-on: ${{ matrix.os }}-latest steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bin/test-minimal-support bootsnap-1.18.3/.github/workflows/cla.yml000066400000000000000000000011211455645557500203300ustar00rootroot00000000000000name: Contributor License Agreement (CLA) on: pull_request_target: types: [opened, synchronize] issue_comment: types: [created] jobs: cla: runs-on: ubuntu-latest if: | (github.event.issue.pull_request && !github.event.issue.pull_request.merged_at && contains(github.event.comment.body, 'signed') ) || (github.event.pull_request && !github.event.pull_request.merged) steps: - uses: Shopify/shopify-cla-action@v1 with: github-token: ${{ secrets.GITHUB_TOKEN }} cla-token: ${{ secrets.CLA_TOKEN }} bootsnap-1.18.3/.gitignore000066400000000000000000000002141455645557500154430ustar00rootroot00000000000000/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ *.bundle *.so *.o *.a *.gem *.db mkmf.log .rubocop-* bootsnap-1.18.3/.rubocop.yml000066400000000000000000000060371455645557500157360ustar00rootroot00000000000000AllCops: Exclude: - 'vendor/**/*' - 'tmp/**/*' TargetRubyVersion: 2.6 NewCops: enable SuggestExtensions: false Bundler/OrderedGems: Enabled: false Gemspec/OrderedDependencies: Enabled: false Gemspec/RequireMFA: Enabled: false Gemspec/DuplicatedAssignment: Enabled: false Metrics/AbcSize: Enabled: false Metrics/MethodLength: Enabled: false Metrics/BlockLength: Enabled: false Metrics/ClassLength: Enabled: false Metrics/CyclomaticComplexity: Enabled: false Metrics/ModuleLength: Enabled: false Metrics/ParameterLists: Enabled: false Metrics/PerceivedComplexity: Enabled: false Naming/MethodName: Exclude: - 'test/**/*' Naming/RescuedExceptionsVariableName: PreferredName: error Naming/VariableNumber: Enabled: false Layout/MultilineMethodCallIndentation: EnforcedStyle: indented Layout/EndAlignment: EnforcedStyleAlignWith: start_of_line Layout/RescueEnsureAlignment: Enabled: false Layout/FirstHashElementIndentation: EnforcedStyle: consistent Layout/FirstArrayElementIndentation: EnforcedStyle: consistent Layout/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space Layout/SpaceAroundMethodCallOperator: Enabled: true Layout/LineLength: Max: 120 # This doesn't take into account retrying from an exception Lint/SuppressedException: Enabled: false Lint/NoReturnInBeginEndBlocks: Enabled: false # False positives... Lint/DuplicateBranch: Enabled: false Lint/AssignmentInCondition: AllowSafeAssignment: true Lint/UnusedMethodArgument: AllowUnusedKeywordArguments: true Lint/RaiseException: Enabled: true Lint/OrAssignmentToConstant: Enabled: false Lint/StructNewOverride: Enabled: true Security/MarshalLoad: Enabled: false Security/YAMLLoad: Enabled: false # allow String.new to create mutable strings Style/EmptyLiteral: Enabled: false Style/EmptyMethod: Enabled: false Style/FetchEnvVar: Enabled: false # allow the use of globals which makes sense in a CLI app like this Style/GlobalVars: Enabled: false Style/PercentLiteralDelimiters: Enabled: false Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: comma Style/TrailingCommaInArrayLiteral: EnforcedStyleForMultiline: comma Style/TrailingCommaInArguments: EnforcedStyleForMultiline: comma Style/SymbolArray: Enabled: false Style/StderrPuts: Enabled: false Style/ModuleFunction: Enabled: false Style/IfUnlessModifier: Enabled: false Style/GuardClause: Enabled: false Style/NumericPredicate: Enabled: false Style/Alias: EnforcedStyle: prefer_alias_method Style/Documentation: Enabled: false Style/DoubleNegation: Enabled: false Style/CommentedKeyword: Enabled: false Style/Next: Enabled: false Style/StringLiterals: EnforcedStyle: double_quotes Style/HashEachMethods: Enabled: true Style/HashTransformKeys: Enabled: true Style/HashTransformValues: Enabled: true Style/RedundantReturn: Enabled: false Style/YodaCondition: Enabled: false Style/ExponentialNotation: Enabled: true Style/OptionalBooleanParameter: Exclude: - '**/core_ext/**' bootsnap-1.18.3/CHANGELOG.md000066400000000000000000000331161455645557500152730ustar00rootroot00000000000000# Unreleased # 1.18.3 * Fix the cache corruption issue in the revalidation feature. See #474. The cache revalidation feature remains opt-in for now, until it is more battle tested. # 1.18.2 * Disable stale cache entries revalidation by default as it seems to cause cache corruption issues. See #471 and #474. Will be re-enabled in a future version once the root cause is identified. * Fix a potential compilation issue on some systems. See #470. # 1.18.1 * Handle `EPERM` errors when opening files with `O_NOATIME`. # 1.18.0 * `Bootsnap.instrumentation` now receive `:hit` events. * Add `Bootsnap.log_stats!` to print hit rate statistics on process exit. Can also be enabled with `BOOTSNAP_STATS=1`. * Revalidate stale cache entries by digesting the source content. This should significantly improve performance in environments where `mtime` isn't preserved (e.g. CI systems doing a git clone, etc). See #468. * Open source files and cache entries with `O_NOATIME` when available to reduce disk accesses. See #469. * `bootsnap precompile --gemfile` now look for `.rb` files in the whole gem and not just the `lib/` directory. See #466. # 1.17.1 * Fix a compatibility issue with the `prism` library that ships with Ruby 3.3. See #463. * Improved the `Kernel#require` decorator to not cause a method redefinition warning. See #461. # 1.17.0 * Ensure `$LOAD_PATH.dup` is Ractor shareable to fix an conflict with `did_you_mean`. * Allow to ignore directories using absolute paths. * Support YAML and JSON CompileCache on TruffleRuby. * Support LoadPathCache on TruffleRuby. # 1.16.0 * Use `RbConfig::CONFIG["rubylibdir"]` instead of `RbConfig::CONFIG["libdir"]` to check for stdlib files. See #431. * Fix the cached version of `YAML.load_file` being slightly more permissive than the default `Psych` one. See #434. `Date` and `Time` values are now properly rejected, as well as aliases. If this causes a regression in your application, it is recommended to load *trusted* YAML files with `YAML.unsafe_load_file`. # 1.15.0 * Add a readonly mode, for environments in which the updated cache wouldn't be persisted. See #428 and #423. # 1.14.0 * Require Ruby 2.6. * Add a way to skip directories during load path scanning. If you have large non-ruby directories in the middle of your load path, it can severely slow down scanning. Typically this is a problem with `node_modules`. See #277. * Fix `Bootsnap.unload_cache!`, it simply wouldn't work at all because of a merge mistake. See #421. # 1.13.0 * Stop decorating `Kernel.load`. This used to be very useful in development because the Rails "classic" autoloader was using `Kernel.load` in dev and `Kernel.require` in production. But Zeitwerk is now the default, and it doesn't use `Kernel.load` at all. People still using the classic autoloader might want to stick to `bootsnap 1.12`. * Add `Bootsnap.unload_cache!`. Applications can call it at the end of their boot sequence when they know no more code will be loaded to reclaim a bit of memory. # 1.12.0 * `bootsnap precompile` CLI will now also precompile `Rakefile` and `.rake` files. * Stop decorating `Module#autoload` as it was only useful for supporting Ruby 2.2 and older. * Remove `uname` and other platform specific version from the cache keys. `RUBY_PLATFORM + RUBY_REVISION` should be enough to ensure bytecode compatibility. This should improve caching for alpine based setups. See #409. # 1.11.1 * Fix the `can't modify frozen Hash` error on load path cache mutation. See #411. # 1.11.0 * Drop dependency on `fileutils`. * Better respect `Kernel#require` duck typing. While it almost never comes up in practice, `Kernel#require` follow a fairly intricate duck-typing protocol on its argument implemented as `rb_get_path(VALUE)` in MRI. So when applicable we bind `rb_get_path` and use it for improved compatibility. See #396 and #406. * Get rid of the `Kernel.require_relative` decorator by resolving `$LOAD_PATH` members to their real path. This way we handle symlinks in `$LOAD_PATH` much more efficiently. See #402 for the detailed explanation. * Drop support for Ruby 2.3 (to allow getting rid of the `Kernel.require_relative` decorator). # 1.10.3 * Fix Regexp and Date type support in YAML compile cache. (#400) * Improve the YAML compile cache to support `UTF-8` symbols. (#398, #399) [The default `MessagePack` symbol serializer assumes all symbols are ASCII](https://github.com/msgpack/msgpack-ruby/pull/211), because of this, non-ASCII compatible symbol would be restored with `ASCII_8BIT` encoding (AKA `BINARY`). Bootsnap now properly cache them in `UTF-8`. Note that the above only apply for actual YAML symbols (e..g `--- :foo`). The issue is still present for string keys parsed with `YAML.load_file(..., symbolize_names: true)`, that is a bug in `msgpack` that will hopefully be solved soon, see: https://github.com/msgpack/msgpack-ruby/pull/246 * Entirely disable the YAML compile cache if `Encoding.default_internal` is set to an encoding not supported by `msgpack`. (#398) `Psych` coerce strings to `Encoding.default_internal`, but `MessagePack` doesn't. So in this scenario we can't provide YAML caching at all without returning the strings in the wrong encoding. This never came up in practice but might as well be safe. # 1.10.2 * Reduce the `Kernel.require` extra stack frames some more. Now bootsnap should only add one extra frame per `require` call. * Better check `freeze` option support in JSON compile cache. Previously `JSON.load_file(..., freeze: true)` would be cached even when the msgpack version is missing support for it. # 1.10.1 * Fix `Kernel#autoload`'s fallback path always being executed. * Consider `unlink` failing with `ENOENT` as a success. # 1.10.0 * Delay requiring `FileUtils`. (#285) `FileUtils` can be installed as a gem, so it's best to wait for bundler to have setup the load path before requiring it. * Improve support of Psych 4. (#392) Since `1.8.0`, `YAML.load_file` was no longer cached when Psych 4 was used. This is because `load_file` loads in safe mode by default, so the Bootsnap cache could defeat that safety. Now when precompiling YAML files, Bootsnap first try to parse them in safe mode, and if it can't fallback to unsafe mode, and the cache contains a flag that records whether it was generated in safe mode or not. `YAML.unsafe_load_file` will use safe caches just fine, but `YAML.load_file` will fallback to uncached YAML parsing if the cache was generated using unsafe parsing. * Minimize the Kernel.require extra stack frames. (#393) This should reduce the noise generated by bootsnap on `LoadError`. # 1.9.4 * Ignore absolute paths in the loaded feature index. (#385) This fixes a compatibility issue with Zeitwerk when Zeitwerk is loaded before bootsnap. It also should reduce the memory usage and improve load performance of Zeitwerk managed files. * Automatically invalidate the load path cache whenever the Ruby version change. (#387) This is to avoid issues in case the same installation path is re-used for subsequent ruby patch releases. # 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 Psych 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 encountering various file system errors. The cache is now best effort, if somehow it can't be saved, bootsnap 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.18.3/CODE_OF_CONDUCT.md000066400000000000000000000062271455645557500162640ustar00rootroot00000000000000# 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.18.3/CONTRIBUTING.md000066400000000000000000000022311455645557500157050ustar00rootroot00000000000000# 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.18.3/Gemfile000066400000000000000000000005071455645557500147530ustar00rootroot00000000000000# 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 gem "bundler" gem "rake" gem "rake-compiler" gem "minitest", "~> 5.0" gem "mocha" group :development do gem "rubocop", "~> 1.50.2" # Ruby 2.6 support end bootsnap-1.18.3/LICENSE.txt000066400000000000000000000021001455645557500152720ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2017-present 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.18.3/README.md000066400000000000000000000313021455645557500147340ustar00rootroot00000000000000# 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` and `JSON`, 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/main/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 ignore_directories: ['node_modules'], # Directory names to skip. 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 compile_cache_json: true, # Compile JSON into a cache readonly: true, # Use the caches but don't update them on miss or stale entries. ) ``` **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. ### 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_READONLY` configure bootsnap to not update the cache on miss or stale entries. - `BOOTSNAP_LOG` configure bootsnap to log all caches misses to STDERR. - `BOOTSNAP_STATS` log hit rate statistics on exit. Can't be used if `BOOTSNAP_LOG` is enabled. - `BOOTSNAP_IGNORE_DIRECTORIES` a comma separated list of directories that shouldn't be scanned. Useful when you have large directories of non-ruby files inside `$LOAD_PATH`. It defaults to ignore any directory named `node_modules`. ### 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 `:hit`, `:miss`, `:stale` or `:revalidated`. 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). * `JSON.load_file` is modified to cache the result of loading a JSON object in MessagePack format ### 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/main/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 and JSON documents during our application boot, and that MessagePack and Marshal are *much* faster at deserialization than YAML and JSON, even with a fast implementation. We use the same strategy of compilation caching for YAML and JSON 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. * `compile_option`, which changes with `RubyVM::InstructionSequence.compile_option` does; * `ruby_revision`, A hash of `RUBY_REVISION`, the exact version of Ruby; * `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.18.3/Rakefile000066400000000000000000000007271455645557500151310ustar00rootroot00000000000000# 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.18.3/bin/000077500000000000000000000000001455645557500142265ustar00rootroot00000000000000bootsnap-1.18.3/bin/console000077500000000000000000000005651455645557500156240ustar00rootroot00000000000000#!/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.18.3/bin/setup000077500000000000000000000002031455645557500153070ustar00rootroot00000000000000#!/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.18.3/bin/test-minimal-support000077500000000000000000000002131455645557500202650ustar00rootroot00000000000000#!/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.18.3/bin/testunit000077500000000000000000000003201455645557500160260ustar00rootroot00000000000000#!/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.18.3/bootsnap.gemspec000066400000000000000000000024151455645557500166520ustar00rootroot00000000000000# frozen_string_literal: true lib = File.expand_path("lib", __dir__) $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/main/CHANGELOG.md", "source_code_uri" => "https://github.com/Shopify/bootsnap", "allowed_push_host" => "https://rubygems.org", } spec.files = `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.6.0" if RUBY_PLATFORM =~ /java/ spec.platform = "java" else spec.platform = Gem::Platform::RUBY spec.extensions = ["ext/bootsnap/extconf.rb"] end spec.add_runtime_dependency("msgpack", "~> 1.2") end bootsnap-1.18.3/dev.yml000066400000000000000000000002461455645557500147610ustar00rootroot00000000000000env: 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.18.3/exe/000077500000000000000000000000001455645557500142375ustar00rootroot00000000000000bootsnap-1.18.3/exe/bootsnap000077500000000000000000000001531455645557500160110ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require "bootsnap/cli" exit Bootsnap::CLI.new(ARGV).run bootsnap-1.18.3/ext/000077500000000000000000000000001455645557500142565ustar00rootroot00000000000000bootsnap-1.18.3/ext/bootsnap/000077500000000000000000000000001455645557500161035ustar00rootroot00000000000000bootsnap-1.18.3/ext/bootsnap/bootsnap.c000066400000000000000000001056001455645557500200760ustar00rootroot00000000000000/* * 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 #include #ifdef __APPLE__ // The symbol is present, however not in the headers // See: https://github.com/Shopify/bootsnap/issues/470 extern int fdatasync(int); #endif #ifndef O_NOATIME #define O_NOATIME 0 #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; // uint64_t digest; uint8_t digest_set; uint8_t pad[15]; } __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 = 5; /* 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_cBootsnap_CompileCache_UNCOMPILABLE; static ID instrumentation_method; static VALUE sym_hit, sym_miss, sym_stale, sym_revalidated; static bool instrumentation_enabled = false; static bool readonly = false; static bool revalidation = false; static bool perm_issue = false; /* Functions exposed as module functions on Bootsnap::CompileCache::Native */ static VALUE bs_instrumentation_enabled_set(VALUE self, VALUE enabled); static VALUE bs_readonly_set(VALUE self, VALUE enabled); static VALUE bs_revalidation_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 */ enum cache_status { miss, hit, stale, }; 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 enum cache_status cache_key_equal_fast_path(struct bs_cache_key * k1, struct bs_cache_key * k2); static int cache_key_equal_slow_path(struct bs_cache_key * current_key, struct bs_cache_key * cached_key, const VALUE input_data); static int update_cache_key(struct bs_cache_key *current_key, struct bs_cache_key *old_key, int cache_fd, const char ** errno_provenance); static void bs_cache_key_digest(struct bs_cache_key * key, const VALUE input_data); 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(const 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_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 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; } static VALUE bs_rb_get_path(VALUE self, VALUE fname) { return rb_get_path(fname); } /* * 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_define_singleton_method(rb_mBootsnap, "rb_get_path", bs_rb_get_path, 1); rb_mBootsnap_CompileCache = rb_define_module_under(rb_mBootsnap, "CompileCache"); rb_mBootsnap_CompileCache_Native = rb_define_module_under(rb_mBootsnap_CompileCache, "Native"); rb_cBootsnap_CompileCache_UNCOMPILABLE = rb_const_get(rb_mBootsnap_CompileCache, rb_intern("UNCOMPILABLE")); rb_global_variable(&rb_cBootsnap_CompileCache_UNCOMPILABLE); current_ruby_revision = get_ruby_revision(); current_ruby_platform = get_ruby_platform(); instrumentation_method = rb_intern("_instrument"); sym_hit = ID2SYM(rb_intern("hit")); sym_miss = ID2SYM(rb_intern("miss")); sym_stale = ID2SYM(rb_intern("stale")); sym_revalidated = ID2SYM(rb_intern("revalidated")); rb_define_module_function(rb_mBootsnap, "instrumentation_enabled=", bs_instrumentation_enabled_set, 1); rb_define_module_function(rb_mBootsnap_CompileCache_Native, "readonly=", bs_readonly_set, 1); rb_define_module_function(rb_mBootsnap_CompileCache_Native, "revalidation=", bs_revalidation_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; } static inline void bs_instrumentation(VALUE event, VALUE path) { if (RB_UNLIKELY(instrumentation_enabled)) { rb_funcall(rb_mBootsnap, instrumentation_method, 2, event, path); } } static VALUE bs_readonly_set(VALUE self, VALUE enabled) { readonly = RTEST(enabled); return enabled; } static VALUE bs_revalidation_set(VALUE self, VALUE enabled) { revalidation = 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; } 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. */ 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); return (uint32_t)(hash >> 32); } /* * 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 enum cache_status cache_key_equal_fast_path(struct bs_cache_key *k1, struct bs_cache_key *k2) { if (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) { if (k1->mtime == k2->mtime) { return hit; } if (revalidation) { return stale; } } return miss; } static int cache_key_equal_slow_path(struct bs_cache_key *current_key, struct bs_cache_key *cached_key, const VALUE input_data) { bs_cache_key_digest(current_key, input_data); return current_key->digest == cached_key->digest; } static int update_cache_key(struct bs_cache_key *current_key, struct bs_cache_key *old_key, int cache_fd, const char ** errno_provenance) { old_key->mtime = current_key->mtime; lseek(cache_fd, 0, SEEK_SET); ssize_t nwrite = write(cache_fd, old_key, KEY_SIZE); if (nwrite < 0) { *errno_provenance = "update_cache_key:write"; return -1; } #ifdef HAVE_FDATASYNC if (fdatasync(cache_fd) < 0) { *errno_provenance = "update_cache_key:fdatasync"; return -1; } #endif return 0; } /* * Fills the cache key digest. */ static void bs_cache_key_digest(struct bs_cache_key *key, const VALUE input_data) { if (key->digest_set) return; key->digest = fnv1a_64(input_data); key->digest_set = 1; } /* * 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); } static int bs_open_noatime(const char *path, int flags) { int fd = 1; if (!perm_issue) { fd = open(path, flags | O_NOATIME); if (fd < 0 && errno == EPERM) { errno = 0; perm_issue = true; } } if (perm_issue) { fd = open(path, flags); } return fd; } /* * Open the file we want to load/cache and generate a cache key for it if it * was loaded. */ static int open_current_file(const char * path, struct bs_cache_key * key, const char ** errno_provenance) { struct stat statbuf; int fd; fd = bs_open_noatime(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"; int previous_errno = errno; close(fd); errno = previous_errno; 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; key->digest_set = false; return fd; } #define ERROR_WITH_ERRNO -1 #define CACHE_MISS -2 #define CACHE_STALE -3 #define CACHE_UNCOMPILABLE -4 /* * 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; if (readonly || !revalidation) { fd = bs_open_noatime(path, O_RDONLY); } else { fd = bs_open_noatime(path, O_RDWR); } 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) { 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 = ERROR_WITH_ERRNO; goto done; } storage_data = rb_str_buf_new(data_size); nread = read(fd, RSTRING_PTR(storage_data), data_size); if (nread < 0) { *errno_provenance = "bs_fetch:fetch_cached_data:read"; ret = ERROR_WITH_ERRNO; goto done; } if (nread != data_size) { ret = CACHE_STALE; goto done; } rb_str_set_len(storage_data, nread); *exception_tag = bs_storage_to_output(handler, args, storage_data, output_data); if (*output_data == rb_cBootsnap_CompileCache_UNCOMPILABLE) { ret = CACHE_UNCOMPILABLE; goto done; } ret = 0; done: 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, returning a Ruby string on success and Qfalse on failure */ static VALUE bs_read_contents(int fd, size_t size, const char ** errno_provenance) { VALUE contents; ssize_t nread; contents = rb_str_buf_new(size); nread = read(fd, RSTRING_PTR(contents), size); if (nread < 0) { *errno_provenance = "bs_fetch:bs_read_contents:read"; return Qfalse; } else { rb_str_set_len(contents, nread); return contents; } } /* * 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; int cache_fd = -1, current_fd = -1; int res, valid_cache = 0, exception_tag = 0; const char * errno_provenance = NULL; VALUE status = Qfalse; VALUE input_data = Qfalse; /* 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 */ VALUE exception_message; /* ruby exception string to use instead of errno_provenance */ /* 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) { exception_message = path_v; 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. */ bs_instrumentation(cache_fd == CACHE_MISS ? sym_miss : sym_stale, path_v); } else if (cache_fd < 0) { exception_message = rb_str_new_cstr(cache_path); goto fail_errno; } else { /* True if the cache existed and no invalidating changes have occurred since * it was generated. */ switch(cache_key_equal_fast_path(¤t_key, &cached_key)) { case hit: status = sym_hit; valid_cache = true; break; case miss: valid_cache = false; break; case stale: valid_cache = false; if ((input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) { exception_message = path_v; goto fail_errno; } valid_cache = cache_key_equal_slow_path(¤t_key, &cached_key, input_data); if (valid_cache) { if (!readonly) { if (update_cache_key(¤t_key, &cached_key, cache_fd, &errno_provenance)) { exception_message = path_v; goto fail_errno; } } status = sym_revalidated; } break; }; if (!valid_cache) { status = sym_stale; } } 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_UNCOMPILABLE) { /* If fetch_cached_data returned `Uncompilable` we fallback to `input_to_output` This happens if we have say, an unsafe YAML cache, but try to load it in safe mode */ if (input_data == Qfalse && (input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) { exception_message = path_v; goto fail_errno; } bs_input_to_output(handler, args, input_data, &output_data, &exception_tag); if (exception_tag != 0) goto raise; goto succeed; } else if (res == CACHE_MISS || res == CACHE_STALE) valid_cache = 0; else if (res == ERROR_WITH_ERRNO){ exception_message = rb_str_new_cstr(cache_path); 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 (input_data == Qfalse && (input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) { exception_message = path_v; goto fail_errno; } /* 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 == rb_cBootsnap_CompileCache_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. */ bs_cache_key_digest(¤t_key, input_data); 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 == rb_cBootsnap_CompileCache_UNCOMPILABLE) { /* If storage_to_output returned `Uncompilable` we fallback to `input_to_output` */ bs_input_to_output(handler, args, input_data, &output_data, &exception_tag); if (exception_tag != 0) goto raise; } else if (NIL_P(output_data)) { /* If output_data is nil, delete the cache entry and generate the output * using input_to_output */ if (unlink(cache_path) < 0) { /* If the cache was already deleted, it might be that another process did it before us. * No point raising an error */ if (errno != ENOENT) { errno_provenance = "bs_fetch:unlink"; exception_message = rb_str_new_cstr(cache_path); 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 (status != Qfalse) bs_instrumentation(status, path_v); \ if (current_fd >= 0) close(current_fd); \ if (cache_fd >= 0) close(cache_fd); succeed: CLEANUP; return output_data; fail_errno: CLEANUP; if (errno_provenance) { exception_message = rb_str_concat( rb_str_new_cstr(errno_provenance), rb_str_concat(rb_str_new_cstr(": "), exception_message) ); } exception = rb_syserr_new_str(errno, exception_message); 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) { if (readonly) { return Qfalse; } struct bs_cache_key cached_key, current_key; int cache_fd = -1, current_fd = -1; int res, valid_cache = 0, exception_tag = 0; const char * errno_provenance = NULL; VALUE input_data = Qfalse; /* 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. */ switch(cache_key_equal_fast_path(¤t_key, &cached_key)) { case hit: valid_cache = true; break; case miss: valid_cache = false; break; case stale: valid_cache = false; if ((input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) { goto fail; } valid_cache = cache_key_equal_slow_path(¤t_key, &cached_key, input_data); if (valid_cache) { if (update_cache_key(¤t_key, &cached_key, cache_fd, &errno_provenance)) { goto fail; } } break; }; } 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 ((input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) goto fail; /* 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 == rb_cBootsnap_CompileCache_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 */ bs_cache_key_digest(¤t_key, input_data); res = atomic_write_cache_file(cache_path, ¤t_key, storage_data, &errno_provenance); if (res < 0) goto fail; goto succeed; #define CLEANUP \ 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 try_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(try_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 int bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * storage_data) { if (readonly) { *storage_data = rb_cBootsnap_CompileCache_UNCOMPILABLE; return 0; } else { int state; struct i2s_data i2s_data = { .handler = handler, .input_data = input_data, .pathval = pathval, }; *storage_data = rb_protect(try_input_to_storage, (VALUE)&i2s_data, &state); return state; } } bootsnap-1.18.3/ext/bootsnap/bootsnap.h000066400000000000000000000001401455645557500200740ustar00rootroot00000000000000#ifndef BOOTSNAP_H #define BOOTSNAP_H 1 /* doesn't expose anything */ #endif /* BOOTSNAP_H */ bootsnap-1.18.3/ext/bootsnap/extconf.rb000066400000000000000000000016041455645557500200770ustar00rootroot00000000000000# frozen_string_literal: true require "mkmf" if %w[ruby truffleruby].include?(RUBY_ENGINE) have_func "fdatasync", "unistd.h" unless RUBY_PLATFORM.match?(/mswin|mingw|cygwin/) append_cppflags ["-D_GNU_SOURCE"] # Needed of O_NOATIME end append_cflags ["-O3", "-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"]) append_cflags([ "-Wall", "-Werror", "-Wextra", "-Wpedantic", "-Wno-unused-parameter", # VALUE self has to be there but we don't care what it is. "-Wno-keyword-macro", # hiding return "-Wno-gcc-compat", # ruby.h 2.6.0 on macos 10.14, dunno "-Wno-compound-token-split-by-macro", ]) end create_makefile("bootsnap/bootsnap") else File.write("Makefile", dummy_makefile($srcdir).join) end bootsnap-1.18.3/lib/000077500000000000000000000000001455645557500142245ustar00rootroot00000000000000bootsnap-1.18.3/lib/bootsnap.rb000066400000000000000000000112001455645557500163700ustar00rootroot00000000000000# 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 def log_stats! stats = {hit: 0, revalidated: 0, miss: 0, stale: 0} self.instrumentation = ->(event, _path) { stats[event] += 1 } Kernel.at_exit do stats.each do |event, count| $stderr.puts "bootsnap #{event}: #{count}" end end end def log! self.logger = $stderr.method(:puts) end def logger=(logger) @logger = logger self.instrumentation = if logger.respond_to?(:debug) ->(event, path) { @logger.debug("[Bootsnap] #{event} #{path}") unless event == :hit } else ->(event, path) { @logger.call("[Bootsnap] #{event} #{path}") unless event == :hit } end end def instrumentation=(callback) @instrumentation = callback if respond_to?(:instrumentation_enabled=, true) self.instrumentation_enabled = !!callback end end def _instrument(event, path) @instrumentation.call(event, path) end def setup( cache_dir:, development_mode: true, load_path_cache: true, ignore_directories: nil, readonly: false, revalidation: false, compile_cache_iseq: true, compile_cache_yaml: true, compile_cache_json: true ) if load_path_cache Bootsnap::LoadPathCache.setup( cache_path: "#{cache_dir}/bootsnap/load-path-cache", development_mode: development_mode, ignore_directories: ignore_directories, readonly: readonly, ) end Bootsnap::CompileCache.setup( cache_dir: "#{cache_dir}/bootsnap/compile-cache", iseq: compile_cache_iseq, yaml: compile_cache_yaml, json: compile_cache_json, readonly: readonly, revalidation: revalidation, ) end def unload_cache! LoadPathCache.unload! end def 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 ignore_directories = if ENV.key?("BOOTSNAP_IGNORE_DIRECTORIES") ENV["BOOTSNAP_IGNORE_DIRECTORIES"].split(",") 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"], compile_cache_yaml: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"], compile_cache_json: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"], readonly: !!ENV["BOOTSNAP_READONLY"], ignore_directories: ignore_directories, ) if ENV["BOOTSNAP_LOG"] log! elsif ENV["BOOTSNAP_STATS"] log_stats! end end end if RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/ def absolute_path?(path) path[1] == ":" end else def absolute_path?(path) path.start_with?("/") end end # This is a semi-accurate ruby implementation of the native `rb_get_path(VALUE)` function. # The native version is very intricate and may behave differently on windows etc. # But we only use it for non-MRI platform. def rb_get_path(fname) path_path = fname.respond_to?(:to_path) ? fname.to_path : fname String.try_convert(path_path) || raise(TypeError, "no implicit conversion of #{path_path.class} into String") end # Allow the C extension to redefine `rb_get_path` without warning. alias_method :rb_get_path, :rb_get_path # rubocop:disable Lint/DuplicateMethods end end bootsnap-1.18.3/lib/bootsnap/000077500000000000000000000000001455645557500160515ustar00rootroot00000000000000bootsnap-1.18.3/lib/bootsnap/bundler.rb000066400000000000000000000004321455645557500200300ustar00rootroot00000000000000# 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.18.3/lib/bootsnap/cli.rb000066400000000000000000000175531455645557500171600ustar00rootroot00000000000000# 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 = cache_dir Bootsnap::CompileCache::YAML.init! Bootsnap::CompileCache::YAML.cache_dir = cache_dir Bootsnap::CompileCache::JSON.init! Bootsnap::CompileCache::JSON.cache_dir = 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 # Gems that include JSON or YAML files usually don't put them in `lib/`. # So we look at the gem root. # Similarly, gems that include Rails engines generally file Ruby files in `app/`. # However some gems embed their tests, they're very unlikely to be loaded, so not worth precompiling. gem_exclude = Regexp.union([exclude, "/spec/", "/test/", "/features/"].compact) gem_pattern = %r{^#{Regexp.escape(Bundler.bundle_path.to_s)}/?(?:bundler/)?gems/[^/]+} gem_paths = $LOAD_PATH.map { |p| p[gem_pattern] || p }.uniq precompile_ruby_files(gem_paths, exclude: gem_exclude) 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) && verbose $stderr.puts(yaml_file) 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) && verbose $stderr.puts(json_file) 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,*.rake,Rakefile}").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) && verbose $stderr.puts(ruby_file) 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 = <<~HELP Path to the bootsnap cache directory. Defaults to tmp/cache HELP opts.on("--cache-dir DIR", help.strip) do |dir| self.cache_dir = dir end help = <<~HELP Print precompiled paths. HELP opts.on("--verbose", "-v", help.strip) do self.verbose = true end help = <<~HELP Number of workers to use. Default to number of processors, set to 0 to disable multi-processing. HELP 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 = <<~HELP Precompile the gems in Gemfile HELP opts.on("--gemfile", help) { self.compile_gemfile = true } help = <<~HELP Path pattern to not precompile. e.g. --exclude 'aws-sdk|google-api' HELP opts.on("--exclude PATTERN", help) { |pattern| exclude_pattern(pattern) } help = <<~HELP Disable ISeq (.rb) precompilation. HELP opts.on("--no-iseq", help) { self.iseq = false } help = <<~HELP Disable YAML precompilation. HELP opts.on("--no-yaml", help) { self.yaml = false } help = <<~HELP Disable JSON precompilation. HELP opts.on("--no-json", help) { self.json = false } end end end end bootsnap-1.18.3/lib/bootsnap/cli/000077500000000000000000000000001455645557500166205ustar00rootroot00000000000000bootsnap-1.18.3/lib/bootsnap/cli/worker_pool.rb000066400000000000000000000056051455645557500215150ustar00rootroot00000000000000# 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.18.3/lib/bootsnap/compile_cache.rb000066400000000000000000000031731455645557500211550ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module CompileCache UNCOMPILABLE = BasicObject.new def UNCOMPILABLE.inspect "" end Error = Class.new(StandardError) def self.setup(cache_dir:, iseq:, yaml:, json:, readonly: false, revalidation: false) 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 if supported? && defined?(Bootsnap::CompileCache::Native) Bootsnap::CompileCache::Native.readonly = readonly Bootsnap::CompileCache::Native.revalidation = revalidation end end def self.supported? # only enable on 'ruby' (MRI) and TruffleRuby for POSIX (darwin, linux, *bsd), Windows (RubyInstaller2) %w[ruby truffleruby].include?(RUBY_ENGINE) && RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/) end end end bootsnap-1.18.3/lib/bootsnap/compile_cache/000077500000000000000000000000001455645557500206245ustar00rootroot00000000000000bootsnap-1.18.3/lib/bootsnap/compile_cache/iseq.rb000066400000000000000000000064351455645557500221220ustar00rootroot00000000000000# frozen_string_literal: true require "bootsnap/bootsnap" require "zlib" module Bootsnap module CompileCache module ISeq class << self attr_reader(:cache_dir) def cache_dir=(cache_dir) @cache_dir = cache_dir.end_with?("/") ? "#{cache_dir}iseq" : "#{cache_dir}-iseq" end def supported? CompileCache.supported? && defined?(RubyVM) end 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 return UNCOMPILABLE # syntax error end begin iseq.to_binary rescue TypeError UNCOMPILABLE # ruby bug #18250 end end else def self.input_to_storage(_, path) RubyVM::InstructionSequence.compile_file(path).to_binary rescue SyntaxError UNCOMPILABLE # syntax error end end def self.storage_to_output(binary, _args) RubyVM::InstructionSequence.load_from_binary(binary) rescue RuntimeError => error if error.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) 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 RuntimeError => error if error.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 if supported? def self.install!(cache_dir) Bootsnap::CompileCache::ISeq.cache_dir = cache_dir return unless supported? Bootsnap::CompileCache::ISeq.compile_option_updated class << RubyVM::InstructionSequence prepend(InstructionSequenceMixin) end end end end end bootsnap-1.18.3/lib/bootsnap/compile_cache/json.rb000066400000000000000000000044671455645557500221350ustar00rootroot00000000000000# frozen_string_literal: true require "bootsnap/bootsnap" module Bootsnap module CompileCache module JSON class << self attr_accessor(:msgpack_factory, :supported_options) attr_reader(:cache_dir) def cache_dir=(cache_dir) @cache_dir = cache_dir.end_with?("/") ? "#{cache_dir}json" : "#{cache_dir}-json" end def input_to_storage(payload, _) obj = ::JSON.parse(payload) msgpack_factory.dump(obj) end def storage_to_output(data, kwargs) if 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) 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 supports_freeze? self.supported_options = [:freeze] end supported_options.freeze end private def supports_freeze? ::JSON.parse('["foo"]', freeze: true).first.frozen? && MessagePack.load(MessagePack.dump("foo"), freeze: true).frozen? 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 ::Bootsnap::CompileCache::Native.fetch( Bootsnap::CompileCache::JSON.cache_dir, File.realpath(path), ::Bootsnap::CompileCache::JSON, kwargs, ) end ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true) end end end end bootsnap-1.18.3/lib/bootsnap/compile_cache/yaml.rb000066400000000000000000000241621455645557500221200ustar00rootroot00000000000000# frozen_string_literal: true require "bootsnap/bootsnap" module Bootsnap module CompileCache module YAML Uncompilable = Class.new(StandardError) UnsupportedTags = Class.new(Uncompilable) SUPPORTED_INTERNAL_ENCODINGS = [ nil, # UTF-8 Encoding::UTF_8, Encoding::ASCII, Encoding::BINARY, ].freeze class << self attr_accessor(:msgpack_factory, :supported_options) attr_reader(:implementation, :cache_dir) def cache_dir=(cache_dir) @cache_dir = cache_dir.end_with?("/") ? "#{cache_dir}yaml" : "#{cache_dir}-yaml" end def precompile(path) return false unless CompileCache::YAML.supported_internal_encoding? CompileCache::Native.precompile( cache_dir, path.to_s, @implementation, ) end def install!(cache_dir) self.cache_dir = cache_dir init! ::YAML.singleton_class.prepend(@implementation::Patch) end # Psych coerce strings to `Encoding.default_internal` but Message Pack only support # UTF-8, US-ASCII and BINARY. So if Encoding.default_internal is set to anything else # we can't safely use the cache def supported_internal_encoding? SUPPORTED_INTERNAL_ENCODINGS.include?(Encoding.default_internal) end module EncodingAwareSymbols extend self def unpack(payload) (+payload).force_encoding(Encoding::UTF_8).to_sym end end def init! require "yaml" require "msgpack" require "date" @implementation = ::YAML::VERSION >= "4" ? Psych4 : Psych3 if @implementation::Patch.method_defined?(:unsafe_load_file) && !::YAML.respond_to?(:unsafe_load_file) @implementation::Patch.send(:remove_method, :unsafe_load_file) end unless const_defined?(:NoTagsVisitor) visitor = Class.new(Psych::Visitors::NoAliasRuby) do def visit(target) if target.tag raise UnsupportedTags, "YAML tags are not supported: #{target.tag}" end super end end const_set(:NoTagsVisitor, visitor) 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, packer: :to_msgpack_ext, unpacker: EncodingAwareSymbols.method(:unpack).to_proc, ) 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]) supported_options << :symbolize_names end if params.include?([:key, :freeze]) && factory.load(factory.dump("yaml"), freeze: true).frozen? supported_options << :freeze end supported_options.freeze end def patch @implementation::Patch end def strict_load(payload) ast = ::YAML.parse(payload) return ast unless ast loader = ::Psych::ClassLoader::Restricted.new(["Symbol"], []) scanner = ::Psych::ScalarScanner.new(loader) NoTagsVisitor.new(scanner, loader).visit(ast) end end module Psych4 extend self def input_to_storage(contents, _) obj = SafeLoad.input_to_storage(contents, nil) if UNCOMPILABLE.equal?(obj) obj = UnsafeLoad.input_to_storage(contents, nil) end obj end module UnsafeLoad extend self def input_to_storage(contents, _) obj = ::YAML.unsafe_load(contents) packer = CompileCache::YAML.msgpack_factory.packer packer.pack(false) # not safe loaded begin packer.pack(obj) rescue NoMethodError, RangeError return UNCOMPILABLE # The object included things that we can't serialize end packer.to_s end def storage_to_output(data, kwargs) if kwargs&.key?(:symbolize_names) kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names) end unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs) unpacker.feed(data) _safe_loaded = unpacker.unpack unpacker.unpack end def input_to_output(data, kwargs) ::YAML.unsafe_load(data, **(kwargs || {})) end end module SafeLoad extend self def input_to_storage(contents, _) obj = begin CompileCache::YAML.strict_load(contents) rescue Psych::DisallowedClass, Psych::BadAlias, Uncompilable return UNCOMPILABLE end packer = CompileCache::YAML.msgpack_factory.packer packer.pack(true) # safe loaded begin packer.pack(obj) rescue NoMethodError, RangeError return UNCOMPILABLE end packer.to_s end def storage_to_output(data, kwargs) if kwargs&.key?(:symbolize_names) kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names) end unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs) unpacker.feed(data) safe_loaded = unpacker.unpack if safe_loaded unpacker.unpack else UNCOMPILABLE end end def input_to_output(data, kwargs) ::YAML.load(data, **(kwargs || {})) end end module Patch def load_file(path, *args) return super unless CompileCache::YAML.supported_internal_encoding? return super if args.size > 1 if (kwargs = args.first) return super unless kwargs.is_a?(Hash) return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty? end CompileCache::Native.fetch( CompileCache::YAML.cache_dir, File.realpath(path), CompileCache::YAML::Psych4::SafeLoad, kwargs, ) end ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true) def unsafe_load_file(path, *args) return super unless CompileCache::YAML.supported_internal_encoding? return super if args.size > 1 if (kwargs = args.first) return super unless kwargs.is_a?(Hash) return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty? end CompileCache::Native.fetch( CompileCache::YAML.cache_dir, File.realpath(path), CompileCache::YAML::Psych4::UnsafeLoad, kwargs, ) end ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true) end end module Psych3 extend self def input_to_storage(contents, _) obj = ::YAML.load(contents) packer = CompileCache::YAML.msgpack_factory.packer packer.pack(false) # not safe loaded begin packer.pack(obj) rescue NoMethodError, RangeError return UNCOMPILABLE # The object included things that we can't serialize end packer.to_s end def storage_to_output(data, kwargs) if kwargs&.key?(:symbolize_names) kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names) end unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs) unpacker.feed(data) _safe_loaded = unpacker.unpack unpacker.unpack end def input_to_output(data, kwargs) ::YAML.load(data, **(kwargs || {})) end module Patch def load_file(path, *args) return super unless CompileCache::YAML.supported_internal_encoding? return super if args.size > 1 if (kwargs = args.first) return super unless kwargs.is_a?(Hash) return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty? end CompileCache::Native.fetch( CompileCache::YAML.cache_dir, File.realpath(path), CompileCache::YAML::Psych3, kwargs, ) end ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true) def unsafe_load_file(path, *args) return super unless CompileCache::YAML.supported_internal_encoding? return super if args.size > 1 if (kwargs = args.first) return super unless kwargs.is_a?(Hash) return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty? end CompileCache::Native.fetch( CompileCache::YAML.cache_dir, File.realpath(path), CompileCache::YAML::Psych3, kwargs, ) end ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true) end end end end end bootsnap-1.18.3/lib/bootsnap/explicit_require.rb000066400000000000000000000027151455645557500217600ustar00rootroot00000000000000# 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.18.3/lib/bootsnap/load_path_cache.rb000066400000000000000000000046731455645557500214660ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap module LoadPathCache FALLBACK_SCAN = BasicObject.new DOT_RB = ".rb" DOT_SO = ".so" SLASH = "/" 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] @enabled = false class << self attr_reader(:load_path_cache, :loaded_features_index, :enabled) alias_method :enabled?, :enabled remove_method(:enabled) def setup(cache_path:, development_mode:, ignore_directories:, readonly: false) 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, readonly: readonly) @loaded_features_index = LoadedFeaturesIndex.new PathScanner.ignored_directories = ignore_directories if ignore_directories @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode) @enabled = true require_relative "load_path_cache/core_ext/kernel_require" require_relative "load_path_cache/core_ext/loaded_features" end def unload! @enabled = false @loaded_features_index = nil @realpath_cache = nil @load_path_cache = nil ChangeObserver.unregister($LOAD_PATH) if supported? end def supported? if RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/) case RUBY_ENGINE when "truffleruby" # https://github.com/oracle/truffleruby/issues/3131 RUBY_ENGINE_VERSION >= "23.1.0" when "ruby" true else false end end 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" end bootsnap-1.18.3/lib/bootsnap/load_path_cache/000077500000000000000000000000001455645557500211275ustar00rootroot00000000000000bootsnap-1.18.3/lib/bootsnap/load_path_cache/cache.rb000066400000000000000000000170651455645557500225300ustar00rootroot00000000000000# 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 TRUFFLERUBY_LIB_DIR_PREFIX = if RUBY_ENGINE == "truffleruby" "#{File.join(RbConfig::CONFIG['libdir'], 'truffle')}#{File::SEPARATOR}" end # { 'enumerator' => nil, 'enumerator.so' => nil, ... } BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features| if TRUFFLERUBY_LIB_DIR_PREFIX && feat.start_with?(TRUFFLERUBY_LIB_DIR_PREFIX) feat = feat.byteslice(TRUFFLERUBY_LIB_DIR_PREFIX.bytesize..-1) end # 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) reinitialize if (@has_relative_paths && dir_changed?) || stale? feature = feature.to_s.freeze return feature if Bootsnap.absolute_path?(feature) if feature.start_with?("./", "../") return expand_path(feature) end @mutex.synchronize do x = search_index(feature) return x if x # 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. return false 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. return FALLBACK_SCAN 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. return FALLBACK_SCAN if @development_mode 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(@path_obj, self) @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? p = p.to_realpath 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? p = p.to_realpath 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(feature) try_index(feature + DOT_RB) || try_index(feature + DLEXT) || try_index(feature + DLEXT2) || try_index(feature) end def maybe_append_extension(feature) try_ext(feature + DOT_RB) || try_ext(feature + DLEXT) || try_ext(feature + DLEXT2) || feature end else def search_index(feature) try_index(feature + DOT_RB) || try_index(feature + DLEXT) || try_index(feature) end def maybe_append_extension(feature) try_ext(feature + DOT_RB) || try_ext(feature + DLEXT) || feature 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(feature) if (path = @index[feature]) -File.join(path, feature).freeze end end else def try_index(feature) if (path = @index[feature]) -File.join(path, feature).untaint end end end else def try_index(feature) if (path = @index[feature]) File.join(path, feature) end end end def try_ext(feature) feature if File.exist?(feature) end end end end bootsnap-1.18.3/lib/bootsnap/load_path_cache/change_observer.rb000066400000000000000000000053571455645557500246220ustar00rootroot00000000000000# 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 def dup [] + self end alias_method :clone, :dup end def self.register(arr, observer) return if arr.frozen? # can't register observer, but no need to. arr.instance_variable_set(:@lpc_observer, observer) ArrayMixin.instance_methods.each do |method_name| arr.singleton_class.send(:define_method, method_name, ArrayMixin.instance_method(method_name)) end end def self.unregister(arr) return unless arr.instance_variable_defined?(:@lpc_observer) && arr.instance_variable_get(:@lpc_observer) ArrayMixin.instance_methods.each do |method_name| arr.singleton_class.send(:remove_method, method_name) end arr.instance_variable_set(:@lpc_observer, nil) end end end end bootsnap-1.18.3/lib/bootsnap/load_path_cache/core_ext/000077500000000000000000000000001455645557500227375ustar00rootroot00000000000000bootsnap-1.18.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb000066400000000000000000000025231455645557500263020ustar00rootroot00000000000000# frozen_string_literal: true module Kernel alias_method :require_without_bootsnap, :require alias_method :require, :require # Avoid method redefinition warnings def require(path) # rubocop:disable Lint/DuplicateMethods return require_without_bootsnap(path) unless Bootsnap::LoadPathCache.enabled? string_path = Bootsnap.rb_get_path(path) return false if Bootsnap::LoadPathCache.loaded_features_index.key?(string_path) resolved = Bootsnap::LoadPathCache.load_path_cache.find(string_path) if Bootsnap::LoadPathCache::FALLBACK_SCAN.equal?(resolved) if (cursor = Bootsnap::LoadPathCache.loaded_features_index.cursor(string_path)) ret = require_without_bootsnap(path) resolved = Bootsnap::LoadPathCache.loaded_features_index.identify(string_path, cursor) Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved) return ret else return require_without_bootsnap(path) end elsif false == resolved return false elsif resolved.nil? return require_without_bootsnap(path) else # Note that require registers to $LOADED_FEATURES while load does not. ret = require_without_bootsnap(resolved) Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved) return ret end end private :require end bootsnap-1.18.3/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb000066400000000000000000000010071455645557500264100ustar00rootroot00000000000000# 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.18.3/lib/bootsnap/load_path_cache/loaded_features_index.rb000066400000000000000000000130731455645557500257750ustar00rootroot00000000000000# 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)..] 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 def cursor(short) unless Bootsnap.absolute_path?(short.to_s) $LOADED_FEATURES.size end end def identify(short, cursor) $LOADED_FEATURES[cursor..].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 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 # `FALLBACK_SCAN` 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) return if Bootsnap.absolute_path?(short) 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 end private STRIP_EXTENSION = /\.[^.]*?$/.freeze 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?(feature) feature.to_s.end_with?(".rb", ".so", ".o", ".dll", ".dylib") end def strip_extension_if_elidable(feature) if extension_elidable?(feature) feature.sub(STRIP_EXTENSION, "") else feature end end end end end bootsnap-1.18.3/lib/bootsnap/load_path_cache/path.rb000066400000000000000000000076331455645557500224210ustar00rootroot00000000000000# 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, real: false) @path = path.to_s.freeze @real = real end def to_realpath return self if @real realpath = begin File.realpath(path) rescue Errno::ENOENT return self end if realpath == path @real = true self else Path.new(realpath, real: true) end end # True if the path exists, but represents a non-directory object def non_directory? !File.stat(path).directory? rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL 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 if @real path else @expanded_path ||= File.expand_path(path).freeze end 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, Errno::EINVAL -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 rubylibdir. RUBY_LIBDIR = RbConfig::CONFIG["rubylibdir"] RUBY_SITEDIR = RbConfig::CONFIG["sitedir"] def stability @stability ||= 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 bootsnap-1.18.3/lib/bootsnap/load_path_cache/path_scanner.rb000066400000000000000000000052301455645557500241210ustar00rootroot00000000000000# 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/.freeze BUNDLE_PATH = if Bootsnap.bundler? (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze else "" end @ignored_directories = %w(node_modules) class << self attr_accessor :ignored_directories 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) next if ignored_directories.include?(name) || ignored_directories.include?(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.18.3/lib/bootsnap/load_path_cache/store.rb000066400000000000000000000065701455645557500226200ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../explicit_require" Bootsnap::ExplicitRequire.with_gems("msgpack") { require "msgpack" } module Bootsnap module LoadPathCache class Store VERSION_KEY = "__bootsnap_ruby_version__" CURRENT_VERSION = "#{RUBY_REVISION}-#{RUBY_PLATFORM}".freeze # rubocop:disable Style/RedundantFreeze NestedTransactionError = Class.new(StandardError) SetOutsideTransactionNotAllowed = Class.new(StandardError) def initialize(store_path, readonly: false) @store_path = store_path @txn_mutex = Mutex.new @dirty = false @readonly = readonly load_data end def get(key) @data[key] end def fetch(key) raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned? v = get(key) unless v v = yield mark_for_mutation! @data[key] = v end v end def set(key, value) raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned? if value != @data[key] mark_for_mutation! @data[key] = value end end def transaction raise(NestedTransactionError) if @txn_mutex.owned? @txn_mutex.synchronize do yield ensure commit_transaction end end private def mark_for_mutation! @dirty = true @data = @data.dup if @data.frozen? end def commit_transaction if @dirty && !@readonly dump_data @dirty = false end end def load_data @data = begin data = File.open(@store_path, encoding: Encoding::BINARY) do |io| MessagePack.load(io, freeze: true) end if data.is_a?(Hash) && data[VERSION_KEY] == CURRENT_VERSION data else default_data end # handle malformed data due to upgrade incompatibility rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError default_data rescue ArgumentError => error if error.message =~ /negative array size/ default_data 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 * 100_000).to_i}.tmp" mkdir_p(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) end File.rename(tmp, @store_path) rescue Errno::EEXIST retry rescue SystemCallError end def default_data {VERSION_KEY => CURRENT_VERSION} end def mkdir_p(path) stack = [] until File.directory?(path) stack.push path path = File.dirname(path) end stack.reverse_each do |dir| Dir.mkdir(dir) rescue SystemCallError raise unless File.directory?(dir) end end end end end bootsnap-1.18.3/lib/bootsnap/setup.rb000066400000000000000000000001261455645557500175350ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../bootsnap" Bootsnap.default_setup bootsnap-1.18.3/lib/bootsnap/version.rb000066400000000000000000000001101455645557500200530ustar00rootroot00000000000000# frozen_string_literal: true module Bootsnap VERSION = "1.18.3" end bootsnap-1.18.3/shipit.rubygems.yml000066400000000000000000000000001455645557500173230ustar00rootroot00000000000000bootsnap-1.18.3/test/000077500000000000000000000000001455645557500144355ustar00rootroot00000000000000bootsnap-1.18.3/test/bundler_test.rb000066400000000000000000000022341455645557500174550ustar00rootroot00000000000000# 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.18.3/test/cli_test.rb000066400000000000000000000052751455645557500166010ustar00rootroot00000000000000# 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 skip_unless_iseq path = Help.set_file("a.rb", "a = a = 3", 100) CompileCache::ISeq.expects(:precompile).with(File.expand_path(path)) assert_equal 0, CLI.new(["precompile", "-j", "0", path]).run end def test_precompile_rake_files skip_unless_iseq path = Help.set_file("a.rake", "a = a = 3", 100) CompileCache::ISeq.expects(:precompile).with(File.expand_path(path)) assert_equal 0, CLI.new(["precompile", "-j", "0", path]).run end def test_precompile_rakefile skip_unless_iseq path = Help.set_file("Rakefile", "a = a = 3", 100) CompileCache::ISeq.expects(:precompile).with(File.expand_path(path)) assert_equal 0, CLI.new(["precompile", "-j", "0", path]).run end def test_no_iseq skip_unless_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 skip_unless_iseq 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)) CompileCache::ISeq.expects(:precompile).with(File.expand_path(path_b)) assert_equal 0, CLI.new(["precompile", "-j", "0", "foo"]).run end def test_precompile_exclude skip_unless_iseq 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)) 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)) 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 private def skip_unless_iseq skip("Unsupported platform") unless defined?(CompileCache::ISeq) && CompileCache::ISeq.supported? end end end bootsnap-1.18.3/test/compile_cache/000077500000000000000000000000001455645557500172105ustar00rootroot00000000000000bootsnap-1.18.3/test/compile_cache/iseq_cache_test.rb000066400000000000000000000005141455645557500226600ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class CompileCacheISeqTest < Minitest::Test include CompileCacheISeqHelper 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.18.3/test/compile_cache/json_test.rb000066400000000000000000000042041455645557500215450ustar00rootroot00000000000000# 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 skip("Unsupported platform") unless Bootsnap::CompileCache.supported? 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.18.3/test/compile_cache/yaml_test.rb000066400000000000000000000172621455645557500215460ustar00rootroot00000000000000# 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 skip("Unsupported platform") unless Bootsnap::CompileCache.supported? 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_strict_load_reject_dates error = assert_raises Psych::DisallowedClass do ::Bootsnap::CompileCache::YAML.strict_load(<<~YAML) --- :foo: 2023-01-20 YAML end assert_includes error.message, "Date" end def test_strict_load_reject_times error = assert_raises Psych::DisallowedClass do ::Bootsnap::CompileCache::YAML.strict_load(<<~YAML) --- :foo: 2023-01-20 13:18:31.083375000 -05:00 YAML end assert_includes error.message, "Time" end def test_strict_load_reject_aliases assert_raises Psych::BadAlias do ::Bootsnap::CompileCache::YAML.strict_load(<<~YAML) --- foo: &foo a: b bar: <<: *foo YAML end end def test_yaml_tags error = assert_raises Bootsnap::CompileCache::YAML::UnsupportedTags do ::Bootsnap::CompileCache::YAML.strict_load("!many Boolean") end assert_equal "YAML tags are not supported: !many", error.message error = assert_raises Bootsnap::CompileCache::YAML::UnsupportedTags do ::Bootsnap::CompileCache::YAML.strict_load("!ruby/object {}") end assert_equal "YAML tags are not supported: !ruby/object", error.message end def test_symbols_encoding symbols = [:ascii, :utf8_fée] Help.set_file("a.yml", YAML.dump(symbols), 100) loaded_symbols = FakeYaml.load_file("a.yml") assert_equal(symbols, loaded_symbols) assert_equal(symbols.map(&:encoding), loaded_symbols.map(&:encoding)) end def test_custom_symbols_encoding sym = "壁に耳あり、障子に目あり".to_sym # rubocop:disable Lint/SymbolConversion Help.set_file("a.yml", YAML.dump(sym), 100) # YAML is limited to UTF-8 and UTF-16 by spec, but Psych does respect Encoding.default_internal # so strings and symbol can actually be of any encoding. assert_raises FakeYaml::Fallback do with_default_encoding_internal(Encoding::EUC_JP) do FakeYaml.load_file("a.yml") end end end if YAML::VERSION >= "4" def test_load_psych_4_with_alias Help.set_file("a.yml", "foo: &foo\n bar: 42\nplop:\n <<: *foo", 100) foo = {"bar" => 42} expected = {"foo" => foo, "plop" => foo} assert_equal(expected, FakeYaml.unsafe_load_file("a.yml")) assert_raises Psych::BadAlias do FakeYaml.load_file("a.yml") end end def test_load_psych_4_with_unsafe_class Help.set_file("a.yml", "---\nfoo: !ruby/regexp /bar/\n", 100) expected = {"foo" => /bar/} assert_equal(expected, FakeYaml.unsafe_load_file("a.yml")) assert_raises Psych::DisallowedClass do FakeYaml.load_file("a.yml") end end def test_yaml_input_to_output_safe document = ::Bootsnap::CompileCache::YAML::Psych4::SafeLoad.input_to_output(<<~YAML, {}) --- :foo: 42 bar: [1] YAML expected = { foo: 42, "bar" => [1], } assert_equal expected, document end def test_yaml_input_to_output_unsafe document = ::Bootsnap::CompileCache::YAML::Psych4::UnsafeLoad.input_to_output(<<~YAML, {}) --- :foo: 42 bar: [1] YAML expected = { foo: 42, "bar" => [1], } assert_equal expected, document end else def test_yaml_input_to_output document = ::Bootsnap::CompileCache::YAML::Psych3.input_to_output(<<~YAML, {}) --- :foo: 42 bar: [1] YAML expected = { foo: 42, "bar" => [1], } assert_equal expected, document end 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 def test_precompile_regexp Help.set_file("a.yml", ::YAML.dump(foo: /bar/), 100) assert Bootsnap::CompileCache::YAML.precompile("a.yml") end def test_precompile_date Help.set_file("a.yml", ::YAML.dump(Date.today), 100) assert Bootsnap::CompileCache::YAML.precompile("a.yml") end def test_precompile_object Help.set_file("a.yml", ::YAML.dump(Object.new), 100) refute Bootsnap::CompileCache::YAML.precompile("a.yml") 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 def test_unsafe_load_file_supports_regexp Help.set_file("a.yml", ::YAML.dump(foo: /bar/), 100) assert_equal({foo: /bar/}, FakeYaml.unsafe_load_file("a.yml")) end end def test_no_read_permission if RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/ # On windows removing read permission doesn't prevent reading. pass else file = "a.yml" Help.set_file(file, ::YAML.dump(Object.new), 100) FileUtils.chmod(0o000, file) exception = assert_raises(Errno::EACCES) do FakeYaml.load_file(file) end assert_match(file, exception.message) end end private def with_default_encoding_internal(encoding) original_internal = Encoding.default_internal $VERBOSE = false Encoding.default_internal = encoding $VERBOSE = true begin yield ensure $VERBOSE = false Encoding.default_internal = original_internal $VERBOSE = true end end end bootsnap-1.18.3/test/compile_cache_handler_errors_test.rb000066400000000000000000000054751455645557500237000ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class CompileCacheHandlerErrorsTest < Minitest::Test include CompileCacheISeqHelper 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).returns(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).returns(Bootsnap::CompileCache::UNCOMPILABLE) Bootsnap::CompileCache::ISeq.expects(:input_to_output).raises(klass, "oops") assert_raises(klass) { load(path) } end end bootsnap-1.18.3/test/compile_cache_key_format_test.rb000066400000000000000000000062711455645557500230220ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" require "tempfile" require "tmpdir" require "fileutils" class CompileCacheKeyFormatTest < Minitest::Test FILE = File.expand_path(__FILE__) include CompileCacheISeqHelper 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, }.freeze def teardown Bootsnap::CompileCache::Native.revalidation = false super end def test_key_version key = cache_key_for_file(FILE) exp = [5].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]].unpack1("Q") 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]].unpack1("Q") assert_equal(exp, act) end def test_fetch target = Help.set_file("a.rb", "foo = 1") cache_dir = File.join(@tmp_dir, "compile_cache") actual = Bootsnap::CompileCache::Native.fetch(cache_dir, target, TestHandler, nil) assert_equal("NEATO #{target.upcase}", actual) entries = Dir["#{cache_dir}/**/*"].select { |f| File.file?(f) } assert_equal 1, entries.size cache_file = entries.first data = File.read(cache_file) assert_equal("neato #{target}", data.force_encoding(Encoding::BINARY)[64..]) actual = Bootsnap::CompileCache::Native.fetch(cache_dir, target, TestHandler, nil) assert_equal("NEATO #{target.upcase}", actual) end def test_revalidation Bootsnap::CompileCache::Native.revalidation = true cache_dir = File.join(@tmp_dir, "compile_cache") target = Help.set_file("a.rb", "foo = 1") actual = Bootsnap::CompileCache::Native.fetch(cache_dir, target, TestHandler, nil) assert_equal("NEATO #{target.upcase}", actual) 10.times do FileUtils.touch(target, mtime: File.mtime(target) + 42) actual = Bootsnap::CompileCache::Native.fetch(cache_dir, target, TestHandler, nil) assert_equal("NEATO #{target.upcase}", actual) end 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.18.3/test/compile_cache_test.rb000066400000000000000000000207441455645557500206030ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class CompileCacheTest < Minitest::Test include CompileCacheISeqHelper include TmpdirHelper def teardown super Bootsnap::CompileCache::Native.readonly = false Bootsnap::CompileCache::Native.revalidation = false Bootsnap.instrumentation = nil end 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 addition 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 = a = 3", 100) folder = File.dirname(Help.cache_path(@tmp_dir, path)) FileUtils.mkdir_p(folder) FileUtils.chmod(0o400, folder) load(path) end end def test_no_read_permission if RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/ # On windows removing read permission doesn't prevent reading. pass else path = Help.set_file("a.rb", "a = a = 3", 100) FileUtils.chmod(0o000, path) exception = assert_raises(LoadError) do load(path) end assert_match(path, exception.message) 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(0o400, 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_dont_store_cache_after_a_miss_when_readonly Bootsnap::CompileCache::Native.readonly = true path = Help.set_file("a.rb", "a = a = 3", 100) output = RubyVM::InstructionSequence.compile_file(path) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).never Bootsnap::CompileCache::ISeq.expects(:storage_to_output).never Bootsnap::CompileCache::ISeq.expects(:input_to_output).once.returns(output) load(path) end def test_dont_store_cache_after_a_stale_when_readonly path = Help.set_file("a.rb", "a = a = 3", 100) load(path) Bootsnap::CompileCache::Native.readonly = true output = RubyVM::InstructionSequence.compile_file(path) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).never Bootsnap::CompileCache::ISeq.expects(:storage_to_output).once.returns(output) Bootsnap::CompileCache::ISeq.expects(:input_to_output).never load(path) end def test_revalidation Bootsnap::CompileCache::Native.revalidation = true file_path = Help.set_file("a.rb", "a = a = 3", 100) load(file_path) calls = [] Bootsnap.instrumentation = ->(event, path) { calls << [event, path] } 5.times do FileUtils.touch("a.rb", mtime: File.mtime("a.rb") + 42) load(file_path) load(file_path) end assert_equal [[:revalidated, "a.rb"], [:hit, "a.rb"]] * 5, calls end def test_dont_revalidate_when_readonly Bootsnap::CompileCache::Native.revalidation = true path = Help.set_file("a.rb", "a = a = 3", 100) load(path) entries = Dir["#{Bootsnap::CompileCache::ISeq.cache_dir}/**/*"].select { |f| File.file?(f) } assert_equal 1, entries.size cache_entry = entries.first old_cache_content = File.binread(cache_entry) Bootsnap::CompileCache::Native.readonly = true output = RubyVM::InstructionSequence.compile_file(path) Bootsnap::CompileCache::ISeq.expects(:input_to_storage).never Bootsnap::CompileCache::ISeq.expects(:storage_to_output).once.returns(output) Bootsnap::CompileCache::ISeq.expects(:input_to_output).never FileUtils.touch(path, mtime: File.mtime(path) + 50) calls = [] Bootsnap.instrumentation = ->(event, source_path) { calls << [event, source_path] } load(path) assert_equal [[:revalidated, "a.rb"]], calls new_cache_content = File.binread(cache_entry) assert_equal old_cache_content, new_cache_content, "Cache entry was mutated" end def test_invalid_cache_file path = Help.set_file("a.rb", "a = a = 3", 100) cp = Help.cache_path("#{@tmp_dir}-iseq", 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 file_path = Help.set_file("a.rb", "a = a = 3", 100) load(file_path) calls = [] Bootsnap.instrumentation = ->(event, path) { calls << [event, path] } load(file_path) assert_equal [[:hit, "a.rb"]], calls end def test_instrumentation_miss file_path = Help.set_file("a.rb", "a = a = 3", 100) calls = [] Bootsnap.instrumentation = ->(event, path) { calls << [event, path] } load(file_path) assert_equal [[:miss, "a.rb"]], calls end def test_instrumentation_revalidate Bootsnap::CompileCache::Native.revalidation = true file_path = Help.set_file("a.rb", "a = a = 3", 100) load(file_path) FileUtils.touch("a.rb", mtime: File.mtime("a.rb") + 42) calls = [] Bootsnap.instrumentation = ->(event, path) { calls << [event, path] } load(file_path) assert_equal [[:revalidated, "a.rb"]], calls end def test_instrumentation_stale file_path = Help.set_file("a.rb", "a = a = 3", 100) load(file_path) file_path = Help.set_file("a.rb", "a = a = 4", 101) calls = [] Bootsnap.instrumentation = ->(event, path) { calls << [event, path] } load(file_path) assert_equal [[:stale, "a.rb"]], calls end end bootsnap-1.18.3/test/helper_test.rb000066400000000000000000000005261455645557500173030ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class HelperTest < Minitest::Test include CompileCacheISeqHelper include TmpdirHelper def test_validate_cache_path path = Help.set_file("a.rb", "a = a = 3", 100) cp = Help.cache_path("#{@tmp_dir}-iseq", path) load(path) assert_equal(true, File.file?(cp)) end end bootsnap-1.18.3/test/integration/000077500000000000000000000000001455645557500167605ustar00rootroot00000000000000bootsnap-1.18.3/test/integration/kernel_test.rb000066400000000000000000000141401455645557500216240ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" module Bootsnap class KernelTest < Minitest::Test include LoadPathCacheHelper include TmpdirHelper def test_require_symlinked_file_twice skip("https://github.com/oracle/truffleruby/issues/3138") if truffleruby? setup_symlinked_files if RUBY_VERSION >= "3.1" # Fixed in https://github.com/ruby/ruby/commit/79a4484a072e9769b603e7b4fbdb15b1d7eccb15 (Ruby 3.1) assert_both_pass(<<~RUBY) require "symlink/test" require "real/test" RUBY else assert_both_pass(<<~RUBY) require "symlink/test" begin require "real/test" rescue RuntimeError exit 0 else exit 1 end RUBY end end def test_require_symlinked_file_twice_aliased setup_symlinked_files assert_both_pass(<<~RUBY) $LOAD_PATH.unshift(File.expand_path("symlink")) require "test" $LOAD_PATH.unshift(File.expand_path("a")) require "test" RUBY end def test_require_relative_symlinked_file_twice skip("https://github.com/oracle/truffleruby/issues/3138") if truffleruby? setup_symlinked_files if RUBY_VERSION >= "3.1" # Fixed in https://github.com/ruby/ruby/commit/79a4484a072e9769b603e7b4fbdb15b1d7eccb15 (Ruby 3.1) assert_both_pass(<<~RUBY) require_relative "symlink/test" require_relative "real/test" RUBY else assert_both_pass(<<~RUBY) require_relative "symlink/test" begin require_relative "real/test" rescue RuntimeError exit 0 else exit 1 end RUBY end end def test_require_and_then_require_relative_symlinked_file setup_symlinked_files assert_both_pass(<<~RUBY) $LOAD_PATH.unshift(File.expand_path("symlink")) require "test" require_relative "real/test" RUBY end def test_require_relative_and_then_require_symlinked_file setup_symlinked_files assert_both_pass(<<~RUBY) require_relative "real/test" $LOAD_PATH.unshift(File.expand_path("symlink")) require "test" RUBY end def test_require_deep_symlinked_file_twice skip("https://github.com/oracle/truffleruby/issues/3138") if truffleruby? setup_symlinked_files if RUBY_VERSION >= "3.1" # Fixed in https://github.com/ruby/ruby/commit/79a4484a072e9769b603e7b4fbdb15b1d7eccb15 (Ruby 3.1) assert_both_pass(<<~RUBY) require "symlink/dir/deep" require "real/dir/deep" RUBY else assert_both_pass(<<~RUBY) require "symlink/dir/deep" begin require "real/dir/deep" rescue RuntimeError exit 0 else exit 1 end RUBY end end def test_require_deep_symlinked_file_twice_aliased setup_symlinked_files assert_both_pass(<<~RUBY) $LOAD_PATH.unshift(File.expand_path("symlink")) require "dir/deep" $LOAD_PATH.unshift(File.expand_path("a")) require "dir/deep" RUBY end def test_require_relative_deep_symlinked_file_twice skip("https://github.com/oracle/truffleruby/issues/3138") if truffleruby? setup_symlinked_files if RUBY_VERSION >= "3.1" # Fixed in https://github.com/ruby/ruby/commit/79a4484a072e9769b603e7b4fbdb15b1d7eccb15 (Ruby 3.1) assert_both_pass(<<~RUBY) require_relative "symlink/dir/deep" require_relative "real/dir/deep" RUBY else assert_both_pass(<<~RUBY) require_relative "symlink/dir/deep" begin require_relative "real/dir/deep" rescue RuntimeError exit 0 else exit 1 end RUBY end end def test_require_and_then_require_relative_deep_symlinked_file setup_symlinked_files assert_both_pass(<<~RUBY) $LOAD_PATH.unshift(File.expand_path("symlink")) require "dir/deep" require_relative "real/dir/deep" RUBY end def test_require_relative_and_then_require_deep_symlinked_file setup_symlinked_files assert_both_pass(<<~RUBY) require_relative "real/dir/deep" $LOAD_PATH.unshift(File.expand_path("symlink")) require "dir/deep" RUBY end private def assert_both_pass(source) Help.set_file("without_bootsnap.rb", source) unless execute("without_bootsnap.rb", "debug.txt") flunk "expected snippet to pass WITHOUT bootsnap enabled:\n#{debug_output}" end Help.set_file("with_bootsnap.rb", %{require "bootsnap/setup"\n#{source}}) unless execute("with_bootsnap.rb", "debug.txt") flunk "expected snippet to pass WITH bootsnap enabled:\n#{debug_output}" end end def debug_output File.read("debug.txt") rescue Errno::ENOENT end def execute(script_path, output_path) system( {"BOOTSNAP_CACHE_DIR" => "tmp/cache"}, RbConfig.ruby, "-I.", script_path, out: output_path, err: output_path ) end def assert_successful(source) Help.set_file("test_case.rb", source) Help.set_file("test_case.rb", %{require "bootsnap/setup"\n#{source}}) assert system({"BOOTSNAP_CACHE_DIR" => "tmp/cache"}, RbConfig.ruby, "-Ilib:.", "test_case.rb") end def setup_symlinked_files skip("Platform doesn't support symlinks") unless File.respond_to?(:symlink) Help.set_file("real/test.rb", <<-RUBY) if $test_already_required raise "test.rb required more than once" else $test_already_required = true end RUBY Help.set_file("real/dir/deep.rb", <<-RUBY) if $deep_already_required raise "deep.rb required more than once" else $deep_already_required = true end RUBY File.symlink("real", "symlink") end def truffleruby? RUBY_ENGINE == "truffleruby" end end end bootsnap-1.18.3/test/load_path_cache/000077500000000000000000000000001455645557500175135ustar00rootroot00000000000000bootsnap-1.18.3/test/load_path_cache/cache_test.rb000066400000000000000000000162531455645557500221510ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" module Bootsnap module LoadPathCache class CacheTest < Minitest::Test include LoadPathCacheHelper def setup super @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_equal false, cache.find("thread") assert_equal false, cache.find("thread.rb") assert_equal false, cache.find("enumerator") if truffleruby? assert_equal false, cache.find("enumerator.rb") else assert_equal false, cache.find("enumerator.so") if RUBY_PLATFORM =~ /darwin/ assert_equal false, cache.find("enumerator.bundle") else assert_same FALLBACK_SCAN, cache.find("enumerator.bundle") end end bundle = RUBY_PLATFORM =~ /darwin/ ? "bundle" : "so" # These are not present in TruffleRuby but that means they will still return falsey. 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")) cache.push_paths(po, @dir2) assert_equal("#{@dir2}/b.rb", cache.find("b")) 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.rb", cache.find("../#{dir1_basename}/a")) assert_equal("#{@dir1}/dl#{DLEXT}", cache.find("./dl")) assert_equal("#{@dir1}/enoent", cache.find("./enoent")) end end def test_unshifted_paths_have_higher_precedence po = [@dir1] cache = Cache.new(NullCache, po) assert_equal("#{@dir1}/conflict.rb", cache.find("conflict")) cache.unshift_paths(po, @dir2) assert_equal("#{@dir2}/conflict.rb", cache.find("conflict")) end def test_pushed_paths_have_lower_precedence po = [@dir1] cache = Cache.new(NullCache, po) assert_equal("#{@dir1}/conflict.rb", cache.find("conflict")) cache.push_paths(po, @dir2) assert_equal("#{@dir1}/conflict.rb", cache.find("conflict")) 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")) assert_equal("#{@dir1}/dl#{DLEXT}", cache.find("dl#{DLEXT}")) assert_equal("#{@dir1}/both.rb", cache.find("both")) assert_equal("#{@dir1}/both.rb", cache.find("both.rb")) assert_equal("#{@dir1}/both#{DLEXT}", cache.find("both#{DLEXT}")) 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_same Bootsnap::LoadPathCache::FALLBACK_SCAN, dev_yes_cache.find("new") 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 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 unless Encoding.default_external == Encoding::UTF_8 # Encoding.default_external != Encoding::UTF_8 is likely a misconfiguration or a barebone system. # Supporting this use case would have an overhead for relatively little gain. skip "Encoding.default_external == #{Encoding.default_external}, expected Encoding::UTF_8." end 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) # TruffleRuby object is a copy and the encoding resets to utf-8. assert_equal(OS_ASCII_PATH_ENCODING, path.encoding) unless truffleruby? File.write(path, "") if truffleruby? assert_equal path, internal_path else assert_same path, internal_path end utf8_path = cache.find("béé") assert_equal("#{@dir1}/béé.rb", utf8_path) require utf8_path internal_utf8_path = $LOADED_FEATURES.last assert_equal(Encoding::UTF_8, internal_utf8_path.encoding) assert_equal(Encoding::UTF_8, utf8_path.encoding) File.write(utf8_path, "") if truffleruby? assert_equal utf8_path, internal_utf8_path else assert_same utf8_path, internal_utf8_path end end end private def truffleruby? RUBY_ENGINE == "truffleruby" end end end end bootsnap-1.18.3/test/load_path_cache/change_observer_test.rb000066400000000000000000000054121455645557500242350ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" module Bootsnap module LoadPathCache class ChangeObserverTest < Minitest::Test include LoadPathCacheHelper def setup super @observer = Object.new @observer.instance_variable_set(:@mutex, Mutex.new) @arr = [] ChangeObserver.register(@arr, @observer) 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.push("h", "i") @observer.expects(:unshift_paths).with(@arr, "j", "k") @arr.prepend("j", "k") end def test_unregister @observer.expects(:push_paths).never @observer.expects(:unshift_paths).never @observer.expects(:reinitialize).never ChangeObserver.unregister(@arr) @arr << "a" @arr.push("b", "c") @arr.append("d", "e") @arr.unshift("f", "g") @arr.push("h", "i") @arr.prepend("j", "k") @arr.delete(3) @arr.compact! @arr.map!(&:upcase) assert_equal %w(J K F G A B C D E H I), @arr 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(@arr.dup.freeze, @observer) end def test_register_twice_observes_once ChangeObserver.register(@arr, @observer) @observer.expects(:push_paths).with(@arr, "a").once @arr << "a" assert_equal(%w(a), @arr) end def test_dup_returns_ractor_shareable_instance return unless defined?(Ractor) ChangeObserver.register(@arr, @observer) Ractor.make_shareable(@arr.dup.freeze) end def test_clone_returns_ractor_shareable_instance return unless defined?(Ractor) ChangeObserver.register(@arr, @observer) Ractor.make_shareable(@arr.clone.freeze) end def test_uniq_without_block @observer.expects(:reinitialize).never @arr.uniq! end def test_uniq_with_block @observer.expects(:reinitialize).once @arr.uniq! { |i| i } end end end end bootsnap-1.18.3/test/load_path_cache/core_ext/000077500000000000000000000000001455645557500213235ustar00rootroot00000000000000bootsnap-1.18.3/test/load_path_cache/core_ext/kernel_require_test.rb000066400000000000000000000050751455645557500257320ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" module Bootsnap class KernelRequireTest < Minitest::Test include LoadPathCacheHelper def test_uses_the_same_duck_type_as_require skip("Need a working Process.fork to test in isolation") unless Process.respond_to?(:fork) begin assert_nil LoadPathCache.load_path_cache cache = Tempfile.new("cache") pid = Process.fork do LoadPathCache.setup(cache_path: cache.path, development_mode: true, ignore_directories: nil) dir = File.realpath(Dir.mktmpdir) $LOAD_PATH.push(dir) FileUtils.touch("#{dir}/a.rb") stringish = mock stringish.expects(:to_str).returns("a").at_least(2) # bootsnap + ruby pathish = mock pathish.expects(:to_path).returns(stringish).at_least(2) # bootsnap + ruby assert pathish.respond_to?(:to_path) require(pathish) FileUtils.rm_rf(dir) end _, status = Process.wait2(pid) assert_predicate status, :success? ensure cache.close cache.unlink end end def test_load_static_libaries skip("Need a working Process.fork to test in isolation") unless Process.respond_to?(:fork) skip("Need some libraries to be compiled statically") unless RUBY_VERSION >= "3.3" begin assert_nil LoadPathCache.load_path_cache cache = Tempfile.new("cache") pid = Process.fork do LoadPathCache.setup(cache_path: cache.path, development_mode: false, ignore_directories: nil) require("prism") end _, status = Process.wait2(pid) assert_predicate status, :success? ensure cache.close cache.unlink end end end class KernelLoadTest < Minitest::Test include TmpdirHelper def setup super @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.binwrite("#{@dir2}/loads.rb", "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) super 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 bootsnap-1.18.3/test/load_path_cache/loaded_features_index_test.rb000066400000000000000000000115001455645557500254110ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" module Bootsnap module LoadPathCache class LoadedFeaturesIndexTest < Minitest::Test include LoadPathCacheHelper def setup super @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_infer_base_from_ext refute(@index.key?("bundler")) refute(@index.key?("bundler.rb")) refute(@index.key?("foo")) @index.register("bundler.rb", nil) 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", nil) 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", nil) 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", nil) 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")) cursor = @index.cursor("bundler") $LOADED_FEATURES << "/a/b/bundler.rb" long = @index.identify("bundler", cursor) @index.register("bundler", long) 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_ignores_absolute_paths path = "#{Dir.mktmpdir}/bundler.rb" assert_nil @index.cursor(path) @index.register(path, path) refute(@index.key?(path)) end end end end bootsnap-1.18.3/test/load_path_cache/path_scanner_test.rb000066400000000000000000000025031455645557500235440ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" module Bootsnap module LoadPathCache class PathScannerTest < Minitest::Test include LoadPathCacheHelper 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.18.3/test/load_path_cache/path_test.rb000066400000000000000000000115241455645557500220360ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" require "bootsnap/load_path_cache" module Bootsnap module LoadPathCache class PathTest < Minitest::Test include LoadPathCacheHelper def setup super @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['rubylibdir']}/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(path.expanded_path).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(path.expanded_path).returns([100, entries, dirs]) @cache.expects(:set).with(path.expanded_path, [101, entries, dirs]) # next read doesn't regen @cache.expects(:get).with(path.expanded_path).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(path.expanded_path).returns(nil) @cache.expects(:set).with(path.expanded_path, [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(path.expanded_path).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.18.3/test/load_path_cache/store_test.rb000066400000000000000000000061731455645557500222420ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" require "tmpdir" require "fileutils" module Bootsnap module LoadPathCache class StoreTest < Minitest::Test include LoadPathCacheHelper def setup super @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_modification_of_loaded_store store.transaction { store.set("a", "b") } store = Store.new(@path) store.transaction { store.set("c", "d") } 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") { 1 + 1 } 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) File.expects(:rename).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 def test_bust_cache_on_ruby_change store.transaction { store.set("a", "b") } assert_equal "b", Store.new(@path).get("a") stub_const(Store, :CURRENT_VERSION, "foobar") do assert_nil Store.new(@path).get("a") end end private def stub_const(owner, const_name, stub_value) original_value = owner.const_get(const_name) owner.send(:remove_const, const_name) owner.const_set(const_name, stub_value) begin yield ensure owner.send(:remove_const, const_name) owner.const_set(const_name, original_value) end end end end end bootsnap-1.18.3/test/minimal_support/000077500000000000000000000000001455645557500176575ustar00rootroot00000000000000bootsnap-1.18.3/test/minimal_support/bootsnap_setup.rb000066400000000000000000000001201455645557500232420ustar00rootroot00000000000000# frozen_string_literal: true require "bundler/setup" require "bootsnap/setup" bootsnap-1.18.3/test/setup_test.rb000066400000000000000000000061571455645557500171720ustar00rootroot00000000000000# 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: true, compile_cache_yaml: true, compile_cache_json: true, ignore_directories: nil, readonly: false, ) 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: true, compile_cache_yaml: true, compile_cache_json: true, ignore_directories: nil, readonly: false, ) 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: true, compile_cache_yaml: true, compile_cache_json: true, ignore_directories: nil, readonly: false, ) 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, ignore_directories: nil, readonly: 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: true, compile_cache_yaml: true, compile_cache_json: true, ignore_directories: nil, readonly: false, ) Bootsnap.expects(:logger=).with($stderr.method(:puts)) Bootsnap.default_setup end def test_default_setup_with_BOOTSNAP_IGNORE_DIRECTORIES ENV["BOOTSNAP_IGNORE_DIRECTORIES"] = "foo,bar" Bootsnap.expects(:setup).with( cache_dir: @tmp_dir, development_mode: true, load_path_cache: true, compile_cache_iseq: true, compile_cache_yaml: true, compile_cache_json: true, ignore_directories: %w[foo bar], readonly: false, ) Bootsnap.default_setup end def test_unload_cache Bootsnap.unload_cache! end end end bootsnap-1.18.3/test/test_helper.rb000066400000000000000000000067431455645557500173120ustar00rootroot00000000000000# frozen_string_literal: true $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) if Warning.respond_to?(:[]=) Warning[:deprecated] = true 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", __dir__) 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. begin GC.verify_compaction_references(expand_heap: true, toward: :empty) rescue NotImplementedError, ArgumentError # some platforms do not support GC compaction end end module TestHandler def self.input_to_storage(_input, path) "neato #{path}" end def self.storage_to_output(data, _kwargs) data.upcase end def self.input_to_output(_data, _kwargs) 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..]}" 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 = nil) FileUtils.mkdir_p(File.dirname(path)) File.write(path, contents) FileUtils.touch(path, mtime: mtime) if mtime path end end end end end module CompileCacheISeqHelper def setup unless defined?(Bootsnap::CompileCache::ISeq) && Bootsnap::CompileCache::ISeq.supported? skip("Unsupported platform") end super end end module LoadPathCacheHelper def setup skip("Unsupported platform") unless Bootsnap::LoadPathCache.supported? super end end module TmpdirHelper def setup super @prev_dir = Dir.pwd @tmp_dir = Dir.mktmpdir("bootsnap-test") Dir.chdir(@tmp_dir) if Bootsnap::CompileCache.supported? set_compile_cache_dir(:ISeq, @tmp_dir) set_compile_cache_dir(:YAML, @tmp_dir) set_compile_cache_dir(:JSON, @tmp_dir) end end def teardown super Dir.chdir(@prev_dir) FileUtils.remove_entry(@tmp_dir) if Bootsnap::CompileCache.supported? restore_compile_cache_dir(:ISeq) restore_compile_cache_dir(:YAML) restore_compile_cache_dir(:JSON) end end private def restore_compile_cache_dir(mod_name) prev = instance_variable_get("@prev_#{mod_name.downcase}") # Restore directly to instance var to avoid duplication of suffix logic. Bootsnap::CompileCache.const_get(mod_name).instance_variable_set(:@cache_dir, prev) if prev end def set_compile_cache_dir(mod_name, dir) mod = Bootsnap::CompileCache.const_get(mod_name) instance_variable_set("@prev_#{mod_name.downcase}", mod.cache_dir) # Use setter method when setting to tmp dir. mod.cache_dir = dir end end bootsnap-1.18.3/test/worker_pool_test.rb000066400000000000000000000011051455645557500203600ustar00rootroot00000000000000# 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, Process.pid.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