Skip to content

Commit 81e6db4

Browse files
authored
Merge pull request #535 from byroot/compilation-backends
Allow to substitute the Ruby compiler
2 parents 7b04583 + 320ca02 commit 81e6db4

14 files changed

Lines changed: 230 additions & 125 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unreleased
22

3+
* Added a hook API to customize Ruby compilation.
4+
35
# 1.23.0
46

57
* Require Ruby 2.7.

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ well together.
7575
`require 'bootsnap/setup'` behavior can be changed using environment variables:
7676

7777
- `BOOTSNAP_CACHE_DIR` allows to define the cache location.
78+
- `BOOTSNAP_CONFIG` allows to change the default config location (`config/bootsnap.rb`).
7879
- `DISABLE_BOOTSNAP` allows to entirely disable bootsnap.
7980
- `DISABLE_BOOTSNAP_LOAD_PATH_CACHE` allows to disable load path caching.
8081
- `DISABLE_BOOTSNAP_COMPILE_CACHE` allows to disable ISeq and YAML caches.
@@ -320,6 +321,34 @@ open /c/nope.bundle -> -1
320321
# (nothing!)
321322
```
322323

324+
## Custom Compilers
325+
326+
Bootsnap allows substituing the default Ruby compiler by another one.
327+
This can be configured from the bootsnap config file (defaults to `config/bootsnap.rb`).
328+
329+
The main use case is to programmatically enable frozen string literals for your project without impacting dependencies:
330+
331+
```ruby
332+
Bootsnap.enable_frozen_string_literal(app_only: true)
333+
```
334+
335+
But it can also be used for more fine grained logic, or to implement all sort of Ruby code preprocessing:
336+
337+
```ruby
338+
# config/bootsnap.rb
339+
gems_root = File.join(Bundler.bundle_path.cleanpath, "")
340+
app_root = File.join(Dir.pwd, "")
341+
Bootsnap::CompileCache::ISeq.compiler_selector = ->(path) do
342+
# Enable `frozen_string_literal: true` for app code, but not gems.
343+
344+
if path.start_with?(app_root) && !path.start_with?(gems_root)
345+
Bootsnap::CompileCache::ISeq::FROZEN_STRING_LITERAL
346+
else
347+
Bootsnap::CompileCache::ISeq::DEFAULT
348+
end
349+
end
350+
```
351+
323352
## Precompilation
324353

325354
In development environments the bootsnap compilation cache is generated on the fly when source files are loaded.

ext/bootsnap/bootsnap.c

Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,16 @@ static VALUE bs_instrumentation_enabled_set(VALUE self, VALUE enabled);
114114
static VALUE bs_readonly_set(VALUE self, VALUE enabled);
115115
static VALUE bs_revalidation_set(VALUE self, VALUE enabled);
116116
static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v);
117-
static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args);
118-
static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
117+
static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler, VALUE args);
118+
static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler);
119119

120120
/* Helpers */
121121
enum cache_status {
122122
miss,
123123
hit,
124124
stale,
125125
};
126-
static void bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_CACHEPATH_SIZE]);
126+
static void bs_cache_path(VALUE cachedir_v, VALUE namespace_v, VALUE path_v, char (* cache_path)[MAX_CACHEPATH_SIZE]);
127127
static int bs_read_key(int fd, struct bs_cache_key * key);
128128
static enum cache_status cache_key_equal_fast_path(struct bs_cache_key * k1, struct bs_cache_key * k2);
129129
static int cache_key_equal_slow_path(struct bs_cache_key * current_key, struct bs_cache_key * cached_key, const VALUE input_data);
@@ -143,7 +143,7 @@ static uint32_t get_ruby_platform(void);
143143
*/
144144
static int bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * output_data);
145145
static VALUE prot_input_to_output(VALUE arg);
146-
static void bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE * output_data, int * exception_tag);
146+
static void bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * output_data, int * exception_tag);
147147
static int bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * storage_data);
148148
struct s2o_data;
149149
struct i2o_data;
@@ -302,8 +302,8 @@ Init_bootsnap(void)
302302
rb_define_module_function(rb_mBootsnap, "instrumentation_enabled=", bs_instrumentation_enabled_set, 1);
303303
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "readonly=", bs_readonly_set, 1);
304304
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "revalidation=", bs_revalidation_set, 1);
305-
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 4);
306-
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 3);
305+
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 5);
306+
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 4);
307307
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "compile_option_crc32=", bs_compile_option_crc32_set, 1);
308308

309309
current_umask = umask(0777);
@@ -420,13 +420,28 @@ get_ruby_platform(void)
420420
* The path will look something like: <cachedir>/12/34567890abcdef
421421
*/
422422
static void
423-
bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_CACHEPATH_SIZE])
423+
bs_cache_path(VALUE cachedir_v, VALUE namespace_v, VALUE path_v, char (* cache_path)[MAX_CACHEPATH_SIZE])
424424
{
425-
uint64_t hash = fnv1a_64(path);
425+
FilePathValue(path_v);
426+
427+
Check_Type(cachedir_v, T_STRING);
428+
Check_Type(path_v, T_STRING);
429+
if (!NIL_P(namespace_v)) {
430+
Check_Type(namespace_v, T_STRING);
431+
}
432+
433+
if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
434+
rb_raise(rb_eArgError, "cachedir too long");
435+
}
436+
437+
const char * cachedir = RSTRING_PTR(cachedir_v);
438+
const char * namespace = NIL_P(namespace_v) ? "" : RSTRING_PTR(namespace_v);
439+
440+
uint64_t hash = fnv1a_64(path_v);
426441
uint8_t first_byte = (hash >> (64 - 8));
427442
uint64_t remainder = hash & 0x00ffffffffffffff;
428443

429-
sprintf(*cache_path, "%s/%02"PRIx8"/%014"PRIx64, cachedir, first_byte, remainder);
444+
sprintf(*cache_path, "%s%s/%02"PRIx8"/%014"PRIx64, cachedir, namespace, first_byte, remainder);
430445
}
431446

432447
/*
@@ -498,25 +513,14 @@ static void bs_cache_key_digest(struct bs_cache_key *key,
498513
* conversions on the ruby VALUE arguments before passing them along.
499514
*/
500515
static VALUE
501-
bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args)
516+
bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler, VALUE args)
502517
{
503-
FilePathValue(path_v);
504-
505-
Check_Type(cachedir_v, T_STRING);
506-
Check_Type(path_v, T_STRING);
507-
508-
if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
509-
rb_raise(rb_eArgError, "cachedir too long");
510-
}
511-
512-
char * cachedir = RSTRING_PTR(cachedir_v);
513-
char * path = RSTRING_PTR(path_v);
514518
char cache_path[MAX_CACHEPATH_SIZE];
515519

516520
/* generate cache path to cache_path */
517-
bs_cache_path(cachedir, path_v, &cache_path);
521+
bs_cache_path(cachedir_v, namespace_v, path_v, &cache_path);
518522

519-
return bs_fetch(path, path_v, cache_path, handler, args);
523+
return bs_fetch(RSTRING_PTR(path_v), path_v, cache_path, handler, args);
520524
}
521525

522526
/*
@@ -525,25 +529,13 @@ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE arg
525529
* and doesn't return the content.
526530
*/
527531
static VALUE
528-
bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
532+
bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler)
529533
{
530-
FilePathValue(path_v);
531-
532-
Check_Type(cachedir_v, T_STRING);
533-
Check_Type(path_v, T_STRING);
534-
535-
if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
536-
rb_raise(rb_eArgError, "cachedir too long");
537-
}
538-
539-
char * cachedir = RSTRING_PTR(cachedir_v);
540-
char * path = RSTRING_PTR(path_v);
541534
char cache_path[MAX_CACHEPATH_SIZE];
542-
543535
/* generate cache path to cache_path */
544-
bs_cache_path(cachedir, path_v, &cache_path);
536+
bs_cache_path(cachedir_v, namespace_v, path_v, &cache_path);
545537

546-
return bs_precompile(path, path_v, cache_path, handler);
538+
return bs_precompile(RSTRING_PTR(path_v), path_v, cache_path, handler);
547539
}
548540

549541
static int bs_open_noatime(const char *path, int flags) {
@@ -963,7 +955,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
963955
exception_message = path_v;
964956
goto fail_errno;
965957
}
966-
bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
958+
bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
967959
if (exception_tag != 0) goto raise;
968960
goto succeed;
969961
} else if (res == CACHE_MISS || res == CACHE_STALE) valid_cache = 0;
@@ -989,7 +981,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
989981
/* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try
990982
* to cache anything; just return input_to_output(input_data) */
991983
if (storage_data == rb_cBootsnap_CompileCache_UNCOMPILABLE) {
992-
bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
984+
bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
993985
if (exception_tag != 0) goto raise;
994986
goto succeed;
995987
}
@@ -1009,7 +1001,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
10091001

10101002
if (output_data == rb_cBootsnap_CompileCache_UNCOMPILABLE) {
10111003
/* If storage_to_output returned `Uncompilable` we fallback to `input_to_output` */
1012-
bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
1004+
bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
10131005
if (exception_tag != 0) goto raise;
10141006
} else if (NIL_P(output_data)) {
10151007
/* If output_data is nil, delete the cache entry and generate the output
@@ -1023,7 +1015,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
10231015
goto fail_errno;
10241016
}
10251017
}
1026-
bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
1018+
bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
10271019
if (exception_tag != 0) goto raise;
10281020
}
10291021

@@ -1181,6 +1173,7 @@ struct i2o_data {
11811173
VALUE handler;
11821174
VALUE args;
11831175
VALUE input_data;
1176+
VALUE pathval;
11841177
};
11851178

11861179
struct i2s_data {
@@ -1210,12 +1203,13 @@ bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * outp
12101203
}
12111204

12121205
static void
1213-
bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE * output_data, int * exception_tag)
1206+
bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE path_v, VALUE * output_data, int * exception_tag)
12141207
{
12151208
struct i2o_data i2o_data = {
12161209
.handler = handler,
12171210
.args = args,
12181211
.input_data = input_data,
1212+
.pathval = path_v,
12191213
};
12201214
*output_data = rb_protect(prot_input_to_output, (VALUE)&i2o_data, exception_tag);
12211215
}
@@ -1224,7 +1218,7 @@ static VALUE
12241218
prot_input_to_output(VALUE arg)
12251219
{
12261220
struct i2o_data * data = (struct i2o_data *)arg;
1227-
return rb_funcall(data->handler, rb_intern("input_to_output"), 2, data->input_data, data->args);
1221+
return rb_funcall(data->handler, rb_intern("input_to_output"), 3, data->input_data, data->pathval, data->args);
12281222
}
12291223

12301224
static VALUE

lib/bootsnap.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ def _instrument(event, path)
4343
@instrumentation.call(event, path)
4444
end
4545

46+
def load_config
47+
config_path = File.expand_path(ENV["BOOTSNAP_CONFIG"] || "config/bootsnap.rb")
48+
if File.exist?(config_path)
49+
require(config_path)
50+
end
51+
end
52+
4653
def setup(
4754
cache_dir:,
4855
development_mode: true,
@@ -76,6 +83,28 @@ def setup(
7683
readonly: readonly,
7784
revalidation: revalidation,
7885
)
86+
87+
load_config
88+
end
89+
90+
def enable_frozen_string_literal(app_only: false)
91+
if app_only
92+
gems_root = File.join(Bundler.bundle_path.cleanpath, "")
93+
app_root = File.join(Dir.pwd, "")
94+
Bootsnap::CompileCache::ISeq.default_compiler = Bootsnap::CompileCache::ISeq::DEFAULT
95+
Bootsnap::CompileCache::ISeq.compiler_selector = lambda { |path|
96+
# Enable `frozen_string_literal: true` for app code, but not gems.
97+
98+
if path.start_with?(app_root) && !path.start_with?(gems_root)
99+
Bootsnap::CompileCache::ISeq::FROZEN_STRING_LITERAL
100+
else
101+
Bootsnap::CompileCache::ISeq::DEFAULT
102+
end
103+
}
104+
else
105+
Bootsnap::CompileCache::ISeq.compiler_selector = nil
106+
Bootsnap::CompileCache::ISeq.default_compiler = Bootsnap::CompileCache::ISeq::FROZEN_STRING_LITERAL
107+
end
79108
end
80109

81110
def unload_cache!

lib/bootsnap/cli.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def precompile_command(*sources)
4343
yaml: yaml,
4444
revalidation: true,
4545
)
46+
Bootsnap.load_config
4647

4748
@work_pool = WorkerPool.create(size: jobs, jobs: {
4849
ruby: method(:precompile_ruby),

0 commit comments

Comments
 (0)