Skip to content

Commit d061c28

Browse files
committed
Reset Bootsnap RBS cache on lockfile changes
Stores the current `Gemfile.lock` digest in Tapioca's dedicated Bootsnap cache and resets the Bootsnap payload when the digest changes. Skips stale read-only caches so consumers do not use rewritten iseqs built for a different lockfile.
1 parent 984af14 commit d061c28

4 files changed

Lines changed: 176 additions & 24 deletions

File tree

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,16 @@ The rewriting is automatic on every `tapioca` invocation: [`require-hooks`](http
857857
$ TAPIOCA_RBS_CACHE=1 bin/tapioca dsl
858858
```
859859
860-
Tapioca configures Bootsnap's iseq cache against a dedicated directory (`tmp/cache/bootsnap-tapioca-rbs` by default; override with `TAPIOCA_BOOTSNAP_CACHE_DIR`). The first run is slower because every file is rewritten and the result is baked into the iseq cache; subsequent runs against the same directory skip the rewrite entirely.
860+
Tapioca configures Bootsnap's iseq cache against a dedicated directory (`tmp/cache/bootsnap-tapioca-rbs` by
861+
default; override with `TAPIOCA_BOOTSNAP_CACHE_DIR`).
862+
863+
Tapioca writes the current `Gemfile.lock` digest to `.gemfile-lock-digest` inside that cache directory. When the
864+
lockfile changes, Tapioca sees the digest mismatch and resets Bootsnap's cache payload before configuring Bootsnap.
865+
This lets gem bumps that affect rewriting, such as `tapioca`, start from a fresh cache without accumulating old cache
866+
directories.
867+
868+
The first run is slower because every file is rewritten and the result is baked into the iseq cache; subsequent runs
869+
against the same lockfile skip the rewrite entirely.
861870
862871
`Bootsnap.setup` mutates a process-wide singleton, and a second call would overwrite Tapioca's dedicated cache directory and start writing rewritten iseqs into the host's normal cache. Tapioca enforces this under `TAPIOCA_RBS_CACHE=1`: after its own setup runs, any subsequent `Bootsnap.setup` raises a clear error pointing at the fix. Gate your host's `Bootsnap.setup` on the same env var. Rails apps do this in `config/boot.rb`:
863872

lib/tapioca/rbs/bootsnap_cache.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "bundler"
5+
require "digest"
6+
require "fileutils"
7+
8+
module Tapioca
9+
module RBS
10+
# Prepares the Bootsnap iseq cache used for RBS rewrite output.
11+
#
12+
# RBS rewrite output can change when the lockfile changes, even if the
13+
# source files are unchanged.
14+
# To account for this, we store the current Gemfile.lock SHA256 in a
15+
# `.gemfile-lock-digest` file.
16+
# On writable runs, a digest mismatch deletes Bootsnap's cache payload and
17+
# records the new digest, so this run rebuilds the cache from scratch. On
18+
# read-only runs, a digest mismatch means the cache is stale and must not be
19+
# used.
20+
module BootsnapCache
21+
PrepareResult = Struct.new(:setup_bootsnap, keyword_init: true)
22+
23+
DIGEST_FILE = ".gemfile-lock-digest" #: String
24+
25+
class << self
26+
#: (String, readonly: bool) -> PrepareResult
27+
def prepare_for_setup(cache_dir, readonly:)
28+
digest = gemfile_lock_digest
29+
30+
if readonly
31+
return PrepareResult.new(setup_bootsnap: digest_matches?(cache_dir, digest))
32+
end
33+
34+
unless digest_matches?(cache_dir, digest)
35+
FileUtils.rm_rf(File.join(cache_dir, "bootsnap"))
36+
FileUtils.rm_f(digest_path(cache_dir))
37+
FileUtils.mkdir_p(cache_dir)
38+
File.write(digest_path(cache_dir), digest)
39+
end
40+
41+
PrepareResult.new(setup_bootsnap: true)
42+
end
43+
44+
private
45+
46+
#: -> String
47+
def gemfile_lock_digest
48+
Digest::SHA256.file(Bundler.default_lockfile).hexdigest
49+
end
50+
51+
#: (String, String) -> bool
52+
def digest_matches?(cache_dir, digest)
53+
path = digest_path(cache_dir)
54+
File.file?(path) && File.read(path).chomp == digest
55+
end
56+
57+
#: (String) -> String
58+
def digest_path(cache_dir)
59+
File.join(cache_dir, DIGEST_FILE)
60+
end
61+
end
62+
end
63+
end
64+
end

lib/tapioca/rbs/rewriter.rb

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# typed: strict
22
# frozen_string_literal: true
33

4+
require "tapioca/rbs/bootsnap_cache"
5+
46
# This code rewrites RBS comments back into Sorbet's signatures as the files are being loaded.
57
# This will allow `sorbet-runtime` to wrap the methods as if they were originally written with the `sig{}` blocks.
68
# This will in turn allow Tapioca to use this signatures to generate typed RBI files.
@@ -19,42 +21,62 @@ module BootsnapGuard
1921
def setup(**_kwargs)
2022
Kernel.raise HostBootsnapSetupError, <<~MSG
2123
Bootsnap.setup was called while TAPIOCA_RBS_CACHE=1 is set. Tapioca already
22-
configured bootsnap with a dedicated cache directory; re-running setup
23-
would overwrite that config and start writing rewritten iseqs into your
24-
host's cache.
24+
configured bootsnap for RBS rewriting; re-running setup would overwrite
25+
that config and start writing rewritten iseqs into your host's cache.
2526
2627
Gate your host's Bootsnap.setup on the env var, e.g. in config/boot.rb:
2728
2829
require "bootsnap/setup" unless ENV["TAPIOCA_RBS_CACHE"] == "1"
2930
MSG
3031
end
3132
end
33+
34+
module BootsnapSetup
35+
class << self
36+
extend T::Sig
37+
38+
sig { void }
39+
def setup
40+
require "bootsnap"
41+
42+
# Respect BOOTSNAP_READONLY for consumers reading a pre-populated cache
43+
# (e.g. a CI prime step).
44+
readonly = !["0", "false", false].include?(ENV.fetch("BOOTSNAP_READONLY") { false })
45+
cache_dir = ENV.fetch("TAPIOCA_BOOTSNAP_CACHE_DIR", File.join(Dir.pwd, "tmp/cache/bootsnap-tapioca-rbs"))
46+
# A read-only cache with a mismatched lockfile digest may contain stale rewritten iseqs,
47+
# and this process cannot reset it.
48+
return unless Tapioca::RBS::BootsnapCache.prepare_for_setup(
49+
cache_dir,
50+
readonly: readonly,
51+
).setup_bootsnap
52+
53+
Bootsnap.setup(
54+
cache_dir: cache_dir,
55+
development_mode: true,
56+
load_path_cache: true,
57+
compile_cache_iseq: true,
58+
compile_cache_yaml: true,
59+
readonly: readonly,
60+
revalidation: true,
61+
)
62+
Bootsnap.log_stats!
63+
ensure
64+
Bootsnap.singleton_class.prepend(Tapioca::RBS::BootsnapGuard) if defined?(Bootsnap)
65+
end
66+
end
67+
end
3268
end
3369
end
3470

35-
# When TAPIOCA_RBS_CACHE=1, set up bootsnap with a dedicated cache directory
36-
# and load require-hooks so the RBS-rewritten iseqs get cached. Subsequent
37-
# runs read the rewritten iseq directly and skip the rewrite.
71+
# When TAPIOCA_RBS_CACHE=1, use a dedicated Bootsnap cache directory for
72+
# RBS-rewritten iseqs. Stale read-only caches are skipped because this process
73+
# cannot reset them.
3874
#
39-
# After our setup, BootsnapGuard is prepended so the host application can't
40-
# replace our cache directory.
75+
# BootsnapGuard is prepended so the host application can't replace our cache
76+
# configuration.
4177
if ENV["TAPIOCA_RBS_CACHE"] == "1"
4278
begin
43-
require "bootsnap"
44-
# Respect BOOTSNAP_READONLY for consumers reading a pre-populated cache
45-
# (e.g. a CI prime step).
46-
readonly = !["0", "false", false].include?(ENV.fetch("BOOTSNAP_READONLY") { false })
47-
Bootsnap.setup(
48-
cache_dir: ENV.fetch("TAPIOCA_BOOTSNAP_CACHE_DIR", File.join(Dir.pwd, "tmp/cache/bootsnap-tapioca-rbs")),
49-
development_mode: true,
50-
load_path_cache: true,
51-
compile_cache_iseq: true,
52-
compile_cache_yaml: true,
53-
readonly: readonly,
54-
revalidation: true,
55-
)
56-
Bootsnap.log_stats!
57-
Bootsnap.singleton_class.prepend(Tapioca::RBS::BootsnapGuard)
79+
Tapioca::RBS::BootsnapSetup.setup
5880
rescue LoadError
5981
# Bootsnap is not in the bundle, skip iseq caching.
6082
end

spec/tapioca/cli/dsl_spec.rb

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,63 @@ class Post
680680
assert_success_status(result)
681681
end
682682

683+
it "resets the bootsnap cache when Gemfile.lock changes" do
684+
@project.write!("lib/post.rb", <<~RB)
685+
require "smart_properties"
686+
687+
class Post
688+
include SmartProperties
689+
property :title, accepts: String
690+
end
691+
RB
692+
693+
env = {
694+
"TAPIOCA_RBS_CACHE" => "1",
695+
"TAPIOCA_BOOTSNAP_CACHE_DIR" => "tmp/cache/test-bootsnap-tapioca-rbs",
696+
}
697+
698+
result = @project.tapioca("dsl --only-bootsnap-rbs-cache Post", env: env)
699+
700+
assert_success_status(result)
701+
@project.write!("tmp/cache/test-bootsnap-tapioca-rbs/bootsnap/stale-cache-entry", "stale")
702+
703+
@project.write!("Gemfile.lock", "#{@gemfile_lock}\n")
704+
result = @project.tapioca("dsl --only-bootsnap-rbs-cache Post", env: env)
705+
706+
assert_success_status(result)
707+
refute_project_file_exist("tmp/cache/test-bootsnap-tapioca-rbs/bootsnap/stale-cache-entry")
708+
assert_project_file_exist("tmp/cache/test-bootsnap-tapioca-rbs/.gemfile-lock-digest")
709+
end
710+
711+
it "skips a stale read-only bootsnap cache when Gemfile.lock changes" do
712+
@project.write!("lib/post.rb", <<~RB)
713+
require "smart_properties"
714+
715+
class Post
716+
include SmartProperties
717+
property :title, accepts: String
718+
end
719+
RB
720+
721+
env = {
722+
"TAPIOCA_RBS_CACHE" => "1",
723+
"TAPIOCA_BOOTSNAP_CACHE_DIR" => "tmp/cache/test-readonly-bootsnap-tapioca-rbs",
724+
}
725+
726+
result = @project.tapioca("dsl --only-bootsnap-rbs-cache Post", env: env)
727+
728+
assert_success_status(result)
729+
original_digest = @project.read("tmp/cache/test-readonly-bootsnap-tapioca-rbs/.gemfile-lock-digest")
730+
@project.write!("Gemfile.lock", "#{@gemfile_lock}\n")
731+
732+
result = @project.tapioca("dsl Post", env: env.merge("BOOTSNAP_READONLY" => "1"))
733+
734+
assert_success_status(result)
735+
assert_empty_stderr(result)
736+
assert_project_file_exist("sorbet/rbi/dsl/post.rbi")
737+
assert_project_file_equal("tmp/cache/test-readonly-bootsnap-tapioca-rbs/.gemfile-lock-digest", original_digest)
738+
end
739+
683740
it "warns when --only-bootsnap-rbs-cache is set without TAPIOCA_RBS_CACHE=1" do
684741
@project.write!("lib/post.rb", <<~RB)
685742
require "smart_properties"

0 commit comments

Comments
 (0)