Skip to content

Commit f9d9501

Browse files
committed
Canonicalize $BUNDLE_PATH in cache keys
Ref: heroku/heroku-buildpack-ruby#979 Heroku buildpacks build the application in one location and then move it elsewhere. This cause all the paths to change, hence all bootsnap cache keys to be invalidated. By replacing $BUNDLE_PATH by a constant string in the cache keys, we allow the bundler directory to be moved without flushing the cache. Ideally we'd use a similar substitution for the "app root", but I need to put more thoughts into it, as I'm not too sure how best to infer it.
1 parent f52e16b commit f9d9501

File tree

3 files changed

+37
-16
lines changed

3 files changed

+37
-16
lines changed

ext/bootsnap/bootsnap.c

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
#define MAX_CREATE_TEMPFILE_ATTEMPT 3
3636

37+
#define BUNDLE_PATH "$BUNDLE_PATH/"
3738
/*
3839
* An instance of this key is written as the first 64 bytes of each cache file.
3940
* The mtime and size members track whether the file contents have changed, and
@@ -87,6 +88,8 @@ static VALUE rb_mBootsnap;
8788
static VALUE rb_mBootsnap_CompileCache;
8889
static VALUE rb_mBootsnap_CompileCache_Native;
8990
static VALUE rb_eBootsnap_CompileCache_Uncompilable;
91+
static char * bundle_path = NULL;
92+
static size_t bundle_path_size = 0;
9093
static ID uncompilable;
9194

9295
/* Functions exposed as module functions on Bootsnap::CompileCache::Native */
@@ -96,7 +99,7 @@ static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE
9699

97100
/* Helpers */
98101
static uint64_t fnv1a_64(const char *str);
99-
static void bs_cache_path(const char * cachedir, const char * path, char (* cache_path)[MAX_CACHEPATH_SIZE]);
102+
static void bs_cache_path(const char * cachedir, const char * path, const long path_length, char (* cache_path)[MAX_CACHEPATH_SIZE]);
100103
static int bs_read_key(int fd, struct bs_cache_key * key);
101104
static int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2);
102105
static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args);
@@ -144,6 +147,13 @@ Init_bootsnap(void)
144147
rb_mBootsnap_CompileCache_Native = rb_define_module_under(rb_mBootsnap_CompileCache, "Native");
145148
rb_eBootsnap_CompileCache_Uncompilable = rb_define_class_under(rb_mBootsnap_CompileCache, "Uncompilable", rb_eStandardError);
146149

150+
VALUE rb_bundle_path = rb_const_get(rb_mBootsnap, rb_intern("BUNDLE_PATH"));
151+
if (RTEST(rb_bundle_path)) {
152+
bundle_path = ALLOC_N(char, RSTRING_LEN(rb_bundle_path) + 1);
153+
memcpy(bundle_path, RSTRING_PTR(rb_bundle_path), RSTRING_LEN(rb_bundle_path) + 1);
154+
bundle_path_size = strlen(bundle_path);
155+
}
156+
147157
current_ruby_revision = get_ruby_revision();
148158
current_ruby_platform = get_ruby_platform();
149159

@@ -267,9 +277,18 @@ get_ruby_platform(void)
267277
* The path will look something like: <cachedir>/12/34567890abcdef
268278
*/
269279
static void
270-
bs_cache_path(const char * cachedir, const char * path, char (* cache_path)[MAX_CACHEPATH_SIZE])
280+
bs_cache_path(const char * cachedir, const char * path, const long path_length, char (* cache_path)[MAX_CACHEPATH_SIZE])
271281
{
272-
uint64_t hash = fnv1a_64(path);
282+
uint64_t hash;
283+
if (bundle_path && !strncmp(bundle_path, path, bundle_path_size)) {
284+
long canonical_path_size = path_length + strlen(BUNDLE_PATH) - bundle_path_size + 1;
285+
char * canonical_path = ALLOCA_N(char, canonical_path_size);
286+
memcpy(canonical_path, BUNDLE_PATH, strlen(BUNDLE_PATH));
287+
memcpy(canonical_path + strlen(BUNDLE_PATH), path + bundle_path_size, path_length - bundle_path_size + 1);
288+
hash = fnv1a_64(canonical_path);
289+
} else {
290+
hash = fnv1a_64(path);
291+
}
273292
uint8_t first_byte = (hash >> (64 - 8));
274293
uint64_t remainder = hash & 0x00ffffffffffffff;
275294

@@ -314,12 +333,13 @@ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE arg
314333
rb_raise(rb_eArgError, "cachedir too long");
315334
}
316335

317-
char * cachedir = RSTRING_PTR(cachedir_v);
318-
char * path = RSTRING_PTR(path_v);
336+
char * cachedir = RSTRING_PTR(cachedir_v);
337+
char * path = RSTRING_PTR(path_v);
338+
long path_length = RSTRING_LEN(path_v);
319339
char cache_path[MAX_CACHEPATH_SIZE];
320340

321341
/* generate cache path to cache_path */
322-
bs_cache_path(cachedir, path, &cache_path);
342+
bs_cache_path(cachedir, path, path_length, &cache_path);
323343

324344
return bs_fetch(path, path_v, cache_path, handler, args);
325345
}
@@ -341,12 +361,13 @@ bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
341361
rb_raise(rb_eArgError, "cachedir too long");
342362
}
343363

344-
char * cachedir = RSTRING_PTR(cachedir_v);
345-
char * path = RSTRING_PTR(path_v);
364+
char * cachedir = RSTRING_PTR(cachedir_v);
365+
char * path = RSTRING_PTR(path_v);
366+
long path_length = RSTRING_LEN(path_v);
346367
char cache_path[MAX_CACHEPATH_SIZE];
347368

348369
/* generate cache path to cache_path */
349-
bs_cache_path(cachedir, path, &cache_path);
370+
bs_cache_path(cachedir, path, path_length, &cache_path);
350371

351372
return bs_precompile(path, path_v, cache_path, handler);
352373
}

lib/bootsnap/bundler.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,10 @@ def bundler?
1212

1313
false
1414
end
15+
16+
BUNDLE_PATH = if bundler?
17+
-(Bundler.bundle_path.cleanpath.to_s << '/')
18+
else
19+
nil
20+
end
1521
end

lib/bootsnap/load_path_cache/path_scanner.rb

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,6 @@ module PathScanner
99
NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
1010
ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/
1111

12-
BUNDLE_PATH = if Bootsnap.bundler?
13-
(Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze
14-
else
15-
''
16-
end
17-
1812
class << self
1913
def call(path)
2014
path = File.expand_path(path.to_s).freeze
@@ -27,7 +21,7 @@ def call(path)
2721
#
2822
# This can happen if, for example, the user adds '.' to the load path,
2923
# and the bundle path is '.bundle'.
30-
contains_bundle_path = BUNDLE_PATH.start_with?(path)
24+
contains_bundle_path = BUNDLE_PATH&.start_with?(path)
3125

3226
dirs = []
3327
requirables = []

0 commit comments

Comments
 (0)